/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.jme3.gde.materialdefinition;

import com.jme3.asset.AssetKey;
import com.jme3.gde.core.assets.ProjectAssetManager;
import com.jme3.gde.core.scene.SceneApplication;
import com.jme3.gde.materialdefinition.fileStructure.MatDefBlock;
import com.jme3.gde.materialdefinition.fileStructure.ShaderNodeBlock;
import com.jme3.gde.materialdefinition.fileStructure.TechniqueBlock;
import com.jme3.gde.materialdefinition.fileStructure.UberStatement;
import com.jme3.gde.materialdefinition.fileStructure.leaves.InputMappingBlock;
import com.jme3.gde.materialdefinition.fileStructure.leaves.LeafStatement;
import com.jme3.gde.materialdefinition.fileStructure.leaves.MatParamBlock;
import com.jme3.gde.materialdefinition.fileStructure.leaves.OutputMappingBlock;
import com.jme3.gde.materialdefinition.navigator.node.MatDefNode;
import com.jme3.material.MatParam;
import com.jme3.material.Material;
import com.jme3.material.MaterialDef;
import com.jme3.material.plugins.J3MLoader;
import com.jme3.material.plugins.MatParseException;
import com.jme3.shader.Glsl100ShaderGenerator;
import com.jme3.shader.Glsl150ShaderGenerator;
import com.jme3.shader.Shader;
import com.jme3.shader.ShaderGenerator;
import com.jme3.util.blockparser.BlockLanguageParser;
import com.jme3.util.blockparser.Statement;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyledDocument;
import org.openide.cookies.EditorCookie;
import org.openide.explorer.ExplorerManager;
import org.openide.filesystems.FileLock;
import org.openide.filesystems.FileObject;
import org.openide.nodes.Node;
import org.openide.text.NbDocument;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.WeakListeners;

/**
 *
 * @author Nehon
 */
public class EditableMatDefFile {

    private FileObject matDefFile;
    private final MatDefDataObject obj;
    private Material material;
    private MatDefBlock matDefStructure;
    private TechniqueBlock currentTechnique;
    private MaterialDef materialDef;
    private ProjectAssetManager assetManager;
    private ShaderGenerator glsl100;
    private ShaderGenerator glsl150;    
    private final static String GLSL100 = "GLSL100";    
    private Lookup lookup;
    private boolean loaded = false;
    private boolean dirty = false;

    public EditableMatDefFile(Lookup lookup) {
        obj = lookup.lookup(MatDefDataObject.class);
        load(lookup);

    }

    public final void load(Lookup lookup) {
        loaded = false;
        this.matDefFile = obj.getPrimaryFile();
        this.assetManager = lookup.lookup(ProjectAssetManager.class);
        this.glsl100 = new Glsl100ShaderGenerator(assetManager);
        this.glsl150 = new Glsl150ShaderGenerator(assetManager);
        this.lookup = lookup;

        if (matDefStructure != null) {
            obj.getLookupContents().remove(matDefStructure);
            matDefStructure = null;
        }
        if (materialDef != null) {
            obj.getLookupContents().remove(materialDef);
            materialDef = null;
        }
        if (material != null) {
            obj.getLookupContents().remove(material);
            matToRemove = material;
            material = null;
        }
        FileLock lock = null;
        InputStream in = null;
        boolean matParseError = false;        
        try {
            lock = matDefFile.lock();
            in = obj.getPrimaryFile().getInputStream();
            List<Statement> sta = BlockLanguageParser.parse(in);
            matDefStructure = new MatDefBlock(sta.get(0));
            if (assetManager != null) {
                AssetKey<MaterialDef> matDefKey = new AssetKey<MaterialDef>(assetManager.getRelativeAssetPath(assetManager.getRelativeAssetPath(matDefFile.getPath())));
                assetManager.deleteFromCache(matDefKey);
                materialDef = (MaterialDef) assetManager.loadAsset(assetManager.getRelativeAssetPath(matDefFile.getPath()));
            }
        } catch (Exception ex) {
            Throwable t = ex.getCause();

            while (t != null) {
                if (t instanceof MatParseException) {
                    Logger.getLogger(EditableMatDefFile.class.getName()).log(Level.SEVERE, t.getMessage());
                    matParseError = true;
                }
                t = t.getCause();
            }
            if (!matParseError) {
                Exceptions.printStackTrace(ex);
            }
        } finally {
            if (lock != null) {
                lock.releaseLock();
            }
            if (in != null) {
                try {
                    in.close();
                } catch (IOException ex) {
                    Exceptions.printStackTrace(ex);
                }
            }
        }
        if (materialDef != null && !matParseError) {
            if(currentTechnique == null){
                currentTechnique = matDefStructure.getTechniques().get(0);
            }
            registerListener(matDefStructure);

            obj.getLookupContents().add(matDefStructure);
            updateLookupWithMaterialData(obj);
            loaded = true;
        }
    }

