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

import com.jme3.asset.AssetManager;
import com.jme3.export.*;
import com.jme3.material.Material;
import com.jme3.renderer.Caps;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.Renderer;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.texture.FrameBuffer;
import com.jme3.texture.Image.Format;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.texture.FrameBuffer.FrameBufferTarget;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

/**
 * Filters are 2D effects applied to the rendered scene.<br>
 * The filter is fed with the rendered scene image rendered in an offscreen frame buffer.<br>
 * This texture is applied on a full-screen quad with a special material.<br>
 * This material uses a shader that applies the desired effect to the scene texture.<br>
 * <br>
 * This class is abstract, any Filter must extend it.<br>
 * Any filter holds a frameBuffer and a texture<br>
 * The getMaterial must return a Material that use a GLSL shader implementing the desired effect<br>
 *
 * @author Rémy Bouquet aka Nehon
 */
public abstract class Filter implements Savable {


    private String name;
    protected Pass defaultPass;
    protected List<Pass> postRenderPasses;
    protected Material material;
    protected boolean enabled = true;
    protected FilterPostProcessor processor;

    public Filter(String name) {
        this.name = name;
    }

    /**
     * Passes are like filters within filters.
     * Some filters will need multiple passes before the final render.
     */
    public class Pass {

        protected FrameBuffer renderFrameBuffer;
        protected Texture2D renderedTexture;
        protected Texture2D depthTexture;
        protected Material passMaterial;
        protected String name;

        public Pass(String name) {
            this.name = name;
        }

        public Pass() {
        }

        /**
         * init the pass called internally
         *
         * @param renderer (not null)
         * @param width the width (in pixels, &ge;0)
         * @param height the height (in pixels, &ge;0)
         * @param textureFormat format of the rendered texture
         * @param depthBufferFormat format of the depth buffer
         * @param numSamples the number of samples per pixel (for multisampling)
         * @param renderDepth true to create a depth texture, false for none
         */
        public void init(Renderer renderer, int width, int height, Format textureFormat, Format depthBufferFormat, int numSamples, boolean renderDepth) {
            Collection<Caps> caps = renderer.getCaps();
            if (numSamples > 1 && caps.contains(Caps.FrameBufferMultisample) && caps.contains(Caps.OpenGL31)) {
                renderFrameBuffer = new FrameBuffer(width, height, numSamples);                
                renderedTexture = new Texture2D(width, height, numSamples, textureFormat);
                renderFrameBuffer.setDepthTarget(FrameBufferTarget.newTarget(depthBufferFormat));
                if (renderDepth) {
                    depthTexture = new Texture2D(width, height, numSamples, depthBufferFormat);
                    renderFrameBuffer.setDepthTarget(FrameBufferTarget.newTarget(depthTexture));
                }
            } else {
                renderFrameBuffer = new FrameBuffer(width, height, 1);
                renderedTexture = new Texture2D(width, height, textureFormat);
                renderFrameBuffer.setDepthTarget(FrameBufferTarget.newTarget(depthBufferFormat));
                if (renderDepth) {
                    depthTexture = new Texture2D(width, height, depthBufferFormat);
                    renderFrameBuffer.setDepthTarget(FrameBufferTarget.newTarget(depthTexture));
                }
            }

            renderFrameBuffer.addColorTarget(FrameBufferTarget.newTarget(renderedTexture));


        }

        /**
         *  init the pass called internally
         *
         * @param renderer (not null)
         * @param width the image width (in pixels, &ge;0)
         * @param height the image height (in pixels, &ge;0)
         * @param textureFormat the format of the rendered texture
         * @param depthBufferFormat the format of the depth buffer
         */
        public void init(Renderer renderer, int width, int height, Format textureFormat, Format depthBufferFormat) {
            init(renderer, width, height, textureFormat, depthBufferFormat, 1);
        }

        public void init(Renderer renderer, int width, int height, Format textureFormat, Format depthBufferFormat, int numSamples) {
            init(renderer, width, height, textureFormat, depthBufferFormat, numSamples, false);
        }

        /**
         *  init the pass called internally
         *
         * @param renderer (not null)
         * @param width the image width (in pixels, &ge;0)
         * @param height the image height (in pixels, &ge;0)
         * @param textureFormat the format of the rendered texture
         * @param depthBufferFormat the format of the depth buffer
         * @param numSample the number of samples per pixel (for multisampling)
         * @param material the Material for this pass
         */
        public void init(Renderer renderer, int width, int height, Format textureFormat, Format depthBufferFormat, int numSample, Material material) {
            init(renderer, width, height, textureFormat, depthBufferFormat, numSample);
            passMaterial = material;
        }

