/**
 * Copyright 2014 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.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;

import com.jogamp.opengl.*;
import com.jogamp.common.ExceptionUtils;
import com.jogamp.common.nio.Buffers;
import com.jogamp.common.util.IntObjectHashMap;
import com.jogamp.common.util.PropertyAccess;

/**
 * Tracking of {@link GLBufferStorage} instances via GL API callbacks.
 * <p>
 * See {@link GLBufferStorage} for generic details.
 * </p>
 * <p>
 * Buffer storage is created via
 * <ul>
 *   <li>{@link GL#glBufferData(int, long, java.nio.Buffer, int)} - storage recreation with target via {@link #createBufferStorage(GLBufferStateTracker, GL, int, long, Buffer, int, int, CreateStorageDispatch, long)}</li>
 *   <li>{@link GL2#glNamedBufferDataEXT(int, long, java.nio.Buffer, int)} - storage recreation, direct, via {@link #createBufferStorage(GL, int, long, Buffer, int, CreateStorageDispatch, long)}</li>
 *   <li>{@link GL4#glNamedBufferData(int, long, java.nio.Buffer, int)} - storage recreation, direct, via {@link #createBufferStorage(GL, int, long, Buffer, int, CreateStorageDispatch, long)}</li>
 *   <li>{@link GL4#glBufferStorage(int, long, Buffer, int)} - storage creation with target via {@link #createBufferStorage(GLBufferStateTracker, GL, int, long, Buffer, int, int, CreateStorageDispatch, long)}</li>
 *   <li>{@link GL4#glNamedBufferStorage(int, long, Buffer, int)} - storage recreation, direct, via {@link #createBufferStorage(GL, int, long, Buffer, int, CreateStorageDispatch, long)}</li>
 * </ul>
 * Note that storage <i>recreation</i> as mentioned above also invalidate a previous storage instance,
 * i.e. disposed the buffer's current storage if exist and attaches a new storage instance.
 * </p>
 * <p>
 * Buffers storage is disposed via
 * <ul>
 *   <li>{@link GL#glDeleteBuffers(int, IntBuffer)} - explicit, direct, via {@link #notifyBuffersDeleted(int, IntBuffer)} or {@link #notifyBuffersDeleted(int, int[], int)}</li>
 *   <li>{@link GL#glBufferData(int, long, java.nio.Buffer, int)} - storage recreation via target</li>
 *   <li>{@link GL2#glNamedBufferDataEXT(int, long, java.nio.Buffer, int)} - storage recreation, direct</li>
 *   <li>{@link GL4#glNamedBufferData(int, long, java.nio.Buffer, int)} - storage recreation, direct</li>
 *   <li>{@link GL4#glBufferStorage(int, long, Buffer, int)} - storage recreation via target</li>
 *   <li>{@link GL4#glNamedBufferStorage(int, long, Buffer, int)} - storage recreation, direct</li>
 * </ul>
 * </p>
 *
 * <p>
 * Implementation throws a {@link GLException} in all <i>construction</i> methods as listed below,
 * if GL-function constraints are not met. <code>createBufferStorage</code> also throws an exception
 * if the native GL-function fails.
 * <ul>
 *   <li>{@link #createBufferStorage(GLBufferStateTracker, GL, int, long, Buffer, int, int, CreateStorageDispatch, long)}, etc ..</li>
 *   <li>{@link #mapBuffer(GLBufferStateTracker, GL, int, int, MapBufferAllDispatch, long)}, etc ..</li>
 * </ul>
 * In <i>destruction</i> and informal methods, i.e. all others, implementation only issues a WARNING debug message, if enabled.
 * </p>
 *
 * <p>
 * Buffer mapping methods like {@link GL#mapBuffer(int, int)} ...,
 * require knowledge of the buffer's storage size as determined via it's creation with
 * {@link GL#glBufferData(int, long, java.nio.Buffer, int)} ....
 * </p>
 * <p>
 * Hence we track the OpenGL buffer's {@link GLBufferStorage} to be able to
 * access the buffer's storage size. The tracked {@link GLBufferStorage} instances
 * also allow users to conveniently access details of their created and maybe mapped buffer storage.
 * </p>
 * <p>
 * The {@link GLBufferObjectTracker} and it's tracked {@link GLBufferStorage} instances
 * maybe shared across multiple OpenGL context, hence this class is thread safe and employs synchronization.
 * </p>
 * <p>
 * Implementation requires and utilizes a local {@link GLBufferStateTracker}
 * to resolve the actual buffer-name bound to the given target.
 * </p>
 * <p>
 * Note: This tracker requires to be notified about all OpenGL buffer storage operations,
 * as well as the local {@link GLBufferStateTracker} to be notified about all
 * OpenGL buffer binding operations.
 * Hence buffer storage cannot be accessed properly if managed via native code.
 * </p>
 */
