package com.jogamp.opengl.test.junit.jogl.acore;

import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.swing.Timer;

import org.junit.Test;

import com.jogamp.common.os.Platform;
import com.jogamp.common.util.InterruptSource;
import com.jogamp.common.util.locks.LockFactory;
import com.jogamp.common.util.locks.RecursiveLock;
import com.jogamp.opengl.*;
import com.jogamp.opengl.test.junit.util.DumpGLInfo;

/**
 * Bug 1160.
 * Test for context sharing with an external context. Creates an external GL context, then
 * sets up an offscreen drawable which shares with it. The test contains two cases: one
 * which creates and repaints the offscreen drawable on the EDT, and one which does so on
 * a dedicated thread. On Windows+NVidia, the former fails.
 */
public class TestSharedExternalContextAWT {

  static final int LATCH_COUNT = 5;

  private void doTest(final boolean aUseEDT) throws Exception {
    final CountDownLatch testLatch = new CountDownLatch(1);
    final CountDownLatch masterLatch = new CountDownLatch(1);
    final CountDownLatch slaveLatch = new CountDownLatch(LATCH_COUNT);
    final MyGLEventListener listener = new MyGLEventListener(aUseEDT, slaveLatch);

    /**
     * For the purpose of this test, this offscreen drawable will be used to create
     * an external GL context. In the actual application, the external context
     * represents a GL context which lives outside the JVM.
     */
    final Runnable runnable = new Runnable() {
      public void run() {
        System.err.println("Master Thread Start: "+Thread.currentThread().getName());
        final GLProfile glProfile = GLProfile.getDefault();
        final GLCapabilities caps = new GLCapabilities(glProfile);
        final GLAutoDrawable buffer = GLDrawableFactory.getFactory(false).createOffscreenAutoDrawable(
            GLProfile.getDefaultDevice(), caps, null, 512, 512
        );
        // The listener will set up the context sharing in its init() method.
        buffer.addGLEventListener(new DumpGLInfo(Platform.getNewline()+Platform.getNewline()+"Master GLContext", false, false, false));
        buffer.addGLEventListener(listener);
        buffer.display();
        masterLatch.countDown();

        // wait until test has finished
        try {
            testLatch.await();
        } catch (final InterruptedException e) {
            e.printStackTrace();
        }
        System.err.println("Master Thread End: "+Thread.currentThread().getName());
      }
    };

    // Kick off thread creating the actual external context
    // which is suppose to lie outside of the JVM.
    // The thread is kept alive, since this detail
    // may be required for the OpenGL driver implementation.
    final Thread thread = new InterruptSource.Thread(null, runnable);
    thread.setDaemon(true);
    thread.start();
    masterLatch.await(3, TimeUnit.SECONDS);

    // Wait for slave to finish.
    slaveLatch.await(3, TimeUnit.SECONDS);

    // signal master test has finished
    testLatch.countDown();

    // If exceptions occurred, fail.
    final Exception e = listener.fException;
    if (e != null) {
      throw e;
    }
  }

  @Test
  public void test01OnEDT() throws Exception {
    doTest(true);
  }

  @Test
  public void test02OnExecutorThread() throws Exception {
    doTest(false);
  }

  /**
   * Listener that creates an external drawable and an offscreen drawable, with context
   * sharing between the two.
   */
  private static class MyGLEventListener implements GLEventListener {
    GLOffscreenAutoDrawable fOffscreenDrawable;
    final boolean fUseEDT;
    final CountDownLatch fLatch;
    final RecursiveLock masterLock = LockFactory.createRecursiveLock();

    private Exception fException = null;

    public MyGLEventListener(final boolean aUseEDT, final CountDownLatch aLatch) {
      fUseEDT = aUseEDT;
      fLatch = aLatch;
    }

