/*
 * Copyright (c) 2003 Sun Microsystems, Inc. All Rights Reserved.
 * Copyright (c) 2010 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:
 *
 * - Redistribution of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistribution 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.
 *
 * Neither the name of Sun Microsystems, Inc. or the names of
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
 * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
 * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
 * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
 * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
 * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
 * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
 * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
 * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
 * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 *
 * You acknowledge that this software is not designed or intended for use
 * in the design, construction, operation or maintenance of any nuclear
 * facility.
 *
 * Sun gratefully acknowledges that this software was originally authored
 * and developed by Kenneth Bradley Russell and Christopher John Kline.
 */

package jogamp.opengl.macosx.cgl;

import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.Map;

import javax.media.nativewindow.AbstractGraphicsConfiguration;
import javax.media.nativewindow.AbstractGraphicsDevice;
import javax.media.nativewindow.NativeSurface;
import javax.media.nativewindow.NativeWindowFactory;
import javax.media.nativewindow.OffscreenLayerSurface;
import javax.media.nativewindow.ProxySurface;
import javax.media.opengl.GL;
import javax.media.opengl.GL2ES2;
import javax.media.opengl.GL3;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLCapabilitiesImmutable;
import javax.media.opengl.GLContext;
import javax.media.opengl.GLException;
import javax.media.opengl.GLProfile;
import javax.media.opengl.GLUniformData;

import jogamp.nativewindow.macosx.OSXUtil;
import jogamp.opengl.GLContextImpl;
import jogamp.opengl.GLDrawableImpl;
import jogamp.opengl.GLFBODrawableImpl;
import jogamp.opengl.GLGraphicsConfigurationUtil;
import jogamp.opengl.macosx.cgl.MacOSXCGLDrawable.GLBackendType;

import com.jogamp.common.nio.Buffers;
import com.jogamp.common.nio.PointerBuffer;
import com.jogamp.common.os.Platform;
import com.jogamp.common.util.VersionNumber;
import com.jogamp.common.util.locks.RecursiveLock;
import com.jogamp.gluegen.runtime.ProcAddressTable;
import com.jogamp.gluegen.runtime.opengl.GLProcAddressResolver;
import com.jogamp.opengl.GLExtensions;
import com.jogamp.opengl.GLRendererQuirks;
import com.jogamp.opengl.util.PMVMatrix;
import com.jogamp.opengl.util.glsl.ShaderCode;
import com.jogamp.opengl.util.glsl.ShaderProgram;

public class MacOSXCGLContext extends GLContextImpl
{
  // Abstract interface for implementation of this context (either
  // NSOpenGL-based or CGL-based)
  protected interface GLBackendImpl {
        boolean isNSContext();
        long create(long share, int ctp, int major, int minor);
        boolean destroy(long ctx);
        void associateDrawable(boolean bound);
        boolean copyImpl(long src, int mask);
        boolean makeCurrent(long ctx);
        boolean release(long ctx);
        boolean detachPBuffer();
        boolean setSwapInterval(int interval);
        boolean swapBuffers();
  }

  /* package */ static final boolean isTigerOrLater;
  /* package */ static final boolean isLionOrLater;

  static {
    final VersionNumber osvn = Platform.getOSVersionNumber();
    isTigerOrLater = osvn.getMajor() > 10 || ( osvn.getMajor() == 10 && osvn.getMinor() >= 4 );
    isLionOrLater  = osvn.getMajor() > 10 || ( osvn.getMajor() == 10 && osvn.getMinor() >= 7 );
  }

  static boolean isGLProfileSupported(int ctp, int major, int minor) {
    boolean ctBwdCompat = 0 != ( CTX_PROFILE_COMPAT & ctp ) ;
    boolean ctCore      = 0 != ( CTX_PROFILE_CORE & ctp ) ;

    // We exclude 3.0, since we would map it's core to GL2. Hence we force mapping 2.1 to GL2
    if(3==major && 1<=minor && minor<=2) {
        // [3.1..3.2] -> GL3*
        if(!isLionOrLater) {
            // no GL3* on pre lion
            return false;
        }
        if(ctBwdCompat) {
            // no compatibility profile on OS X
            return false;
        }
        return ctCore;
    } else if(major<3) {
        // < 3.0 -> GL2
        return true;
    }
    return false; // 3.0 && > 3.2
  }
  static int GLProfile2CGLOGLProfileValue(int ctp, int major, int minor) {
    if(!MacOSXCGLContext.isGLProfileSupported(ctp, major, minor)) {
        throw new GLException("OpenGL profile not supported: "+getGLVersion(major, minor, ctp, "@GLProfile2CGLOGLProfileVersion"));
    }
    boolean ctCore      = 0 != ( CTX_PROFILE_CORE & ctp ) ;
    if( major == 3 && minor >= 1 && ctCore ) {
        return CGL.kCGLOGLPVersion_3_2_Core;
    } else {
        return CGL.kCGLOGLPVersion_Legacy;
    }
  }

  private static final String shaderBasename = "texture01_xxx";
  
  private static ShaderProgram createCALayerShader(GL3 gl) {
      // Create & Link the shader program
      final ShaderProgram sp = new ShaderProgram();
      final ShaderCode vp = ShaderCode.create(gl, GL2ES2.GL_VERTEX_SHADER, MacOSXCGLContext.class, 
                                              "../../shader", "../../shader/bin", shaderBasename, true);
      final ShaderCode fp = ShaderCode.create(gl, GL2ES2.GL_FRAGMENT_SHADER, MacOSXCGLContext.class, 
                                              "../../shader", "../../shader/bin", shaderBasename, true);
      vp.defaultShaderCustomization(gl, true, true);
      fp.defaultShaderCustomization(gl, true, true);
      sp.add(vp);
      sp.add(fp);
      if(!sp.link(gl, System.err)) {
          throw new GLException("Couldn't link program: "+sp);
      }
      sp.useProgram(gl, true);

      // setup mgl_PMVMatrix
      final PMVMatrix pmvMatrix = new PMVMatrix();
      pmvMatrix.glMatrixMode(PMVMatrix.GL_PROJECTION);
      pmvMatrix.glLoadIdentity();
      pmvMatrix.glMatrixMode(PMVMatrix.GL_MODELVIEW);
      pmvMatrix.glLoadIdentity();       
      final GLUniformData pmvMatrixUniform = new GLUniformData("mgl_PMVMatrix", 4, 4, pmvMatrix.glGetPMvMatrixf()); // P, Mv
      pmvMatrixUniform.setLocation(gl, sp.program());
      gl.glUniform(pmvMatrixUniform);

      sp.useProgram(gl, false);
      return sp;
  }
      
  
  private boolean haveSetOpenGLMode = false;
  private GLBackendType openGLMode = GLBackendType.NSOPENGL;

