package com.jme3.system.awt;

import com.jme3.post.SceneProcessor;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.texture.FrameBuffer;
import com.jme3.texture.Image.Format;
import com.jme3.util.BufferUtils;
import com.jme3.util.Screenshots;
import java.awt.AWTException;
import java.awt.BufferCapabilities;
import java.awt.Canvas;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.ImageCapabilities;
import java.awt.RenderingHints;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;

public class AwtPanel extends Canvas implements SceneProcessor {

    private boolean attachAsMain = false;
    
    private BufferedImage img;
    private FrameBuffer fb;
    private ByteBuffer byteBuf;
    private IntBuffer intBuf;
    private RenderManager rm;
    private PaintMode paintMode;
    private ArrayList<ViewPort> viewPorts = new ArrayList<ViewPort>(); 
    
    // Visibility/drawing vars
    private BufferStrategy strategy;
    private AffineTransformOp transformOp;
    private AtomicBoolean hasNativePeer = new AtomicBoolean(false);
    private AtomicBoolean showing = new AtomicBoolean(false);
    private AtomicBoolean repaintRequest = new AtomicBoolean(false);
    
    // Reshape vars
    private int newWidth  = 1;
    private int newHeight = 1;
    private AtomicBoolean reshapeNeeded  = new AtomicBoolean(false);
    private final Object lock = new Object();
    
    public AwtPanel(PaintMode paintMode){
        this.paintMode = paintMode;
        
        if (paintMode == PaintMode.Accelerated){
            setIgnoreRepaint(true);
        }
        
        addComponentListener(new ComponentAdapter(){
            @Override
            public void componentResized(ComponentEvent e) {
                synchronized (lock){
                    int newWidth2 = Math.max(getWidth(), 1);
                    int newHeight2 = Math.max(getHeight(), 1);
                    if (newWidth != newWidth2 || newHeight != newHeight2){
                        newWidth = newWidth2;
                        newHeight = newHeight2;
                        reshapeNeeded.set(true);
                        System.out.println("EDT: componentResized " + newWidth + ", " + newHeight);
                    }
                }
            }
        });
    }
    
    @Override
    public void addNotify(){
        super.addNotify();

        synchronized (lock){
            hasNativePeer.set(true);
            System.out.println("EDT: addNotify");
        }
        
        requestFocusInWindow();
    }

    @Override
    public void removeNotify(){
        synchronized (lock){
            hasNativePeer.set(false);
            System.out.println("EDT: removeNotify");
        }
        
        super.removeNotify();
    }
    
    @Override
    public void paint(Graphics g){
        Graphics2D g2d = (Graphics2D) g;
        synchronized (lock){
            g2d.drawImage(img, transformOp, 0, 0);
        }
    }
    
    public boolean checkVisibilityState(){
        if (!hasNativePeer.get()){
            if (strategy != null){
                strategy.dispose();
                strategy = null;
                System.out.println("OGL: Not visible. Destroy strategy.");
            }
            return false;
        }
        
        boolean currentShowing = isShowing();
        if (showing.getAndSet(currentShowing) != currentShowing){
            if (currentShowing){
                System.out.println("OGL: Enter showing state.");
            }else{
                System.out.println("OGL: Exit showing state.");
            }
        }
        return currentShowing;
    }
    
    public void repaintInThread(){
        // Convert screenshot.
        byteBuf.clear();
        rm.getRenderer().readFrameBuffer(fb, byteBuf);
        
        synchronized (lock){
            // All operations on img must be synchronized
            // as it is accessed from EDT.
            Screenshots.convertScreenShot2(intBuf, img);
            repaint();
        }
    }
    