public class GLBufferObjectTracker {
    protected static final boolean DEBUG;

    static {
        Debug.initSingleton();
        DEBUG = PropertyAccess.isPropertyDefined("jogl.debug.GLBufferObjectTracker", true);
    }

    static final class GLBufferStorageImpl extends GLBufferStorage {
        GLBufferStorageImpl(final int name, final long size, final int mutableUsage, final int immutableFlags) {
            super(name, size, mutableUsage, immutableFlags);
        }
        protected final void reset(final long size, final int mutableUsage, final int immutableFlags) {
            super.reset(size, mutableUsage, immutableFlags);
        }
        protected final void setMappedBuffer(final ByteBuffer buffer) {
            super.setMappedBuffer(buffer);
        }
    }

    /**
     * Map from buffer names to GLBufferObject.
     */
    private final IntObjectHashMap bufferName2StorageMap;

    public GLBufferObjectTracker() {
        bufferName2StorageMap = new IntObjectHashMap();
        bufferName2StorageMap.setKeyNotFoundValue(null);
    }

    public static interface CreateStorageDispatch {
        void create(final int targetOrBufferName, final long size, final Buffer data, final int mutableUsageOrImmutableFlags);
    }

    /**
     * Must be called when [re]creating the GL buffer object via {@link GL#glBufferData(int, long, java.nio.Buffer, int)}
     * and {@link GL4#glBufferStorage(int, long, Buffer, int)},
     * i.e. implies destruction of the buffer.
     *
     * @param bufferStateTracker
     * @param caller
     * @param target
     * @param size
     * @param mutableUsage <code>glBufferData</code>, <code>glNamedBufferData</code> usage
     * @param immutableFlags <code>glBufferStorage</code> flags
     * @throws GLException if buffer is not bound to target
     * @throws GLException if size is less-or-eqaul zero for <code>glBufferStorage</code>, or size is less-than zero otherwise
     * @throws GLException if a native GL-Error occurs
     */
    public synchronized final void createBufferStorage(final GLBufferStateTracker bufferStateTracker, final GL caller,
                                                       final int target, final long size, final Buffer data, final int mutableUsage, final int immutableFlags,
                                                       final CreateStorageDispatch dispatch) throws GLException {
        final int glerrPre = caller.glGetError(); // clear
        if (DEBUG && GL.GL_NO_ERROR != glerrPre) {
            System.err.printf("%s.%s glerr-pre 0x%X%n", msgClazzName, msgCreateBound, glerrPre);
        }
        final int bufferName = bufferStateTracker.getBoundBufferObject(target, caller);
        if ( 0 == bufferName ) {
            throw new GLException(String.format("%s: Buffer for target 0x%X not bound", GL_INVALID_OPERATION, target));
        }
        final boolean mutableBuffer = 0 != mutableUsage;
        final boolean invalidSize = (  mutableBuffer && 0 > size )   // glBufferData, glNamedBufferData
                                 || ( !mutableBuffer && 0 >= size ); // glBufferStorage
        if( invalidSize ) {
            throw new GLException(String.format("%s: Invalid size %d for %s buffer %d on target 0x%X",
                    GL_INVALID_VALUE, size, mutableBuffer ? "mutable" : "immutable", bufferName, target));
        }

        dispatch.create(target, size, data, mutableBuffer ? mutableUsage : immutableFlags);
        final int glerrPost = caller.glGetError(); // be safe, catch failure!
        if(GL.GL_NO_ERROR != glerrPost) {
            throw new GLException(String.format("GL-Error 0x%X while creating %s storage for target 0x%X -> buffer %d of size %d with data %s",
                    glerrPost, mutableBuffer ? "mutable" : "immutable", target, bufferName, size, data));
        }
        final GLBufferStorageImpl objOld = (GLBufferStorageImpl) bufferName2StorageMap.get(bufferName);
        if( null != objOld ) {
            objOld.reset(size, mutableUsage, immutableFlags);
            if (DEBUG) {
                System.err.printf("%s.%s target: 0x%X -> reset %d: %s%n", msgClazzName, msgCreateBound, target, bufferName, objOld);
            }
        } else {
            final GLBufferStorageImpl objNew = new GLBufferStorageImpl(bufferName, size, mutableUsage, immutableFlags);
            bufferName2StorageMap.put(bufferName, objNew);
            if (DEBUG) {
                System.err.printf("%s.%s target: 0x%X -> new %d: %s%n", msgClazzName, msgCreateBound, target, bufferName, objNew);
            }
        }
    }