  // Implementation object (either NSOpenGL-based or CGL-based)
  protected GLBackendImpl impl;

  private CGLExt _cglExt;
  // Table that holds the addresses of the native C-language entry points for
  // CGL extension functions.
  private CGLExtProcAddressTable cglExtProcAddressTable;

  private long updateHandle = 0;
  private int lastWidth, lastHeight;
  
  protected MacOSXCGLContext(GLDrawableImpl drawable,
                   GLContext shareWith) {
    super(drawable, shareWith);
    initOpenGLImpl(getOpenGLMode());
  }

  @Override
  protected void resetStates() {
    // no inner state _cglExt = null;
    cglExtProcAddressTable = null;
    super.resetStates();
  }

  @Override
  public Object getPlatformGLExtensions() {
    return getCGLExt();
  }

  protected boolean isNSContext() {
      return (null != impl) ? impl.isNSContext() : this.openGLMode == GLBackendType.NSOPENGL;
  }

  public CGLExt getCGLExt() {
    if (_cglExt == null) {
      _cglExt = new CGLExtImpl(this);
    }
    return _cglExt;
  }

  @Override
  public final ProcAddressTable getPlatformExtProcAddressTable() {
    return getCGLExtProcAddressTable();
  }

  public final CGLExtProcAddressTable getCGLExtProcAddressTable() {
    return cglExtProcAddressTable;
  }

  @Override
  protected Map<String, String> getFunctionNameMap() { return null; }

  @Override
  protected Map<String, String> getExtensionNameMap() { return null; }

  @Override
  protected long createContextARBImpl(long share, boolean direct, int ctp, int major, int minor) {
    if(!isGLProfileSupported(ctp, major, minor)) {
        if(DEBUG) {
            System.err.println(getThreadName() + ": createContextARBImpl: Not supported "+getGLVersion(major, minor, ctp, "@creation on OSX "+Platform.getOSVersionNumber()));
        }
        return 0;
    }

    // Will throw exception upon error
    long ctx = impl.create(share, ctp, major, minor);
    if(0 != ctx) {
        if (!impl.makeCurrent(ctx)) {
          if(DEBUG) {
              System.err.println(getThreadName() + ": createContextARB couldn't make current "+getGLVersion(major, minor, ctp, "@creation"));
          }
          impl.release(ctx);
          impl.destroy(ctx);
          ctx = 0;
        } else if(DEBUG) {
            System.err.println(getThreadName() + ": createContextARBImpl: OK "+getGLVersion(major, minor, ctp, "@creation")+", share "+share+", direct "+direct+" on OSX "+Platform.getOSVersionNumber());
        }
    } else if(DEBUG) {
        System.err.println(getThreadName() + ": createContextARBImpl: NO "+getGLVersion(major, minor, ctp, "@creation on OSX "+Platform.getOSVersionNumber()));
    }
    return ctx;
  }

  @Override
  protected void destroyContextARBImpl(long _context) {
      impl.release(_context);
      impl.destroy(_context);
  }

  @Override
  public final boolean isGLReadDrawableAvailable() {
    return false;
  }

  protected long createImplPreset(GLContextImpl shareWith) throws GLException {
    long share = 0;
    if (shareWith != null) {
      // Change our OpenGL mode to match that of any share context before we create ourselves
      setOpenGLMode(((MacOSXCGLContext)shareWith).getOpenGLMode());
      share = shareWith.getHandle();
      if (share == 0) {
        throw new GLException("GLContextShareSet returned a NULL OpenGL context");
      }
    }

    MacOSXCGLGraphicsConfiguration config = (MacOSXCGLGraphicsConfiguration) drawable.getNativeSurface().getGraphicsConfiguration();
    GLCapabilitiesImmutable capabilitiesChosen = (GLCapabilitiesImmutable) config.getChosenCapabilities();
    GLProfile glp = capabilitiesChosen.getGLProfile();
    if(glp.isGLES1() || glp.isGLES2() || glp.isGL4() || glp.isGL3() && !isLionOrLater) {
        throw new GLException("OpenGL profile not supported on MacOSX "+Platform.getOSVersionNumber()+": "+glp);
    }

    if (DEBUG) {
      System.err.println("Share context is " + toHexString(share) + " for " + this);
    }
    return share;
  }

  @Override
  protected boolean createImpl(GLContextImpl shareWith) throws GLException {
    long share = createImplPreset(shareWith);
    contextHandle = createContextARB(share, true);
    return 0 != contextHandle;
  }

  @Override
  protected void makeCurrentImpl() throws GLException {
    /** FIXME: won't work w/ special drawables (like FBO) - check for CGL mode regressions!
     *  
    if (getOpenGLMode() != ((MacOSXCGLDrawable)drawable).getOpenGLMode()) {
      setOpenGLMode(((MacOSXCGLDrawable)drawable).getOpenGLMode());
    } */
    if (!impl.makeCurrent(contextHandle)) {
      throw new GLException("Error making Context current: "+this);
    }
    drawableUpdatedNotify();
  }

  @Override
  protected void releaseImpl() throws GLException {
    if (!impl.release(contextHandle)) {
      throw new GLException("Error releasing OpenGL Context: "+this);
    }
  }

  @Override
  protected void destroyImpl() throws GLException {
    releaseUpdateHandle();
    if(!impl.destroy(contextHandle)) {
        throw new GLException("Error destroying OpenGL Context: "+this);
    }
  }
  
