/**
 * Copyright 2011 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 jogamp.opengl;

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;

import javax.media.nativewindow.NativeWindowException;
import javax.media.opengl.GL2GL3;
import javax.media.opengl.GLDebugListener;
import javax.media.opengl.GLDebugMessage;
import javax.media.opengl.GLException;

import com.jogamp.common.os.Platform;
import com.jogamp.gluegen.runtime.ProcAddressTable;
import com.jogamp.opengl.GLExtensions;

/**
 * The GLDebugMessageHandler, handling <i>GL_ARB_debug_output</i> or <i>GL_AMD_debug_output</i>
 * debug messages.<br>
 *
 * <p>An instance must be bound to the current thread's GLContext to achieve thread safety.</p>
 *
 * <p>A native callback function is registered at {@link #enable(boolean) enable(true)},
 * which forwards received messages to the added {@link GLDebugListener} directly.
 * Hence the {@link GLDebugListener#messageSent(GLDebugMessage)} implementation shall
 * return as fast as possible.</p>
 *
 * <p>In case no <i>GL_ARB_debug_output</i> is available, but <i>GL_AMD_debug_output</i>,
 * the messages are translated to <i>ARB</i> {@link GLDebugMessage}, using {@link GLDebugMessage#translateAMDEvent(javax.media.opengl.GLContext, long, int, int, int, String)}.</p>
 */
public class GLDebugMessageHandler {
    private static final boolean DEBUG = Debug.debug("GLDebugMessageHandler");

    private static final int EXT_ARB = 1;
    private static final int EXT_AMD = 2;

    static {
        if ( !initIDs0() ) {
            throw new NativeWindowException("Failed to initialize GLDebugMessageHandler jmethodIDs");
        }
    }

    private final GLContextImpl ctx;
    private final ListenerSyncedImplStub<GLDebugListener> listenerImpl;

    // licefycle: init - EOL
    private String extName;
    private int extType;
    private long glDebugMessageCallbackProcAddress;
    private boolean extAvailable;
    private boolean synchronous;

    // licefycle: enable - disable/EOL
    private long handle;

    /**
     * @param ctx the associated GLContext
     * @param glDebugExtension chosen extension to use
     */
    public GLDebugMessageHandler(GLContextImpl ctx) {
        this.ctx = ctx;
        this.listenerImpl = new ListenerSyncedImplStub<GLDebugListener>();
        this.glDebugMessageCallbackProcAddress = 0;
        this.extName = null;
        this.extType = 0;
        this.extAvailable = false;
        this.handle = 0;
        this.synchronous = true;
    }

    public void init(boolean enable) {
        if(DEBUG) {
            System.err.println("GLDebugMessageHandler.init("+enable+")");
        }
        init();
        if(isAvailable()) {
            enableImpl(enable);
        } else if(DEBUG) {
            System.err.println("GLDebugMessageHandler.init("+enable+") .. n/a");
        }
    }

    private final long getAddressFor(final ProcAddressTable table, final String functionName) {
        return AccessController.doPrivileged(new PrivilegedAction<Long>() {
            @Override
            public Long run() {
                try {
                    return Long.valueOf( table.getAddressFor(functionName) );
                } catch (IllegalArgumentException iae) {
                    return Long.valueOf(0);
                }
            }
        } ).longValue();
    }

    public void init() {
        ctx.validateCurrent();
        if( isAvailable()) {
            return;
        }

        if( !ctx.isGLDebugEnabled() ) {
            if(DEBUG) {
                System.err.println("GLDebugMessageHandler: GL DEBUG not set in ARB ctx options: "+ctx.getGLVersion());
            }
            return;
        }
        if(Platform.OS_TYPE == Platform.OSType.WINDOWS && Platform.is32Bit()) {
            // Currently buggy, ie. throws an exception after leaving the native callback.
            // Probably a 32bit on 64bit JVM / OpenGL-driver issue.
            if(DEBUG) {
                System.err.println("GLDebugMessageHandler: Windows 32bit currently not supported!");
            }
            return;
        }
        if( ctx.isExtensionAvailable(GLExtensions.ARB_debug_output) ) {
            extName = GLExtensions.ARB_debug_output;
            extType = EXT_ARB;
        } else if( ctx.isExtensionAvailable(GLExtensions.AMD_debug_output) ) {
            extName = GLExtensions.AMD_debug_output;
            extType = EXT_AMD;
        }
        if(DEBUG) {
            System.err.println("GLDebugMessageHandler: Using extension: <"+extName+">");
        }

        if(0 == extType) {
            if(DEBUG) {
                System.err.println("GLDebugMessageHandler: No extension available! "+ctx.getGLVersion());
                System.err.println("GL_EXTENSIONS  "+ctx.getGLExtensionCount());
                System.err.println(ctx.getGLExtensionsString());
            }
            return;
        }

        final ProcAddressTable procAddressTable = ctx.getGLProcAddressTable();
        if( !ctx.isGLES1() && !ctx.isGLES2() ) {
            switch(extType) {
                case EXT_ARB:
                    glDebugMessageCallbackProcAddress = getAddressFor(procAddressTable, "glDebugMessageCallbackARB");
                    break;
                case EXT_AMD:
                    glDebugMessageCallbackProcAddress = getAddressFor(procAddressTable, "glDebugMessageCallbackAMD");
                    break;
            }
        } else {
            glDebugMessageCallbackProcAddress = 0;
            if(DEBUG) {
                System.err.println("Non desktop context not supported");
            }
        }
        extAvailable = 0 < extType && null != extName && 0 != glDebugMessageCallbackProcAddress;

        if(DEBUG) {
            System.err.println("GLDebugMessageHandler: extAvailable: "+extAvailable+", glDebugMessageCallback* : 0x"+Long.toHexString(glDebugMessageCallbackProcAddress));
        }

        if(!extAvailable) {
            glDebugMessageCallbackProcAddress = 0;
        }

        handle = 0;
    }

