/*
 * Copyright 2009 Sun Microsystems, Inc. All Rights Reserved.
 * 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 jogamp.opengl.util.glsl.fixedfunc;

import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;

import javax.media.opengl.GL;
import javax.media.opengl.GL2;
import javax.media.opengl.GL2ES1;
import javax.media.opengl.GL2ES2;
import javax.media.opengl.GL2GL3;
import javax.media.opengl.GLArrayData;
import javax.media.opengl.GLES2;
import javax.media.opengl.GLException;
import javax.media.opengl.GLRunnable2;
import javax.media.opengl.GLUniformData;
import javax.media.opengl.fixedfunc.GLLightingFunc;
import javax.media.opengl.fixedfunc.GLPointerFunc;
import javax.media.opengl.fixedfunc.GLPointerFuncUtil;

import jogamp.opengl.Debug;

import com.jogamp.common.nio.Buffers;
import com.jogamp.common.util.IntIntHashMap;
import com.jogamp.common.util.PropertyAccess;
import com.jogamp.opengl.util.PMVMatrix;
import com.jogamp.opengl.util.glsl.ShaderCode;
import com.jogamp.opengl.util.glsl.ShaderProgram;
import com.jogamp.opengl.util.glsl.ShaderState;
import com.jogamp.opengl.util.glsl.fixedfunc.ShaderSelectionMode;

/**
 *
 * <p>
 * Note: Certain GL FFP state values (e.g.: alphaTestFunc and cullFace)
 *       are mapped to a lower number range so they can be stored in low precision storage,
 *       i.e. in a 'lowp int' (GL ES2).
 * </p>
 */
public class FixedFuncPipeline {
    protected static final boolean DEBUG;

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

    /** The maximum texture units which could be used, depending on {@link ShaderSelectionMode}. */
    public static final int MAX_TEXTURE_UNITS = 8;
    public static final int MAX_LIGHTS        = 8;

    public FixedFuncPipeline(final GL2ES2 gl, final ShaderSelectionMode mode, final PMVMatrix pmvMatrix) {
        shaderRootClass = FixedFuncPipeline.class;
        shaderSrcRoot = shaderSrcRootDef;
        shaderBinRoot = shaderBinRootDef;
        vertexColorFile = vertexColorFileDef;
        vertexColorLightFile = vertexColorLightFileDef;
        fragmentColorFile = fragmentColorFileDef;
        fragmentColorTextureFile = fragmentColorTextureFileDef;
        init(gl, mode, pmvMatrix);
    }
    public FixedFuncPipeline(final GL2ES2 gl, final ShaderSelectionMode mode, final PMVMatrix pmvMatrix,
                             final Class<?> shaderRootClass, final String shaderSrcRoot,
                             final String shaderBinRoot,
                             final String vertexColorFile, final String vertexColorLightFile,
                             final String fragmentColorFile, final String fragmentColorTextureFile) {
        this.shaderRootClass = shaderRootClass;
        this.shaderSrcRoot = shaderSrcRoot;
        this.shaderBinRoot = shaderBinRoot;
        this.vertexColorFile = vertexColorFile;
        this.vertexColorLightFile = vertexColorLightFile;
        this.fragmentColorFile = fragmentColorFile;
        this.fragmentColorTextureFile = fragmentColorTextureFile;
        init(gl, mode, pmvMatrix);
    }

    public ShaderSelectionMode getShaderSelectionMode() { return requestedShaderSelectionMode; }
    public void setShaderSelectionMode(final ShaderSelectionMode mode) { requestedShaderSelectionMode=mode; }
    public ShaderSelectionMode getCurrentShaderSelectionMode() { return currentShaderSelectionMode; }

    public boolean verbose() { return verbose; }

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

    public boolean isValid() {
        return shaderState.linked();
    }

    public ShaderState getShaderState() {
        return shaderState;
    }

    public int getActiveTextureUnit() {
        return activeTextureUnit;
    }

    public void destroy(final GL2ES2 gl) {
        if(null != shaderProgramColor) {
            shaderProgramColor.release(gl, true);
        }
        if(null != shaderProgramColorLight) {
            shaderProgramColorLight.release(gl, true);
        }
        if(null != shaderProgramColorTexture2) {
            shaderProgramColorTexture2.release(gl, true);
        }
        if(null != shaderProgramColorTexture4) {
            shaderProgramColorTexture4.release(gl, true);
        }
        if(null != shaderProgramColorTexture4) {
            shaderProgramColorTexture4.release(gl, true);
        }
        if(null != shaderProgramColorTexture8Light) {
            shaderProgramColorTexture8Light.release(gl, true);
        }
        shaderState.destroy(gl);
    }

    //
    // Simple Globals
    //
    public void glColor4f(final GL2ES2 gl, final float red, final float green, final float blue, final float alpha) {
        colorStatic.put(0, red);
        colorStatic.put(1, green);
        colorStatic.put(2, blue);
        colorStatic.put(3, alpha);

        shaderState.useProgram(gl, true);
        final GLUniformData ud = shaderState.getUniform(mgl_ColorStatic);
        if(null!=ud) {
            // same data object ..
            shaderState.uniform(gl, ud);
        } else {
            throw new GLException("Failed to update: mgl_ColorStatic");
        }
    }

    //
    // Arrays / States
    //

    public void glEnableClientState(final GL2ES2 gl, final int glArrayIndex) {
        glToggleClientState(gl, glArrayIndex, true);
    }

    public void glDisableClientState(final GL2ES2 gl, final int glArrayIndex) {
        glToggleClientState(gl, glArrayIndex, false);
    }

    private void glToggleClientState(final GL2ES2 gl, final int glArrayIndex, final boolean enable) {
        final String arrayName = GLPointerFuncUtil.getPredefinedArrayIndexName(glArrayIndex, clientActiveTextureUnit);
        if(null == arrayName) {
            throw new GLException("arrayIndex "+toHexString(glArrayIndex)+" unknown");
        }
        shaderState.useProgram(gl, true);
        if(enable) {
            shaderState.enableVertexAttribArray(gl, arrayName );
        } else {
            shaderState.disableVertexAttribArray(gl, arrayName );
        }
        switch( glArrayIndex ) {
            case GLPointerFunc.GL_TEXTURE_COORD_ARRAY:
                final int enableV = enable ? 1 : 0;
                // enable-bitwise:  textureCoordsEnabled |=  (1 << clientActiveTextureUnit);
                // disable-bitwise: textureCoordsEnabled &= ~(1 << clientActiveTextureUnit);
                if ( textureCoordEnabled.get(clientActiveTextureUnit) != enableV) {
                    textureCoordEnabled.put(clientActiveTextureUnit, enableV);
                    textureCoordEnabledDirty = true;
                }
                break;
            case GLPointerFunc.GL_COLOR_ARRAY:
                colorVAEnabledDirty = true;
                break;
        }
    }

    public void glVertexPointer(final GL2ES2 gl, final GLArrayData data) {
        shaderState.useProgram(gl, true);
        shaderState.vertexAttribPointer(gl, data);
    }

