/*
 * Copyright (c) 2009-2012 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * 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.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "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 THE COPYRIGHT OWNER 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.
 */
package com.jme3.material;

import com.jme3.material.logic.TechniqueDefLogic;
import com.jme3.asset.AssetManager;
import com.jme3.export.*;
import com.jme3.renderer.Caps;
import com.jme3.shader.*;
import com.jme3.shader.Shader.ShaderType;

import java.io.IOException;
import java.util.*;

/**
 * Describes a technique definition.
 *
 * @author Kirill Vainer
 */
public class TechniqueDef implements Savable {

    /**
     * Version #1: Separate shader language for each shader source.
     */
    public static final int SAVABLE_VERSION = 1;

    /**
     * The default technique name.
     *
     * The technique with this name is selected if no specific technique is
     * requested by the user. Currently set to "Default".
     */
    public static final String DEFAULT_TECHNIQUE_NAME = "Default";

    /**
     * Describes light rendering mode.
     */
    public enum LightMode {
        /**
         * Disable light-based rendering
         */
        Disable,

        /**
         * Enable light rendering by using a single pass.
         * <p>
         * An array of light positions and light colors is passed to the shader
         * containing the world light list for the geometry being rendered.
         */
        SinglePass,

        /**
         * Enable light rendering by using multi-pass rendering.
         * <p>
         * The geometry will be rendered once for each light. Each time the
         * light position and light color uniforms are updated to contain
         * the values for the current light. The ambient light color uniform
         * is only set to the ambient light color on the first pass, future
         * passes have it set to black.
         */
        MultiPass,

        /**
         * @deprecated OpenGL1 is not supported anymore
         */
        @Deprecated
        FixedPipeline,
    }

    public enum ShadowMode {
        Disable,
        InPass,
        PostPass,
    }

    private final EnumSet<Caps> requiredCaps = EnumSet.noneOf(Caps.class);
    private String name;
    private int sortId;
    
    private EnumMap<Shader.ShaderType,String> shaderLanguages;
    private EnumMap<Shader.ShaderType,String> shaderNames;

    private String shaderPrologue;
    private ArrayList<String> defineNames;
    private ArrayList<VarType> defineTypes;
    private HashMap<String, Integer> paramToDefineId;
    private final HashMap<DefineList, Shader> definesToShaderMap;
    
    private boolean usesNodes = false;
    private List<ShaderNode> shaderNodes;
    private ShaderGenerationInfo shaderGenerationInfo;

    private boolean noRender = false;
    private RenderState renderState;
    private RenderState forcedRenderState;

    private LightMode lightMode = LightMode.Disable;
    private ShadowMode shadowMode = ShadowMode.Disable;
    private TechniqueDefLogic logic;

    private ArrayList<UniformBinding> worldBinds;

    /**
     * Creates a new technique definition.
     * <p>
     * Used internally by the J3M/J3MD loader.
     *
     * @param name The name of the technique
     */
    public TechniqueDef(String name, int sortId){
        this();
        this.sortId = sortId;
        this.name = name;
    }

    /**
     * Serialization only. Do not use.
     */
    public TechniqueDef() {
        shaderLanguages = new EnumMap<Shader.ShaderType, String>(Shader.ShaderType.class);
        shaderNames = new EnumMap<Shader.ShaderType, String>(Shader.ShaderType.class);
        defineNames = new ArrayList<String>();
        defineTypes = new ArrayList<VarType>();
        paramToDefineId = new HashMap<String, Integer>();
        definesToShaderMap = new HashMap<DefineList, Shader>();
    }
    
    /**
     * @return A unique sort ID. 
     * No other technique definition can have the same ID.
     */
    public int getSortId() {
        return sortId;
    }

    /**
     * Returns the name of this technique as specified in the J3MD file.
     * Default
     * techniques have the name {@link #DEFAULT_TECHNIQUE_NAME}.
     *
     * @return the name of this technique
     */
    public String getName(){
        return name;
    }

    /**
     * Returns the light mode.
     * @return the light mode.
     * @see LightMode
     */
    public LightMode getLightMode() {
        return lightMode;
    }

    /**
     * Set the light mode
     *
     * @param lightMode the light mode
     *
     * @see LightMode
     */
    public void setLightMode(LightMode lightMode) {
        this.lightMode = lightMode;
    }
    