  private final long getUpdateHandle() {
    if( 0 == updateHandle ) {
        lastWidth = -1;
        lastHeight = -1;
        if( isCreated() && drawable.getChosenGLCapabilities().isOnscreen() && isNSContext() ) {
            final boolean incompleteView;
            final NativeSurface surface = drawable.getNativeSurface();
            if( surface instanceof ProxySurface ) {
              incompleteView = ((ProxySurface)surface).containsUpstreamOptionBits( ProxySurface.OPT_UPSTREAM_WINDOW_INVISIBLE );
            } else {
              incompleteView = false;
            }
            if(!incompleteView) {        
                updateHandle = CGL.updateContextRegister(contextHandle, drawable.getHandle());
                if(0 == updateHandle) {
                    throw new InternalError("XXX2");
                }
            }
        }
    }
    return updateHandle;
  }
  
  private final void releaseUpdateHandle() {
    if ( 0 != updateHandle ) {
        CGL.updateContextUnregister(updateHandle);
        updateHandle = 0;
    }      
  }
  
  @Override
  protected void drawableUpdatedNotify() throws GLException {
    if( drawable.getChosenGLCapabilities().isOnscreen() ) {
        final long _updateHandle = getUpdateHandle();
        final int w = drawable.getWidth();
        final int h = drawable.getHeight();
        final boolean updateContext = ( 0!=_updateHandle && CGL.updateContextNeedsUpdate(_updateHandle) ) ||
                                      w != lastWidth || h != lastHeight;
        if(updateContext) {
            lastWidth = w;
            lastHeight = h;
            if (contextHandle == 0) {
              throw new GLException("Context not created");
            }
            CGL.updateContext(contextHandle);
        }
    }
  }
  
  @Override
  protected void associateDrawable(boolean bound) {
      // context stuff depends on drawable stuff
      if(bound) {
          super.associateDrawable(true);   // 1) init drawable stuff
          impl.associateDrawable(true);    // 2) init context stuff
          getUpdateHandle();
      } else {
          releaseUpdateHandle();
          impl.associateDrawable(false);   // 1) free context stuff
          super.associateDrawable(false);  // 2) free drawable stuff
      }
  }
  
  /* pp */ void detachPBuffer() {
      impl.detachPBuffer();
  }

  
  @Override
  protected void copyImpl(GLContext source, int mask) throws GLException {
    if( isNSContext() != ((MacOSXCGLContext)source).isNSContext() ) {
        throw new GLException("Source/Destination OpenGL Context tyoe mismatch: source "+source+", dest: "+this);
    }
    if(!impl.copyImpl(source.getHandle(), mask)) {
        throw new GLException("Error copying OpenGL Context: source "+source+", dest: "+this);
    }
  }

  protected void swapBuffers() {
    // single-buffer is already filtered out @ GLDrawableImpl#swapBuffers()
    if(!impl.swapBuffers()) {
        throw new GLException("Error swapping buffers: "+this);
    }
  }

  @Override
  protected boolean setSwapIntervalImpl(int interval) {
    return impl.setSwapInterval(interval);
  }

  @Override
  public ByteBuffer glAllocateMemoryNV(int arg0, float arg1, float arg2, float arg3) {
    // FIXME: apparently the Apple extension doesn't require a custom memory allocator
    throw new GLException("Not yet implemented");
  }

  @Override
  protected final void updateGLXProcAddressTable() {
    final AbstractGraphicsConfiguration aconfig = drawable.getNativeSurface().getGraphicsConfiguration();
    final AbstractGraphicsDevice adevice = aconfig.getScreen().getDevice();
    final String key = "MacOSX-"+adevice.getUniqueID();
    if (DEBUG) {
      System.err.println(getThreadName() + ": Initializing CGL extension address table: "+key);
    }
    ProcAddressTable table = null;
    synchronized(mappedContextTypeObjectLock) {
        table = mappedGLXProcAddress.get( key );
    }
    if(null != table) {
        cglExtProcAddressTable = (CGLExtProcAddressTable) table;
        if(DEBUG) {
            System.err.println(getThreadName() + ": GLContext CGL ProcAddressTable reusing key("+key+") -> "+toHexString(table.hashCode()));
        }
    } else {
        cglExtProcAddressTable = new CGLExtProcAddressTable(new GLProcAddressResolver());
        resetProcAddressTable(getCGLExtProcAddressTable());
        synchronized(mappedContextTypeObjectLock) {
            mappedGLXProcAddress.put(key, getCGLExtProcAddressTable());
            if(DEBUG) {
                System.err.println(getThreadName() + ": GLContext CGL ProcAddressTable mapping key("+key+") -> "+toHexString(getCGLExtProcAddressTable().hashCode()));
            }
        }
    }
  }

  @Override
  protected final StringBuilder getPlatformExtensionsStringImpl() {
    return new StringBuilder();
  }

  @Override
  public boolean isExtensionAvailable(String glExtensionName) {
    if (glExtensionName.equals(GLExtensions.ARB_pbuffer) ||
        glExtensionName.equals(GLExtensions.ARB_pixel_format)) {
      return true;
    }
    return super.isExtensionAvailable(glExtensionName);
  }

  // Support for "mode switching" as described in MacOSXCGLDrawable
  public void setOpenGLMode(GLBackendType mode) {
      if (mode == openGLMode) {
        return;
      }
      if (haveSetOpenGLMode) {
        throw new GLException("Can't switch between using NSOpenGLPixelBuffer and CGLPBufferObj more than once");
      }
      destroyImpl();
      ((MacOSXCGLDrawable)drawable).setOpenGLMode(mode);
      if (DEBUG) {
        System.err.println("MacOSXCGLContext: Switching context mode " + openGLMode + " -> " + mode);
      }
      initOpenGLImpl(mode);
      openGLMode = mode;
      haveSetOpenGLMode = true;
  }
  public final GLBackendType getOpenGLMode() { return openGLMode; }

