/**
 * Copyright 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:
 *
 *    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 com.jogamp.opengl.util.glsl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import javax.media.opengl.GL;
import javax.media.opengl.GL2ES2;
import javax.media.opengl.GLArrayData;
import javax.media.opengl.GLException;
import javax.media.opengl.GLUniformData;

import jogamp.opengl.Debug;

import com.jogamp.common.os.Platform;
import com.jogamp.common.util.PropertyAccess;
import com.jogamp.opengl.util.GLArrayDataEditable;

/**
 * ShaderState allows to sharing data between shader programs,
 * while updating the attribute and uniform locations when switching.
 * <p>
 * This allows seamless switching of programs using <i>almost</i> same data
 * but performing different artifacts.
 * </p>
 * <p>
 * A {@link #useProgram(GL2ES2, boolean) used} ShaderState is attached to the current GL context
 * and can be retrieved via {@link #getShaderState(GL)}.
 * </p>
 */
public class ShaderState {
    public static final boolean DEBUG;

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

    public ShaderState() {
    }

    public boolean verbose() { return verbose; }

    public void setVerbose(final boolean v) { verbose = DEBUG || v; }

    /**
     * Returns the attached user object for the given name to this ShaderState.
     */
    public final Object getAttachedObject(final String name) {
      return attachedObjectsByString.get(name);
    }

    /**
     * Attach user object for the given name to this ShaderState.
     * Returns the previously set object or null.
     *
     * @return the previous mapped object or null if none
     */
    public final Object attachObject(final String name, final Object obj) {
      return attachedObjectsByString.put(name, obj);
    }

    /**
     * @param name name of the mapped object to detach
     *
     * @return the previous mapped object or null if none
     */
    public final Object detachObject(final String name) {
        return attachedObjectsByString.remove(name);
    }

    /**
     * Turns the shader program on or off.<br>
     *
     * @throws GLException if no program is attached
     *
     * @see com.jogamp.opengl.util.glsl.ShaderState#useProgram(GL2ES2, boolean)
     */
    public synchronized void useProgram(final GL2ES2 gl, final boolean on) throws GLException {
        if(null==shaderProgram) { throw new GLException("No program is attached"); }
        if(on) {
            if(shaderProgram.linked()) {
                shaderProgram.useProgram(gl, true);
                if(resetAllShaderData) {
                    resetAllAttributes(gl);
                    resetAllUniforms(gl);
                }
            } else {
                if(resetAllShaderData) {
                    setAllAttributes(gl);
                }
                if(!shaderProgram.link(gl, System.err)) {
                    throw new GLException("could not link program: "+shaderProgram);
                }
                shaderProgram.useProgram(gl, true);
                if(resetAllShaderData) {
                    resetAllUniforms(gl);
                }
            }
            resetAllShaderData = false;
        } else {
            shaderProgram.useProgram(gl, false);
        }
    }

    public boolean linked() {
        return (null!=shaderProgram)?shaderProgram.linked():false;
    }

    public boolean inUse() {
        return (null!=shaderProgram)?shaderProgram.inUse():false;
    }

    /**
     * Attach or switch a shader program
     *
     * <p>Attaching a shader program the first time,
     * as well as switching to another program on the fly,
     * while managing all attribute and uniform data.</p>
     *
     * <p>[Re]sets all data and use program in case of a program switch.</p>
     *
     * <p>Use program, {@link #useProgram(GL2ES2, boolean)},
     * if <code>enable</code> is <code>true</code>.</p>
     *
     * @return true if shader program was attached, otherwise false (already attached)
     *
     * @throws GLException if program was not linked and linking fails
     */
    public synchronized boolean attachShaderProgram(final GL2ES2 gl, final ShaderProgram prog, final boolean enable) throws GLException {
        if(verbose) {
            final int curId = (null!=shaderProgram)?shaderProgram.id():-1;
            final int newId = (null!=prog)?prog.id():-1;
            System.err.println("ShaderState: attachShaderProgram: "+curId+" -> "+newId+" (enable: "+enable+")\n\t"+shaderProgram+"\n\t"+prog);
            if(DEBUG) {
                Thread.dumpStack();
            }
        }
        if(null!=shaderProgram) {
            if(shaderProgram.equals(prog)) {
                if(enable) {
                    useProgram(gl, true);
                }
                // nothing else to do ..
                if(verbose) {
                    System.err.println("ShaderState: attachShaderProgram: No switch, equal id: "+shaderProgram.id()+", enabling "+enable);
                }
                return false;
            }
            if(shaderProgram.inUse()) {
                if(null != prog && enable) {
                    shaderProgram.notifyNotInUse();
                } else {
                    // no new 'enabled' program - disable
                    useProgram(gl, false);
                }
            }
            resetAllShaderData = true;
        }

        // register new one
        shaderProgram = prog;

        if(null!=shaderProgram) {
            // [re]set all data and use program if switching program,
            // or  use program if program is linked
            if(resetAllShaderData || enable) {
                useProgram(gl, true); // may reset all data
                if(!enable) {
                    useProgram(gl, false);
                }
            }
        }
        if(DEBUG) {
            System.err.println("Info: attachShaderProgram: END");
        }
        return true;
    }