    private void registerListener(Statement sta) {
        if (sta instanceof UberStatement) {
            ((UberStatement) sta).addPropertyChangeListener(WeakListeners.propertyChange(changeListener, ((UberStatement) sta)));
        } else if (sta instanceof LeafStatement) {
            ((LeafStatement) sta).addPropertyChangeListener(WeakListeners.propertyChange(changeListener, ((LeafStatement) sta)));
        }
        if (sta.getContents() != null) {
            for (Statement statement : sta.getContents()) {
                registerListener(statement);
            }
        }
    }

    public void buildOverview(ExplorerManager mgr) {
        if (materialDef != null) {
            mgr.setRootContext(new MatDefNode(lookup));

        } else {
            mgr.setRootContext(Node.EMPTY);
        }
    }

    public String getShaderCode(String version, Shader.ShaderType type) {
        try {
            material.selectTechnique(currentTechnique.getName(), SceneApplication.getApplication().getRenderManager());
            Shader s;
            if (version.equals(GLSL100)) {
                glsl100.initialize(material.getActiveTechnique());
                s = glsl100.generateShader();
            } else {
                glsl150.initialize(material.getActiveTechnique());
                s = glsl150.generateShader();
            }
            for (Shader.ShaderSource source : s.getSources()) {
                if (source.getType() == type) {
                    return source.getSource();
                }
            }
            return "";
        } catch (Exception e) {
            Exceptions.printStackTrace(e);
            return "error generating shader " + e.getMessage();
        }
    }

    public TechniqueBlock getCurrentTechnique() {
        return currentTechnique;
    }
    
    public void setCurrentTechnique(TechniqueBlock tech){
        this.currentTechnique = tech;
    }

    public MatDefBlock getMatDefStructure() {
        return matDefStructure;
    }
    private final MatStructChangeListener changeListener = new MatStructChangeListener();
    J3MLoader loader = new J3MLoader();

    private void updateLookupWithMaterialData(MatDefDataObject obj) {
        obj.getLookupContents().add(materialDef);
        material = new Material(materialDef);

        try {
            //material.selectTechnique("Default", SceneApplication.getApplication().getRenderManager());
            if (matToRemove != null) {
                for (MatParam matParam : matToRemove.getParams()) {
                    try {
                        material.setParam(matParam.getName(), matParam.getVarType(), matParam.getValue());
                    } catch (IllegalArgumentException ie) {
                        matToRemove.clearParam(matParam.getName());
                    }

                }
                obj.getLookupContents().remove(matToRemove);
                matToRemove = null;
            }
            obj.getLookupContents().add(material);
        } catch (Exception e) {
            Logger.getLogger(EditableMatDefFile.class.getName()).log(Level.WARNING, "Error making material {0}", e.getMessage());
            material = matToRemove;
        }
    }

    public boolean isLoaded() {
        return loaded;
    }

    public boolean isDirty() {
        return dirty;
    }

    public void setDirty(boolean dirty) {
        this.dirty = dirty;
    }

    public void setLoaded(boolean loaded) {
        this.loaded = loaded;
    }

    private class MatStructChangeListener implements PropertyChangeListener {

