/**
 * Copyright 2012 JogAmp Community. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of
 *       conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list
 *       of conditions and the following disclaimer in the documentation and/or other materials
 *       provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * The views and conclusions contained in the software and documentation are those of the
 * authors and should not be interpreted as representing official policies, either expressed
 * or implied, of JogAmp Community.
 */
package com.jogamp.opengl.test.junit.jogl.awt;

import java.applet.Applet;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.DisplayMode;
import java.awt.EventQueue;
import java.awt.Frame;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.lang.reflect.InvocationTargetException;
import java.util.Timer;
import java.util.TimerTask;

import com.jogamp.opengl.*;
import com.jogamp.opengl.awt.GLCanvas;

import org.junit.Assume;
import org.junit.Test;
import org.junit.FixMethodOrder;
import org.junit.runners.MethodSorters;

import com.jogamp.common.os.Clock;
import com.jogamp.common.os.Platform;
import com.jogamp.common.util.VersionNumber;
import com.jogamp.common.util.awt.AWTEDTExecutor;
import com.jogamp.opengl.util.AnimatorBase;
import com.jogamp.opengl.test.junit.util.MiscUtils;
import com.jogamp.opengl.test.junit.util.UITestCase;

/**
 * Sample program that relies on JOGL's mechanism to handle the OpenGL context
 * and rendering loop when using an AWT canvas attached to an Applet.
 * <p>
 * BUG on OSX/CALayer w/ Java6:
 * If frame.setTitle() is issued right after initialization the call hangs in
 * <pre>
 * at apple.awt.CWindow._setTitle(Native Method)
 *  at apple.awt.CWindow.setTitle(CWindow.java:765) [1.6.0_37, build 1.6.0_37-b06-434-11M3909]
 * </pre>
 * </p>
 * <p>
 * OSX/CALayer is forced by using an Applet component in this unit test.
 * </p>
 * <p>
 * Similar deadlock has been experienced w/ other mutable operation on an AWT Container owning a GLCanvas child,
 * e.g. setResizable*().
 * </p>
 * <p>
 * Users shall make sure all mutable AWT calls are performed on the EDT, even before 1st setVisible(true) !
 * </p>
 */
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestGLCanvasAWTActionDeadlock02AWT extends UITestCase {
    static int framesPerTest = 240; // frames

    static class MiniPApplet extends Applet implements MouseMotionListener, KeyListener {
        private static final long serialVersionUID = 1L;

        /////////////////////////////////////////////////////////////
        //
        // Test parameters

        public int frameRate           = 120;
        public int numSamples          = 4;

        public boolean fullScreen      = false;
        public boolean useAnimator     = true;
        public boolean resizeableFrame = true;

        public boolean restartCanvas   = true;
        public int restartTimeout      = 100; // in number of frames.

        public boolean printThreadInfo = false;
        public boolean printEventInfo  = false;

        /////////////////////////////////////////////////////////////
        //
        // Internal variables

        int width;
        int height;

        String OPENGL_VENDOR;
        String OPENGL_RENDERER;
        String OPENGL_VERSION;
        String OPENGL_EXTENSIONS;

        int currentSamples = -1;

        private Frame frame;
        private GLProfile profile;
        private GLCapabilities capabilities;
        private GLCanvas canvas;

        private SimpleListener listener;
        private CustomAnimator animator;

        private long beforeTime;
        private long overSleepTime;
        private final long frameRatePeriod = 1000000000L / frameRate;

        private boolean initialized = false;
        private boolean osxCALayerAWTModBug = false;
        boolean justInitialized = true;

        private double theta = 0;
        private double s = 0;
        private double c = 0;

        private long millisOffset;
        private int fcount, lastm;
        private float frate;
        private final int fint = 3;

        private boolean setFramerate = false;
        private boolean restarted = false;

        private int frameCount = 0;

        void run() throws InterruptedException, InvocationTargetException {
            // Thread loop = new Thread("Animation Thread") {
            // public void run() {
            frameCount = 0;
            while ( frameCount < framesPerTest ) {
                if (!initialized) {
                    setup();
                }

                if (restartCanvas && restartTimeout == frameCount) {
                    restart();
                }

                if (useAnimator) {
                    animator.requestRender();
                } else {
                    canvas.display();
                }

                clock();

                frameCount++;
                if( null == frame ) {
                    break;
                }
            }
            dispose();
            // }
            // };
            // loop.start();
        }

        void setup() throws InterruptedException, InvocationTargetException {
            if (printThreadInfo) System.out.println("Current thread at setup(): " + Thread.currentThread());

            millisOffset = System.currentTimeMillis();

            final VersionNumber version170 = new VersionNumber(1, 7, 0);
            osxCALayerAWTModBug = Platform.OSType.MACOS == Platform.getOSType() &&
                    0 > Platform.getJavaVersionNumber().compareTo(version170);
                    System.err.println("OSX CALayer AWT-Mod Bug "+osxCALayerAWTModBug);
                    System.err.println("OSType "+Platform.getOSType());
                    System.err.println("Java Version "+Platform.getJavaVersionNumber());

                    // Frame setup ----------------------------------------------------------

                    width = 300;
                    height = 300;
                    final MiniPApplet applet = this;

                    final GraphicsEnvironment environment =
                            GraphicsEnvironment.getLocalGraphicsEnvironment();
                    final GraphicsDevice displayDevice = environment.getDefaultScreenDevice();
                    frame = new Frame(displayDevice.getDefaultConfiguration());

                    final Rectangle fullScreenRect;
                    if (fullScreen) {
                        final DisplayMode mode = displayDevice.getDisplayMode();
                        fullScreenRect = new Rectangle(0, 0, mode.getWidth(), mode.getHeight());
                    } else {
                        fullScreenRect = null;
                    }
                    // All AWT Mods on AWT-EDT, especially due to the follow-up complicated code!
                    AWTEDTExecutor.singleton.invoke(true, new Runnable() {
                        @Override
                        public void run() {
                            frame.setTitle("MiniPApplet");
                        } } );
                    if (fullScreen) {
                        try {
                            javax.swing.SwingUtilities.invokeAndWait(new Runnable() {
                                @Override
                                public void run() {
                                    frame.setUndecorated(true);
                                    frame.setBackground(Color.GRAY);
                                    frame.setBounds(fullScreenRect);
                                    frame.setVisible(true);
                                }});
                        } catch (final Throwable t) {
                            t.printStackTrace();
                            Assume.assumeNoException(t);
                        }
                    }
                    try {
                        javax.swing.SwingUtilities.invokeAndWait(new Runnable() {
                            @Override
                            public void run() {
                                frame.setLayout(null);
                                frame.add(applet);
                                if (fullScreen) {
                                    frame.invalidate();
                                } else {
                                    frame.pack();
                                }
                                frame.setResizable(resizeableFrame);
                                if (fullScreen) {
                                    // After the pack(), the screen bounds are gonna be 0s
                                    frame.setBounds(fullScreenRect);
                                    applet.setBounds((fullScreenRect.width - applet.width) / 2,
                                            (fullScreenRect.height - applet.height) / 2,
                                            applet.width, applet.height);
                                } else {
                                    final Insets insets = frame.getInsets();

                                    final int windowW = applet.width + insets.left + insets.right;
                                    final int windowH = applet.height + insets.top + insets.bottom;
                                    final int locationX = 100;
                                    final int locationY = 100;

                                    frame.setSize(windowW, windowH);
                                    frame.setLocation(locationX, locationY);

                                    final int usableWindowH = windowH - insets.top - insets.bottom;
                                    applet.setBounds((windowW - width)/2, insets.top + (usableWindowH - height)/2, width, height);
                                }
                            }});
                    } catch (final Throwable t) {
                        t.printStackTrace();
                        Assume.assumeNoException(t);
                    }


                    frame.add(this);
                    frame.addWindowListener(new WindowAdapter() {
                        @Override
                        public void windowClosing(final WindowEvent e) {
                            try {
                                dispose();
                            } catch (final Exception ex) {
                                Assume.assumeNoException(ex);
                            }
                        }
                    });

                    javax.swing.SwingUtilities.invokeAndWait(new Runnable() {
                        @Override
                        public void run() {
                            frame.setVisible(true);
                        } } );

                    // Canvas setup ----------------------------------------------------------

                    profile = GLProfile.getDefault();
                    capabilities = new GLCapabilities(profile);
                    capabilities.setSampleBuffers(true);
                    capabilities.setNumSamples(numSamples);
                    capabilities.setDepthBits(24);
                    // capabilities.setStencilBits(8); // No Stencil on OSX w/ hw-accel !
                    capabilities.setAlphaBits(8);

                    canvas = new GLCanvas(capabilities);
                    canvas.setBounds(0, 0, width, height);

                    javax.swing.SwingUtilities.invokeAndWait(new Runnable() {
                        @Override
                        public void run() {
                            MiniPApplet.this.setLayout(new BorderLayout());
                            MiniPApplet.this.add(canvas, BorderLayout.CENTER);
                            MiniPApplet.this.validate();
                        } } );
                    canvas.addMouseMotionListener(this);
                    canvas.addKeyListener(this);

                    // Setting up animation
                    listener = new SimpleListener();
                    canvas.addGLEventListener(listener);
                    if (useAnimator) {
                        animator = new CustomAnimator(canvas);
                        animator.start();
                    }
                    initialized = true;
        }

        void restart() throws InterruptedException, InvocationTargetException {
            System.out.println("Restarting surface...");

            // Stopping animation, removing current canvas.
            if (useAnimator) {
                animator.stop();
                animator.remove(canvas);
            }
            canvas.disposeGLEventListener(listener, true);
            this.remove(canvas);

            capabilities = new GLCapabilities(profile);
            capabilities.setSampleBuffers(true);
            capabilities.setNumSamples(numSamples);

            canvas = new GLCanvas(capabilities);
            canvas.setBounds(0, 0, width, height);

            // Setting up animation again
            javax.swing.SwingUtilities.invokeAndWait(new Runnable() {
                @Override
                public void run() {
                    MiniPApplet.this.setLayout(new BorderLayout());
                    MiniPApplet.this.add(canvas, BorderLayout.CENTER);
                    MiniPApplet.this.validate();
                } } );
            canvas.addMouseMotionListener(this);
            canvas.addKeyListener(this);

            canvas.addGLEventListener(listener);
            if (useAnimator) {
                animator.add(canvas);
                animator.start();
            }

            setFramerate = false;
            restarted = true;

            System.out.println("Done");
        }

        void dispose() throws InterruptedException, InvocationTargetException {
            if( null == frame ) {
                return;
            }

            // Stopping animation, removing current canvas.
            if (useAnimator) {
                animator.stop();
                animator.remove(canvas);
            }
            canvas.removeGLEventListener(listener);
            if( EventQueue.isDispatchThread() ) {
                MiniPApplet.this.remove(canvas);
                frame.remove(MiniPApplet.this);
                frame.validate();
                frame.dispose();
                frame = null;
            } else {
                javax.swing.SwingUtilities.invokeAndWait(new Runnable() {
                    @Override
                    public void run() {
                        MiniPApplet.this.remove(canvas);
                        frame.remove(MiniPApplet.this);
                        frame.validate();
                        frame.dispose();
                        frame = null;
                    }});
            }
        }

        void draw(final GL2 gl) {
            if( !osxCALayerAWTModBug || !justInitialized ) {
                AWTEDTExecutor.singleton.invoke(true, new Runnable() {
                    @Override
                    public void run() {
                        frame.setTitle("frame " + frameCount);
                    } } );
            }

            if (printThreadInfo) System.out.println("Current thread at draw(): " + Thread.currentThread());

            if (OPENGL_VENDOR == null) {
                OPENGL_VENDOR     = gl.glGetString(GL.GL_VENDOR);
                OPENGL_RENDERER   = gl.glGetString(GL.GL_RENDERER);
                OPENGL_VERSION    = gl.glGetString(GL.GL_VERSION);
                OPENGL_EXTENSIONS = gl.glGetString(GL.GL_EXTENSIONS);
                System.out.println(OPENGL_VENDOR);
                System.out.println(OPENGL_RENDERER);
                System.out.println(OPENGL_VERSION);
                System.out.println(OPENGL_EXTENSIONS);

                final int[] temp = { 0 };
                gl.glGetIntegerv(GL.GL_MAX_SAMPLES, temp, 0);
                System.out.println("Maximum number of samples supported by the hardware: " + temp[0]);
                System.out.println("Frame: "+frame);
                System.out.println("Applet: "+MiniPApplet.this);
                System.out.println("GLCanvas: "+canvas);
                System.out.println("GLDrawable: "+canvas.getDelegatedDrawable());
            }

            if (currentSamples == -1) {
                final int[] temp = { 0 };
                gl.glGetIntegerv(GL.GL_SAMPLES, temp, 0);
                currentSamples = temp[0];
                if (numSamples != currentSamples) {
                    System.err.println("Requested sampling level " + numSamples + " not supported. Using " + currentSamples + " samples instead.");
                }
            }

            if (!setFramerate) {
                if (60 < frameRate) {
                    // Disables vsync
                    gl.setSwapInterval(0);
                } else if (30 < frameRate) {
                    gl.setSwapInterval(1);
                } else {
                    gl.setSwapInterval(2);
                }
                setFramerate = true;
            }

            if (restarted) {
                final int[] temp = { 0 };
                gl.glGetIntegerv(GL.GL_SAMPLES, temp, 0);
                if (numSamples != temp[0]) {
                    System.err.println("Multisampling level requested " + numSamples + " not supported. Using " + temp[0] + "samples instead.");
                }
            }

            gl.glClearColor(0, 0, 0, 1);
            gl.glClear(GL.GL_COLOR_BUFFER_BIT);

            theta += 0.01;
            s = Math.sin(theta);
            c = Math.cos(theta);

            gl.glBegin(GL.GL_TRIANGLES);
            gl.glColor3f(1, 0, 0);
            gl.glVertex2d(-c, -c);
            gl.glColor3f(0, 1, 0);
            gl.glVertex2d(0, c);
            gl.glColor3f(0, 0, 1);
            gl.glVertex2d(s, -s);
            gl.glEnd();

            gl.glFlush();

            fcount += 1;
            final int m = (int) (System.currentTimeMillis() - millisOffset);
            if (m - lastm > 1000 * fint) {
                frate = (float)(fcount) / fint;
                fcount = 0;
                lastm = m;
                System.err.println("fps: " + frate);
            }
        }

        void clock() {
            final long afterTime = Clock.currentNanos();
            final long timeDiff = afterTime - beforeTime;
            final long sleepTime = (frameRatePeriod - timeDiff) - overSleepTime;

            if (sleepTime > 0) {  // some time left in this cycle
                try {
                    Thread.sleep(sleepTime / 1000000L, (int) (sleepTime % 1000000L));
                } catch (final InterruptedException ex) { }

                overSleepTime = (Clock.currentNanos() - afterTime) - sleepTime;

            } else {    // sleepTime <= 0; the frame took longer than the period
                overSleepTime = 0L;
            }

            beforeTime = Clock.currentNanos();
        }

        class SimpleListener implements GLEventListener {
            @Override
            public void display(final GLAutoDrawable drawable) {
                draw(drawable.getGL().getGL2());
                justInitialized = false;
            }

            @Override
            public void dispose(final GLAutoDrawable drawable) { }

            @Override
            public void init(final GLAutoDrawable drawable) {
                justInitialized = true;
            }

            @Override
            public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int w, final int h) { }
        }

        @Override
        public void mouseDragged(final MouseEvent ev) {
            if (printEventInfo) {
                System.err.println("Mouse dragged event: " + ev);
            }
        }

        @Override
        public void mouseMoved(final MouseEvent ev) {
            if (printEventInfo) {
                System.err.println("Mouse moved event: " + ev);
            }
        }

        @Override
        public void keyPressed(final KeyEvent ev) {
            if (printEventInfo) {
                System.err.println("Key pressed event: " + ev);
            }
        }

        @Override
        public void keyReleased(final KeyEvent ev) {
            if (printEventInfo) {
                System.err.println("Key released event: " + ev);
            }
        }

        @Override
        public void keyTyped(final KeyEvent ev) {
            if (printEventInfo) {
                System.err.println("Key typed event: " + ev);
            }
        }

        /** An Animator subclass which renders one frame at the time
         *  upon calls to the requestRender() method.
         **/
        public static class CustomAnimator extends AnimatorBase {
            private Timer timer = null;
            private TimerTask task = null;
            private volatile boolean shouldRun;

            @Override
            protected String getBaseName(final String prefix) {
                return "Custom" + prefix + "Animator" ;
            }

            /** Creates an CustomAnimator with an initial drawable to
             * animate. */
            public CustomAnimator(final GLAutoDrawable drawable) {
                if (drawable != null) {
                    add(drawable);
                }
            }

            public synchronized void requestRender() {
                shouldRun = true;
            }

            @Override
            public final synchronized boolean isStarted() {
                return (timer != null);
            }

            @Override
            public final synchronized boolean isAnimating() {
                return (timer != null) && (task != null);
            }

            private void startTask() {
                if(null != task) {
                    return;
                }

                task = new TimerTask() {
                    private boolean firstRun = true;
                    @Override
                    public void run() {
                        if (firstRun) {
                            Thread.currentThread().setName("OPENGL");
                            firstRun = false;
                        }
                        if(CustomAnimator.this.shouldRun) {
                            CustomAnimator.this.animThread = Thread.currentThread();
                            // display impl. uses synchronized block on the animator instance
                            display();
                            synchronized (this) {
                                // done with current frame.
                                shouldRun = false;
                            }
                        }
                    }
                };

                fpsCounter.resetFPSCounter();
                shouldRun = false;

                timer.schedule(task, 0, 1);
            }

            @Override
            public synchronized boolean  start() {
                if (timer != null) {
                    return false;
                }
                timer = new Timer();
                startTask();
                return true;
            }

            /** Stops this CustomAnimator. */
            @Override
            public synchronized boolean stop() {
                if (timer == null) {
                    return false;
                }
                shouldRun = false;
                if(null != task) {
                    task.cancel();
                    task = null;
                }
                if(null != timer) {
                    timer.cancel();
                    timer = null;
                }
                animThread = null;
                try {
                    Thread.sleep(20); // ~ 1/60 hz wait, since we can't ctrl stopped threads / holding the lock is OK here!
                } catch (final InterruptedException e) { }
                return true;
            }

            @Override
            public final synchronized boolean isPaused() { return false; }
            @Override
            public synchronized boolean resume() { return false; }
            @Override
            public synchronized boolean pause() { return false; }
        }
    }

    @Test
    public void test00() {
        TestGLCanvasAWTActionDeadlock02AWT.MiniPApplet mini;
        try {
            final Class<?> c = Thread.currentThread().getContextClassLoader().loadClass(TestGLCanvasAWTActionDeadlock02AWT.MiniPApplet.class.getName());
            mini = (TestGLCanvasAWTActionDeadlock02AWT.MiniPApplet) c.newInstance();
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }
        if (mini != null) {
            try {
                mini.run();
            } catch (final Exception ex) {
                Assume.assumeNoException(ex);
            }
        }
    }

    public static void main(final String args[]) {
        for(int i=0; i<args.length; i++) {
            if(args[i].equals("-frames")) {
                framesPerTest = MiscUtils.atoi(args[++i], framesPerTest);
            }
        }
        org.junit.runner.JUnitCore.main(TestGLCanvasAWTActionDeadlock02AWT.class.getName());
    }

}