    public ShaderProgram shaderProgram() { return shaderProgram; }

    /**
     * Calls {@link #release(GL2ES2, boolean, boolean, boolean) release(gl, true, true, true)}
     *
     * @see #glReleaseAllVertexAttributes
     * @see #glReleaseAllUniforms
     * @see #release(GL2ES2, boolean, boolean, boolean)
     */
    public synchronized void destroy(final GL2ES2 gl) {
        release(gl, true, true, true);
        attachedObjectsByString.clear();
    }

    /**
     * Calls {@link #release(GL2ES2, boolean, boolean, boolean) release(gl, false, false, false)}
     *
     * @see #glReleaseAllVertexAttributes
     * @see #glReleaseAllUniforms
     * @see #release(GL2ES2, boolean, boolean, boolean)
     */
    public synchronized void releaseAllData(final GL2ES2 gl) {
        release(gl, false, false, false);
    }

    /**
     * @see #glReleaseAllVertexAttributes
     * @see #glReleaseAllUniforms
     * @see ShaderProgram#release(GL2ES2, boolean)
     */
    public synchronized void release(final GL2ES2 gl, final boolean destroyBoundAttributes, final boolean destroyShaderProgram, final boolean destroyShaderCode) {
        if(null!=shaderProgram && shaderProgram.linked() ) {
            shaderProgram.useProgram(gl, false);
        }
        if(destroyBoundAttributes) {
            for(final Iterator<GLArrayData> iter = managedAttributes.iterator(); iter.hasNext(); ) {
                iter.next().destroy(gl);
            }
        }
        releaseAllAttributes(gl);
        releaseAllUniforms(gl);
        if(null!=shaderProgram && destroyShaderProgram) {
            shaderProgram.release(gl, destroyShaderCode);
        }
    }

    //
    // Shader attribute handling
    //

    /**
     * Gets the cached location of a shader attribute.
     *
     * @return -1 if there is no such attribute available,
     *         otherwise >= 0
     *
     * @see #bindAttribLocation(GL2ES2, int, String)
     * @see #bindAttribLocation(GL2ES2, int, GLArrayData)
     * @see #getAttribLocation(GL2ES2, String)
     * @see GL2ES2#glGetAttribLocation(int, String)
     */
    public int getCachedAttribLocation(final String name) {
        final Integer idx = activeAttribLocationMap.get(name);
        return (null!=idx)?idx.intValue():-1;
    }

    /**
     * Get the previous cached vertex attribute data.
     *
     * @return the GLArrayData object, null if not previously set.
     *
     * @see #ownAttribute(GLArrayData, boolean)
     *
     * @see #glEnableVertexAttribArray
     * @see #glDisableVertexAttribArray
     * @see #glVertexAttribPointer
     * @see #getVertexAttribPointer
     * @see #glReleaseAllVertexAttributes
     * @see #glResetAllVertexAttributes
     * @see ShaderProgram#glReplaceShader
     */
    public GLArrayData getAttribute(final String name) {
        return activeAttribDataMap.get(name);
    }

    public boolean isActiveAttribute(final GLArrayData attribute) {
        return attribute == activeAttribDataMap.get(attribute.getName());
    }

    /**
     * Binds or unbinds the {@link GLArrayData} lifecycle to this ShaderState.
     *
     * <p>If an attribute location is cached (ie {@link #bindAttribLocation(GL2ES2, int, String)})
     * it is promoted to the {@link GLArrayData} instance.</p>
     *
     * <p>The attribute will be destroyed with {@link #destroy(GL2ES2)}
     * and it's location will be reset when switching shader with {@link #attachShaderProgram(GL2ES2, ShaderProgram)}.</p>
     *
     * <p>The data will not be transfered to the GPU, use {@link #vertexAttribPointer(GL2ES2, GLArrayData)} additionally.</p>
     *
     * <p>The data will also be {@link GLArrayData#associate(Object, boolean) associated} with this ShaderState.</p>
     *
     * @param attribute the {@link GLArrayData} which lifecycle shall be managed
     * @param own true if <i>owning</i> shall be performs, false if <i>disowning</i>.
     *
     * @see #bindAttribLocation(GL2ES2, int, String)
     * @see #getAttribute(String)
     * @see GLArrayData#associate(Object, boolean)
     */
    public void ownAttribute(final GLArrayData attribute, final boolean own) {
        if(own) {
            final int location = getCachedAttribLocation(attribute.getName());
            if(0<=location) {
                attribute.setLocation(location);
            }
            managedAttributes.add(managedAttributes.size(), attribute);
        } else {
            managedAttributes.remove(attribute);
        }
        attribute.associate(this, own);
    }