  protected void initOpenGLImpl(GLBackendType backend) {
    switch (backend) {
      case NSOPENGL:
        impl = new NSOpenGLImpl();
        break;
      case CGL:
        impl = new CGLImpl();
        break;
      default:
        throw new InternalError("Illegal implementation mode " + backend);
    }
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append(getClass().getSimpleName());
    sb.append(" [");
    super.append(sb);
    sb.append(", mode ");
    sb.append(openGLMode);
    sb.append("] ");
    return sb.toString();
  }

  // NSOpenGLContext-based implementation
  class NSOpenGLImpl implements GLBackendImpl {
      private OffscreenLayerSurface backingLayerHost = null;
      /** lifecycle:  [create - destroy] */
      private long pixelFormat = 0;
      /** microSec - defaults to 1/60s */
      private int screenVSyncTimeout = 16666;
      /** microSec - for nsOpenGLLayer mode - defaults to 1/60s + 1ms */
      private volatile int vsyncTimeout = 16666 + 1000;
      private int lastWidth=0, lastHeight=0; // allowing to detect size change
      private boolean needsSetContextPBuffer = false;
      private ShaderProgram gl3ShaderProgram = null;
      
      @Override
      public boolean isNSContext() { return true; }

      
      /** Only returns a valid NSView. If !NSView, return null and mark either pbuffer and FBO. */
      private long getNSViewHandle(boolean[] isPBuffer, boolean[] isFBO) {
          final long nsViewHandle;
          if(drawable instanceof GLFBODrawableImpl) {
              nsViewHandle = 0;
              isPBuffer[0] = false;
              isFBO[0] = true;
              if(DEBUG) {
                  System.err.println("NS viewHandle.1: GLFBODrawableImpl drawable: isFBO "+isFBO+", isPBuffer "+isPBuffer+", "+drawable.getClass().getName()+",\n\t"+drawable);
              }
          } else {
              final long drawableHandle = drawable.getHandle();
              final boolean isNSView = OSXUtil.isNSView(drawableHandle);
              final boolean isNSWindow = OSXUtil.isNSWindow(drawableHandle);
              isPBuffer[0] = CGL.isNSOpenGLPixelBuffer(drawableHandle);
              isFBO[0] = false;

              if( isNSView ) {
                  nsViewHandle = drawableHandle;
              } else if( isNSWindow ) {
                  nsViewHandle = OSXUtil.GetNSView(drawableHandle);
              } else if( isPBuffer[0] ) {
                  nsViewHandle = 0;
              } else {
                  throw new RuntimeException("Drawable's handle neither NSView, NSWindow nor PBuffer: drawableHandle "+toHexString(drawableHandle)+", isNSView "+isNSView+", isNSWindow "+isNSWindow+", isFBO "+isFBO[0]+", isPBuffer "+isPBuffer[0]+", "+drawable.getClass().getName()+",\n\t"+drawable);
              }
              if(DEBUG) {
                  System.err.println("NS viewHandle.2: drawableHandle "+toHexString(drawableHandle)+" -> nsViewHandle "+toHexString(nsViewHandle)+": isNSView "+isNSView+", isNSWindow "+isNSWindow+", isFBO "+isFBO[0]+", isPBuffer "+isPBuffer[0]+", "+drawable.getClass().getName()+",\n\t"+drawable);
              }
          }
          needsSetContextPBuffer = isPBuffer[0];
          return nsViewHandle;
      }
      
      @Override
      public long create(long share, int ctp, int major, int minor) {
          long ctx = 0;
          final NativeSurface surface = drawable.getNativeSurface();        
          final MacOSXCGLGraphicsConfiguration config = (MacOSXCGLGraphicsConfiguration) surface.getGraphicsConfiguration();
          final GLCapabilitiesImmutable chosenCaps = (GLCapabilitiesImmutable) config.getChosenCapabilities();
          final long nsViewHandle;
          final boolean isPBuffer;
          final boolean isFBO;
          {
              boolean[] _isPBuffer = { false };
              boolean[] _isFBO = { false };
              nsViewHandle = getNSViewHandle(_isPBuffer, _isFBO);
              isPBuffer = _isPBuffer[0];
              isFBO = _isFBO[0];
          }
          final OffscreenLayerSurface backingLayerHost = NativeWindowFactory.getOffscreenLayerSurface(surface, true);

          boolean incompleteView = null != backingLayerHost;
          if( !incompleteView && surface instanceof ProxySurface ) {
              incompleteView = ((ProxySurface)surface).containsUpstreamOptionBits( ProxySurface.OPT_UPSTREAM_WINDOW_INVISIBLE );
          }
          {
              final GLCapabilitiesImmutable targetCaps;
              if( isFBO ) {
                  // Use minimum GLCapabilities for the target surface w/ same profile
                  targetCaps = new GLCapabilities( chosenCaps.getGLProfile() );
              } else {
                  targetCaps = chosenCaps;
              }
              pixelFormat = MacOSXCGLGraphicsConfiguration.GLCapabilities2NSPixelFormat(targetCaps, ctp, major, minor);
          }
          if (pixelFormat == 0) {
              if(DEBUG) {
                  System.err.println("Unable to allocate pixel format with requested GLCapabilities: "+chosenCaps);
              }
              return 0;
          }
          final GLCapabilitiesImmutable fixedCaps;
          if( isFBO ) {
              // pixelformat of target doesn't affect caps w/ FBO
              fixedCaps = chosenCaps;
          } else {
              final GLCapabilities _fixedCaps = MacOSXCGLGraphicsConfiguration.NSPixelFormat2GLCapabilities(chosenCaps.getGLProfile(), pixelFormat);
              if( !_fixedCaps.isPBuffer() && isPBuffer ) {
                  throw new InternalError("handle is PBuffer, fixedCaps not: "+drawable);
              }
              // determine on-/offscreen caps, since pformat is ambiguous 
              _fixedCaps.setPBuffer( isPBuffer ); // exclusive
              _fixedCaps.setBitmap( false );      // n/a in our OSX impl.
              _fixedCaps.setOnscreen( !isFBO && !isPBuffer );
              fixedCaps = GLGraphicsConfigurationUtil.fixOpaqueGLCapabilities(_fixedCaps, chosenCaps.isBackgroundOpaque());
          }
          final int sRefreshRate = OSXUtil.GetScreenRefreshRate(drawable.getNativeSurface().getGraphicsConfiguration().getScreen().getIndex());
          if( 0 < sRefreshRate ) {
              screenVSyncTimeout = 1000000 / sRefreshRate;
          }
          if(DEBUG) {
              System.err.println("NS create OSX>=lion "+isLionOrLater);
              System.err.println("NS create incompleteView: "+incompleteView);
              System.err.println("NS create backingLayerHost: "+backingLayerHost);
              System.err.println("NS create share: "+share);
              System.err.println("NS create drawable type: "+drawable.getClass().getName());
              System.err.println("NS create drawable handle: isPBuffer "+isPBuffer+", isFBO "+isFBO);
              System.err.println("NS create pixelFormat: "+toHexString(pixelFormat));
              System.err.println("NS create chosenCaps: "+chosenCaps);
              System.err.println("NS create fixedCaps: "+fixedCaps);
              System.err.println("NS create drawable native-handle: "+toHexString(drawable.getHandle()));
              System.err.println("NS create drawable NSView-handle: "+toHexString(nsViewHandle));
              System.err.println("NS create screen refresh-rate: "+sRefreshRate+" hz, "+screenVSyncTimeout+" micros");
              // Thread.dumpStack();
          }
          config.setChosenCapabilities(fixedCaps);
          
          final IntBuffer viewNotReady = Buffers.newDirectIntBuffer(1);
          // Try to allocate a context with this
          ctx = CGL.createContext(share, nsViewHandle, incompleteView,
                  pixelFormat, chosenCaps.isBackgroundOpaque(), viewNotReady);
          if (0 == ctx) {
              if(DEBUG) {
                  System.err.println("NS create failed: viewNotReady: "+ (1 == viewNotReady.get(0)));
              }
              return 0;
          }

          if (chosenCaps.isOnscreen() && !chosenCaps.isBackgroundOpaque()) {
              // Set the context opacity
              CGL.setContextOpacity(ctx, 0);
          }
          return ctx;
      }