    /**
     * Must be called when [re]creating the GL buffer object
     * via {@link GL2#glNamedBufferDataEXT(int, long, java.nio.Buffer, int) glNamedBufferDataEXT},
     * {@link GL4#glNamedBufferData(int, long, java.nio.Buffer, int) glNamedBufferData},
     * and {@link GL4#glNamedBufferStorage(int, long, Buffer, int)},
     * i.e. implies destruction of the buffer.
     *
     * @param bufferName
     * @param size
     * @param mutableUsage
     * @throws GLException if size is less-than zero
     * @throws GLException if a native GL-Error occurs
     */
    public synchronized final void createBufferStorage(final GL caller,
                                                       final int bufferName, final long size, final Buffer data, final int mutableUsage, final int immutableFlags,
                                                       final CreateStorageDispatch dispatch) throws GLException {
        final int glerrPre = caller.glGetError(); // clear
        if (DEBUG && GL.GL_NO_ERROR != glerrPre) {
            System.err.printf("%s.%s glerr-pre 0x%X%n", msgClazzName, msgCreateNamed, glerrPre);
        }
        final boolean mutableBuffer = 0 != mutableUsage;
        final boolean invalidSize = (  mutableBuffer && 0 > size )   // glBufferData, glNamedBufferData
                                 || ( !mutableBuffer && 0 >= size ); // glBufferStorage
        if( invalidSize ) {
            throw new GLException(String.format("%s: Invalid size %d for %s buffer %d",
                    GL_INVALID_VALUE, size, mutableBuffer ? "mutable" : "immutable", bufferName));
        }
        dispatch.create(bufferName, size, data, mutableBuffer ? mutableUsage : immutableFlags);
        final int glerrPost = caller.glGetError(); // be safe, catch failure!
        if(GL.GL_NO_ERROR != glerrPost) {
            throw new GLException(String.format("GL-Error 0x%X while creating %s storage for buffer %d of size %d with data %s",
                                                glerrPost, mutableBuffer ? "mutable" : "immutable", bufferName, size, data));
        }
        final GLBufferStorageImpl objOld = (GLBufferStorageImpl) bufferName2StorageMap.get(bufferName);
        if( null != objOld ) {
            objOld.reset(size, mutableUsage, immutableFlags);
            if (DEBUG) {
                System.err.printf("%s.%s direct: reset %d: %s%n", msgClazzName, msgCreateNamed, bufferName, objOld);
            }
        } else {
            final GLBufferStorageImpl objNew = new GLBufferStorageImpl(bufferName, size, mutableUsage, immutableFlags);
            bufferName2StorageMap.put(bufferName, objNew);
            if (DEBUG) {
                System.err.printf("%s.%s direct: new %d: %s%n", msgClazzName, msgCreateNamed, bufferName, objNew);
            }
        }
    }

    /**
     * Must be called when deleting GL buffer objects vis <code>glDeleteBuffers</code>.
     * @param count
     * @param bufferNames
     * @param offset
     */
    public synchronized final void notifyBuffersDeleted(final int count, final int[] bufferNames, final int offset) {
        for(int i=0; i<count; i++) {
            notifyBufferDeleted(bufferNames[i+offset], i, count);
        }
    }
    /**
     * Must be called when deleting GL buffer objects vis <code>glDeleteBuffers</code>.
     * @param n
     * @param bufferNames
     */
    public synchronized final void notifyBuffersDeleted(final int n, final IntBuffer bufferNames) {
        final int offset = bufferNames.position();
        for(int i=0; i<n; i++) {
            notifyBufferDeleted(bufferNames.get(i+offset), i, n);
        }
    }
    /**
     * Must be called when deleting GL buffer objects vis {@link GL#glDeleteBuffers(int, IntBuffer)}.
     * @param bufferName
     * @param i
     * @param count
     */
    private synchronized final void notifyBufferDeleted(final int bufferName, final int i, final int count) {
        final GLBufferStorageImpl objOld = (GLBufferStorageImpl) bufferName2StorageMap.put(bufferName, null);
        if (DEBUG) {
            System.err.printf("%s.notifyBuffersDeleted()[%d/%d]: %d: %s -> null%n", msgClazzName, i+1, count, bufferName, objOld);
        }
        if( null == objOld ) {
            if (DEBUG) {
                System.err.printf("%s: %s.notifyBuffersDeleted()[%d/%d]: Buffer %d not tracked%n", warning, msgClazzName, i+1, count, bufferName);
                ExceptionUtils.dumpStack(System.err);
            }
            return;
        }
        objOld.setMappedBuffer(null);
    }

