package com.jogamp.nativewindow.awt;

import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.security.PrivilegedAction;

import com.jogamp.common.ExceptionUtils;
import com.jogamp.common.util.RunnableTask;
import com.jogamp.common.util.SecurityUtil;
import com.jogamp.common.util.UnsafeUtil;

import jogamp.nativewindow.jawt.JAWTUtil;

/**
 * Instance of this class holds information about a {@link ThreadGroup} associated {@link sun.awt.AppContext}.
 * <p>
 * Non intrusive workaround for Bug 983 and Bug 1004, see {@link #getCachedThreadGroup()}.
 * </p>
 */
public class AppContextInfo {
  private static final boolean DEBUG;

  private static final Method getAppContextMethod;
  private static final Object mainThreadAppContextLock = new Object();
  private volatile WeakReference<Object> mainThreadAppContextWR = null;
  private volatile WeakReference<ThreadGroup> mainThreadGroupWR = null;

  static {
      DEBUG = JAWTUtil.DEBUG;
      final Method[] _getAppContextMethod = { null };
      SecurityUtil.doPrivileged(new PrivilegedAction<Object>() {
          @Override
          public Object run() {
              return UnsafeUtil.doWithoutIllegalAccessLogger(new PrivilegedAction<Object>() {
                  @Override
                  public Object run() {
                      try {
                          final Class<?> appContextClass = Class.forName("sun.awt.AppContext");
                          _getAppContextMethod[0] = appContextClass.getMethod("getAppContext");
                          _getAppContextMethod[0].setAccessible(true);
                      } catch(final Throwable ex) {
                          ExceptionUtils.dumpThrowable("AppContextInfo(Bug 1004)", ex);
                      }
                      return null;
                  }}); }});
      getAppContextMethod = _getAppContextMethod[0];
  }

  public AppContextInfo(final String info) {
      update(info);
  }

  /**
   * Returns <code>true</code> if this instance has valid {@link sun.awt.AppContext} information,
   * i.e. {@link #getCachedThreadGroup()} returns not <code>null</code>.
   */
  public final boolean isValid() {
      return null != getCachedThreadGroup();
  }

  /**
   * Returns the {@link ThreadGroup} belonging to the
   * last known {@link sun.awt.AppContext} as queried via {@link #update(String)}.
   * <p>
   * Returns <code>null</code> if no {@link sun.awt.AppContext} has been queried.
   * </p>
   * <p>
   * The returned {@link ThreadGroup} allows users to create a custom thread
   * belonging to it and hence mitigating Bug 983 and Bug 1004.
   * </p>
   * <p>
   * {@link #update(String)} should be called from a thread belonging to the
   * desired {@link sun.awt.AppContext}, i.e. early from within the special threaded application.
   * </p>
   * <p>
   * E.g. {@link JAWTWindow} issues {@link #update(String)} in it's constructor.
   * </p>
   */
  public final ThreadGroup getCachedThreadGroup() {
      final WeakReference<ThreadGroup> tgRef = mainThreadGroupWR;
      return null != tgRef ? tgRef.get() : null;
  }

  /**
   * Invokes <code>runnable</code> on a {@link Thread} belonging to the {@link sun.awt.AppContext} {@link ThreadGroup},
   * see {@link #getCachedThreadGroup()}.
   * <p>
   * {@link #update(String)} is issued first, which returns <code>true</code>
   * if the current thread belongs to an AppContext {@link ThreadGroup}.
   * In this case the <code>runnable</code> is invoked on the current thread,
   * otherwise a new {@link Thread} will be started.
   * </p>
   * <p>
   * If a new {@link Thread} is required, the AppContext {@link ThreadGroup} is being used
   * if {@link #isValid() available}, otherwise the default system {@link ThreadGroup}.
   * </p>
   *
   * @param waitUntilDone if <code>true</code>, waits until <code>runnable</code> execution is completed, otherwise returns immediately.
   * @param runnable the {@link Runnable} to be executed. If <code>waitUntilDone</code> is <code>true</code>,
   *                 the runnable <b>must exist</b>, i.e. not loop forever.
   * @param threadBaseName the base name for the new thread if required.
   *        The resulting thread name will have either '-OnAppContextTG' or '-OnSystemTG' appended
   * @return the {@link Thread} used to invoke the <code>runnable</code>, which may be the current {@link Thread} or a newly created one, see above.
   */
  public RunnableTask invokeOnAppContextThread(final boolean waitUntilDone, final Runnable runnable, final String threadBaseName) {
      final RunnableTask rt;
      if( update("invoke") ) {
          rt = RunnableTask.invokeOnCurrentThread(runnable);
          if( DEBUG ) {
              System.err.println("Bug 1004: Invoke.0 on current AppContext: "+rt);
          }
      } else {
          final ThreadGroup tg = getCachedThreadGroup();
          final String tName = threadBaseName + ( null != tg ? "-OnAppContextTG" : "-OnSystemTG" );
          rt = RunnableTask.invokeOnNewThread(tg, tName, waitUntilDone, runnable);
          if( DEBUG ) {
              final int tgHash = null != tg ? tg.hashCode() : 0;
              System.err.println("Bug 1004: Invoke.1 on new AppContext: "+rt+", tg "+tg+" "+toHexString(tgHash));
          }
      }
      return rt;
  }