    public boolean ownsAttribute(final GLArrayData attribute) {
        return managedAttributes.contains(attribute);
    }

    /**
     * Binds a shader attribute to a location.
     * Multiple names can be bound to one location.
     * The value will be cached and can be retrieved via {@link #getCachedAttribLocation(String)}
     * before or after linking.
     *
     * @throws GLException if no program is attached
     * @throws GLException if the program is already linked
     *
     * @see javax.media.opengl.GL2ES2#glBindAttribLocation(int, int, String)
     * @see #getAttribLocation(GL2ES2, String)
     * @see #getCachedAttribLocation(String)
     */
    public void bindAttribLocation(final GL2ES2 gl, final int location, final String name) {
        if(null==shaderProgram) throw new GLException("No program is attached");
        if(shaderProgram.linked()) throw new GLException("Program is already linked");
        activeAttribLocationMap.put(name, Integer.valueOf(location));
        gl.glBindAttribLocation(shaderProgram.program(), location, name);
    }

    /**
     * Binds a shader {@link GLArrayData} attribute to a location.
     * Multiple names can be bound to one location.
     * The value will be cached and can be retrieved via {@link #getCachedAttribLocation(String)}
     * and {@link #getAttribute(String)}before or after linking.
     * The {@link GLArrayData}'s location will be set as well.
     *
     * @throws GLException if no program is attached
     * @throws GLException if the program is already linked
     *
     * @see javax.media.opengl.GL2ES2#glBindAttribLocation(int, int, String)
     * @see #getAttribLocation(GL2ES2, String)
     * @see #getCachedAttribLocation(String)
     * @see #getAttribute(String)
     */
    public void bindAttribLocation(final GL2ES2 gl, final int location, final GLArrayData data) {
        if(null==shaderProgram) throw new GLException("No program is attached");
        if(shaderProgram.linked()) throw new GLException("Program is already linked");
        final String name = data.getName();
        activeAttribLocationMap.put(name, Integer.valueOf(location));
        data.setLocation(gl, shaderProgram.program(), location);
        activeAttribDataMap.put(data.getName(), data);
    }

    /**
     * Gets the location of a shader attribute with given <code>name</code>.<br>
     * Uses either the cached value {@link #getCachedAttribLocation(String)} if valid,
     * or the GLSL queried via {@link GL2ES2#glGetAttribLocation(int, String)}.<br>
     * The location will be cached.
     *
     * @return -1 if there is no such attribute available,
     *         otherwise >= 0
     * @throws GLException if no program is attached
     * @throws GLException if the program is not linked and no location was cached.
     *
     * @see #getCachedAttribLocation(String)
     * @see #bindAttribLocation(GL2ES2, int, GLArrayData)
     * @see #bindAttribLocation(GL2ES2, int, String)
     * @see GL2ES2#glGetAttribLocation(int, String)
     */
    public int getAttribLocation(final GL2ES2 gl, final String name) {
        if(null==shaderProgram) throw new GLException("No program is attached");
        int location = getCachedAttribLocation(name);
        if(0>location) {
            if(!shaderProgram.linked()) throw new GLException("Program is not linked");
            location = gl.glGetAttribLocation(shaderProgram.program(), name);
            if(0<=location) {
                activeAttribLocationMap.put(name, Integer.valueOf(location));
                if(DEBUG) {
                    System.err.println("ShaderState: glGetAttribLocation: "+name+", loc: "+location);
                }
            } else if(verbose) {
                System.err.println("ShaderState: glGetAttribLocation failed, no location for: "+name+", loc: "+location);
                if(DEBUG) {
                    Thread.dumpStack();
                }
            }
        }
        return location;
    }

    /**
     * Validates and returns the location of a shader attribute.<br>
     * Uses either the cached value {@link #getCachedAttribLocation(String)} if valid,
     * or the GLSL queried via {@link GL2ES2#glGetAttribLocation(int, String)}.<br>
     * The location will be cached and set in the
     * {@link GLArrayData} object.
     *
     * @return -1 if there is no such attribute available,
     *         otherwise >= 0
     *
     * @throws GLException if no program is attached
     * @throws GLException if the program is not linked and no location was cached.
     *
     * @see #getCachedAttribLocation(String)
     * @see #bindAttribLocation(GL2ES2, int, GLArrayData)
     * @see #bindAttribLocation(GL2ES2, int, String)
     * @see GL2ES2#glGetAttribLocation(int, String)
     * @see #getAttribute(String)
     */
    public int getAttribLocation(final GL2ES2 gl, final GLArrayData data) {
        if(null==shaderProgram) throw new GLException("No program is attached");
        final String name = data.getName();
        int location = getCachedAttribLocation(name);
        if(0<=location) {
            data.setLocation(location);
        } else {
            if(!shaderProgram.linked()) throw new GLException("Program is not linked");
            location = data.setLocation(gl, shaderProgram.program());
            if(0<=location) {
                activeAttribLocationMap.put(name, Integer.valueOf(location));
                if(DEBUG) {
                    System.err.println("ShaderState: glGetAttribLocation: "+name+", loc: "+location);
                }
            } else if(verbose) {
                System.err.println("ShaderState: glGetAttribLocation failed, no location for: "+name+", loc: "+location);
                if(DEBUG) {
                    Thread.dumpStack();
                }
            }
        }
        activeAttribDataMap.put(data.getName(), data);
        return location;
    }