    public static interface MapBufferDispatch {
        ByteBuffer allocNioByteBuffer(final long addr, final long length);
    }
    public static interface MapBufferRangeDispatch extends MapBufferDispatch {
        long mapBuffer(final int targetOrBufferName, final long offset, final long length, final int access);
    }
    public static interface MapBufferAllDispatch extends MapBufferDispatch {
        long mapBuffer(final int targetOrBufferName, final int access);
    }

    private static final String GL_INVALID_OPERATION = "GL_INVALID_OPERATION";
    private static final String GL_INVALID_VALUE = "GL_INVALID_VALUE";

    /**
     * Must be called when mapping GL buffer objects via {@link GL#mapBuffer(int, int)}.
     * @throws GLException if buffer is not bound to target
     * @throws GLException if buffer is not tracked
     * @throws GLException if buffer is already mapped
     * @throws GLException if buffer has invalid store size, i.e. less-than zero
     */
    public synchronized final GLBufferStorage mapBuffer(final GLBufferStateTracker bufferStateTracker,
                                                        final GL caller, final int target, final int access,
                                                        final MapBufferAllDispatch dispatch) throws GLException {
        return this.mapBufferImpl(bufferStateTracker, caller, target, false /* useRange */, 0 /* offset */, 0 /* length */, access, dispatch);
    }
    /**
     * Must be called when mapping GL buffer objects via {@link GL#mapBufferRange(int, long, long, int)}.
     * @throws GLException if buffer is not bound to target
     * @throws GLException if buffer is not tracked
     * @throws GLException if buffer is already mapped
     * @throws GLException if buffer has invalid store size, i.e. less-than zero
     * @throws GLException if buffer mapping range does not fit, incl. offset
     */
    public synchronized final GLBufferStorage mapBuffer(final GLBufferStateTracker bufferStateTracker,
                                                        final GL caller, final int target, final long offset, final long length, final int access,
                                                        final MapBufferRangeDispatch dispatch) throws GLException {
        return this.mapBufferImpl(bufferStateTracker, caller, target, true /* useRange */, offset, length, access, dispatch);
    }
    /**
     * Must be called when mapping GL buffer objects via {@link GL2#mapNamedBufferEXT(int, int)}.
     * @throws GLException if buffer is not tracked
     * @throws GLException if buffer is already mapped
     * @throws GLException if buffer has invalid store size, i.e. less-than zero
     */
    public synchronized final GLBufferStorage mapBuffer(final int bufferName, final int access, final MapBufferAllDispatch dispatch) throws GLException {
        return this.mapBufferImpl(0 /* target */, bufferName, true /* isNamedBuffer */, false /* useRange */, 0 /* offset */, 0 /* length */, access, dispatch);
    }
    /**
     * Must be called when mapping GL buffer objects via {@link GL2#mapNamedBufferRangeEXT(int, long, long, int)}.
     * @throws GLException if buffer is not tracked
     * @throws GLException if buffer is already mapped
     * @throws GLException if buffer has invalid store size, i.e. less-than zero
     * @throws GLException if buffer mapping range does not fit, incl. offset
     */
    public synchronized final GLBufferStorage mapBuffer(final int bufferName, final long offset, final long length, final int access, final MapBufferRangeDispatch dispatch) throws GLException {
        return this.mapBufferImpl(0 /* target */, bufferName, true /* isNamedBuffer */, true /* useRange */, offset, length, access, dispatch);
    }
    /**
     * @throws GLException if buffer is not bound to target
     * @throws GLException if buffer is not tracked
     * @throws GLException if buffer is already mapped
     * @throws GLException if buffer has invalid store size, i.e. less-than zero
     * @throws GLException if buffer mapping range does not fit, incl. optional offset
     */
    private synchronized final GLBufferStorage mapBufferImpl(final GLBufferStateTracker bufferStateTracker,
                                                             final GL caller, final int target, final boolean useRange,
                                                             final long offset, final long length, final int access,
                                                             final MapBufferDispatch dispatch) throws GLException {
        final int bufferName = bufferStateTracker.getBoundBufferObject(target, caller);
        if( 0 == bufferName ) {
            throw new GLException(String.format("%s.%s: %s Buffer for target 0x%X not bound", msgClazzName, msgMapBuffer, GL_INVALID_OPERATION, target));
        }
        return this.mapBufferImpl(target, bufferName, false /* isNamedBuffer */, useRange, offset, length, access, dispatch);
    }
    /**
     * <p>
     * A zero store size will avoid a native call and returns the unmapped {@link GLBufferStorage}.
     * </p>
     * <p>
     * A null native mapping result indicating an error will
     * not cause a GLException but returns the unmapped {@link GLBufferStorage}.
     * This allows the user to handle this case.
     * </p>
     * @throws GLException if buffer is not tracked
     * @throws GLException if buffer is already mapped
     * @throws GLException if buffer has invalid store size, i.e. less-than zero
     * @throws GLException if buffer mapping range does not fit, incl. optional offset
     */
    private synchronized final GLBufferStorage mapBufferImpl(final int target, final int bufferName, final boolean isNamedBuffer, final boolean useRange, long offset,
                                                             long length, final int access, final MapBufferDispatch dispatch) throws GLException {
        final GLBufferStorageImpl store = (GLBufferStorageImpl)bufferName2StorageMap.get(bufferName);
        if ( null == store ) {
            throw new GLException("Buffer with name "+bufferName+" not tracked");
        }
        if( null != store.getMappedBuffer() ) {
            throw new GLException(String.format("%s.%s: %s Buffer storage of target 0x%X -> %d: %s is already mapped", msgClazzName, msgMapBuffer, GL_INVALID_OPERATION, target, bufferName, store));
        }
        final long storeSize = store.getSize();
        if ( 0 > storeSize ) {
            throw new GLException(String.format("%s.%s: %s Buffer storage of target 0x%X -> %d: %s is of less-than zero", msgClazzName, msgMapBuffer, GL_INVALID_OPERATION, target, bufferName, store));
        }
        if( !useRange ) {
            length = storeSize;
            offset = 0;
        }
        if( length + offset > storeSize ) {
            throw new GLException(String.format("%s.%s: %s Out of range: offset %d, length %d, buffer storage of target 0x%X -> %d: %s", msgClazzName, msgMapBuffer, GL_INVALID_VALUE, offset, length, target, bufferName, store));
        }
        if( 0 >= length || 0 > offset ) {
            throw new GLException(String.format("%s.%s: %s Invalid values: offset %d, length %d, buffer storage of target 0x%X -> %d: %s", msgClazzName, msgMapBuffer, GL_INVALID_VALUE, offset, length, target, bufferName, store));
        }
        if( 0 == storeSize ) {
            return store;
        }
        final long addr;
        if( isNamedBuffer ) {
            if( useRange ) {
                addr = ((MapBufferRangeDispatch)dispatch).mapBuffer(bufferName, offset, length, access);
            } else {
                addr = ((MapBufferAllDispatch)dispatch).mapBuffer(bufferName, access);
            }
        } else {
            if( useRange ) {
                addr = ((MapBufferRangeDispatch)dispatch).mapBuffer(target, offset, length, access);
            } else {
                addr = ((MapBufferAllDispatch)dispatch).mapBuffer(target, access);
            }
        }
        // GL's map-buffer implementation always returns NULL on error,
        // user shall validate the result and the corresponding getGLError() value!
        if ( 0 == addr ) {
            if( DEBUG ) {
                System.err.printf("%s.%s: %s MapBuffer null result for target 0x%X -> %d: %s, off %d, len %d, acc 0x%X%n", msgClazzName, msgMapBuffer, warning, target, bufferName, store, offset, length, access);
                ExceptionUtils.dumpStack(System.err);
            }
            // User shall handle the glError !
        } else {
            final ByteBuffer buffer = dispatch.allocNioByteBuffer(addr, length);
            Buffers.nativeOrder(buffer);
            if( DEBUG ) {
                System.err.printf("%s.%s: Target 0x%X -> %d: %s, off %d, len %d, acc 0x%X%n", msgClazzName, msgClazzName, target, bufferName, store.toString(false), offset, length, access);
            }
            store.setMappedBuffer(buffer);
        }
        return store;
    }