    public final boolean isAvailable() { return extAvailable; }

    /**
     * @return The extension implementing the GLDebugMessage feature,
     *         either {@link #GL_ARB_debug_output} or {@link #GL_AMD_debug_output}.
     *         If unavailable <i>null</i> is returned.
     */
    public final String getExtension() {
        return extName;
    }

    public final boolean isExtensionARB() {
        return extName == GLExtensions.ARB_debug_output;
    }

    public final boolean isExtensionAMD() {
        return extName == GLExtensions.AMD_debug_output;
    }

    /**
     * @see javax.media.opengl.GLContext#isGLDebugSynchronous()
     */
    public final boolean isSynchronous() { return synchronous; }

    /**
     * @see javax.media.opengl.GLContext#setGLDebugSynchronous(boolean)
     */
    public final void setSynchronous(boolean synchronous) {
        this.synchronous = synchronous;
        if( isEnabled() ) {
            setSynchronousImpl();
        }
    }
    private final void setSynchronousImpl() {
        if(isExtensionARB()) {
            if(synchronous) {
                ctx.getGL().glEnable(GL2GL3.GL_DEBUG_OUTPUT_SYNCHRONOUS);
            } else {
                ctx.getGL().glDisable(GL2GL3.GL_DEBUG_OUTPUT_SYNCHRONOUS);
            }
            if(DEBUG) {
                System.err.println("GLDebugMessageHandler: synchronous "+synchronous);
            }
        }
    }

    /**
     * @see javax.media.opengl.GLContext#enableGLDebugMessage(boolean)
     */
    public final void enable(boolean enable) throws GLException {
        ctx.validateCurrent();
        if(!isAvailable()) {
            return;
        }
        enableImpl(enable);
    }
    final void enableImpl(boolean enable) throws GLException {
        if(enable) {
            if(0 == handle) {
                setSynchronousImpl();
                handle = register0(glDebugMessageCallbackProcAddress, extType);
                if(0 == handle) {
                    throw new GLException("Failed to register via \"glDebugMessageCallback*\" using "+extName);
                }
            }
        } else {
            if(0 != handle) {
                unregister0(glDebugMessageCallbackProcAddress, handle);
                handle = 0;
            }
        }
        if(DEBUG) {
            System.err.println("GLDebugMessageHandler: enable("+enable+") -> 0x" + Long.toHexString(handle));
        }
    }

    public final boolean isEnabled() { return 0 != handle; }

    public final int listenerSize() {
        return listenerImpl.size();
    }

    public final void addListener(GLDebugListener listener) {
        listenerImpl.addListener(-1, listener);
    }

    public final void addListener(int index, GLDebugListener listener) {
        listenerImpl.addListener(index, listener);
    }

    public final void removeListener(GLDebugListener listener) {
        listenerImpl.removeListener(listener);
    }

    private final void sendMessage(GLDebugMessage msg) {
        synchronized(listenerImpl) {
            if(DEBUG) {
                System.err.println("GLDebugMessageHandler: "+msg);
            }
            final ArrayList<GLDebugListener> listeners = listenerImpl.getListeners();
            for(int i=0; i<listeners.size(); i++) {
                listeners.get(i).messageSent(msg);
            }
        }
    }

    public static class StdErrGLDebugListener implements GLDebugListener {
        boolean threadDump;

        public StdErrGLDebugListener(boolean threadDump) {
            this.threadDump = threadDump;
        }
        @Override
        public void messageSent(GLDebugMessage event) {
            System.err.println(event);
            if(threadDump) {
                Thread.dumpStack();
            }
        }
    }

    //
    // native -> java
    //

    protected final void glDebugMessageARB(int source, int type, int id, int severity, String msg) {
        final GLDebugMessage event = new GLDebugMessage(ctx, System.currentTimeMillis(), source, type, id, severity, msg);
        sendMessage(event);
    }

    protected final void glDebugMessageAMD(int id, int category, int severity, String msg) {
        final GLDebugMessage event = GLDebugMessage.translateAMDEvent(ctx, System.currentTimeMillis(), id, category, severity, msg);
        sendMessage(event);
    }

    //
    // java -> native
    //

    private static native boolean initIDs0();
    private native long register0(long glDebugMessageCallbackProcAddress, int extType);
    private native void unregister0(long glDebugMessageCallbackProcAddress, long handle);
}