      @Override
      public boolean destroy(long ctx) {
          if(0!=pixelFormat) {
              CGL.deletePixelFormat(pixelFormat);
              pixelFormat = 0;
          }
          return CGL.deleteContext(ctx, true);
      }

      /**
       * NSOpenGLLayer creation and it's attachment is performed on the main-thread w/o [infinite] blocking.
       * <p>
       * Since NSOpenGLLayer creation requires this context for it's shared context creation,
       * this method attempts to acquire the surface and context lock with {@link #screenVSyncTimeout}/2 maximum wait time.
       * If the surface and context lock could not be acquired, this runnable is being re-queued for later execution. 
       * </p>
       * <p>
       * Hence this method blocks the main-thread only for a short period of time.
       * </p>
       */                  
      class AttachGLLayerCmd implements Runnable {
          final OffscreenLayerSurface ols;
          final long ctx;
          final int shaderProgram;
          final long pfmt;
          final long pbuffer;
          final int texID;
          final boolean isOpaque;
          final int width;
          final int height;
          /** Synchronized by instance's monitor */
          long nsOpenGLLayer;
          /** Synchronized by instance's monitor */
          boolean valid;
          
          AttachGLLayerCmd(OffscreenLayerSurface ols, long ctx, int shaderProgram, long pfmt, long pbuffer, int texID, boolean isOpaque, int width, int height) {
              this.ols = ols;
              this.ctx = ctx;
              this.shaderProgram = shaderProgram;
              this.pfmt = pfmt;
              this.pbuffer = pbuffer;
              this.texID = texID;
              this.isOpaque = isOpaque;
              this.width = width;
              this.height = height;
              this.valid = false;
              this.nsOpenGLLayer = 0;
          }
          
          public final String contentToString() {
              return "valid "+valid+", size "+width+"x"+height+", ctx "+toHexString(ctx)+", opaque "+isOpaque+", texID "+texID+", pbuffer "+toHexString(pbuffer)+", nsOpenGLLayer "+toHexString(nsOpenGLLayer);
          }
          
          @Override
          public final String toString() {
              return "AttachGLLayerCmd["+contentToString()+"]";
          }
          
          @Override
          public void run() {
              synchronized(this) {
                  if( !valid ) {
                      try {
                          final int maxwait = screenVSyncTimeout/2000; // TO 1/2 of current screen-vsync in [ms]
                          final RecursiveLock surfaceLock = ols.getLock(); 
                          if( surfaceLock.tryLock( maxwait ) ) {
                              try {
                                  if( MacOSXCGLContext.this.lock.tryLock( maxwait ) ) {
                                      try {
                                          nsOpenGLLayer = CGL.createNSOpenGLLayer(ctx, shaderProgram, pfmt, pbuffer, texID, isOpaque, width, height);
                                          ols.attachSurfaceLayer(nsOpenGLLayer);
                                          final int currentInterval = MacOSXCGLContext.this.getSwapInterval();
                                          final int interval = 0 <= currentInterval ? currentInterval : 1;
                                          setSwapIntervalImpl(nsOpenGLLayer, interval); // enabled per default in layered surface
                                          valid = true;
                                          if (DEBUG) {
                                              System.err.println("NSOpenGLLayer.Attach: OK, layer "+toHexString(nsOpenGLLayer)+" w/ pbuffer "+toHexString(pbuffer)+", texID "+texID+", texSize "+lastWidth+"x"+lastHeight+", drawableHandle "+toHexString(drawable.getHandle())+" - "+getThreadName());
                                          }
                                      } finally {
                                          MacOSXCGLContext.this.lock.unlock();
                                      }
                                  }
                              } finally {
                                  surfaceLock.unlock();
                              }
                          }
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      if( !valid ) {
                          // could not acquire lock, re-queue
                          if (DEBUG) {
                              System.err.println("NSOpenGLLayer.Attach: Re-Queue, drawableHandle "+toHexString(drawable.getHandle())+" - "+getThreadName());
                          }
                          OSXUtil.RunLater(this, 1);
                      }
                  }
              }
          }
      }
      AttachGLLayerCmd attachGLLayerCmd = null;
      
