/*
 * Copyright (c) 2009-2010 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.asset.AssetManager;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.InputCapsule;
import com.jme3.export.OutputCapsule;
import com.jme3.export.Savable;
import com.jme3.shader.DefineList;
import com.jme3.shader.Shader;
import com.jme3.shader.ShaderKey;
import com.jme3.shader.Uniform;
import com.jme3.shader.UniformBinding;
import com.jme3.shader.VarType;
import com.jme3.util.ListMap;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Logger;

/**
 * Represents a technique instance.
 */
public class Technique implements Savable {

    private static final Logger logger = Logger.getLogger(Technique.class.getName());
    private TechniqueDef def;
    private Material owner;
    private ArrayList<Uniform> worldBindUniforms;
    private DefineList defines;
    private Shader shader;
    private boolean needReload = true;
    
    ListMap<String, MatParam> paramValues;
    
    Uniform[] uniformArray;
    String[] defineNameArray;

    /**
     * Creates a new technique instance that implements the given
     * technique definition.
     * 
     * @param owner The material that will own this technique
     * @param def The technique definition being implemented.
     */
    public Technique(Material owner, TechniqueDef def) {
        this.owner = owner;
        this.def = def;
        if (def.isUsingShaders()) {
            this.worldBindUniforms = new ArrayList<Uniform>();
            this.defines = new DefineList();
            for (MatParam param : owner.getParams()) {
                String defineName = def.getShaderParamDefine(param.getName());
                if (defineName != null) {
                    defines.set(defineName, param.getVarType(), param.getValue());
                }
            }
        }
    }

    /**
     * Serialization only. Do not use.
     */
    public Technique() {
    }

    /**
     * Returns the technique definition that is implemented by this technique
     * instance. 
     * 
     * @return the technique definition that is implemented by this technique
     * instance. 
     */
    public TechniqueDef getDef() {
        return def;
    }

    /**
     * Returns the shader currently used by this technique instance.
     * <p>
     * Shaders are typically loaded dynamically when the technique is first
     * used, therefore, this variable will most likely be null most of the time.
     * 
     * @return the shader currently used by this technique instance.
     */
    public Shader getShader() {
        return shader;
    }

    /**
     * Returns a list of uniforms that implements the world parameters
     * that were requested by the material definition.
     * 
     * @return a list of uniforms implementing the world parameters.
     */
    public List<Uniform> getWorldBindUniforms() {
        return worldBindUniforms;
    }

    /**
     * Called by the material to tell the technique a parameter was modified
     */
    void notifySetParam(String paramName, VarType type, Object value) {
        String defineName = def.getShaderParamDefine(paramName);
        if (defineName != null) {
            defines.set(defineName, type, value);
            needReload = true;
        }
//        if (shader != null) {
//            updateUniformParam(paramName, type, value);
//        }
    }
    void notifySetParam(int paramIndex, VarType type, Object value) {
        if (defineNameArray == null) {
            return;
        }
        String defineName = defineNameArray[paramIndex];
        if (defineName != null) {
            defines.set(defineName, type, value);
            needReload = true;
        }
    }

    /**
     * Called by the material to tell the technique a parameter was cleared
     */
    void notifyClearParam(String paramName) {
        String defineName = def.getShaderParamDefine(paramName);
        if (defineName != null) {
            defines.remove(defineName);
            needReload = true;
        }
        if (shader != null) {
            if (!paramName.startsWith("m_")) {
                paramName = "m_" + paramName;
            }
            shader.removeUniform(paramName);
            for(int i=uniformArray.length-1;i>=0;i--) {
                if (paramName.equals(uniformArray[i].getName())) {
                    uniformArray[i] = null;
                }
            }
        }
    }
    void notifyClearParam(int paramIndex) {
        if (shader == null) {
            return;
        }
        String defineName = defineNameArray[paramIndex];
        if (defineName != null) {
            defines.remove(defineName);
            needReload = true;
        }
        if (shader != null) {
            String paramName = uniformArray[paramIndex].getName();
            shader.removeUniform(paramName);
            uniformArray[paramIndex] = null;
        }
    }

    void updateUniformParam(String paramName, VarType type, Object value, boolean ifNotOwner) {
        Uniform u = shader.getUniform(paramName);

//        if (ifNotOwner && u.getLastChanger() == owner)
//            return;

        switch (type) {
            case Texture2D: // fall intentional
            case Texture3D:
            case TextureArray:
            case TextureCubeMap:
            case Int:
                u.setValue(VarType.Int, value);
                break;
            default:
                u.setValue(type, value);
                break;
        }
//        u.setLastChanger(owner);
    }
    void updateUniformParam(int paramIndex, VarType type, Object value, boolean ifNotOwner) {
        Uniform u = uniformArray[paramIndex];
        if (u == null) {
            u = shader.getUniform(paramValues.getValue(paramIndex).getPrefixedName());
            uniformArray[paramIndex] = u;
        }

//        if (ifNotOwner && u.getLastChanger() == owner)
//            return;

        switch (type) {
            case Texture2D: // fall intentional
            case Texture3D:
            case TextureArray:
            case TextureCubeMap:
            case Int:
                u.setValue(VarType.Int, value);
                break;
            default:
                u.setValue(type, value);
                break;
        }
//        u.setLastChanger(owner);
    }