    public void glColorPointer(final GL2ES2 gl, final GLArrayData data) {
        shaderState.useProgram(gl, true);
        shaderState.vertexAttribPointer(gl, data);
    }

    public void glNormalPointer(final GL2ES2 gl, final GLArrayData data) {
        shaderState.useProgram(gl, true);
        shaderState.vertexAttribPointer(gl, data);
    }

    //
    // MULTI-TEXTURE
    //

    /** Enables/Disables the named texture unit (if changed), returns previous state */
    private boolean glEnableTexture(final boolean enable, final int unit) {
        final boolean isEnabled = 0 != ( textureEnabledBits & ( 1 << activeTextureUnit ) );
        if( isEnabled != enable ) {
            if(enable) {
                textureEnabledBits |=  ( 1 << unit );
                textureEnabled.put(unit, 1);
            } else {
                textureEnabledBits &= ~( 1 << unit );
                textureEnabled.put(unit, 0);
            }
            textureEnabledDirty=true;
        }
        return isEnabled;
    }

    public void glClientActiveTexture(int textureUnit) {
        textureUnit -= GL.GL_TEXTURE0;
        if(0 <= textureUnit && textureUnit<MAX_TEXTURE_UNITS) {
            clientActiveTextureUnit = textureUnit;
        } else {
            throw new GLException("glClientActiveTexture textureUnit not within GL_TEXTURE0 + [0.."+MAX_TEXTURE_UNITS+"]: "+textureUnit);
        }
    }

    public void glActiveTexture(int textureUnit) {
        textureUnit -= GL.GL_TEXTURE0;
        if(0 <= textureUnit && textureUnit<MAX_TEXTURE_UNITS) {
            activeTextureUnit = textureUnit;
        } else {
            throw new GLException("glActivateTexture textureUnit not within GL_TEXTURE0 + [0.."+MAX_TEXTURE_UNITS+"]: "+textureUnit);
        }
    }

    public void glTexCoordPointer(final GL2ES2 gl, final GLArrayData data) {
        if( GLPointerFunc.GL_TEXTURE_COORD_ARRAY != data.getIndex() ) {
            throw new GLException("Invalid GLArrayData Index "+toHexString(data.getIndex())+", "+data);
        }
        shaderState.useProgram(gl, true);
        data.setName( GLPointerFuncUtil.getPredefinedArrayIndexName(data.getIndex(), clientActiveTextureUnit) ) ;
        shaderState.vertexAttribPointer(gl, data);
    }

    public void glBindTexture(final int target, final int texture) {
        if(GL.GL_TEXTURE_2D == target) {
            if( texture != boundTextureObject[activeTextureUnit] ) {
                boundTextureObject[activeTextureUnit] = texture;
                textureFormatDirty = true;
            }
        } else {
            System.err.println("FixedFuncPipeline: Unimplemented glBindTexture for target "+toHexString(target)+". Texture name "+toHexString(texture));
        }
    }

    public void glTexImage2D(final int target, /* int level, */ final int internalformat, /*, int width, int height, int border, */
                             final int format /*, int type,  Buffer pixels */) {
        final int ifmt;
        if(GL.GL_TEXTURE_2D == target) {
            switch(internalformat) {
            case 3:
            case GL.GL_RGB:
            case GL.GL_RGB565:
            case GL.GL_RGB8:
            case GL.GL_RGB10:
                ifmt = 3;
                break;
            case 4:
            case GL.GL_RGBA:
            case GL.GL_RGB5_A1:
            case GL.GL_RGBA4:
            case GL.GL_RGBA8:
            case GL.GL_RGB10_A2:
                ifmt = 4;
                break;
            default:
                System.err.println("FixedFuncPipeline: glTexImage2D TEXTURE_2D: Unimplemented internalformat "+toHexString(internalformat));
                ifmt = 4;
                break;
            }
            if( ifmt != texID2Format.put(boundTextureObject[activeTextureUnit], ifmt) ) {
                textureFormatDirty = true;
                // System.err.println("glTexImage2D TEXTURE_2D: internalformat ifmt "+toHexString(internalformat)+" fmt "+toHexString(format)+" -> "+toHexString(ifmt));
            }
        } else {
            System.err.println("FixedFuncPipeline: Unimplemented glTexImage2D: target "+toHexString(target)+", internalformat "+toHexString(internalformat));
        }
    }
    /*
    public void glTexImage2D(int target, int level, int internalformat, int width, int height, int border,
                             int format, int type,  long pixels_buffer_offset) {
        textureFormat.put(activeTextureUnit, internalformat);
        textureFormatDirty = true;
    }*/

    public void glTexEnvi(final int target, final int pname, final int value) {
        if(GL2ES1.GL_TEXTURE_ENV == target && GL2ES1.GL_TEXTURE_ENV_MODE == pname) {
            final int mode;
            switch( value ) {
                case GL2ES1.GL_ADD:
                    mode = 1;
                    break;
                case GL2ES1.GL_MODULATE:
                    mode = 2;
                    break;
                case GL2ES1.GL_DECAL:
                    mode = 3;
                    break;
                case GL.GL_BLEND:
                    mode = 4;
                    break;
                case GL.GL_REPLACE:
                    mode = 5;
                    break;
                case GL2ES1.GL_COMBINE:
                    mode = 2; // FIXME
                    System.err.println("FixedFuncPipeline: glTexEnv GL_TEXTURE_ENV_MODE: unimplemented mode: "+toHexString(value));
                    break;
                default:
                    throw new GLException("glTexEnv GL_TEXTURE_ENV_MODE: invalid mode: "+toHexString(value));
            }
            setTextureEnvMode(mode);
        } else if(verbose) {
            System.err.println("FixedFuncPipeline: Unimplemented TexEnv: target "+toHexString(target)+", pname "+toHexString(pname)+", mode: "+toHexString(value));
        }
    }
    private void setTextureEnvMode(final int value) {
        if( value != textureEnvMode.get(activeTextureUnit) ) {
            textureEnvMode.put(activeTextureUnit, value);
            textureEnvModeDirty = true;
        }
    }
    public void glGetTexEnviv(final int target, final int pname,  final IntBuffer params) { // FIXME
        System.err.println("FixedFuncPipeline: Unimplemented glGetTexEnviv: target "+toHexString(target)+", pname "+toHexString(pname));
    }
    public void glGetTexEnviv(final int target, final int pname,  final int[] params, final int params_offset) { // FIXME
        System.err.println("FixedFuncPipeline: Unimplemented glGetTexEnviv: target "+toHexString(target)+", pname "+toHexString(pname));
    }

