/*
 * Copyright (c) 2009-2014 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.util;

import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetManager;
import com.jme3.asset.plugins.UrlAssetInfo;
import com.jme3.input.InputManager;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.Trigger;
import com.jme3.material.MatParam;
import com.jme3.material.Material;
import com.jme3.post.Filter;
import com.jme3.post.Filter.Pass;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.RendererException;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import com.jme3.shader.Shader;
import java.io.File;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This appState is for debug purpose only, and was made to provide an easy way 
 * to test shaders, with a live update capability.
 * 
 * This calss provides and easy way to reload a material and catches compilation
 * errors when needed and displays the error in the console.
 * 
 * If no error accur on compilation, the material is reloaded in the scene.
 * 
 * You can either trigger the reload when pressing a key (or whatever input is 
 * supported by Triggers you can attach to the input manager), or trigger it 
 * when a specific file (the shader source) has been changed on the hard drive.
 * 
 * Usage : 
 * 
 * MaterialDebugAppState matDebug = new MaterialDebugAppState();
 * stateManager.attach(matDebug);
 * matDebug.registerBinding(new KeyTrigger(KeyInput.KEY_R), whateverGeometry);  
 * 
 * this will reload the material of whateverGeometry when pressing the R key.
 * 
 * matDebug.registerBinding("Shaders/distort.frag", whateverGeometry); 
 * 
 * this will reload the material of whateverGeometry when the given file is 
 * changed on the hard drive.
 * 
 * you can also register bindings to the appState with a post process Filter
 *  
 * @author Nehon
 */
public class MaterialDebugAppState extends AbstractAppState {