    @Override
    public void init(final GLAutoDrawable drawable) {
      // FIXME: We actually need to hook into GLContext make-current lock
      masterLock.lock();
      try {
          final GL2 gl = drawable.getGL().getGL2();
          gl.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
          gl.glClear(GL.GL_COLOR_BUFFER_BIT);

          System.err.println(); System.err.println();
          System.err.println("Master (orig) Ct: "+drawable.getContext());
          // Create the external context on the caller thread.
          final GLContext master = GLDrawableFactory.getFactory(false).createExternalGLContext();
          System.err.println(); System.err.println();
          System.err.println("External Context: "+master);

          // This runnable creates an offscreen drawable which shares with the external context.
          final Runnable initializer = new Runnable() {
            public void run() {
                // FIXME: We actually need to hook into GLContext make-current lock
                // masterLock.lock();
                try {
                    fOffscreenDrawable = GLDrawableFactory.getFactory(false).createOffscreenAutoDrawable(
                            GLProfile.getDefaultDevice(),
                            new GLCapabilities(GLProfile.getDefault()),
                            null, // new DefaultGLCapabilitiesChooser(),
                            512, 512
                            );
                    fOffscreenDrawable.setSharedContext(master);
                    fOffscreenDrawable.addGLEventListener(new DumpGLInfo(Platform.getNewline()+Platform.getNewline()+"Slave GLContext", false, false, false));

                    try {
                        System.err.println(); System.err.println();
                        System.err.println("Current: "+GLContext.getCurrent());
                        fOffscreenDrawable.display();
                    } catch (final GLException e) {
                        fException = e;
                        throw e;
                    }
                } finally {
                    // masterLock.unlock();
                }
            }
          };

          /**
           * Depending on the test case, invoke the initialization on the EDT or on an
           * executor thread. The test also displays the offscreen drawable a few times
           * before finishing.
           */
          if (fUseEDT) {
            // Initialize using invokeLater().
            try {
              // We cannot use EventQueue.invokeAndWait(..) since it will
              // block this will block the current thread, holding the context!
              // The whole issue w/ an external shared context is make-current
              // synchronization. JOGL attempts to lock the surface/drawable
              // of the master context to avoid concurrent usage.
              // The semantic constraints of a shared context are not well defined,
              // i.e. some driver may allow creating a shared context w/ a master context
              // to be in use - others don't.
              // Hence it is up to the user to sync the external master context in this case,
              // see 'masterLock' of in this code!
              //    EventQueue.invokeAndWait(initializer);
              EventQueue.invokeLater(initializer);
            } catch (final Exception e) {
              fException = e;
            }

            // Display using a Swing timer, i.e. also on the EDT.
            final Timer t = new Timer(200, new ActionListener() {
              int i = 0;

              @Override
              public void actionPerformed(final ActionEvent e) {
                if (++i > LATCH_COUNT) {
                  return;
                }

                System.err.println("Update on EDT");
                fOffscreenDrawable.display();
                fLatch.countDown();
              }
            });
            t.start();
          } else {
            // Initialize and display using a single-threaded executor.
            final ScheduledExecutorService exe = Executors.newSingleThreadScheduledExecutor();
            exe.submit(initializer);
            exe.scheduleAtFixedRate(new Runnable() {
              int i = 0;

              @Override
              public void run() {
                if (++i > LATCH_COUNT) {
                  return;
                }

                System.err.println("Update on Executor thread");
                fOffscreenDrawable.display();
                fLatch.countDown();
              }
            }, 0, 200, TimeUnit.MILLISECONDS);
          }
      } finally {
          masterLock.unlock();
      }
    }

    @Override
    public void dispose(final GLAutoDrawable drawable) {
      // FIXME: We actually need to hook into GLContext make-current lock
      masterLock.lock();
      try {
      } finally {
          masterLock.unlock();
      }
    }

    @Override
    public void display(final GLAutoDrawable drawable) {
      // FIXME: We actually need to hook into GLContext make-current lock
      masterLock.lock();
      try {
      } finally {
          masterLock.unlock();
      }
    }

    @Override
    public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int width, final int height) {
      // FIXME: We actually need to hook into GLContext make-current lock
      masterLock.lock();
      try {
      } finally {
          masterLock.unlock();
      }
    }
  }

  public static void main(final String[] pArgs)
  {
      org.junit.runner.JUnitCore.main(TestSharedExternalContextAWT.class.getName());
  }
}