    //
    // Point Sprites
    //
    public void glPointSize(final float size) {
        pointParams.put(0, size);
        pointParamsDirty = true;
    }
    public  void glPointParameterf(final int pname, final float param) {
        switch(pname) {
            case GL2ES1.GL_POINT_SIZE_MIN:
                pointParams.put(2, param);
                break;
            case GL2ES1.GL_POINT_SIZE_MAX:
                pointParams.put(3, param);
                break;
            case GL.GL_POINT_FADE_THRESHOLD_SIZE:
                pointParams.put(4+3, param);
                break;
        }
        pointParamsDirty = true;
    }
    public  void glPointParameterfv(final int pname, final float[] params, final int params_offset) {
        switch(pname) {
            case GL2ES1.GL_POINT_DISTANCE_ATTENUATION:
                pointParams.put(4+0, params[params_offset + 0]);
                pointParams.put(4+1, params[params_offset + 1]);
                pointParams.put(4+2, params[params_offset + 2]);
                break;
        }
        pointParamsDirty = true;
    }
    public  void glPointParameterfv(final int pname, final java.nio.FloatBuffer params) {
        final int o = params.position();
        switch(pname) {
            case GL2ES1.GL_POINT_DISTANCE_ATTENUATION:
                pointParams.put(4+0, params.get(o + 0));
                pointParams.put(4+1, params.get(o + 1));
                pointParams.put(4+2, params.get(o + 2));
                break;
        }
        pointParamsDirty = true;
    }

    // private int[] pointTexObj = new int[] { 0 };

    private void glDrawPoints(final GL2ES2 gl, final GLRunnable2<Object,Object> glDrawAction, final Object args) {
        if(gl.isGL2GL3()) {
            gl.glEnable(GL2GL3.GL_VERTEX_PROGRAM_POINT_SIZE);
        }
        if(gl.isGL2ES1()) {
            gl.glEnable(GL2ES1.GL_POINT_SPRITE);
        }
        loadShaderPoints(gl);
        shaderState.attachShaderProgram(gl, shaderProgramPoints, true);
        validate(gl, false); // sync uniforms

        glDrawAction.run(gl, args);

        if(gl.isGL2ES1()) {
            gl.glDisable(GL2ES1.GL_POINT_SPRITE);
        }
        if(gl.isGL2GL3()) {
            gl.glDisable(GL2GL3.GL_VERTEX_PROGRAM_POINT_SIZE);
        }
        shaderState.attachShaderProgram(gl, selectShaderProgram(gl, currentShaderSelectionMode), true);
    }
    private static final GLRunnable2<Object, Object> glDrawArraysAction = new GLRunnable2<Object,Object>() {
        @Override
        public Object run(final GL gl, final Object args) {
            final int[] _args = (int[])args;
            gl.glDrawArrays(GL.GL_POINTS, _args[0], _args[1]);
            return null;
        }
    };
    private final void glDrawPointArrays(final GL2ES2 gl, final int first, final int count) {
        glDrawPoints(gl, glDrawArraysAction, new int[] { first, count });
    }

    //
    // Lighting
    //

    public void glLightfv(final GL2ES2 gl, int light, final int pname, final java.nio.FloatBuffer params) {
        shaderState.useProgram(gl, true);
        light -=GLLightingFunc.GL_LIGHT0;
        if(0 <= light && light < MAX_LIGHTS) {
            GLUniformData ud = null;
            switch(pname) {
                case  GLLightingFunc.GL_AMBIENT:
                    ud = shaderState.getUniform(mgl_LightSource+"["+light+"].ambient");
                    break;
                case  GLLightingFunc.GL_DIFFUSE:
                    ud = shaderState.getUniform(mgl_LightSource+"["+light+"].diffuse");
                    break;
                case  GLLightingFunc.GL_SPECULAR:
                    ud = shaderState.getUniform(mgl_LightSource+"["+light+"].specular");
                    break;
                case GLLightingFunc.GL_POSITION:
                    ud = shaderState.getUniform(mgl_LightSource+"["+light+"].position");
                    break;
                case GLLightingFunc.GL_SPOT_DIRECTION:
                    ud = shaderState.getUniform(mgl_LightSource+"["+light+"].spotDirection");
                    break;
                case GLLightingFunc.GL_SPOT_EXPONENT:
                    ud = shaderState.getUniform(mgl_LightSource+"["+light+"].spotExponent");
                    break;
                case GLLightingFunc.GL_SPOT_CUTOFF:
                    ud = shaderState.getUniform(mgl_LightSource+"["+light+"].spotCutoff");
                    break;
                case GLLightingFunc.GL_CONSTANT_ATTENUATION:
                    ud = shaderState.getUniform(mgl_LightSource+"["+light+"].constantAttenuation");
                    break;
                case GLLightingFunc.GL_LINEAR_ATTENUATION:
                    ud = shaderState.getUniform(mgl_LightSource+"["+light+"].linearAttenuation");
                    break;
                case GLLightingFunc.GL_QUADRATIC_ATTENUATION:
                    ud = shaderState.getUniform(mgl_LightSource+"["+light+"].quadraticAttenuation");
                    break;
                default:
                    throw new GLException("glLightfv invalid pname: "+toHexString(pname));
            }
            if(null!=ud) {
                ud.setData(params);
                shaderState.uniform(gl, ud);
            }
        } else {
            throw new GLException("glLightfv light not within [0.."+MAX_LIGHTS+"]: "+light);
        }
    }

    public void glMaterialfv(final GL2ES2 gl, final int face, final int pname, final java.nio.FloatBuffer params) {
        shaderState.useProgram(gl, true);

        switch (face) {
            case GL.GL_FRONT:
            case GL.GL_FRONT_AND_BACK:
                break;
            case GL.GL_BACK:
                System.err.println("FixedFuncPipeline: Unimplemented glMaterialfv GL_BACK face");
                return;
            default:
        }

        GLUniformData ud = null;
        switch(pname) {
            case  GLLightingFunc.GL_AMBIENT:
                ud = shaderState.getUniform(mgl_FrontMaterial+".ambient");
                break;
            case  GLLightingFunc.GL_AMBIENT_AND_DIFFUSE:
                {
                    ud = shaderState.getUniform(mgl_FrontMaterial+".ambient");
                    if(null!=ud) {
                        ud.setData(params);
                        shaderState.uniform(gl, ud);
                    }
                }
                // fall through intended ..
            case  GLLightingFunc.GL_DIFFUSE:
                ud = shaderState.getUniform(mgl_FrontMaterial+".diffuse");
                break;
            case  GLLightingFunc.GL_SPECULAR:
                ud = shaderState.getUniform(mgl_FrontMaterial+".specular");
                break;
            case  GLLightingFunc.GL_EMISSION:
                ud = shaderState.getUniform(mgl_FrontMaterial+".emission");
                break;
            case  GLLightingFunc.GL_SHININESS:
                ud = shaderState.getUniform(mgl_FrontMaterial+".shininess");
                break;
            default:
                throw new GLException("glMaterialfv invalid pname: "+toHexString(pname));
        }
        if(null!=ud) {
            ud.setData(params);
            shaderState.uniform(gl, ud);
        } else if(verbose) {

        }
    }

    //
    // Misc States
    //

    public void glShadeModel(final GL2ES2 gl, final int mode) {
        shaderState.useProgram(gl, true);
        final GLUniformData ud = shaderState.getUniform(mgl_ShadeModel);
        if(null!=ud) {
            ud.setData(mode);
            shaderState.uniform(gl, ud);
        }
    }