      class DetachGLLayerCmd implements Runnable {
        final AttachGLLayerCmd cmd;
        
        DetachGLLayerCmd(AttachGLLayerCmd cmd) {
            this.cmd = cmd;
        }
        
        @Override
        public final String toString() {
            return "DetachGLLayerCmd["+cmd.contentToString()+"]";
        }
        
        @Override
        public void run() {
            synchronized( cmd ) {
                if( cmd.valid ) {
                    // still having a valid OLS attached to surface (parent OLS could have been removed)
                    try {
                        final OffscreenLayerSurface ols = cmd.ols;
                        final long l = ols.getAttachedSurfaceLayer();
                        if( 0 != l ) {
                            ols.detachSurfaceLayer();
                        }
                    } catch(Throwable t) {
                        System.err.println("Catched Exception on thread "+getThreadName()); 
                        t.printStackTrace();
                    }
                    CGL.releaseNSOpenGLLayer(cmd.nsOpenGLLayer);
                    if(DEBUG) {
                        System.err.println("NSOpenGLLayer.Detach: OK, layer "+toHexString(cmd.nsOpenGLLayer)+" - "+getThreadName());
                    }
                    cmd.nsOpenGLLayer = 0;
                    cmd.valid = false;
                } else if(DEBUG) {
                    System.err.println("NSOpenGLLayer.Detach: Skipped "+toHexString(cmd.nsOpenGLLayer)+" - "+getThreadName());
                }
            }
        }          
      }
      
      @Override
      public void associateDrawable(boolean bound) {
          backingLayerHost = NativeWindowFactory.getOffscreenLayerSurface(drawable.getNativeSurface(), true);
          
          if(DEBUG) {
              System.err.println("MaxOSXCGLContext.NSOpenGLImpl.associateDrawable: "+bound+", ctx "+toHexString(contextHandle)+
                                 ", hasBackingLayerHost "+(null!=backingLayerHost)+", attachGLLayerCmd "+attachGLLayerCmd);
              // Thread.dumpStack();
          }          
          
          if( bound ) {              
              if( null != backingLayerHost ) {
                  final GLCapabilitiesImmutable chosenCaps;
                  final long ctx;
                  final int texID;
                  final long pbufferHandle;
                  final int gl3ShaderProgramName;
                  
                  //
                  // handled layered surface
                  //
                  chosenCaps = drawable.getChosenGLCapabilities();
                  ctx = MacOSXCGLContext.this.getHandle();
                  final long drawableHandle = drawable.getHandle();
                  if(drawable instanceof GLFBODrawableImpl) {
                      final GLFBODrawableImpl fbod = (GLFBODrawableImpl)drawable;
                      texID = fbod.getTextureBuffer(GL.GL_FRONT).getName();
                      pbufferHandle = 0;
                      fbod.setSwapBufferContext(new GLFBODrawableImpl.SwapBufferContext() {
                          public void swapBuffers(boolean doubleBuffered) {
                              MacOSXCGLContext.NSOpenGLImpl.this.swapBuffers();                            
                          } } ) ;                    
                  } else if( CGL.isNSOpenGLPixelBuffer(drawableHandle) ) {
                      texID = 0;
                      pbufferHandle = drawableHandle;
                      if(0 != drawableHandle) { // complete 'validatePBufferConfig(..)' procedure
                          CGL.setContextPBuffer(ctx, pbufferHandle);
                          needsSetContextPBuffer = false;
                      }
                  } else {
                      throw new GLException("BackingLayerHost w/ unknown handle (!FBO, !PBuffer): "+drawable);
                  }
                  lastWidth = drawable.getWidth();
                  lastHeight = drawable.getHeight();
                  if(0>=lastWidth || 0>=lastHeight || !drawable.isRealized()) {
                      throw new GLException("Drawable not realized yet or invalid texture size, texSize "+lastWidth+"x"+lastHeight+", "+drawable);
                  }
                  if( MacOSXCGLContext.this.isGL3core() ) {
                      if( null == gl3ShaderProgram) {
                          gl3ShaderProgram = createCALayerShader(MacOSXCGLContext.this.gl.getGL3());
                      }
                      gl3ShaderProgramName = gl3ShaderProgram.program();
                  } else {
                      gl3ShaderProgramName = 0;
                  }                                     
                   
                  // All CALayer lifecycle ops are deferred on main-thread
                  attachGLLayerCmd = new AttachGLLayerCmd( 
                          backingLayerHost, ctx, gl3ShaderProgramName, pixelFormat, pbufferHandle, texID, 
                          chosenCaps.isBackgroundOpaque(), lastWidth, lastHeight );
                  if(DEBUG) {
                      System.err.println("MaxOSXCGLContext.NSOpenGLImpl.associateDrawable(true): "+attachGLLayerCmd);
                  }                            
                  OSXUtil.RunOnMainThread(false, attachGLLayerCmd);
              } else { // -> null == backingLayerHost                  
                  lastWidth = drawable.getWidth();
                  lastHeight = drawable.getHeight();                  
                  boolean[] isPBuffer = { false };
                  boolean[] isFBO = { false };
                  CGL.setContextView(contextHandle, getNSViewHandle(isPBuffer, isFBO));
              }
          } else { // -> !bound
              if( null != backingLayerHost ) {
                  final AttachGLLayerCmd cmd = attachGLLayerCmd;
                  attachGLLayerCmd = null;
                  if( null == cmd ) {
                      throw new GLException("Null attachGLLayerCmd: "+drawable);
                  }
                  if( 0 != cmd.pbuffer ) {
                      CGL.setContextPBuffer(contextHandle, 0);
                  }
                  synchronized(cmd) {
                      if( !cmd.valid ) {
                          cmd.valid = true; // skip pending creation
                      } else {
                          // All CALayer lifecycle ops are deferred on main-thread
                          final DetachGLLayerCmd dCmd = new DetachGLLayerCmd(cmd);
                          if(DEBUG) {
                              System.err.println("MaxOSXCGLContext.NSOpenGLImpl.associateDrawable(false): "+dCmd);
                          }                            
                          OSXUtil.RunOnMainThread(false, dCmd);
                          if( null != gl3ShaderProgram ) {
                              gl3ShaderProgram.destroy(MacOSXCGLContext.this.gl.getGL3());
                              gl3ShaderProgram = null;
                          }
                      }
                  }
              }
              CGL.clearDrawable(contextHandle);
              backingLayerHost = null;
          }
      }