    //
    // Enabled Vertex Arrays and its data
    //

    /**
     * @return true if the named attribute is enable
     */
    public final boolean isVertexAttribArrayEnabled(final String name) {
        final Boolean v = activedAttribEnabledMap.get(name);
        return null != v && v.booleanValue();
    }

    /**
     * @return true if the {@link GLArrayData} attribute is enable
     */
    public final boolean isVertexAttribArrayEnabled(final GLArrayData data) {
        return isVertexAttribArrayEnabled(data.getName());
    }

    private boolean enableVertexAttribArray(final GL2ES2 gl, final String name, int location) {
        activedAttribEnabledMap.put(name, Boolean.TRUE);
        if(0>location) {
            location = getAttribLocation(gl, name);
            if(0>location) {
                if(verbose) {
                    System.err.println("ShaderState: glEnableVertexAttribArray failed, no index for: "+name);
                    if(DEBUG) {
                        Thread.dumpStack();
                    }
                }
                return false;
            }
        }
        if(DEBUG) {
            System.err.println("ShaderState: glEnableVertexAttribArray: "+name+", loc: "+location);
        }
        gl.glEnableVertexAttribArray(location);
        return true;
    }

    /**
     * Enables a vertex attribute array.
     *
     * This method retrieves the the location via {@link #getAttribLocation(GL2ES2, GLArrayData)}
     * hence {@link #enableVertexAttribArray(GL2ES2, GLArrayData)} shall be preferred.
     *
     * Even if the attribute is not found in the current shader,
     * it is marked enabled in this state.
     *
     * @return false, if the name is not found, otherwise true
     *
     * @throws GLException if the program is not linked and no location was cached.
     *
     * @see #glEnableVertexAttribArray
     * @see #glDisableVertexAttribArray
     * @see #glVertexAttribPointer
     * @see #getVertexAttribPointer
     */
    public boolean enableVertexAttribArray(final GL2ES2 gl, final String name) {
        return enableVertexAttribArray(gl, name, -1);
    }


    /**
     * Enables a vertex attribute array, usually invoked by {@link GLArrayDataEditable#enableBuffer(GL, boolean)}.
     *
     * This method uses the {@link GLArrayData}'s location if set
     * and is the preferred alternative to {@link #enableVertexAttribArray(GL2ES2, String)}.
     * If data location is unset it will be retrieved via {@link #getAttribLocation(GL2ES2, GLArrayData)} set
     * and cached in this state.
     *
     * Even if the attribute is not found in the current shader,
     * it is marked enabled in this state.
     *
     * @return false, if the name is not found, otherwise true
     *
     * @throws GLException if the program is not linked and no location was cached.
     *
     * @see #glEnableVertexAttribArray
     * @see #glDisableVertexAttribArray
     * @see #glVertexAttribPointer
     * @see #getVertexAttribPointer
     * @see GLArrayDataEditable#enableBuffer(GL, boolean)
     */
    public boolean enableVertexAttribArray(final GL2ES2 gl, final GLArrayData data) {
        if(0 > data.getLocation()) {
            getAttribLocation(gl, data);
        } else {
            // ensure data is the current bound one
            activeAttribDataMap.put(data.getName(), data);
        }
        return enableVertexAttribArray(gl, data.getName(), data.getLocation());
    }

    private boolean disableVertexAttribArray(final GL2ES2 gl, final String name, int location) {
        activedAttribEnabledMap.put(name, Boolean.FALSE);
        if(0>location) {
            location = getAttribLocation(gl, name);
            if(0>location) {
                if(verbose) {
                    System.err.println("ShaderState: glDisableVertexAttribArray failed, no index for: "+name);
                    if(DEBUG) {
                        Thread.dumpStack();
                    }
                }
                return false;
            }
        }
        if(DEBUG) {
            System.err.println("ShaderState: glDisableVertexAttribArray: "+name);
        }
        gl.glDisableVertexAttribArray(location);
        return true;
    }