    /** ES2 supports CullFace implicit
    public void glCullFace(int faceName) {
        int _cullFace;
        switch(faceName) {
            case GL.GL_FRONT:
                _cullFace = 1;
                break;
            case GL.GL_BACK:
                _cullFace = 2;
                break;
            case GL.GL_FRONT_AND_BACK:
                _cullFace = 3;
                break;
            default:
                throw new GLException("glCullFace invalid faceName: "+toHexString(faceName));
        }
        if(0 < _cullFace) {
            if(0>cullFace) {
                _cullFace *= -1;
            }
            if(cullFace != _cullFace) {
                cullFace = _cullFace;
                cullFaceDirty=true;
            }
        }
    } */

    public  void glAlphaFunc(final int func, final float ref) {
        int _func;
        switch(func) {
            case GL.GL_NEVER:
                _func = 1;
                break;
            case GL.GL_LESS:
                _func = 2;
                break;
            case GL.GL_EQUAL:
                _func = 3;
                break;
            case GL.GL_LEQUAL:
                _func = 4;
                break;
            case GL.GL_GREATER:
                _func = 5;
                break;
            case GL.GL_NOTEQUAL:
                _func = 6;
                break;
            case GL.GL_GEQUAL:
                _func = 7;
                break;
            case GL.GL_ALWAYS:
                _func = 8;
                break;
            default:
                throw new GLException("glAlphaFunc invalid func: "+toHexString(func));
        }
        if(0 < _func) {
            if(0>alphaTestFunc) {
                _func *= -1;
            }
            if( alphaTestFunc != _func || alphaTestRef != ref ) {
                alphaTestFunc = _func;
                alphaTestRef = ref;
                alphaTestDirty=true;
            }
        }
    }

    /**
     * @return false if digested in regard to GL2ES2 spec,
     *         eg this call must not be passed to an underlying ES2 implementation.
     *         true if this call shall be passed to an underlying GL2ES2/ES2 implementation as well.
     */
    public boolean glEnable(final int cap, final boolean enable) {
        switch(cap) {
            case GL.GL_BLEND:
            case GL.GL_DEPTH_TEST:
            case GL.GL_DITHER:
            case GL.GL_POLYGON_OFFSET_FILL:
            case GL.GL_SAMPLE_ALPHA_TO_COVERAGE:
            case GL.GL_SAMPLE_COVERAGE:
            case GL.GL_SCISSOR_TEST:
            case GL.GL_STENCIL_TEST:
                return true;

            case GL.GL_CULL_FACE:
                /** ES2 supports CullFace implicit
                final int _cullFace;
                if(0>cullFace && enable || 0<cullFace && !enable) {
                    _cullFace = cullFace * -1;
                } else {
                    _cullFace = cullFace;
                }
                if(_cullFace != cullFace) {
                    cullFaceDirty=true;
                    cullFace=_cullFace;
                } */
                return true;

            case GL.GL_TEXTURE_2D:
                glEnableTexture(enable, activeTextureUnit);
                return false;

            case GLLightingFunc.GL_LIGHTING:
                lightingEnabled=enable;
                return false;

            case GL2ES1.GL_ALPHA_TEST:
                final int _alphaTestFunc;
                if(0>alphaTestFunc && enable || 0<alphaTestFunc && !enable) {
                    _alphaTestFunc = alphaTestFunc * -1;
                } else {
                    _alphaTestFunc = alphaTestFunc;
                }
                if(_alphaTestFunc != alphaTestFunc) {
                    alphaTestDirty=true;
                    alphaTestFunc=_alphaTestFunc;
                }
                return false;

            case GL2ES1.GL_POINT_SMOOTH:
                pointParams.put(1, enable ? 1.0f : 0.0f);
                pointParamsDirty = true;
                return false;

            case GL2ES1.GL_POINT_SPRITE:
                // gl_PointCoord always enabled
                return false;
        }

        final int light = cap - GLLightingFunc.GL_LIGHT0;
        if(0 <= light && light < MAX_LIGHTS) {
            if ( (lightsEnabled.get(light)==1) != enable ) {
                lightsEnabled.put(light, enable?1:0);
                lightsEnabledDirty = true;
                return false;
            }
        }
        System.err.println("FixedFunctionPipeline: "+(enable ? "glEnable" : "glDisable")+" "+toHexString(cap)+" not handled in emulation and not supported in ES2");
        return false; // ignore!
    }

    //
    // Draw
    //

    public void glDrawArrays(final GL2ES2 gl, int mode, final int first, final int count) {
        switch(mode) {
            case GL2.GL_QUAD_STRIP:
                mode=GL.GL_TRIANGLE_STRIP;
                break;
            case GL2.GL_POLYGON:
                mode=GL.GL_TRIANGLE_FAN;
                break;
            case GL.GL_POINTS:
                glDrawPointArrays(gl, first, count);
                return;
        }
        validate(gl, true);
        if ( GL2GL3.GL_QUADS == mode && !gl.isGL2() ) {
            for (int j = first; j < count - 3; j += 4) {
                gl.glDrawArrays(GL.GL_TRIANGLE_FAN, j, 4);
            }
        } else {
            gl.glDrawArrays(mode, first, count);
        }
    }
    public void glDrawElements(final GL2ES2 gl, final int mode, final int count, final int type, final java.nio.Buffer indices) {
        validate(gl, true);
        if ( GL2GL3.GL_QUADS == mode && !gl.isGL2() ) {
            final int idx0 = indices.position();

            if( GL.GL_UNSIGNED_BYTE == type ) {
                final ByteBuffer b = (ByteBuffer) indices;
                for (int j = 0; j < count; j++) {
                    gl.glDrawArrays(GL.GL_TRIANGLE_FAN, 0x000000ff & b.get(idx0+j), 4);
                }
            } else if( GL.GL_UNSIGNED_SHORT == type ){
                final ShortBuffer b = (ShortBuffer) indices;
                for (int j = 0; j < count; j++) {
                    gl.glDrawArrays(GL.GL_TRIANGLE_FAN, 0x0000ffff & b.get(idx0+j), 4);
                }
            } else {
                final IntBuffer b = (IntBuffer) indices;
                for (int j = 0; j < count; j++) {
                    gl.glDrawArrays(GL.GL_TRIANGLE_FAN, 0xffffffff & b.get(idx0+j), 4);
                }
            }
        } else {
            // FIXME: Impl. VBO usage .. or unroll (see above)!
            if( !gl.getContext().isCPUDataSourcingAvail() ) {
                throw new GLException("CPU data sourcing n/a w/ "+gl.getContext());
            }
            // if( GL.GL_POINTS != mode ) {
                ((GLES2)gl).glDrawElements(mode, count, type, indices);
            /* } else {
                // FIXME GL_POINTS !
                ((GLES2)gl).glDrawElements(mode, count, type, indices);
            } */
        }
    }
    public void glDrawElements(final GL2ES2 gl, final int mode, final int count, final int type, final long indices_buffer_offset) {
        validate(gl, true);
        if ( GL2GL3.GL_QUADS == mode && !gl.isGL2() ) {
            throw new GLException("Cannot handle indexed QUADS on !GL2 w/ VBO due to lack of CPU index access");
        } else /* if( GL.GL_POINTS != mode ) */ {
            gl.glDrawElements(mode, count, type, indices_buffer_offset);
        } /* else {
            // FIXME GL_POINTS !
            gl.glDrawElements(mode, count, type, indices_buffer_offset);
        } */
    }

