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

import com.jogamp.common.os.Platform;

import java.util.HashSet;
import java.util.Iterator;
import java.io.PrintStream;

public final class ShaderProgram {

    public ShaderProgram() {
        id = getNextID();
    }

    public boolean linked() {
        return programLinked;
    }

    public boolean inUse() {
        return programInUse;
    }

    /** Returns the shader program name, which is non zero if valid. */
    public int program() { return shaderProgram; }

    /**
     * returns the uniq shader id as an integer
     */
    public int id() { return id; }

    /**
     * Detaches all shader codes and deletes the program.
     * Destroys the shader codes as well.
     * Calls release(gl, true)
     *
     * @see #release(GL2ES2, boolean)
     */
    public synchronized void destroy(final GL2ES2 gl) {
        release(gl, true);
    }

    /**
     * Detaches all shader codes and deletes the program,
     * but leaves the shader code intact.
     * Calls release(gl, false)
     *
     * @see #release(GL2ES2, boolean)
     */
    public synchronized void release(final GL2ES2 gl) {
        release(gl, false);
    }

    /**
     * Detaches all shader codes and deletes the program.
     * If <code>destroyShaderCode</code> is true it destroys the shader codes as well.
     */
    public synchronized void release(final GL2ES2 gl, final boolean destroyShaderCode) {
        if( programLinked ) {
            useProgram(gl, false);
        }
        for(final Iterator<ShaderCode> iter=allShaderCode.iterator(); iter.hasNext(); ) {
            final ShaderCode shaderCode = iter.next();
            if(attachedShaderCode.remove(shaderCode)) {
                ShaderUtil.detachShader(gl, shaderProgram, shaderCode.shader());
            }
            if(destroyShaderCode) {
                shaderCode.destroy(gl);
            }
        }
        allShaderCode.clear();
        attachedShaderCode.clear();
        if( 0 != shaderProgram ) {
            gl.glDeleteProgram(shaderProgram);
            shaderProgram=0;
        }
        programLinked=false;
    }

    //
    // ShaderCode handling
    //

    /**
     * Adds a new shader to this program.
     *
     * <p>This command does not compile and attach the shader,
     * use {@link #add(GL2ES2, ShaderCode)} for this purpose.</p>
     */
    public synchronized void add(final ShaderCode shaderCode) throws GLException {
        allShaderCode.add(shaderCode);
    }

    public synchronized boolean contains(final ShaderCode shaderCode) {
        return allShaderCode.contains(shaderCode);
    }

    /**
     * Warning slow O(n) operation ..
     * @param id
     * @return
     */
    public synchronized ShaderCode getShader(final int id) {
        for(final Iterator<ShaderCode> iter=allShaderCode.iterator(); iter.hasNext(); ) {
            final ShaderCode shaderCode = iter.next();
            if(shaderCode.id() == id) {
                return shaderCode;
            }
        }
        return null;
    }

    //
    // ShaderCode / Program handling
    //

    /**
     * Creates the empty GL program object using {@link GL2ES2#glCreateProgram()},
     * if not already created.
     *
     * @param gl
     * @return true if shader program is valid, i.e. not zero
     */
    public synchronized final boolean init(final GL2ES2 gl) {
        if( 0 == shaderProgram ) {
            shaderProgram = gl.glCreateProgram();
        }
        return 0 != shaderProgram;
    }

    /**
     * Adds a new shader to a this non running program.
     *
     * <p>Compiles and attaches the shader, if not done yet.</p>
     *
     * @return true if the shader was successfully added, false if compilation failed.
     */
    public synchronized boolean add(final GL2ES2 gl, final ShaderCode shaderCode, final PrintStream verboseOut) {
        if( !init(gl) ) { return false; }
        if( allShaderCode.add(shaderCode) ) {
            if( !shaderCode.compile(gl, verboseOut) ) {
                return false;
            }
            if( attachedShaderCode.add(shaderCode) ) {
                ShaderUtil.attachShader(gl, shaderProgram, shaderCode.shader());
            }
        }
        return true;
    }