    public void setLogic(TechniqueDefLogic logic) {
        this.logic = logic;
    }

    public TechniqueDefLogic getLogic() {
        return logic;
    }
    
    /**
     * Returns the shadow mode.
     * @return the shadow mode.
     */
    public ShadowMode getShadowMode() {
        return shadowMode;
    }

    /**
     * Set the shadow mode.
     *
     * @param shadowMode the shadow mode.
     *
     * @see ShadowMode
     */
    public void setShadowMode(ShadowMode shadowMode) {
        this.shadowMode = shadowMode;
    }

    /**
     * Returns the render state that this technique is using
     * @return the render state that this technique is using
     * @see #setRenderState(com.jme3.material.RenderState)
     */
    public RenderState getRenderState() {
        return renderState;
    }

    /**
     * Sets the render state that this technique is using.
     *
     * @param renderState the render state that this technique is using.
     *
     * @see RenderState
     */
    public void setRenderState(RenderState renderState) {
        this.renderState = renderState;
    }

    /**
     * Sets if this technique should not be used to render.
     *
     * @param noRender not render or render ?
     *
     * @see NoRender
     */
    public void setNoRender(boolean noRender) {
        this.noRender = noRender;
    }

    /**
     * Returns true if this technique should not be used to render.
     * (eg. to not render a material with default technique)
     *
     * @return true if this technique should not be rendered, false otherwise.
     *
     */
    public boolean isNoRender(){
        return noRender;
    }

    /**
     * Returns true if this technique uses Shader Nodes, false otherwise.
     *
     * @return true if this technique uses Shader Nodes, false otherwise.
     *
     */
    public boolean isUsingShaderNodes(){
        return usesNodes;
    }

    /**
     * Gets the {@link Caps renderer capabilities} that are required
     * by this technique.
     *
     * @return the required renderer capabilities
     */
    public EnumSet<Caps> getRequiredCaps() {
        return requiredCaps;
    }

    /**
     * Sets the shaders that this technique definition will use.
     *
     * @param vertexShader The name of the vertex shader
     * @param fragmentShader The name of the fragment shader
     * @param vertLanguage The vertex shader language
     * @param fragLanguage The fragment shader language
     */
    public void setShaderFile(String vertexShader, String fragmentShader, String vertLanguage, String fragLanguage) {
        this.shaderLanguages.put(Shader.ShaderType.Vertex, vertLanguage);
        this.shaderNames.put(Shader.ShaderType.Vertex, vertexShader);
        this.shaderLanguages.put(Shader.ShaderType.Fragment, fragLanguage);
        this.shaderNames.put(Shader.ShaderType.Fragment, fragmentShader);

        requiredCaps.clear();
        Caps vertCap = Caps.valueOf(vertLanguage);
        requiredCaps.add(vertCap);
        Caps fragCap = Caps.valueOf(fragLanguage);
        requiredCaps.add(fragCap);
    }

    /**
     * Set a string which is prepended to every shader used by this technique.
     * 
     * Typically this is used for preset defines.
     * 
     * @param shaderPrologue The prologue to append before the technique's shaders.
     */
    public void setShaderPrologue(String shaderPrologue) {
        this.shaderPrologue = shaderPrologue;
    }
    
    /**
     * @return the shader prologue which is prepended to every shader.
     */
    public String getShaderPrologue() {
        return shaderPrologue;
    }
    
    /**
     * Returns the define name which the given material parameter influences.
     *
     * @param paramName The parameter name to look up
     * @return The define name
     *
     * @see #addShaderParamDefine(java.lang.String, java.lang.String)
     */
    public String getShaderParamDefine(String paramName){
        Integer defineId = paramToDefineId.get(paramName);
        if (defineId != null) {
            return defineNames.get(defineId);
        } else {
            return null;
        }
    }
    
    /**
     * Get the define ID for a given material parameter.
     *
     * @param paramName The parameter name to look up
     * @return The define ID, or null if not found.
     */
    public Integer getShaderParamDefineId(String paramName) {
        return paramToDefineId.get(paramName);
    }

    /**
     * Get the type of a particular define.
     *
     * @param defineId The define ID to lookup.
     * @return The type of the define, or null if not found.
     */
    public VarType getDefineIdType(int defineId) {
        return defineId < defineTypes.size() ? defineTypes.get(defineId) : null;
    }
    