    private final int textureEnabledCount() {
        int n=0;
        for(int i=MAX_TEXTURE_UNITS-1; i>=0; i--) {
            if( 0 != textureEnabled.get(i) ) {
                n++;
            }
        }
        return n;
    }

    public void validate(final GL2ES2 gl, final boolean selectShader) {
        if( selectShader ) {
            if( ShaderSelectionMode.AUTO == requestedShaderSelectionMode) {
                final ShaderSelectionMode newMode;

                // pre-validate shader switch
                if( 0 != textureEnabledBits ) {
                    if(lightingEnabled) {
                        newMode = ShaderSelectionMode.COLOR_TEXTURE8_LIGHT_PER_VERTEX;
                    } else {
                        final int n = textureEnabledCount();
                        if( 4 < n ) {
                            newMode = ShaderSelectionMode.COLOR_TEXTURE8;
                        } else if ( 2 < n ) {
                            newMode = ShaderSelectionMode.COLOR_TEXTURE4;
                        } else {
                            newMode = ShaderSelectionMode.COLOR_TEXTURE2;
                        }
                    }
                } else {
                    if(lightingEnabled) {
                        newMode = ShaderSelectionMode.COLOR_LIGHT_PER_VERTEX;
                    } else {
                        newMode = ShaderSelectionMode.COLOR;
                    }
                }
                shaderState.attachShaderProgram(gl, selectShaderProgram(gl, newMode), true); // enables shader-program implicit
            } else {
                shaderState.useProgram(gl, true);
            }
        }

        GLUniformData ud;
        if( pmvMatrix.update() ) {
            ud = shaderState.getUniform(mgl_PMVMatrix);
            if(null!=ud) {
                final FloatBuffer m;
                if(ShaderSelectionMode.COLOR_TEXTURE8_LIGHT_PER_VERTEX == currentShaderSelectionMode ||
                   ShaderSelectionMode.COLOR_LIGHT_PER_VERTEX== currentShaderSelectionMode ) {
                    m = pmvMatrix.glGetPMvMvitMatrixf();
                } else {
                    m = pmvMatrix.glGetPMvMatrixf();
                }
                if(m != ud.getBuffer()) {
                    ud.setData(m);
                }
                // same data object ..
                shaderState.uniform(gl, ud);
            } else {
                throw new GLException("Failed to update: mgl_PMVMatrix");
            }
        }
        if(colorVAEnabledDirty) {
            ud = shaderState.getUniform(mgl_ColorEnabled);
            if(null!=ud) {
                final int ca = true == shaderState.isVertexAttribArrayEnabled(GLPointerFuncUtil.mgl_Color) ? 1 : 0 ;
                if(ca!=ud.intValue()) {
                    ud.setData(ca);
                    shaderState.uniform(gl, ud);
                }
            } else {
                throw new GLException("Failed to update: mgl_ColorEnabled");
            }
            colorVAEnabledDirty = false;
        }
        /** ES2 supports CullFace implicit
        if(cullFaceDirty) {
            ud = shaderState.getUniform(mgl_CullFace);
            if(null!=ud) {
                ud.setData(cullFace);
                shaderState.uniform(gl, ud);
            }
            cullFaceDirty = false;
        } */

        if(alphaTestDirty) {
            ud = shaderState.getUniform(mgl_AlphaTestFunc);
            if(null!=ud) {
                ud.setData(alphaTestFunc);
                shaderState.uniform(gl, ud);
            }
            ud = shaderState.getUniform(mgl_AlphaTestRef);
            if(null!=ud) {
                ud.setData(alphaTestRef);
                shaderState.uniform(gl, ud);
            }
            alphaTestDirty = false;
        }
        if(pointParamsDirty) {
            ud = shaderState.getUniform(mgl_PointParams);
            if(null!=ud) {
                // same data object
                shaderState.uniform(gl, ud);
            }
            pointParamsDirty = false;
        }

        if(lightsEnabledDirty) {
            ud = shaderState.getUniform(mgl_LightsEnabled);
            if(null!=ud) {
                // same data object
                shaderState.uniform(gl, ud);
            }
            lightsEnabledDirty=false;
        }

        if(textureCoordEnabledDirty) {
            ud = shaderState.getUniform(mgl_TexCoordEnabled);
            if(null!=ud) {
                // same data object
                shaderState.uniform(gl, ud);
            }
            textureCoordEnabledDirty=false;
        }

        if(textureEnvModeDirty) {
            ud = shaderState.getUniform(mgl_TexEnvMode);
            if(null!=ud) {
                // same data object
                shaderState.uniform(gl, ud);
            }
            textureEnvModeDirty = false;
        }

        if(textureFormatDirty) {
            for(int i = 0; i<MAX_TEXTURE_UNITS; i++) {
                textureFormat.put(i, texID2Format.get(boundTextureObject[i]));
            }
            ud = shaderState.getUniform(mgl_TexFormat);
            if(null!=ud) {
                // same data object
                shaderState.uniform(gl, ud);
            }
            textureFormatDirty = false;
        }
        if(textureEnabledDirty) {
            ud = shaderState.getUniform(mgl_TextureEnabled);
            if(null!=ud) {
                // same data object
                shaderState.uniform(gl, ud);
            }
            textureEnabledDirty=false;
        }

        if(verbose) {
            System.err.println("validate: "+toString(null, DEBUG).toString());
        }
    }

    public StringBuilder toString(StringBuilder sb, final boolean alsoUnlocated) {
        if(null == sb) {
            sb = new StringBuilder();
        }
        sb.append("FixedFuncPipeline[");
        sb.append(", textureEnabled: "+toHexString(textureEnabledBits)+", "); Buffers.toString(sb, null, textureEnabled);
        sb.append("\n\t, textureCoordEnabled: "); Buffers.toString(sb, null, textureCoordEnabled);
        sb.append("\n\t lightingEnabled: "+lightingEnabled);
        sb.append(", lightsEnabled: "); Buffers.toString(sb, null, lightsEnabled);
        sb.append("\n\t, shaderProgramColor: "+shaderProgramColor);
        sb.append("\n\t, shaderProgramColorTexture2: "+shaderProgramColorTexture2);
        sb.append("\n\t, shaderProgramColorTexture4: "+shaderProgramColorTexture4);
        sb.append("\n\t, shaderProgramColorTexture8: "+shaderProgramColorTexture8);
        sb.append("\n\t, shaderProgramColorLight: "+shaderProgramColorLight);
        sb.append("\n\t, shaderProgramColorTexture8Light: "+shaderProgramColorTexture8Light);
        sb.append("\n\t, ShaderState: ");
        shaderState.toString(sb, alsoUnlocated);
        sb.append("]");
        return sb;
    }
    @Override
    public String toString() {
        return toString(null, DEBUG).toString();
    }