    private RenderManager renderManager;
    private AssetManager assetManager;
    private InputManager inputManager;
    private List<Binding> bindings = new ArrayList<Binding>();
    private Map<Trigger,List<Binding>> fileTriggers = new HashMap<Trigger,List<Binding>> ();
    

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        renderManager = app.getRenderManager();
        assetManager = app.getAssetManager();
        inputManager = app.getInputManager();
        for (Binding binding : bindings) {
            bind(binding);
        }
        super.initialize(stateManager, app);
    }

    /**
     * Will reload the spatial's materials whenever the trigger is fired
     * @param trigger the trigger
     * @param spat the spatial to reload
     */
    public void registerBinding(Trigger trigger, final Spatial spat) {
        if(spat instanceof Geometry){
            GeometryBinding binding = new GeometryBinding(trigger, (Geometry)spat);           
            bindings.add(binding);
            if (isInitialized()) {
                bind(binding);
            }
        }else if (spat instanceof Node){
            for (Spatial child : ((Node)spat).getChildren()) {
                registerBinding(trigger, child);
            }
        }
    }
    
    

    /**
     * Will reload the filter's materials whenever the trigger is fired.
     * @param trigger the trigger
     * @param filter the filter to reload
     */
    public void registerBinding(Trigger trigger, final Filter filter) {
        FilterBinding binding = new FilterBinding(trigger, filter);
        bindings.add(binding);
        if (isInitialized()) {
            bind(binding);
        }
    }

    
    /**
     * Will reload the filter's materials whenever the shader file is changed 
     * on the hard drive
     * @param shaderName the shader name (relative path to the asset folder or 
     * to a registered asset path)
     * @param filter the filter to reload
     */
    public void registerBinding(String shaderName, final Filter filter) {
        registerBinding(new FileChangedTrigger(shaderName), filter);
    }

    /**
     * Will reload the spatials's materials whenever the shader file is changed 
     * on the hard drive
     * @param shaderName the shader name (relative path to the asset folder or 
     * to a registered asset path)
     * @param spat the spatial to reload
     */
    public void registerBinding(String shaderName, final Spatial spat) {
        registerBinding(new FileChangedTrigger(shaderName), spat);
    }

    private void bind(final Binding binding) {
        if (binding.getTrigger() instanceof FileChangedTrigger) {
            FileChangedTrigger t = (FileChangedTrigger) binding.getTrigger();
            List<Binding> b = fileTriggers.get(t);
            if(b == null){
                t.init();
                b = new ArrayList<Binding>();
                fileTriggers.put(t, b);
            }
            b.add(binding);
        } else {
            final String actionName = binding.getActionName();
            inputManager.addListener(new ActionListener() {
                public void onAction(String name, boolean isPressed, float tpf) {
                    if (actionName.equals(name) && isPressed) {
                        //reloading the material
                        binding.reload();
                    }
                }
            }, actionName);

            inputManager.addMapping(actionName, binding.getTrigger());
        }
    }

    public Material reloadMaterial(Material mat) {
        //clear the entire cache, there might be more clever things to do, like clearing only the matdef, and the associated shaders.
        assetManager.clearCache();

        //creating a dummy mat with the mat def of the mat to reload
        Material dummy = new Material(mat.getMaterialDef());

        for (MatParam matParam : mat.getParams()) {
            dummy.setParam(matParam.getName(), matParam.getVarType(), matParam.getValue());
        }
        
        dummy.getAdditionalRenderState().set(mat.getAdditionalRenderState());        

        //creating a dummy geom and assigning the dummy material to it
        Geometry dummyGeom = new Geometry("dummyGeom", new Box(1f, 1f, 1f));
        dummyGeom.setMaterial(dummy);

        try {
            //preloading the dummyGeom, this call will compile the shader again
            renderManager.preloadScene(dummyGeom);
        } catch (RendererException e) {
            //compilation error, the shader code will be output to the console
            //the following code will output the error
            //System.err.println(e.getMessage());
            Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.SEVERE, e.getMessage());
            return null;
        }

        Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.INFO, "Material succesfully reloaded");
        //System.out.println("Material succesfully reloaded");
        return dummy;
    }
   
    @Override
    public void update(float tpf) {
        super.update(tpf); //To change body of generated methods, choose Tools | Templates.
        for (Trigger trigger : fileTriggers.keySet()) {
            if (trigger instanceof FileChangedTrigger) {
                FileChangedTrigger t = (FileChangedTrigger) trigger;
                if (t.shouldFire()) {
                    List<Binding> b = fileTriggers.get(t);
                    for (Binding binding : b) {
                        binding.reload();
                    }
                }
            }
        }       
    }

    private interface Binding {

        public String getActionName();

        public void reload();

        public Trigger getTrigger();
    }

    private class GeometryBinding implements Binding {

        Trigger trigger;
        Geometry geom;

        public GeometryBinding(Trigger trigger, Geometry geom) {
            this.trigger = trigger;
            this.geom = geom;

        }

        public void reload() {
            Material reloadedMat = reloadMaterial(geom.getMaterial());
            //if the reload is successful, we re setup the material with its params and reassign it to the box
            if (reloadedMat != null) {
                // setupMaterial(reloadedMat);
                geom.setMaterial(reloadedMat);
            }
        }

        public String getActionName() {
            return geom.getName() + "Reload";

        }

        public Trigger getTrigger() {
            return trigger;
        }
    }

    private class FilterBinding implements Binding {

        Trigger trigger;
        Filter filter;

        public FilterBinding(Trigger trigger, Filter filter) {
            this.trigger = trigger;
            this.filter = filter;
        }

        public void reload() {
            Field[] fields1 = filter.getClass().getDeclaredFields();
            Field[] fields2 = filter.getClass().getSuperclass().getDeclaredFields();

            List<Field> fields = new ArrayList<Field>();
            fields.addAll(Arrays.asList(fields1));
            fields.addAll(Arrays.asList(fields2));
            Material m = new Material();
            Filter.Pass p = filter.new Pass();
            try {
                for (Field field : fields) {
                    if (field.getType().isInstance(m)) {
                        field.setAccessible(true);
                        Material mat = reloadMaterial((Material) field.get(filter));
                        if (mat == null) {
                            return;
                        } else {
                            field.set(filter, mat);
                        }

                    }
                    if (field.getType().isInstance(p)) {
                        field.setAccessible(true);
                        p = (Filter.Pass) field.get(filter);
                        if (p!= null && p.getPassMaterial() != null) {
                            Material mat = reloadMaterial(p.getPassMaterial());
                            if (mat == null) {
                                return;
                            } else {
                                p.setPassMaterial(mat);
                            }
                        }
                    }
                    if (field.getName().equals("postRenderPasses")) {
                        field.setAccessible(true);
                        List<Pass> passes = new ArrayList<Pass>();
                        passes = (List<Pass>) field.get(filter);
                        if (passes != null) {
                            for (Pass pass : passes) {
                                Material mat = reloadMaterial(pass.getPassMaterial());
                                if (mat == null) {
                                    return;
                                } else {
                                    pass.setPassMaterial(mat);
                                }
                            }
                        }
                    }
                }
            } catch (IllegalArgumentException ex) {
                Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.SEVERE, null, ex);
            } catch (IllegalAccessException ex) {
                Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.SEVERE, null, ex);
            }

        }

        public String getActionName() {
            return filter.getName() + "Reload";
        }

        public Trigger getTrigger() {
            return trigger;
        }
    }

    private class FileChangedTrigger implements Trigger {

        String fileName;
        File file;
        Long fileLastM;

        public FileChangedTrigger(String fileName) {
            this.fileName = fileName;
        }

        public void init() {
            AssetInfo info = assetManager.locateAsset(new AssetKey<Shader>(fileName));
            if (info != null && info instanceof UrlAssetInfo) {
                try {
                    Field f = info.getClass().getDeclaredField("url");
                    f.setAccessible(true);
                    URL url = (URL) f.get(info);
                    file = new File(url.getFile());
                    fileLastM = file.lastModified();

                } catch (NoSuchFieldException ex) {
                    Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.SEVERE, null, ex);
                } catch (SecurityException ex) {
                    Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.SEVERE, null, ex);
                } catch (IllegalArgumentException ex) {
                    Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.SEVERE, null, ex);
                } catch (IllegalAccessException ex) {
                    Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }

        public boolean shouldFire() {
            if (file.lastModified() != fileLastM) {
                fileLastM = file.lastModified();
                return true;
            }
            return false;
        }

        public String getName() {
            return fileName;
        }

        public int triggerHashCode() {
            return 0;
        }
    }
}