    void updateUniformParam(String paramName, VarType type, Object value) {
        updateUniformParam(paramName, type, value, false);
    }
    void updateUniformParam(int paramIndex, VarType type, Object value) {
        updateUniformParam(paramIndex, type, value, false);
    }
    /**
     * Returns true if the technique must be reloaded.
     * <p>
     * If a technique needs to reload, then the {@link Material} should
     * call {@link #makeCurrent(com.jme3.asset.AssetManager) } on this
     * technique.
     * 
     * @return true if the technique must be reloaded.
     */
    public boolean isNeedReload() {
        return needReload;
    }

    public void setNeedReload(boolean needReload) {
        this.needReload = needReload;
    }

    /**
     * Prepares the technique for use by loading the shader and setting
     * the proper defines based on material parameters.
     * 
     * @param assetManager The asset manager to use for loading shaders.
     */
    public void makeCurrent(AssetManager assetManager, ListMap<String, MatParam> paramValues) {
        this.paramValues = paramValues;
        defineNameArray = new String[paramValues.size()];
        for(int i=paramValues.size()-1;i>=0;i--) {
            defineNameArray[i] = def.getShaderParamDefine(paramValues.getValue(i).getName());
        }
        // check if reload is needed..
        if (def.isUsingShaders()) {
//            DefineList newDefines = new DefineList();
//            Collection<MatParam> params = owner.getParams();
//            for (MatParam param : params) {
//                String defineName = def.getShaderParamDefine(param.getName());
//                if (defineName != null) {
//                    newDefines.set(defineName, param.getVarType(), param.getValue());
//                }
//            }

            if (!needReload /*&& defines.getCompiled().equals(newDefines.getCompiled())*/) {
//                newDefines = null;
                // defines have not been changed..
            } else {
//                defines.clear();
//                defines.addFrom(newDefines);
                // defines changed, recompile needed
                loadShader(assetManager);
                if (shader != null) {
                    uniformArray = new Uniform[paramValues.size()];
//                    for(int i=paramValues.size()-1;i>=0;i--) {
//                        uniformArray[i] = shader.getUniform(paramValues.getValue(i).getPrefixedName());
//                    }
                    defineNameArray = new String[paramValues.size()];
                    for(int i=paramValues.size()-1;i>=0;i--) {
                        defineNameArray[i] = def.getShaderParamDefine(paramValues.getValue(i).getName());
                    }
                }
            }
        }
    }

    private void loadShader(AssetManager manager) {
        // recompute define list
        DefineList allDefines = new DefineList();
        allDefines.addFrom(def.getShaderPresetDefines());
        allDefines.addFrom(defines);

        ShaderKey key = new ShaderKey(def.getVertexShaderName(),
                def.getFragmentShaderName(),
                allDefines,
                def.getShaderLanguage());
        shader = manager.loadShader(key);
        if (shader == null) {
            logger.warning("Failed to reload shader!");
            return;
        }

        // refresh the uniform links
        //owner.updateUniformLinks();

        // register the world bound uniforms
        worldBindUniforms.clear();
        for (UniformBinding binding : def.getWorldBindings()) {
            Uniform uniform = shader.getUniform("g_" + binding.name());
            uniform.setBinding(binding);
            if (uniform != null) {
                worldBindUniforms.add(uniform);

            }
        }

        needReload = false;
    }
    
    public void write(JmeExporter ex) throws IOException {
        OutputCapsule oc = ex.getCapsule(this);
        oc.write(def, "def", null);
        // TODO:
        // oc.write(owner, "owner", null);
        oc.writeSavableArrayList(worldBindUniforms, "worldBindUniforms", null);
        oc.write(defines, "defines", null);
        oc.write(shader, "shader", null);
    }

    public void read(JmeImporter im) throws IOException {
        InputCapsule ic = im.getCapsule(this);
        def = (TechniqueDef) ic.readSavable("def", null);
        worldBindUniforms = ic.readSavableArrayList("worldBindUniforms", null);
        defines = (DefineList) ic.readSavable("defines", null);
        shader = (Shader) ic.readSavable("shader", null);
        //if (shader != null)
        //    owner.updateUniformLinks();
    }
}