    public void drawFrameInThread(){
        // Convert screenshot.
        byteBuf.clear();
        rm.getRenderer().readFrameBuffer(fb, byteBuf);
        Screenshots.convertScreenShot2(intBuf, img);
        
        synchronized (lock){
            // All operations on strategy should be synchronized (?)
            if (strategy == null){
                try {
                    createBufferStrategy(1, 
                            new BufferCapabilities(
                                new ImageCapabilities(true), 
                                new ImageCapabilities(true), 
                                BufferCapabilities.FlipContents.UNDEFINED)
                                        );
                } catch (AWTException ex) {
                    ex.printStackTrace();
                }
                strategy = getBufferStrategy();
                System.out.println("OGL: Visible. Create strategy.");
            }
            
            // Draw screenshot.
            do {
                do {
                    Graphics2D g2d = (Graphics2D) strategy.getDrawGraphics();
                    if (g2d == null){
                        System.out.println("OGL: DrawGraphics was null.");
                        return;
                    }
                    
                    g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
                                         RenderingHints.VALUE_RENDER_SPEED);
                    
                    g2d.drawImage(img, transformOp, 0, 0);
                    g2d.dispose();
                    strategy.show();
                } while (strategy.contentsRestored());
            } while (strategy.contentsLost());
        }
    }
    
    public boolean isActiveDrawing(){
        return paintMode != PaintMode.OnRequest && showing.get();
    }
    
    public void attachTo(boolean overrideMainFramebuffer, ViewPort ... vps){
        if (viewPorts.size() > 0){
            for (ViewPort vp : viewPorts){
                vp.setOutputFrameBuffer(null);
            }
            viewPorts.get(viewPorts.size()-1).removeProcessor(this);
        }
        
        viewPorts.addAll(Arrays.asList(vps));
        viewPorts.get(viewPorts.size()-1).addProcessor(this);
        
        this.attachAsMain = overrideMainFramebuffer;
    }
    
    public void initialize(RenderManager rm, ViewPort vp) {
        if (this.rm == null){
            // First time called in OGL thread
            this.rm = rm;
            reshapeInThread(1, 1);
        }
    }

    private void reshapeInThread(int width, int height) {
        byteBuf = BufferUtils.ensureLargeEnough(byteBuf, width * height * 4);
        intBuf = byteBuf.asIntBuffer();
        
        fb = new FrameBuffer(width, height, 1);
        fb.setDepthBuffer(Format.Depth);
        fb.setColorBuffer(Format.RGB8);
        
        if (attachAsMain){
            rm.getRenderer().setMainFrameBufferOverride(fb);
        }
        
        synchronized (lock){
            img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        }
        
//        synchronized (lock){
//            img = (BufferedImage) getGraphicsConfiguration().createCompatibleImage(width, height);
//        }
        
        AffineTransform tx = AffineTransform.getScaleInstance(1, -1);
        tx.translate(0, -img.getHeight());
        transformOp = new AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
        
        for (ViewPort vp : viewPorts){
            if (!attachAsMain){
                vp.setOutputFrameBuffer(fb);
            }
            vp.getCamera().resize(width, height, true);
            
            // NOTE: Hack alert. This is done ONLY for custom framebuffers.
            // Main framebuffer should use RenderManager.notifyReshape().
            for (SceneProcessor sp : vp.getProcessors()){
                sp.reshape(vp, width, height);
            }
        }
    }

    public boolean isInitialized() {
        return fb != null;
    }

    public void preFrame(float tpf) {
    }

    public void postQueue(RenderQueue rq) {
    }
    
    @Override
    public void invalidate(){
        // For "PaintMode.OnDemand" only.
        repaintRequest.set(true);
    }

    public void postFrame(FrameBuffer out) {
        if (!attachAsMain && out != fb){
            throw new IllegalStateException("Why did you change the output framebuffer?");
        }
        
        if (reshapeNeeded.getAndSet(false)){
            reshapeInThread(newWidth, newHeight);
        }else{
            if (!checkVisibilityState()){
                return;
            }
            
            switch (paintMode){
                case Accelerated:
                    drawFrameInThread();
                    break;
                case Repaint:
                    repaintInThread();
                    break;
                case OnRequest:
                    if (repaintRequest.getAndSet(false)){
                        repaintInThread();
                    }
                    break;
            }
        }
    }
    
    public void reshape(ViewPort vp, int w, int h) {
    }

    public void cleanup() {
    }
}