    /**
     * Disables a vertex attribute array
     *
     * This method retrieves the the location via {@link #getAttribLocation(GL2ES2, GLArrayData)}
     * hence {@link #disableVertexAttribArray(GL2ES2, GLArrayData)} shall be preferred.
     *
     * Even if the attribute is not found in the current shader,
     * it is removed from this state enabled list.
     *
     * @return false, if the name is not found, otherwise true
     *
     * @throws GLException if no program is attached
     * @throws GLException if the program is not linked and no location was cached.
     *
     * @see #glEnableVertexAttribArray
     * @see #glDisableVertexAttribArray
     * @see #glVertexAttribPointer
     * @see #getVertexAttribPointer
     */
    public boolean disableVertexAttribArray(final GL2ES2 gl, final String name) {
        return disableVertexAttribArray(gl, name, -1);
    }

    /**
     * Disables a vertex attribute array
     *
     * This method uses the {@link GLArrayData}'s location if set
     * and is the preferred alternative to {@link #disableVertexAttribArray(GL2ES2, String)}.
     * If data location is unset it will be retrieved via {@link #getAttribLocation(GL2ES2, GLArrayData)} set
     * and cached in this state.
     *
     * Even if the attribute is not found in the current shader,
     * it is removed from this state enabled list.
     *
     * @return false, if the name is not found, otherwise true
     *
     * @throws GLException if no program is attached
     * @throws GLException if the program is not linked and no location was cached.
     *
     * @see #glEnableVertexAttribArray
     * @see #glDisableVertexAttribArray
     * @see #glVertexAttribPointer
     * @see #getVertexAttribPointer
     */
    public boolean disableVertexAttribArray(final GL2ES2 gl, final GLArrayData data) {
        if(0 > data.getLocation()) {
            getAttribLocation(gl, data);
        }
        return disableVertexAttribArray(gl, data.getName(), data.getLocation());
    }

    /**
     * Set the {@link GLArrayData} vertex attribute data, if it's location is valid, i.e. &ge; 0.
     * <p>
     * This method uses the {@link GLArrayData}'s location if valid, i.e. &ge; 0.<br/>
     * If data's location is invalid, it will be retrieved via {@link #getAttribLocation(GL2ES2, GLArrayData)},
     * set and cached in this state.
     * </p>
     *
     * @return false, if the location could not be determined, otherwise true
     *
     * @throws GLException if no program is attached
     * @throws GLException if the program is not linked and no location was cached.
     *
     * @see #glEnableVertexAttribArray
     * @see #glDisableVertexAttribArray
     * @see #glVertexAttribPointer
     * @see #getVertexAttribPointer
     */
    public boolean vertexAttribPointer(final GL2ES2 gl, final GLArrayData data) {
        int location = data.getLocation();
        if(0 > location) {
            location = getAttribLocation(gl, data);
        }
        if(0 <= location) {
            // only pass the data, if the attribute exists in the current shader
            if(DEBUG) {
                System.err.println("ShaderState: glVertexAttribPointer: "+data);
            }
            gl.glVertexAttribPointer(data);
            return true;
        }
        return false;
    }

    /**
     * Releases all mapped vertex attribute data,
     * disables all enabled attributes and loses all indices
     *
     * @see #glEnableVertexAttribArray
     * @see #glDisableVertexAttribArray
     * @see #glVertexAttribPointer
     * @see #getVertexAttribPointer
     * @see #glReleaseAllVertexAttributes
     * @see #glResetAllVertexAttributes
     * @see #glResetAllVertexAttributes
     * @see ShaderProgram#glReplaceShader
     */
    public void releaseAllAttributes(final GL2ES2 gl) {
        if(null!=shaderProgram) {
            for(final Iterator<GLArrayData> iter = activeAttribDataMap.values().iterator(); iter.hasNext(); ) {
                disableVertexAttribArray(gl, iter.next());
            }
            for(final Iterator<String> iter = activedAttribEnabledMap.keySet().iterator(); iter.hasNext(); ) {
                disableVertexAttribArray(gl, iter.next());
            }
        }
        activeAttribDataMap.clear();
        activedAttribEnabledMap.clear();
        activeAttribLocationMap.clear();
        managedAttributes.clear();
    }

    /**
     * Disables all vertex attribute arrays.
     *
     * Their enabled stated will be removed from this state only
     * if 'removeFromState' is true.
     *
     * This method purpose is more for debugging.
     *
     * @see #glEnableVertexAttribArray
     * @see #glDisableVertexAttribArray
     * @see #glVertexAttribPointer
     * @see #getVertexAttribPointer
     * @see #glReleaseAllVertexAttributes
     * @see #glResetAllVertexAttributes
     * @see #glResetAllVertexAttributes
     * @see ShaderProgram#glReplaceShader
     */
    public void disableAllVertexAttributeArrays(final GL2ES2 gl, final boolean removeFromState) {
        for(final Iterator<String> iter = activedAttribEnabledMap.keySet().iterator(); iter.hasNext(); ) {
            final String name = iter.next();
            if(removeFromState) {
                activedAttribEnabledMap.remove(name);
            }
            final int index = getAttribLocation(gl, name);
            if(0<=index) {
                gl.glDisableVertexAttribArray(index);
            }
        }
    }