      private final void validatePBufferConfig(long ctx) {
          final long drawableHandle = drawable.getHandle();
          if( needsSetContextPBuffer && 0 != drawableHandle && CGL.isNSOpenGLPixelBuffer(drawableHandle) ) {
              // Must associate the pbuffer with our newly-created context
              needsSetContextPBuffer = false;
              CGL.setContextPBuffer(ctx, drawableHandle);
              if(DEBUG) {
                  System.err.println("NS.validateDrawableConfig bind pbuffer "+toHexString(drawableHandle)+" -> ctx "+toHexString(ctx)); 
              }
          }
      }
      
      /** Returns true if size has been updated, otherwise false (same size). */
      private final boolean validateDrawableSizeConfig(long ctx) {
          final int width = drawable.getWidth();
          final int height = drawable.getHeight();
          if( lastWidth != width || lastHeight != height ) {
              lastWidth = drawable.getWidth();
              lastHeight = drawable.getHeight();
              if(DEBUG) {
                  System.err.println("NS.validateDrawableConfig size changed"); 
              }
              return true;
          }
          return false;
      }
      
      @Override
      public boolean copyImpl(long src, int mask) {
          CGL.copyContext(contextHandle, src, mask);
          return true;
      }

      @Override
      public boolean makeCurrent(long ctx) {
          final long cglCtx = CGL.getCGLContext(ctx);
          if(0 == cglCtx) {
              throw new InternalError("Null CGLContext for: "+this);
          }
          int err = CGL.CGLLockContext(cglCtx);
          if(CGL.kCGLNoError == err) {
              validatePBufferConfig(ctx); // required to handle pbuffer change ASAP
              return CGL.makeCurrentContext(ctx);
          } else if(DEBUG) {
              System.err.println("NSGL: Could not lock context: err 0x"+Integer.toHexString(err)+": "+this);
          }
          return false;
      }

      @Override
      public boolean release(long ctx) {
          try {
              if( hasRendererQuirk(GLRendererQuirks.GLFlushBeforeRelease) && null != MacOSXCGLContext.this.getGLProcAddressTable() ) {
                  gl.glFlush();
              }
          } catch (GLException gle) {
              if(DEBUG) {
                  System.err.println("MacOSXCGLContext.NSOpenGLImpl.release: INFO: glFlush() catched exception:");
                  gle.printStackTrace();
              }
          }
          final boolean res = CGL.clearCurrentContext(ctx);
          final long cglCtx = CGL.getCGLContext(ctx);
          if(0 == cglCtx) {
              throw new InternalError("Null CGLContext for: "+this);
          }
          final int err = CGL.CGLUnlockContext(cglCtx);
          if(DEBUG && CGL.kCGLNoError != err) {
              System.err.println("CGL: Could not unlock context: err 0x"+Integer.toHexString(err)+": "+this);
          }
          return res && CGL.kCGLNoError == err;
      }

      @Override
      public boolean detachPBuffer() {
          needsSetContextPBuffer = true;
          // CGL.setContextPBuffer(contextHandle, 0); // doesn't work, i.e. not taking nil
          return true;
      }
      
      @Override
      public boolean setSwapInterval(int interval) {
          final AttachGLLayerCmd cmd = attachGLLayerCmd;
          if(null != cmd) {
              synchronized(cmd) {
                  if( cmd.valid && 0 != cmd.nsOpenGLLayer) {
                      setSwapIntervalImpl(cmd.nsOpenGLLayer, interval);
                      return true;
                  }
              }
          }
          setSwapIntervalImpl(0, interval);
          return true;
      }

      private void setSwapIntervalImpl(final long l, int interval) {
          if( 0 != l ) {
              CGL.setNSOpenGLLayerSwapInterval(l, interval);
              if( 0 < interval ) {
                  vsyncTimeout = interval * screenVSyncTimeout + 1000; // +1ms
              }
              if(DEBUG) { System.err.println("NS setSwapInterval: "+interval+" -> "+vsyncTimeout+" micros"); }
          }
          if(DEBUG) { System.err.println("CGL setSwapInterval: "+interval); }
          CGL.setSwapInterval(contextHandle, interval);
      }
      
      private int skipSync=0;
      
      @Override
      public boolean swapBuffers() {
          final AttachGLLayerCmd cmd = attachGLLayerCmd;
          if(null != cmd) {
              synchronized(cmd) {
                  if( cmd.valid && 0 != cmd.nsOpenGLLayer) {
                      if( validateDrawableSizeConfig(contextHandle) ) {
                          // skip wait-for-vsync for a few frames if size has changed,
                          // allowing to update the texture IDs ASAP.
                          skipSync = 10;
                      }
                      
                      final boolean res;
                      final int texID;
                      final boolean valid;
                      final boolean isFBO = drawable instanceof GLFBODrawableImpl;
                      if( isFBO ){
                          texID = ((GLFBODrawableImpl)drawable).getTextureBuffer(GL.GL_FRONT).getName();
                          valid = 0 != texID;
                      } else {
                          texID = 0;
                          valid = 0 != drawable.getHandle();
                      }
                      if(valid) {
                          res = CGL.flushBuffer(contextHandle);
                          if(res) {
                              if(0 == skipSync) {
                                  // If v-sync is disabled, frames will be drawn as quickly as possible w/o delay, 
                                  // while still synchronizing w/ CALayer.
                                  // If v-sync is enabled wait until next swap interval (v-sync).
                                  CGL.waitUntilNSOpenGLLayerIsReady(cmd.nsOpenGLLayer, vsyncTimeout);
                              } else {
                                  skipSync--;
                              }
                              if(isFBO) {
                                  // trigger CALayer to update incl. possible surface change (texture)
                                  CGL.setNSOpenGLLayerNeedsDisplayFBO(cmd.nsOpenGLLayer, texID);
                              } else {
                                  // trigger CALayer to update incl. possible surface change (new pbuffer handle)
                                  CGL.setNSOpenGLLayerNeedsDisplayPBuffer(cmd.nsOpenGLLayer, drawable.getHandle());                          
                              }
                          }
                      } else {
                          res = true;
                      }
                      return res;
                  }
              }
          }
          return CGL.flushBuffer(contextHandle);
      }

  }