    /**
     * Replace a shader in a program and re-links the program.
     *
     * @param gl
     * @param oldShader   the to be replace Shader
     * @param newShader   the new ShaderCode
     * @param verboseOut  the optional verbose output stream
     *
     * @return true if all steps are valid, shader compilation, attachment and linking; otherwise false.
     *
     * @see ShaderState#glEnableVertexAttribArray
     * @see ShaderState#glDisableVertexAttribArray
     * @see ShaderState#glVertexAttribPointer
     * @see ShaderState#getVertexAttribPointer
     * @see ShaderState#glReleaseAllVertexAttributes
     * @see ShaderState#glResetAllVertexAttributes
     * @see ShaderState#glResetAllVertexAttributes
     * @see ShaderState#glResetAllVertexAttributes
     */
    public synchronized boolean replaceShader(final GL2ES2 gl, final ShaderCode oldShader, final ShaderCode newShader, final PrintStream verboseOut) {
        if(!init(gl) || !newShader.compile(gl, verboseOut)) {
            return false;
        }

        final boolean shaderWasInUse = inUse();
        if(shaderWasInUse) {
            useProgram(gl, false);
        }

        if(null != oldShader && allShaderCode.remove(oldShader)) {
            if(attachedShaderCode.remove(oldShader)) {
                ShaderUtil.detachShader(gl, shaderProgram, oldShader.shader());
            }
        }

        add(newShader);
        if(attachedShaderCode.add(newShader)) {
            ShaderUtil.attachShader(gl, shaderProgram, newShader.shader());
        }

        gl.glLinkProgram(shaderProgram);

        programLinked = ShaderUtil.isProgramLinkStatusValid(gl, shaderProgram, verboseOut);
        if ( programLinked && shaderWasInUse )  {
            useProgram(gl, true);
        }
        return programLinked;
    }

    /**
     * Links the shader code to the program.
     *
     * <p>Compiles and attaches the shader code to the program if not done by yet</p>
     *
     * <p>Within this process, all GL resources (shader and program objects) are created if necessary.</p>
     *
     * @param gl
     * @param verboseOut
     * @return true if program was successfully linked and is valid, otherwise false
     *
     * @see #init(GL2ES2)
     */
    public synchronized boolean link(final GL2ES2 gl, final PrintStream verboseOut) {
        if( !init(gl) ) {
            programLinked = false; // mark unlinked due to user attempt to [re]link
            return false;
        }

        for(final Iterator<ShaderCode> iter=allShaderCode.iterator(); iter.hasNext(); ) {
            final ShaderCode shaderCode = iter.next();
            if(!shaderCode.compile(gl, verboseOut)) {
                programLinked = false; // mark unlinked due to user attempt to [re]link
                return false;
            }
            if(attachedShaderCode.add(shaderCode)) {
                ShaderUtil.attachShader(gl, shaderProgram, shaderCode.shader());
            }
        }

        // Link the program
        gl.glLinkProgram(shaderProgram);

        programLinked = ShaderUtil.isProgramLinkStatusValid(gl, shaderProgram, verboseOut);

        return programLinked;
    }

    @Override
    public boolean equals(final Object obj) {
        if(this == obj)  { return true; }
        if(obj instanceof ShaderProgram) {
            return id()==((ShaderProgram)obj).id();
        }
        return false;
    }

    @Override
    public int hashCode() {
        return id;
    }

    public StringBuilder toString(StringBuilder sb) {
        if(null == sb) {
            sb = new StringBuilder();
        }
        sb.append("ShaderProgram[id=").append(id);
        sb.append(", linked="+programLinked+", inUse="+programInUse+", program: "+shaderProgram+", "+allShaderCode.size()+" code: ");
        if( 0 < allShaderCode.size() ) {
            for(final Iterator<ShaderCode> iter=allShaderCode.iterator(); iter.hasNext(); ) {
                sb.append(Platform.getNewline()).append("   ").append(iter.next());
            }
        } else {
            sb.append("none");
        }
        sb.append("]");
        return sb;
    }

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

    /**
     * Performs {@link GL2ES2#glValidateProgram(int)} via {@link ShaderUtil#isProgramExecStatusValid(GL, int, PrintStream)}.
     * @see ShaderUtil#isProgramExecStatusValid(GL, int, PrintStream)
     **/
    public synchronized boolean validateProgram(final GL2ES2 gl, final PrintStream verboseOut) {
        return ShaderUtil.isProgramExecStatusValid(gl, shaderProgram, verboseOut);
    }

    public synchronized void useProgram(final GL2ES2 gl, boolean on) {
        if(on && !programLinked) {
            System.err.println("Error: ShaderProgram.useProgram(on "+on+") not linked: "+this);
            throw new GLException("Program is not linked");
        }
        if(programInUse==on) { return; }
        if( 0 == shaderProgram ) {
            on = false;
        }
        gl.glUseProgram( on ? shaderProgram : 0 );
        programInUse = on;
    }
    public synchronized void notifyNotInUse() {
        programInUse = false;
    }

    public void dumpSource(final PrintStream out) {
        out.println();
        out.println(toString());
        for(final Iterator<ShaderCode> iter=allShaderCode.iterator(); iter.hasNext(); ) {
            iter.next().dumpSource(out);
        }
        out.println();
    }

    private boolean programLinked = false;
    private boolean programInUse = false;
    private int shaderProgram = 0; // non zero is valid!
    private final HashSet<ShaderCode> allShaderCode = new HashSet<ShaderCode>();
    private final HashSet<ShaderCode> attachedShaderCode = new HashSet<ShaderCode>();
    private final int id;

    private static synchronized int getNextID() {
        return nextID++;
    }
    private static int nextID = 1;
}