    private final void relocateAttribute(final GL2ES2 gl, final GLArrayData attribute) {
        // get new location .. note: 'activeAttribLocationMap' is cleared before
        final String name = attribute.getName();
        final int loc = attribute.setLocation(gl, shaderProgram.program());
        if(0<=loc) {
            activeAttribLocationMap.put(name, Integer.valueOf(loc));
            if(DEBUG) {
                System.err.println("ShaderState: relocateAttribute: "+name+", loc: "+loc);
            }
            if(isVertexAttribArrayEnabled(name)) {
                // enable attrib, VBO and pass location/data
                gl.glEnableVertexAttribArray(loc);
            }

            if( attribute.isVBO() ) {
                gl.glBindBuffer(GL.GL_ARRAY_BUFFER, attribute.getVBOName());
                gl.glVertexAttribPointer(attribute);
                gl.glBindBuffer(GL.GL_ARRAY_BUFFER, 0);
            } else {
                gl.glVertexAttribPointer(attribute);
            }
        }
    }

    /**
     * Reset all previously enabled mapped vertex attribute data.
     *
     * <p>
     * Attribute data is bound to the GL state, i.e. VBO data itself will not be updated.
     * </p>
     *
     * <p>
     * Attribute location and it's data assignment is bound to the program,
     * hence both are updated.
     * </p>
     *
     * <p>
     * Note: Such update could only be prevented,
     * if tracking am attribute/program dirty flag.
     * </p>
     *
     * @throws GLException is the program is not linked
     *
     * @see #attachShaderProgram(GL2ES2, ShaderProgram)
     */
    private final void resetAllAttributes(final GL2ES2 gl) {
        if(!shaderProgram.linked()) throw new GLException("Program is not linked");
        activeAttribLocationMap.clear();

        for(int i=0; i<managedAttributes.size(); i++) {
            managedAttributes.get(i).setLocation(-1);
        }
        for(final Iterator<GLArrayData> iter = activeAttribDataMap.values().iterator(); iter.hasNext(); ) {
            relocateAttribute(gl, iter.next());
        }
    }

    private final void setAttribute(final GL2ES2 gl, final GLArrayData attribute) {
        // get new location ..
        final String name = attribute.getName();
        final int loc = attribute.getLocation();

        if(0<=loc) {
            bindAttribLocation(gl, loc, name);

            if(isVertexAttribArrayEnabled(name)) {
                // enable attrib, VBO and pass location/data
                gl.glEnableVertexAttribArray(loc);
            }

            if( attribute.isVBO() ) {
                gl.glBindBuffer(GL.GL_ARRAY_BUFFER, attribute.getVBOName());
                gl.glVertexAttribPointer(attribute);
                gl.glBindBuffer(GL.GL_ARRAY_BUFFER, 0);
            } else {
                gl.glVertexAttribPointer(attribute);
            }
        }
    }

    /**
     * preserves the attribute location .. (program not linked)
     */
    private final void setAllAttributes(final GL2ES2 gl) {
        for(final Iterator<GLArrayData> iter = activeAttribDataMap.values().iterator(); iter.hasNext(); ) {
            setAttribute(gl, iter.next());
        }
    }

    //
    // Shader Uniform handling
    //

    /**
     * Gets the cached location of the shader uniform.
     *
     * @return -1 if there is no such uniform available,
     *         otherwise >= 0
     */
    public final int getCachedUniformLocation(final String name) {
        final Integer idx = activeUniformLocationMap.get(name);
        return (null!=idx)?idx.intValue():-1;
    }

    /**
     * Bind the {@link GLUniform} lifecycle to this ShaderState.
     *
     * <p>If a uniform location is cached it is promoted to the {@link GLUniformData} instance.</p>
     *
     * <p>The attribute will be destroyed with {@link #destroy(GL2ES2)}
     * and it's location will be reset when switching shader with {@link #attachShaderProgram(GL2ES2, ShaderProgram)}.</p>
     *
     * <p>The data will not be transfered to the GPU, use {@link #uniform(GL2ES2, GLUniformData)} additionally.</p>
     *
     * @param uniform the {@link GLUniformData} which lifecycle shall be managed
     *
     * @see #getUniform(String)
     */
    public void ownUniform(final GLUniformData uniform) {
        final int location = getCachedUniformLocation(uniform.getName());
        if(0<=location) {
            uniform.setLocation(location);
        }
        activeUniformDataMap.put(uniform.getName(), uniform);
        managedUniforms.add(uniform);
    }

    public boolean ownsUniform(final GLUniformData uniform) {
        return managedUniforms.contains(uniform);
    }