    /**
     * Adds a define linked to a material parameter.
     * <p>
     * Any time the material parameter on the parent material is altered,
     * the appropriate define on the technique will be modified as well.
     * When set, the material parameter will be mapped to an integer define, 
     * typically <code>1</code> if it is set, unless it is an integer or a float,
     * in which case it will converted into an integer.
     *
     * @param paramName The name of the material parameter to link to.
     * @param paramType The type of the material parameter to link to.
     * @param defineName The name of the define parameter, e.g. USE_LIGHTING
     */
    public void addShaderParamDefine(String paramName, VarType paramType, String defineName){
        int defineId = defineNames.size();
        
        if (defineId >= DefineList.MAX_DEFINES) {
            throw new IllegalStateException("Cannot have more than " + 
                    DefineList.MAX_DEFINES + " defines on a technique.");
        }
        
        paramToDefineId.put(paramName, defineId);
        defineNames.add(defineName);
        defineTypes.add(paramType);
    }

    /**
     * Add an unmapped define which can only be set by define ID.
     * 
     * Unmapped defines are used by technique renderers to 
     * configure the shader internally before rendering.
     * 
     * @param defineName The define name to create
     * @return The define ID of the created define
     */
    public int addShaderUnmappedDefine(String defineName, VarType defineType) {
        int defineId = defineNames.size();
        
        if (defineId >= DefineList.MAX_DEFINES) {
            throw new IllegalStateException("Cannot have more than " + 
                    DefineList.MAX_DEFINES + " defines on a technique.");
        }
        
        defineNames.add(defineName);
        defineTypes.add(defineType);
        return defineId;
    }

    /**
     * Get the names of all defines declared on this technique definition.
     *
     * The defines are returned in order of declaration.
     *
     * @return the names of all defines declared.
     */
    public String[] getDefineNames() {
        return defineNames.toArray(new String[0]);
    }

    /**
     * Get the types of all defines declared on this technique definition.
     *
     * The types are returned in order of declaration.
     *
     * @return the types of all defines declared.
     */
    public VarType[] getDefineTypes() {
        return defineTypes.toArray(new VarType[0]);
    }
    
    /**
     * Create a define list with the size matching the number
     * of defines on this technique.
     * 
     * @return a define list with the size matching the number
     * of defines on this technique.
     */
    public DefineList createDefineList() {
        return new DefineList(defineNames.size());
    }
    
    private Shader loadShader(AssetManager assetManager, EnumSet<Caps> rendererCaps, DefineList defines) {
        StringBuilder sb = new StringBuilder();
        sb.append(shaderPrologue);
        defines.generateSource(sb, defineNames, defineTypes);
        String definesSourceCode = sb.toString();

        Shader shader;
        if (isUsingShaderNodes()) {
            ShaderGenerator shaderGenerator = assetManager.getShaderGenerator(rendererCaps);
            if (shaderGenerator == null) {
                throw new UnsupportedOperationException("ShaderGenerator was not initialized, "
                        + "make sure assetManager.getGenerator(caps) has been called");
            }
            shaderGenerator.initialize(this);
            shader = shaderGenerator.generateShader(definesSourceCode);
        } else {
            shader = new Shader();
            for (ShaderType type : ShaderType.values()) {
                String language = shaderLanguages.get(type);
                String shaderSourceAssetName = shaderNames.get(type);
                if (language == null || shaderSourceAssetName == null) {
                    continue;
                }
                String shaderSourceCode = (String) assetManager.loadAsset(shaderSourceAssetName);
                shader.addSource(type, shaderSourceAssetName, shaderSourceCode, definesSourceCode, language);
            }
        }

        if (getWorldBindings() != null) {
           for (UniformBinding binding : getWorldBindings()) {
               shader.addUniformBinding(binding);
           }
        }
        
        return shader;
    }
    
    public Shader getShader(AssetManager assetManager, EnumSet<Caps> rendererCaps, DefineList defines) {
          Shader shader = definesToShaderMap.get(defines);
          if (shader == null) {
              shader = loadShader(assetManager, rendererCaps, defines);
              definesToShaderMap.put(defines.deepClone(), shader);
          }
          return shader;
     }
    