    private static final String constMaxTextures0 = "#define MAX_TEXTURE_UNITS 0\n";
    private static final String constMaxTextures2 = "#define MAX_TEXTURE_UNITS 2\n";
    private static final String constMaxTextures4 = "#define MAX_TEXTURE_UNITS 4\n";
    private static final String constMaxTextures8 = "#define MAX_TEXTURE_UNITS 8\n";

    private final void customizeShader(final GL2ES2 gl, final ShaderCode vp, final ShaderCode fp, final String maxTextureDefine) {
        final int rsVpPos = vp.defaultShaderCustomization(gl, true, true);
        final int rsFpPos = fp.defaultShaderCustomization(gl, true, true);
        vp.insertShaderSource(0, rsVpPos, maxTextureDefine);
        fp.insertShaderSource(0, rsFpPos, maxTextureDefine);
    }

    private final void loadShaderPoints(final GL2ES2 gl) {
        if( null != shaderProgramPoints ) {
            return;
        }

        final ShaderCode vp = ShaderCode.create( gl, GL2ES2.GL_VERTEX_SHADER, shaderRootClass, shaderSrcRoot,
                                         shaderBinRoot, shaderPointFileDef, true);
        final ShaderCode fp = ShaderCode.create( gl, GL2ES2.GL_FRAGMENT_SHADER, shaderRootClass, shaderSrcRoot,
                                           shaderBinRoot, shaderPointFileDef, true);
        customizeShader(gl, vp, fp, constMaxTextures2);
        shaderProgramPoints = new ShaderProgram();
        shaderProgramPoints.add(vp);
        shaderProgramPoints.add(fp);
        if(!shaderProgramPoints.link(gl, System.err)) {
            throw new GLException("Couldn't link VertexColor program: "+shaderProgramPoints);
        }
    }

    private final void loadShader(final GL2ES2 gl, final ShaderSelectionMode mode) {
        final boolean loadColor = ShaderSelectionMode.COLOR == mode;
        final boolean loadColorTexture2 = ShaderSelectionMode.COLOR_TEXTURE2 == mode;
        final boolean loadColorTexture4 = ShaderSelectionMode.COLOR_TEXTURE4 == mode;
        final boolean loadColorTexture8 = ShaderSelectionMode.COLOR_TEXTURE8 == mode;
        final boolean loadColorTexture = loadColorTexture2 || loadColorTexture4 || loadColorTexture8 ;
        final boolean loadColorLightPerVertex = ShaderSelectionMode.COLOR_LIGHT_PER_VERTEX == mode;
        final boolean loadColorTexture8LightPerVertex = ShaderSelectionMode.COLOR_TEXTURE8_LIGHT_PER_VERTEX == mode;

        if( null != shaderProgramColor && loadColor ||
            null != shaderProgramColorTexture2 && loadColorTexture2 ||
            null != shaderProgramColorTexture4 && loadColorTexture4 ||
            null != shaderProgramColorTexture8 && loadColorTexture8 ||
            null != shaderProgramColorLight && loadColorLightPerVertex ||
            null != shaderProgramColorTexture8Light && loadColorTexture8LightPerVertex ) {
            return;
        }

        if( loadColor ) {
            final ShaderCode vp = ShaderCode.create( gl, GL2ES2.GL_VERTEX_SHADER, shaderRootClass, shaderSrcRoot,
                                             shaderBinRoot, vertexColorFile, true);
            final ShaderCode fp = ShaderCode.create( gl, GL2ES2.GL_FRAGMENT_SHADER, shaderRootClass, shaderSrcRoot,
                                               shaderBinRoot, fragmentColorFile, true);
            customizeShader(gl, vp, fp, constMaxTextures0);
            shaderProgramColor = new ShaderProgram();
            shaderProgramColor.add(vp);
            shaderProgramColor.add(fp);
            if(!shaderProgramColor.link(gl, System.err)) {
                throw new GLException("Couldn't link VertexColor program: "+shaderProgramColor);
            }
        } else  if( loadColorTexture ) {
            final ShaderCode vp = ShaderCode.create( gl, GL2ES2.GL_VERTEX_SHADER, shaderRootClass, shaderSrcRoot, shaderBinRoot, vertexColorFile, true);
            final ShaderCode fp = ShaderCode.create( gl, GL2ES2.GL_FRAGMENT_SHADER, shaderRootClass, shaderSrcRoot,
                                                     shaderBinRoot, fragmentColorTextureFile, true);

            if( loadColorTexture2 ) {
                customizeShader(gl, vp, fp, constMaxTextures2);
                shaderProgramColorTexture2 = new ShaderProgram();
                shaderProgramColorTexture2.add(vp);
                shaderProgramColorTexture2.add(fp);
                if(!shaderProgramColorTexture2.link(gl, System.err)) {
                    throw new GLException("Couldn't link VertexColorTexture2 program: "+shaderProgramColorTexture2);
                }
            } else if( loadColorTexture4 ) {
                customizeShader(gl, vp, fp, constMaxTextures4);
                shaderProgramColorTexture4 = new ShaderProgram();
                shaderProgramColorTexture4.add(vp);
                shaderProgramColorTexture4.add(fp);
                if(!shaderProgramColorTexture4.link(gl, System.err)) {
                    throw new GLException("Couldn't link VertexColorTexture4 program: "+shaderProgramColorTexture4);
                }
            } else if( loadColorTexture8 ) {
                customizeShader(gl, vp, fp, constMaxTextures8);
                shaderProgramColorTexture8 = new ShaderProgram();
                shaderProgramColorTexture8.add(vp);
                shaderProgramColorTexture8.add(fp);
                if(!shaderProgramColorTexture8.link(gl, System.err)) {
                    throw new GLException("Couldn't link VertexColorTexture8 program: "+shaderProgramColorTexture8);
                }
            }
        } else if( loadColorLightPerVertex ) {
            final ShaderCode vp = ShaderCode.create( gl, GL2ES2.GL_VERTEX_SHADER, shaderRootClass, shaderSrcRoot,
                                               shaderBinRoot, vertexColorLightFile, true);
            final ShaderCode fp = ShaderCode.create( gl, GL2ES2.GL_FRAGMENT_SHADER, shaderRootClass, shaderSrcRoot,
                                               shaderBinRoot, fragmentColorFile, true);
            customizeShader(gl, vp, fp, constMaxTextures0);
            shaderProgramColorLight = new ShaderProgram();
            shaderProgramColorLight.add(vp);
            shaderProgramColorLight.add(fp);
            if(!shaderProgramColorLight.link(gl, System.err)) {
                throw new GLException("Couldn't link VertexColorLight program: "+shaderProgramColorLight);
            }
        }  else if( loadColorTexture8LightPerVertex ) {
            final ShaderCode vp = ShaderCode.create( gl, GL2ES2.GL_VERTEX_SHADER, shaderRootClass, shaderSrcRoot,
                                               shaderBinRoot, vertexColorLightFile, true);
            final ShaderCode fp = ShaderCode.create( gl, GL2ES2.GL_FRAGMENT_SHADER, shaderRootClass, shaderSrcRoot,
                                                     shaderBinRoot, fragmentColorTextureFile, true);
            customizeShader(gl, vp, fp, constMaxTextures8);
            shaderProgramColorTexture8Light = new ShaderProgram();
            shaderProgramColorTexture8Light.add(vp);
            shaderProgramColorTexture8Light.add(fp);
            if(!shaderProgramColorTexture8Light.link(gl, System.err)) {
                throw new GLException("Couldn't link VertexColorLight program: "+shaderProgramColorTexture8Light);
            }
        }
    }