    /**
     * Gets the location of a shader uniform with given <code>name</code>.<br>
     * Uses either the cached value {@link #getCachedUniformLocation(String)} if valid,
     * or the GLSL queried via {@link GL2ES2#glGetUniformLocation(int, String)}.<br>
     * The location will be cached.
     * <p>
     * The current shader program ({@link #attachShaderProgram(GL2ES2, ShaderProgram)})
     * must be in use ({@link #useProgram(GL2ES2, boolean) }) !</p>
     *
     * @return -1 if there is no such attribute available,
     *         otherwise >= 0

     * @throws GLException is the program is not linked
     *
     * @see #glGetUniformLocation
     * @see javax.media.opengl.GL2ES2#glGetUniformLocation
     * @see #getUniformLocation
     * @see ShaderProgram#glReplaceShader
     */
    public final int getUniformLocation(final GL2ES2 gl, final String name) {
        if(!shaderProgram.inUse()) throw new GLException("Program is not in use");
        int location = getCachedUniformLocation(name);
        if(0>location) {
            if(!shaderProgram.linked()) throw new GLException("Program is not linked");
            location = gl.glGetUniformLocation(shaderProgram.program(), name);
            if(0<=location) {
                activeUniformLocationMap.put(name, Integer.valueOf(location));
            } else if(verbose) {
                System.err.println("ShaderState: glUniform failed, no location for: "+name+", index: "+location);
                if(DEBUG) {
                    Thread.dumpStack();
                }
            }
        }
        return location;
    }

    /**
     * Validates and returns the location of a shader uniform.<br>
     * Uses either the cached value {@link #getCachedUniformLocation(String)} if valid,
     * or the GLSL queried via {@link GL2ES2#glGetUniformLocation(int, String)}.<br>
     * The location will be cached and set in the
     * {@link GLUniformData} object.
     * <p>
     * The current shader program ({@link #attachShaderProgram(GL2ES2, ShaderProgram)})
     * must be in use ({@link #useProgram(GL2ES2, boolean) }) !</p>
     *
     * @return -1 if there is no such attribute available,
     *         otherwise >= 0

     * @throws GLException is the program is not linked
     *
     * @see #glGetUniformLocation
     * @see javax.media.opengl.GL2ES2#glGetUniformLocation
     * @see #getUniformLocation
     * @see ShaderProgram#glReplaceShader
     */
    public int getUniformLocation(final GL2ES2 gl, final GLUniformData data) {
        if(!shaderProgram.inUse()) throw new GLException("Program is not in use");
        final String name = data.getName();
        int location = getCachedUniformLocation(name);
        if(0<=location) {
            data.setLocation(location);
        } else {
            if(!shaderProgram.linked()) throw new GLException("Program is not linked");
            location = data.setLocation(gl, shaderProgram.program());
            if(0<=location) {
                activeUniformLocationMap.put(name, Integer.valueOf(location));
            } else if(verbose) {
                System.err.println("ShaderState: glUniform failed, no location for: "+name+", index: "+location);
                if(DEBUG) {
                    Thread.dumpStack();
                }
            }
        }
        activeUniformDataMap.put(name, data);
        return location;
    }

    /**
     * Set the uniform data, if it's location is valid, i.e. &ge; 0.
     * <p>
     * This method uses the {@link GLUniformData}'s location if valid, i.e. &ge; 0.<br/>
     * If data's location is invalid, it will be retrieved via {@link #getUniformLocation(GL2ES2, GLUniformData)},
     * set and cached in this state.
     * </p>
     *
     * @return false, if the location could not be determined, otherwise true
     *
     * @see #glGetUniformLocation
     * @see javax.media.opengl.GL2ES2#glGetUniformLocation
     * @see javax.media.opengl.GL2ES2#glUniform
     * @see #getUniformLocation
     * @see ShaderProgram#glReplaceShader
     */
    public boolean uniform(final GL2ES2 gl, final GLUniformData data) {
        if(!shaderProgram.inUse()) throw new GLException("Program is not in use");
        int location = data.getLocation();
        if(0>location) {
            location = getUniformLocation(gl, data);
        }
        if(0<=location) {
            // only pass the data, if the uniform exists in the current shader
            if(DEBUG) {
                System.err.println("ShaderState: glUniform: "+data);
            }
            gl.glUniform(data);
            return true;
        }
        return false;
    }

    /**
     * Get the uniform data, previously set.
     *
     * @return the GLUniformData object, null if not previously set.
     */
    public GLUniformData getUniform(final String name) {
        return activeUniformDataMap.get(name);
    }

    /**
     * Releases all mapped uniform data
     * and loses all indices
     */
    public void releaseAllUniforms(final GL2ES2 gl) {
        activeUniformDataMap.clear();
        activeUniformLocationMap.clear();
        managedUniforms.clear();
    }