    /**
     * Sets the shaders that this technique definition will use.
     *
     * @param shaderNames EnumMap containing all shader names for this stage
     * @param shaderLanguages EnumMap containing all shader languages for this stage
     */
    public void setShaderFile(EnumMap<Shader.ShaderType, String> shaderNames, EnumMap<Shader.ShaderType, String> shaderLanguages) {
        requiredCaps.clear();

        for (Shader.ShaderType shaderType : shaderNames.keySet()) {
            String language = shaderLanguages.get(shaderType);
            String shaderFile = shaderNames.get(shaderType);

            this.shaderLanguages.put(shaderType, language);
            this.shaderNames.put(shaderType, shaderFile);

            Caps vertCap = Caps.valueOf(language);
            requiredCaps.add(vertCap);

            if (shaderType.equals(Shader.ShaderType.Geometry)) {
                requiredCaps.add(Caps.GeometryShader);
            } else if (shaderType.equals(Shader.ShaderType.TessellationControl)) {
                requiredCaps.add(Caps.TesselationShader);
            }
        }
    }

    /**
     * Returns the name of the fragment shader used by the technique, or null
     * if no fragment shader is specified.
     *
     * @return the name of the fragment shader to be used.
     */
    public String getFragmentShaderName() {
        return shaderNames.get(Shader.ShaderType.Fragment);
    }


    /**
     * Returns the name of the vertex shader used by the technique, or null
     * if no vertex shader is specified.
     *
     * @return the name of the vertex shader to be used.
     */
    public String getVertexShaderName() {
        return shaderNames.get(Shader.ShaderType.Vertex);
    }

    /**
     * Returns the language of the fragment shader used in this technique.
     */
    public String getFragmentShaderLanguage() {
        return shaderLanguages.get(Shader.ShaderType.Fragment);
    }

    /**
     * Returns the language of the vertex shader used in this technique.
     */
    public String getVertexShaderLanguage() {
        return shaderLanguages.get(Shader.ShaderType.Vertex);
    }

    /**Returns the language for each shader program
     * @param shaderType
     */
    public String getShaderProgramLanguage(Shader.ShaderType shaderType){
        return shaderLanguages.get(shaderType);
    }
    /**Returns the name for each shader program
     * @param shaderType
     */
    public String getShaderProgramName(Shader.ShaderType shaderType){
        return shaderNames.get(shaderType);
    }

    /**
     * Adds a new world parameter by the given name.
     *
     * @param name The world parameter to add.
     * @return True if the world parameter name was found and added
     * to the list of world parameters, false otherwise.
     */
    public boolean addWorldParam(String name) {
        if (worldBinds == null){
            worldBinds = new ArrayList<UniformBinding>();
        }

        try {
            worldBinds.add( UniformBinding.valueOf(name) );
            return true;
        } catch (IllegalArgumentException ex){
            return false;
        }
    }

    public RenderState getForcedRenderState() {
        return forcedRenderState;
    }

    public void setForcedRenderState(RenderState forcedRenderState) {
        this.forcedRenderState = forcedRenderState;
    }

    /**
     * Returns a list of world parameters that are used by this
     * technique definition.
     *
     * @return The list of world parameters
     */
    public List<UniformBinding> getWorldBindings() {
        return worldBinds;
    }

    public void write(JmeExporter ex) throws IOException{
        OutputCapsule oc = ex.getCapsule(this);
        oc.write(name, "name", null);

        oc.write(shaderNames.get(Shader.ShaderType.Vertex), "vertName", null);
        oc.write(shaderNames.get(Shader.ShaderType.Fragment), "fragName", null);
        oc.write(shaderNames.get(Shader.ShaderType.Geometry), "geomName", null);
        oc.write(shaderNames.get(Shader.ShaderType.TessellationControl), "tsctrlName", null);
        oc.write(shaderNames.get(Shader.ShaderType.TessellationEvaluation), "tsevalName", null);
        oc.write(shaderLanguages.get(Shader.ShaderType.Vertex), "vertLanguage", null);
        oc.write(shaderLanguages.get(Shader.ShaderType.Fragment), "fragLanguage", null);
        oc.write(shaderLanguages.get(Shader.ShaderType.Geometry), "geomLanguage", null);
        oc.write(shaderLanguages.get(Shader.ShaderType.TessellationControl), "tsctrlLanguage", null);
        oc.write(shaderLanguages.get(Shader.ShaderType.TessellationEvaluation), "tsevalLanguage", null);

        oc.write(shaderPrologue, "shaderPrologue", null);
        oc.write(lightMode, "lightMode", LightMode.Disable);
        oc.write(shadowMode, "shadowMode", ShadowMode.Disable);
        oc.write(renderState, "renderState", null);
        oc.write(noRender, "noRender", false);
        oc.write(usesNodes, "usesNodes", false);
        oc.writeSavableArrayList((ArrayList)shaderNodes,"shaderNodes", null);
        oc.write(shaderGenerationInfo, "shaderGenerationInfo", null);

        // TODO: Finish this when Map<String, String> export is available
//        oc.write(defineParams, "defineParams", null);
        // TODO: Finish this when List<Enum> export is available
//        oc.write(worldBinds, "worldBinds", null);
    }