  class CGLImpl implements GLBackendImpl {
      @Override
      public boolean isNSContext() { return false; }

      @Override
      public long create(long share, int ctp, int major, int minor) {
          long ctx = 0;
          MacOSXCGLGraphicsConfiguration config = (MacOSXCGLGraphicsConfiguration) drawable.getNativeSurface().getGraphicsConfiguration();
          GLCapabilitiesImmutable chosenCaps = (GLCapabilitiesImmutable)config.getChosenCapabilities();
          long pixelFormat = MacOSXCGLGraphicsConfiguration.GLCapabilities2CGLPixelFormat(chosenCaps, ctp, major, minor);
          if (pixelFormat == 0) {
              throw new GLException("Unable to allocate pixel format with requested GLCapabilities");
          }
          try {
              // Create new context
              PointerBuffer ctxPB = PointerBuffer.allocateDirect(1);
              if (DEBUG) {
                  System.err.println("Share context for CGL-based pbuffer context is " + toHexString(share));
              }
              int res = CGL.CGLCreateContext(pixelFormat, share, ctxPB);
              if (res != CGL.kCGLNoError) {
                  throw new GLException("Error code " + res + " while creating context");
              }
              ctx = ctxPB.get(0);

              if (0 != ctx) {
                  GLCapabilities fixedCaps = MacOSXCGLGraphicsConfiguration.CGLPixelFormat2GLCapabilities(pixelFormat);
                  fixedCaps = GLGraphicsConfigurationUtil.fixOpaqueGLCapabilities(fixedCaps, chosenCaps.isBackgroundOpaque());
                  { // determine on-/offscreen caps, since pformat is ambiguous 
                      fixedCaps.setFBO( false );         // n/a for CGLImpl 
                      fixedCaps.setPBuffer( fixedCaps.isPBuffer() && !chosenCaps.isOnscreen() );
                      fixedCaps.setBitmap( false );      // n/a in our OSX impl.
                      fixedCaps.setOnscreen( !fixedCaps.isPBuffer() );
                  }
                  config.setChosenCapabilities(fixedCaps);
                  if(DEBUG) {
                      System.err.println("CGL create fixedCaps: "+fixedCaps);
                  }
                  if(fixedCaps.isPBuffer()) {
                      // Must now associate the pbuffer with our newly-created context
                      res = CGL.CGLSetPBuffer(ctx, drawable.getHandle(), 0, 0, 0);
                      if (res != CGL.kCGLNoError) {
                          throw new GLException("Error code " + res + " while attaching context to pbuffer");
                      }
                  }              
              }
          } finally {
              CGL.CGLDestroyPixelFormat(pixelFormat);
          }
          return ctx;
      }

      @Override
      public boolean destroy(long ctx) {
          return CGL.CGLDestroyContext(ctx) == CGL.kCGLNoError;
      }

      @Override
      public void associateDrawable(boolean bound) {
      }

      @Override
      public boolean copyImpl(long src, int mask) {
          CGL.CGLCopyContext(src, contextHandle, mask);
          return true;
      }

      @Override
      public boolean makeCurrent(long ctx) {
          int err = CGL.CGLLockContext(ctx);
          if(CGL.kCGLNoError == err) {
              err = CGL.CGLSetCurrentContext(ctx);
              if(CGL.kCGLNoError == err) {
                  return true;
              } else if(DEBUG) {
                  System.err.println("CGL: Could not make context current: err 0x"+Integer.toHexString(err)+": "+this);
              }
          } else if(DEBUG) {
              System.err.println("CGL: Could not lock context: err 0x"+Integer.toHexString(err)+": "+this);
          }
          return false;
      }

      @Override
      public boolean release(long ctx) {
          try {
              if( hasRendererQuirk(GLRendererQuirks.GLFlushBeforeRelease) && null != MacOSXCGLContext.this.getGLProcAddressTable() ) {
                  gl.glFlush();
              }
          } catch (GLException gle) {
              if(DEBUG) {
                  System.err.println("MacOSXCGLContext.CGLImpl.release: INFO: glFlush() catched exception:");
                  gle.printStackTrace();
              }
          }
          int err = CGL.CGLSetCurrentContext(0);
          if(DEBUG && CGL.kCGLNoError != err) {
              System.err.println("CGL: Could not release current context: err 0x"+Integer.toHexString(err)+": "+this);
          }
          int err2 = CGL.CGLUnlockContext(ctx);
          if(DEBUG && CGL.kCGLNoError != err2) {
              System.err.println("CGL: Could not unlock context: err 0x"+Integer.toHexString(err2)+": "+this);
          }
          return CGL.kCGLNoError == err && CGL.kCGLNoError == err2;
      }

      @Override
      public boolean detachPBuffer() {
          /* Doesn't work, i.e. not taking NULL
          final int res = CGL.CGLSetPBuffer(contextHandle, 0, 0, 0, 0);
          if (res != CGL.kCGLNoError) {
              throw new GLException("Error code " + res + " while detaching context from pbuffer");
          } */
          return true;
      }
      
      @Override
      public boolean setSwapInterval(int interval) {
          final IntBuffer lval = Buffers.newDirectIntBuffer(1);
          lval.put(0, interval);
          CGL.CGLSetParameter(contextHandle, CGL.kCGLCPSwapInterval, lval);
          return true;
      }
      @Override
      public boolean swapBuffers() {
          return CGL.kCGLNoError == CGL.CGLFlushDrawable(contextHandle);
      }
  }
}