    /**
     * Reset all previously mapped uniform data
     * <p>
     * Uniform data and location is bound to the program,
     * hence both are updated.
     * </p>
     * <p>
     * Note: Such update could only be prevented,
     * if tracking a uniform/program dirty flag.
     * </p>
     *
     * @throws GLException is the program is not in use
     *
     * @see #attachShaderProgram(GL2ES2, ShaderProgram)
     */
    private final void resetAllUniforms(final GL2ES2 gl) {
        if(!shaderProgram.inUse()) throw new GLException("Program is not in use");
        activeUniformLocationMap.clear();
        for(final Iterator<GLUniformData> iter = managedUniforms.iterator(); iter.hasNext(); ) {
            iter.next().setLocation(-1);
        }
        for(final Iterator<GLUniformData> iter = activeUniformDataMap.values().iterator(); iter.hasNext(); ) {
            final GLUniformData data = iter.next();
            final int loc = data.setLocation(gl, shaderProgram.program());
            if( 0 <= loc ) {
                // only pass the data, if the uniform exists in the current shader
                activeUniformLocationMap.put(data.getName(), Integer.valueOf(loc));
                if(DEBUG) {
                    System.err.println("ShaderState: resetAllUniforms: "+data);
                }
                gl.glUniform(data);
            }
        }
    }

    public StringBuilder toString(StringBuilder sb, final boolean alsoUnlocated) {
        if(null==sb) {
            sb = new StringBuilder();
        }

        sb.append("ShaderState[ ");

        sb.append(Platform.getNewline()).append(" ");
        if(null != shaderProgram) {
            shaderProgram.toString(sb);
        } else {
            sb.append("ShaderProgram: null");
        }
        sb.append(Platform.getNewline()).append(" enabledAttributes [");
        {
            final Iterator<String> names = activedAttribEnabledMap.keySet().iterator();
            final Iterator<Boolean> values = activedAttribEnabledMap.values().iterator();
            while( names.hasNext() ) {
                sb.append(Platform.getNewline()).append("  ").append(names.next()).append(": ").append(values.next());
            }
        }
        sb.append(Platform.getNewline()).append(" ],").append(" activeAttributes [");
        for(final Iterator<GLArrayData> iter = activeAttribDataMap.values().iterator(); iter.hasNext(); ) {
            final GLArrayData ad = iter.next();
            if( alsoUnlocated || 0 <= ad.getLocation() ) {
                sb.append(Platform.getNewline()).append("  ").append(ad);
            }
        }
        sb.append(Platform.getNewline()).append(" ],").append(" managedAttributes [");
        for(final Iterator<GLArrayData> iter = managedAttributes.iterator(); iter.hasNext(); ) {
            final GLArrayData ad = iter.next();
            if( alsoUnlocated || 0 <= ad.getLocation() ) {
                sb.append(Platform.getNewline()).append("  ").append(ad);
            }
        }
        sb.append(Platform.getNewline()).append(" ],").append(" activeUniforms [");
        for(final Iterator<GLUniformData> iter=activeUniformDataMap.values().iterator(); iter.hasNext(); ) {
            final GLUniformData ud = iter.next();
            if( alsoUnlocated || 0 <= ud.getLocation() ) {
                sb.append(Platform.getNewline()).append("  ").append(ud);
            }
        }
        sb.append(Platform.getNewline()).append(" ],").append(" managedUniforms [");
        for(final Iterator<GLUniformData> iter = managedUniforms.iterator(); iter.hasNext(); ) {
            final GLUniformData ud = iter.next();
            if( alsoUnlocated || 0 <= ud.getLocation() ) {
                sb.append(Platform.getNewline()).append("  ").append(ud);
            }
        }
        sb.append(Platform.getNewline()).append(" ]").append(Platform.getNewline()).append("]");
        return sb;
    }

    @Override
    public String toString() {
        return toString(null, DEBUG).toString();
    }

    private boolean verbose = DEBUG;
    private ShaderProgram shaderProgram=null;

    private final HashMap<String, Boolean> activedAttribEnabledMap = new HashMap<String, Boolean>();
    private final HashMap<String, Integer> activeAttribLocationMap = new HashMap<String, Integer>();
    private final HashMap<String, GLArrayData> activeAttribDataMap = new HashMap<String, GLArrayData>();
    private final ArrayList<GLArrayData> managedAttributes = new ArrayList<GLArrayData>();

    private final HashMap<String, Integer> activeUniformLocationMap = new HashMap<String, Integer>();
    private final HashMap<String, GLUniformData> activeUniformDataMap = new HashMap<String, GLUniformData>();
    private final ArrayList<GLUniformData> managedUniforms = new ArrayList<GLUniformData>();

    private final HashMap<String, Object> attachedObjectsByString = new HashMap<String, Object>();
    private boolean resetAllShaderData = false;
}