    public void read(JmeImporter im) throws IOException{
        InputCapsule ic = im.getCapsule(this);
        name = ic.readString("name", null);
        shaderNames.put(Shader.ShaderType.Vertex,ic.readString("vertName", null));
        shaderNames.put(Shader.ShaderType.Fragment,ic.readString("fragName", null));
        shaderNames.put(Shader.ShaderType.Geometry,ic.readString("geomName", null));
        shaderNames.put(Shader.ShaderType.TessellationControl,ic.readString("tsctrlName", null));
        shaderNames.put(Shader.ShaderType.TessellationEvaluation,ic.readString("tsevalName", null));
        shaderPrologue = ic.readString("shaderPrologue", null);
        lightMode = ic.readEnum("lightMode", LightMode.class, LightMode.Disable);
        shadowMode = ic.readEnum("shadowMode", ShadowMode.class, ShadowMode.Disable);
        renderState = (RenderState) ic.readSavable("renderState", null);
        noRender = ic.readBoolean("noRender", false);

        if (ic.getSavableVersion(TechniqueDef.class) == 0) {
            // Old version
            shaderLanguages.put(Shader.ShaderType.Vertex,ic.readString("shaderLang", null));
            shaderLanguages.put(Shader.ShaderType.Fragment,shaderLanguages.get(Shader.ShaderType.Vertex));
        } else {
            // New version
            shaderLanguages.put(Shader.ShaderType.Vertex,ic.readString("vertLanguage", null));
            shaderLanguages.put(Shader.ShaderType.Fragment,ic.readString("fragLanguage", null));
            shaderLanguages.put(Shader.ShaderType.Geometry,ic.readString("geomLanguage", null));
            shaderLanguages.put(Shader.ShaderType.TessellationControl,ic.readString("tsctrlLanguage", null));
            shaderLanguages.put(Shader.ShaderType.TessellationEvaluation,ic.readString("tsevalLanguage", null));
        }

        usesNodes = ic.readBoolean("usesNodes", false);
        shaderNodes = ic.readSavableArrayList("shaderNodes", null);
        shaderGenerationInfo = (ShaderGenerationInfo) ic.readSavable("shaderGenerationInfo", null);
    }

    public List<ShaderNode> getShaderNodes() {
        return shaderNodes;
    }

    public void setShaderNodes(List<ShaderNode> shaderNodes) {
        this.shaderNodes = shaderNodes;
        usesNodes = true;
    }

    /**
     * Returns the Enum containing the ShaderProgramNames;
     * @return
     */
    public EnumMap<Shader.ShaderType, String> getShaderProgramNames() {
        return shaderNames;
    }

    /**
     * Returns the Enum containing the ShaderProgramLanguages;
     * @return
     */
    public EnumMap<Shader.ShaderType, String> getShaderProgramLanguages() {
        return shaderLanguages;
    }

    public ShaderGenerationInfo getShaderGenerationInfo() {
        return shaderGenerationInfo;
    }

    public void setShaderGenerationInfo(ShaderGenerationInfo shaderGenerationInfo) {
        this.shaderGenerationInfo = shaderGenerationInfo;
    }

    @Override
    public String toString() {
        return "TechniqueDef[name=" + name
                + ", requiredCaps=" + requiredCaps
                + ", noRender=" + noRender
                + ", lightMode=" + lightMode
                + ", usesNodes=" + usesNodes
                + ", renderState=" + renderState
                + ", forcedRenderState=" + forcedRenderState + "]";
    }
}