  /**
   * Update {@link sun.awt.AppContext} information for the current ThreadGroup if uninitialized or {@link sun.awt.AppContext} changed.
   * <p>
   * See {@link #getCachedThreadGroup()} for usage.
   * </p>
   * @param info informal string for logging purposes
   * @return <code>true</code> if the current ThreadGroup is mapped to an {@link sun.awt.AppContext} and the information is good, otherwise false.
   */
  public final boolean update(final String info) {
      if ( null != getAppContextMethod ) {
          // Test whether the current thread's ThreadGroup is mapped to an AppContext.
          final Object thisThreadAppContext = fetchAppContext();
          final boolean tgMapped = null != thisThreadAppContext;

          final Thread thread = Thread.currentThread();
          final ThreadGroup threadGroup = thread.getThreadGroup();
          final Object mainThreadAppContext;
          {
              final WeakReference<Object> _mainThreadAppContextWR = mainThreadAppContextWR;
              mainThreadAppContext = null != _mainThreadAppContextWR ? _mainThreadAppContextWR.get() : null;
          }

          if( tgMapped ) { // null != thisThreadAppContext
              // Update info is possible
              if( null == mainThreadAppContext ||
                  mainThreadAppContext != thisThreadAppContext ) {
                  // GC'ed or 1st fetch !
                  final int mainThreadAppContextHash = null != mainThreadAppContext ? mainThreadAppContext.hashCode() : 0;
                  final int thisThreadAppContextHash;
                  synchronized(mainThreadAppContextLock) {
                      mainThreadGroupWR = new WeakReference<ThreadGroup>(threadGroup);
                      mainThreadAppContextWR = new WeakReference<Object>(thisThreadAppContext);
                      thisThreadAppContextHash = thisThreadAppContext.hashCode();
                  }
                  if( DEBUG ) {
                      System.err.println("Bug 1004[TGMapped "+tgMapped+"]: Init AppContext @ "+info+" on thread "+thread.getName()+" "+toHexString(thread.hashCode())+
                                         ": tg "+threadGroup.getName()+" "+toHexString(threadGroup.hashCode())+
                                         " -> appCtx [ main "+mainThreadAppContext+" "+toHexString(mainThreadAppContextHash)+
                                         " -> this "+thisThreadAppContext+" "+toHexString(thisThreadAppContextHash) + " ] ");
                  }
              } else {
                  // old info is OK
                  if( DEBUG ) {
                      final int mainThreadAppContextHash = mainThreadAppContext.hashCode();
                      final int thisThreadAppContextHash = thisThreadAppContext.hashCode();
                      System.err.println("Bug 1004[TGMapped "+tgMapped+"]: OK AppContext @ "+info+" on thread "+thread.getName()+" "+toHexString(thread.hashCode())+
                                         ": tg "+threadGroup.getName()+" "+toHexString(threadGroup.hashCode())+
                                         "  : appCtx [ this "+thisThreadAppContext+" "+toHexString(thisThreadAppContextHash)+
                                         "  , main "+mainThreadAppContext+" "+toHexString(mainThreadAppContextHash) + " ] ");
                  }
              }
              return true;
          } else {
              if( DEBUG ) {
                  final int mainThreadAppContextHash = null != mainThreadAppContext ? mainThreadAppContext.hashCode() : 0;
                  final int thisThreadAppContextHash = null != thisThreadAppContext ? thisThreadAppContext.hashCode() : 0;
                  System.err.println("Bug 1004[TGMapped "+tgMapped+"]: No AppContext @ "+info+" on thread "+thread.getName()+" "+toHexString(thread.hashCode())+
                                     ": tg "+threadGroup.getName()+" "+toHexString(threadGroup.hashCode())+
                                     " -> appCtx [ this "+thisThreadAppContext+" "+toHexString(thisThreadAppContextHash)+
                                     " -> main "+mainThreadAppContext+" "+toHexString(mainThreadAppContextHash) + " ] ");
              }
          }
      }
      return false;
  }
  private static Object fetchAppContext() {
      try {
          return getAppContextMethod.invoke(null);
      } catch(final Exception ex) {
          ExceptionUtils.dumpThrowable("AppContextInfo(Bug 1004)", ex);
          return null;
      }
  }

  private static String toHexString(final int i) {
      return "0x"+Integer.toHexString(i);
  }

}