    private ShaderProgram selectShaderProgram(final GL2ES2 gl, ShaderSelectionMode newMode) {
        if(ShaderSelectionMode.AUTO == newMode) {
            newMode = ShaderSelectionMode.COLOR;
        }
        loadShader(gl, newMode);
        final ShaderProgram sp;
        switch(newMode) {
            case COLOR_LIGHT_PER_VERTEX:
                sp = shaderProgramColorLight;
                break;
            case COLOR_TEXTURE2:
                sp = shaderProgramColorTexture2;
                break;
            case COLOR_TEXTURE4:
                sp = shaderProgramColorTexture4;
                break;
            case COLOR_TEXTURE8:
                sp = shaderProgramColorTexture8;
                break;
            case COLOR_TEXTURE8_LIGHT_PER_VERTEX:
                sp = shaderProgramColorTexture8Light;
                break;
            case COLOR:
            default:
                sp = shaderProgramColor;
        }
        currentShaderSelectionMode = newMode;
        return sp;
    }

    private void init(final GL2ES2 gl, final ShaderSelectionMode mode, final PMVMatrix pmvMatrix) {
        if(null==pmvMatrix) {
            throw new GLException("PMVMatrix is null");
        }
        this.pmvMatrix=pmvMatrix;
        this.requestedShaderSelectionMode = mode;
        this.shaderState=new ShaderState();
        this.shaderState.setVerbose(verbose);

        shaderState.attachShaderProgram(gl, selectShaderProgram(gl, requestedShaderSelectionMode), true);

        // mandatory ..
        if(!shaderState.uniform(gl, new GLUniformData(mgl_PMVMatrix, 4, 4, pmvMatrix.glGetPMvMvitMatrixf()))) {
            throw new GLException("Error setting PMVMatrix in shader: "+this);
        }

        shaderState.uniform(gl, new GLUniformData(mgl_ColorEnabled,  0));
        shaderState.uniform(gl, new GLUniformData(mgl_ColorStatic, 4, colorStatic));

        texID2Format.setKeyNotFoundValue(0);
        shaderState.uniform(gl, new GLUniformData(mgl_TexCoordEnabled,  1, textureCoordEnabled));
        shaderState.uniform(gl, new GLUniformData(mgl_TexEnvMode, 1, textureEnvMode));
        shaderState.uniform(gl, new GLUniformData(mgl_TexFormat, 1, textureFormat));
        shaderState.uniform(gl, new GLUniformData(mgl_TextureEnabled, 1, textureEnabled));
        for(int i=0; i<MAX_TEXTURE_UNITS; i++) {
            shaderState.uniform(gl, new GLUniformData(mgl_Texture+i, i));
        }
        shaderState.uniform(gl, new GLUniformData(mgl_ShadeModel, 0));
        /** ES2 supports CullFace implicit
        shaderState.uniform(gl, new GLUniformData(mgl_CullFace, cullFace)); */
        shaderState.uniform(gl, new GLUniformData(mgl_AlphaTestFunc, alphaTestFunc));
        shaderState.uniform(gl, new GLUniformData(mgl_AlphaTestRef, alphaTestRef));
        shaderState.uniform(gl, new GLUniformData(mgl_PointParams, 4, pointParams));
        for(int i=0; i<MAX_LIGHTS; i++) {
            shaderState.uniform(gl, new GLUniformData(mgl_LightSource+"["+i+"].ambient", 4, defAmbient));
            shaderState.uniform(gl, new GLUniformData(mgl_LightSource+"["+i+"].diffuse", 4, 0==i ? one4f : defDiffuseN));
            shaderState.uniform(gl, new GLUniformData(mgl_LightSource+"["+i+"].specular", 4, 0==i ? one4f : defSpecularN));
            shaderState.uniform(gl, new GLUniformData(mgl_LightSource+"["+i+"].position", 4, defPosition));
            shaderState.uniform(gl, new GLUniformData(mgl_LightSource+"["+i+"].spotDirection", 3, defSpotDir));
            shaderState.uniform(gl, new GLUniformData(mgl_LightSource+"["+i+"].spotExponent", defSpotExponent));
            shaderState.uniform(gl, new GLUniformData(mgl_LightSource+"["+i+"].spotCutoff", defSpotCutoff));
            shaderState.uniform(gl, new GLUniformData(mgl_LightSource+"["+i+"].constantAttenuation", defConstantAtten));
            shaderState.uniform(gl, new GLUniformData(mgl_LightSource+"["+i+"].linearAttenuation", defLinearAtten));
            shaderState.uniform(gl, new GLUniformData(mgl_LightSource+"["+i+"].quadraticAttenuation", defQuadraticAtten));
        }
        shaderState.uniform(gl, new GLUniformData(mgl_LightModel+".ambient", 4, defLightModelAmbient));
        shaderState.uniform(gl, new GLUniformData(mgl_LightsEnabled,  1, lightsEnabled));
        shaderState.uniform(gl, new GLUniformData(mgl_FrontMaterial+".ambient", 4, defMatAmbient));
        shaderState.uniform(gl, new GLUniformData(mgl_FrontMaterial+".diffuse", 4, defMatDiffuse));
        shaderState.uniform(gl, new GLUniformData(mgl_FrontMaterial+".specular", 4, defMatSpecular));
        shaderState.uniform(gl, new GLUniformData(mgl_FrontMaterial+".emission", 4, defMatEmission));
        shaderState.uniform(gl, new GLUniformData(mgl_FrontMaterial+".shininess", defMatShininess));

        shaderState.useProgram(gl, false);
        if(verbose) {
            System.err.println("init: "+toString(null, DEBUG).toString());
        }
    }

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

    protected boolean verbose = DEBUG;

    private final FloatBuffer colorStatic = Buffers.copyFloatBuffer(one4f);