        public void propertyChange(PropertyChangeEvent evt) {
            if (evt.getSource() instanceof ShaderNodeBlock && evt.getPropertyName().equals("name")) {
                String oldValue = (String) evt.getOldValue();
                String newValue = (String) evt.getNewValue();
                for (ShaderNodeBlock shaderNodeBlock : currentTechnique.getShaderNodes()) {
                    List<InputMappingBlock> lin = shaderNodeBlock.getInputs();
                    if (lin != null) {
                        for (InputMappingBlock inputMappingBlock : shaderNodeBlock.getInputs()) {
                            if (inputMappingBlock.getLeftNameSpace().equals(oldValue)) {
                                inputMappingBlock.setLeftNameSpace(newValue);
                            }
                            if (inputMappingBlock.getRightNameSpace().equals(oldValue)) {
                                inputMappingBlock.setRightNameSpace(newValue);
                            }
                        }
                    }
                    List<OutputMappingBlock> l = shaderNodeBlock.getOutputs();
                    if (l != null) {
                        for (OutputMappingBlock outputMappingBlock : l) {
                            if (outputMappingBlock.getRightNameSpace().equals(oldValue)) {
                                outputMappingBlock.setRightNameSpace(newValue);
                            }
                        }
                    }
                }
            }
            if (evt.getPropertyName().equals(MatDefBlock.REMOVE_MAT_PARAM)) {
                MatParamBlock oldValue = (MatParamBlock) evt.getOldValue();

                for (ShaderNodeBlock shaderNodeBlock : currentTechnique.getShaderNodes()) {

                    if (shaderNodeBlock.getCondition() != null && shaderNodeBlock.getCondition().contains(oldValue.getName())) {
                        shaderNodeBlock.setCondition(shaderNodeBlock.getCondition().replaceAll(oldValue.getName(), "").trim());
                    }
                    List<InputMappingBlock> lin = shaderNodeBlock.getInputs();
                    if (lin != null) {
                        for (InputMappingBlock inputMappingBlock : shaderNodeBlock.getInputs()) {
                            if (inputMappingBlock.getCondition() != null && inputMappingBlock.getCondition().contains(oldValue.getName())) {
                                inputMappingBlock.setCondition(inputMappingBlock.getCondition().replaceAll(oldValue.getName(), "").trim());
                            }
                        }
                    }
                    List<OutputMappingBlock> l = shaderNodeBlock.getOutputs();
                    if (l != null) {
                        for (OutputMappingBlock outputMappingBlock : l) {
                            if (outputMappingBlock.getCondition() != null && outputMappingBlock.getCondition().contains(oldValue.getName())) {
                                outputMappingBlock.setCondition(outputMappingBlock.getCondition().replaceAll(oldValue.getName(), "").trim());
                            }
                        }
                    }
                }
            }
            if (evt.getPropertyName().equals(MatDefBlock.ADD_MAT_PARAM)
                    || evt.getPropertyName().equals(TechniqueBlock.ADD_SHADER_NODE)
                    || evt.getPropertyName().equals(ShaderNodeBlock.ADD_MAPPING)) {
                registerListener((Statement) evt.getNewValue());
            }
            applyChange();
        }
    }
    Material matToRemove;

    private void applyChange() {

        try {
            EditorCookie ec = lookup.lookup(EditorCookie.class);
            final StyledDocument doc = ec.getDocument();
            final BadLocationException[] exc = new BadLocationException[]{null};
            NbDocument.runAtomicAsUser(ec.getDocument(), new Runnable() {
                public void run() {
                    try {
                    doc.remove(0, doc.getLength());
                    doc.insertString(doc.getLength(),
                    matDefStructure.toString(),
                    SimpleAttributeSet.EMPTY);
                    } catch (BadLocationException e) {
                        exc[0] = e;
                    }
                }
            });
        } catch (BadLocationException ex) {
            Exceptions.printStackTrace(ex);
        }
        AssetKey<MaterialDef> key = new AssetKey<MaterialDef>(assetManager.getRelativeAssetPath(matDefFile.getPath()));
        obj.getLookupContents().remove(materialDef);
        matToRemove = material;

        List<Statement> l = new ArrayList<Statement>();
        l.add(matDefStructure);
        try {
            materialDef = loader.loadMaterialDef(l, assetManager, key);
        } catch (IOException ex) {
            Logger.getLogger(EditableMatDefFile.class.getName()).log(Level.SEVERE, ex.getMessage());
        }
        updateLookupWithMaterialData(obj);
    }
    
    public void cleanup(){
        if (matDefStructure != null) {
            obj.getLookupContents().remove(matDefStructure);
            matDefStructure = null;
        }
        if (materialDef != null) {
            obj.getLookupContents().remove(materialDef);
            materialDef = null;
        }
        if (material != null) {
            obj.getLookupContents().remove(material);
            matToRemove = material;
            material = null;
        }
        
        setCurrentTechnique(null);
        setLoaded(false);
    }
}