    public static interface UnmapBufferDispatch {
        boolean unmap(final int targetOrBufferName);
    }

    /**
     * Must be called when unmapping GL buffer objects via {@link GL#glUnmapBuffer(int)}.
     * <p>
     * Only clear mapped buffer reference of {@link GLBufferStorage}
     * if native unmapping was successful.
     * </p>
     */
    public synchronized final boolean unmapBuffer(final GLBufferStateTracker bufferStateTracker, final GL caller,
                                                  final int target,
                                                  final UnmapBufferDispatch dispatch) {
        final int bufferName = bufferStateTracker.getBoundBufferObject(target, caller);
        final GLBufferStorageImpl store;
        if( 0 == bufferName ) {
            if (DEBUG) {
                System.err.printf("%s: %s.%s: Buffer for target 0x%X not bound%n", warning, msgClazzName, msgUnmapped, target);
                ExceptionUtils.dumpStack(System.err);
            }
            store = null;
        } else {
            store = (GLBufferStorageImpl) bufferName2StorageMap.get(bufferName);
            if( DEBUG && null == store ) {
                System.err.printf("%s: %s.%s: Buffer %d not tracked%n", warning, msgClazzName, msgUnmapped, bufferName);
                ExceptionUtils.dumpStack(System.err);
            }
        }
        final boolean res = dispatch.unmap(target);
        if( res && null != store ) {
            store.setMappedBuffer(null);
        }
        if( DEBUG ) {
            System.err.printf("%s.%s %s target: 0x%X -> %d: %s%n", msgClazzName, msgUnmapped, res ? "OK" : "Failed", target, bufferName, store.toString(false));
            if(!res) {
                ExceptionUtils.dumpStack(System.err);
            }
        }
        return res;
    }
    /**
     * Must be called when unmapping GL buffer objects via {@link GL2#glUnmapNamedBuffer(int)}.
     * <p>
     * Only clear mapped buffer reference of {@link GLBufferStorage}
     * if native unmapping was successful.
     * </p>
     */
    public synchronized final boolean unmapBuffer(final int bufferName,
                                                  final UnmapBufferDispatch dispatch) {
        final GLBufferStorageImpl store = (GLBufferStorageImpl) bufferName2StorageMap.get(bufferName);
        if (DEBUG && null == store ) {
            System.err.printf("%s: %s.%s: Buffer %d not tracked%n", warning, msgClazzName, msgUnmapped, bufferName);
            ExceptionUtils.dumpStack(System.err);
        }
        final boolean res = dispatch.unmap(bufferName);
        if( res && null != store ) {
            store.setMappedBuffer(null);
        }
        if (DEBUG) {
            System.err.printf("%s.%s %s %d: %s%n", msgClazzName, msgUnmapped, res ? "OK" : "Failed", bufferName, store.toString(false));
            if(!res) {
                ExceptionUtils.dumpStack(System.err);
            }
        }
        return res;
    }

    public synchronized final GLBufferStorage getBufferStorage(final int bufferName) {
        return (GLBufferStorageImpl)bufferName2StorageMap.get(bufferName);
    }

    /**
     * Clear all tracked buffer object knowledge.
     * <p>
     * Shall only be called at GLContext destruction <i>iff</i>
     * there are no other shared GLContext instances left.
     * </p>
     */
    public synchronized final void clear() {
        if (DEBUG) {
          System.err.printf("%s.clear() - Thread %s%n", msgClazzName, Thread.currentThread().getName());
          // ExceptionUtils.dumpStackTrace(System.err);
        }
        bufferName2StorageMap.clear();
    }

    private static final String warning  = "WARNING";
    private static final String msgClazzName = "GLBufferObjectTracker";
    private static final String msgUnmapped = "notifyBufferUnmapped()";
    private static final String msgCreateBound = "createBoundBufferStorage()";
    private static final String msgCreateNamed = "createNamedBufferStorage()";
    private static final String msgMapBuffer = "mapBuffer()";
}