    private int activeTextureUnit=0;
    private int clientActiveTextureUnit=0;
    private final IntIntHashMap texID2Format = new IntIntHashMap();
    private final int[] boundTextureObject = new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }; // per unit
    private int textureEnabledBits = 0;
    private final IntBuffer textureEnabled = Buffers.newDirectIntBuffer(new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }); // per unit
    private boolean textureEnabledDirty = false;
    private final IntBuffer textureCoordEnabled = Buffers.newDirectIntBuffer(new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }); // per unit
    private boolean textureCoordEnabledDirty = false;
    // textureEnvMode: 1 GL_ADD, 2 GL_MODULATE (default), 3 GL_DECAL, 4 GL_BLEND, 5 GL_REPLACE, 6 GL_COMBINE
    private final IntBuffer textureEnvMode = Buffers.newDirectIntBuffer(new int[] { 2, 2, 2, 2, 2, 2, 2, 2 });
    private boolean textureEnvModeDirty = false;
    private final IntBuffer textureFormat = Buffers.newDirectIntBuffer(new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }); // per unit
    private boolean textureFormatDirty = false;

    /** ES2 supports CullFace implicit
    private int cullFace=-2; // <=0 disabled, 1 GL_FRONT, 2 GL_BACK (default) and 3 GL_FRONT_AND_BACK
    private boolean cullFaceDirty = false;
    private static final String mgl_CullFace         = "mgl_CullFace";        //  1i (lowp int) */

    private boolean colorVAEnabledDirty = false;
    private boolean lightingEnabled=false;
    private final IntBuffer lightsEnabled = Buffers.newDirectIntBuffer(new int[] { 0, 0, 0, 0, 0, 0, 0, 0 });
    private boolean   lightsEnabledDirty = false;

    private boolean alphaTestDirty=false;
    private int alphaTestFunc=-8; // <=0 disabled; 1 GL_NEVER, 2 GL_LESS, 3 GL_EQUAL, 4 GL_LEQUAL, 5 GL_GREATER, 6 GL_NOTEQUAL, 7 GL_GEQUAL, and 8 GL_ALWAYS (default)
    private float alphaTestRef=0f;

    private boolean pointParamsDirty = false;
    /** ( pointSize, pointSmooth, attn. pointMinSize, attn. pointMaxSize ) , ( attenuation coefficients 1f 0f 0f, attenuation fade theshold 1f )   */
    private final FloatBuffer pointParams = Buffers.newDirectFloatBuffer(new float[] {  1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f });

    private PMVMatrix pmvMatrix;
    private ShaderState shaderState;
    private ShaderProgram shaderProgramColor;
    private ShaderProgram shaderProgramColorTexture2, shaderProgramColorTexture4, shaderProgramColorTexture8;
    private ShaderProgram shaderProgramColorLight;
    private ShaderProgram shaderProgramColorTexture8Light;
    private ShaderProgram shaderProgramPoints;

    private ShaderSelectionMode requestedShaderSelectionMode = ShaderSelectionMode.AUTO;
    private ShaderSelectionMode currentShaderSelectionMode = requestedShaderSelectionMode;

    // uniforms ..
    private static final String mgl_PMVMatrix        = "mgl_PMVMatrix";       // m4fv[4] - P, Mv, Mvi and Mvit
    private static final String mgl_ColorEnabled     = "mgl_ColorEnabled";    //  1i
    private static final String mgl_ColorStatic      = "mgl_ColorStatic";     //  4fv

    private static final String mgl_LightModel       = "mgl_LightModel";      //  struct mgl_LightModelParameters
    private static final String mgl_LightSource      = "mgl_LightSource";     //  struct mgl_LightSourceParameters[MAX_LIGHTS]
    private static final String mgl_FrontMaterial    = "mgl_FrontMaterial";   //  struct mgl_MaterialParameters
    private static final String mgl_LightsEnabled    = "mgl_LightsEnabled";   //  int mgl_LightsEnabled[MAX_LIGHTS];

    private static final String mgl_AlphaTestFunc    = "mgl_AlphaTestFunc";   //  1i (lowp int)
    private static final String mgl_AlphaTestRef     = "mgl_AlphaTestRef";    //  1f
    private static final String mgl_ShadeModel       = "mgl_ShadeModel";      //  1i
    private static final String mgl_PointParams      = "mgl_PointParams";     //  vec4[2]: { (sz, smooth, attnMinSz, attnMaxSz), (attnCoeff(3), attnFadeTs) }

    private static final String mgl_TextureEnabled   = "mgl_TextureEnabled";  //  int mgl_TextureEnabled[MAX_TEXTURE_UNITS];
    private static final String mgl_Texture          = "mgl_Texture";         //  sampler2D mgl_Texture<0..7>
    private static final String mgl_TexCoordEnabled  = "mgl_TexCoordEnabled"; //  int mgl_TexCoordEnabled[MAX_TEXTURE_UNITS];
    private static final String mgl_TexEnvMode       = "mgl_TexEnvMode";      //  int mgl_TexEnvMode[MAX_TEXTURE_UNITS];
    private static final String mgl_TexFormat        = "mgl_TexFormat";       //  int mgl_TexFormat[MAX_TEXTURE_UNITS];

    // private static final FloatBuffer zero4f = Buffers.newDirectFloatBuffer(new float[] { 0.0f, 0.0f, 0.0f, 0.0f });
    private static final FloatBuffer neut4f = Buffers.newDirectFloatBuffer(new float[] { 0.0f, 0.0f, 0.0f, 1.0f });
    private static final FloatBuffer one4f  = Buffers.newDirectFloatBuffer(new float[] { 1.0f, 1.0f, 1.0f, 1.0f });

    public static final FloatBuffer defAmbient   = neut4f;
    public static final FloatBuffer defDiffuseN  = neut4f;
    public static final FloatBuffer defSpecularN = neut4f;
    public static final FloatBuffer defPosition  = Buffers.newDirectFloatBuffer(new float[] { 0f, 0f, 1f, 0f });
    public static final FloatBuffer defSpotDir   = Buffers.newDirectFloatBuffer(new float[] { 0f, 0f, -1f });
    public static final float defSpotExponent    = 0f;
    public static final float defSpotCutoff      = 180f;
    public static final float defConstantAtten   = 1f;
    public static final float defLinearAtten     = 0f;
    public static final float defQuadraticAtten  = 0f;

    public static final FloatBuffer defLightModelAmbient = Buffers.newDirectFloatBuffer(new float[] { 0.2f, 0.2f, 0.2f, 1.0f });

    public static final FloatBuffer defMatAmbient   = Buffers.newDirectFloatBuffer(new float[] { 0.2f, 0.2f, 0.2f, 1.0f });
    public static final FloatBuffer defMatDiffuse   = Buffers.newDirectFloatBuffer(new float[] { 0.8f, 0.8f, 0.8f, 1.0f });
    public static final FloatBuffer defMatSpecular  = neut4f;
    public static final FloatBuffer defMatEmission  = neut4f;
    public static final float       defMatShininess = 0f;

    private static final String vertexColorFileDef           = "FixedFuncColor";
    private static final String vertexColorLightFileDef      = "FixedFuncColorLight";
    private static final String fragmentColorFileDef         = "FixedFuncColor";
    private static final String fragmentColorTextureFileDef  = "FixedFuncColorTexture";
    private static final String shaderPointFileDef           = "FixedFuncPoints";
    private static final String shaderSrcRootDef             = "shaders" ;
    private static final String shaderBinRootDef             = "shaders/bin" ;

    private final Class<?> shaderRootClass;
    private final String shaderSrcRoot;
    private final String shaderBinRoot;
    private final String vertexColorFile;
    private final String vertexColorLightFile;
    private final String fragmentColorFile;
    private final String fragmentColorTextureFile;
}

