/**
 * Copyright 2015 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.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.Label;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.InvocationTargetException;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import com.jogamp.opengl.test.junit.jogl.demos.es2.GearsES2;
import com.jogamp.opengl.test.junit.util.AWTRobotUtil;
import com.jogamp.opengl.test.junit.util.MiscUtils;
import com.jogamp.opengl.test.junit.util.UITestCase;

import org.junit.Assert;

import com.jogamp.common.ExceptionUtils;
import com.jogamp.common.util.InterruptedRuntimeException;
import com.jogamp.common.util.SourcedInterruptedException;
import com.jogamp.nativewindow.NativeWindowFactory;
import com.jogamp.opengl.GLAutoDrawable;
import com.jogamp.opengl.GLEventListener;
import com.jogamp.opengl.awt.GLCanvas;

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

/**
 * Test to check if interrupt on AWT-EventQueue causes a malfunction in JOGL.
 * <p>
 * After tests are displaying an ever color rotating rectangle in an AWT component alone
 * and with an additional GearsES2 within a GLCanvas.
 * </p>
 * <p>
 * The AWT component is issuing an interrupt during paint on the AWT-EDT.
 * </p>
 * <p>
 * The reporter claims that an interrupt on the AWT-EDT shall not disturb neither AWT nor JOGL's GLCanvas
 * and rendering shall continue.
 * <ul>
 *   <li>This seems to be true for JRE 1.8.0_60</li>
 *   <li>This seems to be false for JRE 1.7.0_45. This JRE's AWT-EDT even dies occasionally when interrupted.</li>
 * </ul>
 * </p>
 * <p>
 * The test passes on GNU/Linux and Windows using JRE 1.8.0_60.
 * </p>
 */
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestBug1225EventQueueInterruptedAWT extends UITestCase {
    static long durationPerTest = 1000; // ms

    private void setVisible(final JFrame frame, final boolean v) throws InterruptedException, InvocationTargetException {
        javax.swing.SwingUtilities.invokeAndWait(new Runnable() {
            public void run() {
                frame.pack();
                // frame.setSize(new Dimension(800, 600));
                frame.setVisible(v);
            }});
    }
    private void dispose(final JFrame jFrame) throws InterruptedException, InvocationTargetException {
        SwingUtilities.invokeAndWait(new Runnable() {
                public void run() {
                    jFrame.dispose();
                } } ) ;
    }


    @Test(timeout=180000) // TO 3 min
    public void test01_NoGL() throws InterruptedException, InvocationTargetException {
        testImpl(false);
    }

    @Test(timeout=180000) // TO 3 min
    public void test02_WithGL() throws InterruptedException, InvocationTargetException {
        testImpl(true);
    }

    class OurUncaughtExceptionHandler implements UncaughtExceptionHandler {
        public volatile Thread thread = null;
        public volatile Throwable exception = null;

        @Override
        public void uncaughtException(final Thread t, final Throwable e) {
            thread = t;
            exception = e;
            System.err.println("*** UncaughtException (this Thread "+Thread.currentThread().getName()+") : Thread <"+t.getName()+">, "+e.getClass().getName()+": "+e.getMessage());
            ExceptionUtils.dumpThrowable("", e);
        }
    }
    void testImpl(final boolean useGL) throws InterruptedException, InvocationTargetException {
        if( !AWTRobotUtil.isAWTEDTAlive() ) {
            System.err.println("Test aborted: AWT not alive");
            return;
        }
        // Assume.assumeTrue("AWT not alive", AWTRobotUtil.isAWTEDTAlive());
        // Assert.assertTrue("AWT not alive", AWTRobotUtil.isAWTEDTAlive());
        final OurUncaughtExceptionHandler uncaughtHandler = new OurUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler( uncaughtHandler );

        final Dimension csize = new Dimension(800, 400);
        final JPanel panel = new JPanel(new GridLayout(2, 1));
        final GLCanvas glc;
        final InterruptableGLEL iglel;
        if( useGL ) {
            glc = new GLCanvas();
            {
                final GearsES2 gears = new GearsES2();
                gears.setVerbose(false);
                glc.addGLEventListener(gears);
            }
            iglel = new InterruptableGLEL();
            glc.addGLEventListener(iglel);
            glc.setSize(csize);
            glc.setPreferredSize(csize);
            panel.add(glc);
        } else {
            NativeWindowFactory.initSingleton();
            glc = null;
            iglel = null;
            final Label l = new Label("No GL Object");
            l.setSize(csize);
            l.setPreferredSize(csize);
            panel.add(l);
        }
        final InterruptingComponent icomp = new InterruptingComponent();
        panel.add(icomp);
        icomp.setSize(csize);
        icomp.setPreferredSize(csize);

        final JFrame frame = new JFrame();
        frame.setResizable(true);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        frame.getContentPane().add(panel, BorderLayout.CENTER);
        setVisible(frame, true);
        if( useGL ) {
            Assert.assertTrue(AWTRobotUtil.waitForRealized(glc, true, null));
        }
        Assert.assertTrue(AWTRobotUtil.waitForRealized(icomp, true, null));

        final InterruptableLoop loop = new InterruptableLoop(icomp, glc);
        final Thread thread = new Thread(loop);

        synchronized(loop) {
            thread.start();
            try {
                loop.notifyAll();  // wake-up startup-block
                while( !loop.isRunning && !loop.shallStop ) {
                    loop.wait();  // wait until started
                }
                loop.ack = true;
                loop.notifyAll();  // wake-up startup-block
            } catch (final InterruptedException e) {
                Assert.assertNull("while starting loop", new InterruptedRuntimeException(e));
            }
        }

        for(int i=0; thread.isAlive() && null == loop.exception && null == uncaughtHandler.exception && i<100; i++) {
            icomp.interruptAWTEventQueue();
            Thread.sleep(durationPerTest/100);
        }

        loop.shallStop = true;
        synchronized(loop) {
            try {
                loop.notifyAll();  // wake-up pause-block (opt)
                while( loop.isRunning ) {
                    loop.wait();  // wait until stopped
                }
            } catch (final InterruptedException e) {
                Assert.assertNull("while stopping loop", new InterruptedRuntimeException(e));
            }
        }

        //
        // Notifications only!
        //
        // Note:
        //   On JRE 1.8.0_60: Interrupt is cleared on AWT-EDT
        //   On JRE 1.7.0_45: Interrupt is *NOT* cleared on AWT-EDT
        //
        if( null != iglel && null != iglel.exception ) {
            ExceptionUtils.dumpThrowable("GLEventListener", iglel.exception);
        }
        if( null != icomp.exception ) {
            ExceptionUtils.dumpThrowable("InterruptingComponent", icomp.exception);
        }
        if( null != loop.exception ) {
            ExceptionUtils.dumpThrowable("loop", loop.exception);
        }
        if( null != uncaughtHandler.exception ) {
            ExceptionUtils.dumpThrowable("uncaughtHandler", uncaughtHandler.exception);
        }
        if( !AWTRobotUtil.isAWTEDTAlive() ) {
            System.err.println("AWT is not alive anymore!!! Ooops");
            // cannot do anything anymore on AWT-EDT .. frame.dispose();
        } else {
            dispose(frame);
        }

        //
        // Fail if interrupt was propagated to loop or uncaught handler
        //
        Assert.assertNull("Caught Exception in loop", loop.exception);
        Assert.assertNull("Caught Exception via uncaughtHandler", uncaughtHandler.exception);
    }

    static class InterruptableLoop implements Runnable {
        public volatile Exception exception = null;
        public volatile boolean shallStop = false;
        public volatile boolean isRunning = false;
        public volatile boolean ack = false;
        final InterruptingComponent icomp;
        final GLCanvas glc;
        boolean alt = false;;

        InterruptableLoop(final InterruptingComponent icomp, final GLCanvas glc) {
            this.icomp = icomp;
            this.glc = glc;
        }

        public void stop() {
            shallStop = true;
        }

        @Override
        public void run()
        {
            synchronized ( this ) {
                isRunning = true;
                this.notifyAll();
                try {
                    while( !ack ) {
                        this.wait();  // wait until ack
                    }
                    this.notifyAll();
                } catch (final InterruptedException e) {
                    throw new InterruptedRuntimeException(e);
                }
                ack = false;
            }
            synchronized ( this ) {
                try {
                    while( !shallStop ) {
                        if( alt ) {
                            icomp.repaint(); // issues paint of GLCanvas on AWT-EDT
                        } else if( null != glc ) {
                            // Avoid invokeAndWait(..) in GLCanvas.display() if AWT-EDT dies!
                            glc.repaint(); // issues paint of GLCanvas on AWT-EDT, which then issues display()!
                        }
                        alt = !alt;
                        Thread.sleep(16);
                        if( Thread.interrupted() ) {
                            final InterruptedRuntimeException e = new InterruptedRuntimeException(new InterruptedException("Interrupt detected in loop, thread: "+Thread.currentThread().getName()));
                            throw e;
                        }
                    }
                } catch (final InterruptedException e) {
                    exception = SourcedInterruptedException.wrap(e);
                    ExceptionUtils.dumpThrowable("", exception);
                } catch (final Exception e) {
                    exception = e;
                    ExceptionUtils.dumpThrowable("", exception);
                } finally {
                    isRunning = false;
                    this.notifyAll();
                }
            }
        }
    }

    static class InterruptableGLEL implements GLEventListener {
        public volatile InterruptedException exception = null;
        @Override
        public void init(final GLAutoDrawable drawable) {
        }
        @Override
        public void dispose(final GLAutoDrawable drawable) {
        }
        @Override
        public void display(final GLAutoDrawable drawable) {
            final Thread c = Thread.currentThread();
            if( c.isInterrupted() && null == exception ) {
                exception = new InterruptedException("Interrupt detected in GLEventListener, thread: "+c.getName());
                drawable.removeGLEventListener(this);
            }
        }
        @Override
        public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int width, final int height) {
        }
    }

    static class InterruptingComponent extends Component {
        private static final long serialVersionUID = 1L;
        public volatile InterruptedException exception = null;

        private volatile boolean doInterrupt = false;

        private final Color[] colors =
                new Color[] { Color.BLACK, Color.BLUE, Color.DARK_GRAY, Color.GRAY, Color.LIGHT_GRAY };
        private int colorIdx = 0;

        public InterruptingComponent() {
        }

        public void interruptAWTEventQueue() {
            doInterrupt = true;
        }

        @Override
        public void paint(final Graphics g)
        {
            final Thread c = Thread.currentThread();
            if( c.isInterrupted() && null == exception ) {
                exception = new InterruptedException("Interrupt detected in AWT Component, thread: "+c.getName());
            }

            g.setColor(colors[colorIdx++]);
            if( colorIdx >= colors.length ) {
                colorIdx = 0;
            }
            g.fillRect(0, 0, getWidth(), getHeight());

            if(doInterrupt) {
                System.err.println("Thread "+c.getName()+": *Interrupting*");
                doInterrupt = false;
                c.interrupt();
            }
        }
    }

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