        public boolean requiresSceneAsTexture() {
            return false;
        }

        public boolean requiresDepthAsTexture() {
            return false;
        }

        public void beforeRender() {
        }

        public FrameBuffer getRenderFrameBuffer() {
            return renderFrameBuffer;
        }

        public void setRenderFrameBuffer(FrameBuffer renderFrameBuffer) {
            this.renderFrameBuffer = renderFrameBuffer;
        }

        public Texture2D getDepthTexture() {
            return depthTexture;
        }

        public Texture2D getRenderedTexture() {
            return renderedTexture;
        }

        public void setRenderedTexture(Texture2D renderedTexture) {
            this.renderedTexture = renderedTexture;
        }

        public Material getPassMaterial() {
            return passMaterial;
        }

        public void setPassMaterial(Material passMaterial) {
            this.passMaterial = passMaterial;
        }

        public void cleanup(Renderer r) {
            renderFrameBuffer.dispose();
            renderedTexture.getImage().dispose();
            if(depthTexture!=null){
                depthTexture.getImage().dispose();
            }  
        }

        @Override
        public String toString() {
            return name == null ? super.toString() : name;
        }
    }

    /**
     * @return the default pass texture format
     */
    protected Format getDefaultPassTextureFormat() {
        return processor.getDefaultPassTextureFormat();
    }

    /**
     * returns the default pass depth format
     * @return the enum value
     */
    protected Format getDefaultPassDepthFormat() {
        return Format.Depth;
    }

    /**
     * construct a Filter
     */
    protected Filter() {
        this("filter");
    }

    /**
     *
     * initialize this filter
     * use InitFilter for overriding filter initialization
     * @param manager the assetManager
     * @param renderManager the renderManager
     * @param vp the viewport
     * @param w the width
     * @param h the height
     */
    protected final void init(AssetManager manager, RenderManager renderManager, ViewPort vp, int w, int h) {
        //  cleanup(renderManager.getRenderer());
        defaultPass = new Pass();
        defaultPass.init(renderManager.getRenderer(), w, h, getDefaultPassTextureFormat(), getDefaultPassDepthFormat());
        initFilter(manager, renderManager, vp, w, h);
    }

    /**
     * cleanup this filter
     *
     * @param r the Renderer
     */
    protected final void cleanup(Renderer r) {   
        processor = null;
        if (defaultPass != null) {
            defaultPass.cleanup(r);
        }
        if (postRenderPasses != null) {
            for (Iterator<Pass> it = postRenderPasses.iterator(); it.hasNext();) {
                Pass pass = it.next();
                pass.cleanup(r);
            }
        }
        cleanUpFilter(r);
    }

    /**
     * Initialization of filter subclasses.
     * This method is called once when the filter is added to the FilterPostProcessor
     * It should contain Material initializations and extra passes initialization
     * @param manager the assetManager
     * @param renderManager the renderManager
     * @param vp the viewPort where this filter is rendered
     * @param w the width of the filter
     * @param h the height of the filter
     */
    protected abstract void initFilter(AssetManager manager, RenderManager renderManager, ViewPort vp, int w, int h);

    /**
     * override this method if you have some cleanup to do
     * @param r the renderer
     */
    protected void cleanUpFilter(Renderer r) {
    }

    /**
     * Must return the material used for this filter.
     * this method is called every frame.
     *
     * @return the material used for this filter.
     */
    protected abstract Material getMaterial();
    
    /**
     * Override if you want to do something special with the depth texture;
     *
     * @param depthTexture the desired Texture
     */
    protected void setDepthTexture(Texture depthTexture){
        getMaterial().setTexture("DepthTexture", depthTexture);
    }

    /**
     * Override this method if you want to make a pre pass, before the actual rendering of the frame
     *
     * @param queue the RenderQueue
     */
    protected void postQueue(RenderQueue queue) {
    }

    /**
     * Override this method if you want to modify parameters according to tpf before the rendering of the frame.
     * This is useful for animated filters
     * Also it can be the place to render pre passes
     * @param tpf the time used to render the previous frame
     */
    protected void preFrame(float tpf) {
    }

    /**
     * Override this method if you want to make a pass just after the frame has been rendered and just before the filter rendering
     *
     * @param renderManager for rendering
     * @param viewPort for rendering
     * @param prevFilterBuffer the FrameBuffer of the previous filter
     * @param sceneBuffer the FrameBuffer of the scene
     */
    protected void postFrame(RenderManager renderManager, ViewPort viewPort, FrameBuffer prevFilterBuffer, FrameBuffer sceneBuffer) {
    }

    /**
     * Override this method if you want to save extra properties when the filter is saved else only basic properties of the filter will be saved
     * This method should always begin by super.write(ex);
     *
     * @param ex the exporter (not null)
     * @throws IOException from the exporter
     */
    @Override
    public void write(JmeExporter ex) throws IOException {
        OutputCapsule oc = ex.getCapsule(this);
        oc.write(name, "name", "");
        oc.write(enabled, "enabled", true);
    }

    /**
     * Override this method if you want to load extra properties when the filter
     * is loaded else only basic properties of the filter will be loaded
     * This method should always begin by super.read(im);
     */
    @Override
    public void read(JmeImporter im) throws IOException {
        InputCapsule ic = im.getCapsule(this);
        name = ic.readString("name", "");
        enabled = ic.readBoolean("enabled", true);
    }

    /**
     * returns the name of the filter
     * @return the Filter's name
     */
    public String getName() {
        return name;
    }

    /**
     * Sets the name of the filter
     *
     * @param name the desired name (alias created)
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * returns the default pass frame buffer
     * @return the pre-existing buffer
     */
    protected FrameBuffer getRenderFrameBuffer() {
        return defaultPass.renderFrameBuffer;
    }

    /**
     * sets the default pass frame buffer
     *
     * @param renderFrameBuffer the buffer to use (alias created)
     */
    protected void setRenderFrameBuffer(FrameBuffer renderFrameBuffer) {
        this.defaultPass.renderFrameBuffer = renderFrameBuffer;
    }

    /**
     * returns the rendered texture of this filter
     * @return the pre-existing texture
     */
    protected Texture2D getRenderedTexture() {
        return defaultPass.renderedTexture;
    }

    /**
     * sets the rendered texture of this filter
     * @param renderedTexture the desired Texture (alias created)
     */
    protected void setRenderedTexture(Texture2D renderedTexture) {
        this.defaultPass.renderedTexture = renderedTexture;
    }

    /**
     * Override this method and return true if your Filter needs the depth texture
     *
     * @return true if your Filter need the depth texture
     */
    protected boolean isRequiresDepthTexture() {
        return false;
    }

    /**
     * Override this method and return false if your Filter does not need the scene texture
     *
     * @return false if your Filter does not need the scene texture
     */
    protected boolean isRequiresSceneTexture() {
        return true;
    }

    /**
     * Override this method and return true if you want the scene (input) texture
     * to use bilinear filtering or false to use nearest filtering.
     * 
     * Typically, filters that perform samples <em>in between</em> pixels
     * should enable bilinear filtering.
     * 
     * @return true to use linear filtering, false to use nearest filtering.
     */
    protected boolean isRequiresBilinear() {
        return false;
    }
    
    /**
     * returns the list of the postRender passes
     * @return the pre-existing List
     */
    protected List<Pass> getPostRenderPasses() {
        return postRenderPasses;
    }

    /**
     * Enable or disable this filter
     * @param enabled true to enable
     */
    public void setEnabled(boolean enabled) {
        if (processor != null) {
            processor.setFilterState(this, enabled);
        } else {
            this.enabled = enabled;
        }
    }

    /**
     * returns true if the filter is enabled
     * @return enabled
     */
    public boolean isEnabled() {
        return enabled;
    }

    /**
     * sets a reference to the FilterPostProcessor to which this filter is attached
     *
     * @param proc the desired FPP (alias created)
     */
    protected void setProcessor(FilterPostProcessor proc) {
        processor = proc;
    }

    /**
     * This method is called right after the filter has been rendered to the 
     * framebuffer.
     * Note that buffer will be null if the filter is the last one in the stack 
     * and has been rendered to screen
     * @param r the renderer
     * @param buffer the framebuffer on which the filter has been rendered.
     */
    protected void postFilter(Renderer r, FrameBuffer buffer){        
    }
}
