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

import com.jme3.asset.AssetManager;
import com.jme3.material.Material;
import com.jme3.math.*;
import com.jme3.post.SceneProcessor;
import com.jme3.profile.AppProfiler;
import com.jme3.renderer.*;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Quad;
import com.jme3.texture.*;
import com.jme3.texture.Image.Format;
import com.jme3.texture.Texture.WrapMode;
import com.jme3.ui.Picture;

/**
 *
 * Simple Water renders a simple plane that use reflection and refraction to look like water.
 * It's pretty basic, but much faster than the WaterFilter
 * It's useful if you aim for low specs hardware and still want a good looking water.
 * Usage is :
 * <code>
 *      SimpleWaterProcessor waterProcessor = new SimpleWaterProcessor(assetManager);
 *      //setting the scene to use for reflection
 *      waterProcessor.setReflectionScene(mainScene);
 *      //setting the light position
 *      waterProcessor.setLightPosition(lightPos);
 *
 *      //setting the water plane
 *      Vector3f waterLocation=new Vector3f(0,-20,0);
 *      waterProcessor.setPlane(new Plane(Vector3f.UNIT_Y, waterLocation.dot(Vector3f.UNIT_Y)));
 *      //setting the water color
 *      waterProcessor.setWaterColor(ColorRGBA.Brown);
 *
 *      //creating a quad to render water to
 *      Quad quad = new Quad(400,400);
 *
 *      //the texture coordinates define the general size of the waves
 *      quad.scaleTextureCoordinates(new Vector2f(6f,6f));
 *
 *      //creating a geom to attach the water material
 *      Geometry water=new Geometry("water", quad);
 *      water.setLocalTranslation(-200, -20, 250);
 *      water.setLocalRotation(new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X));
 *      //finally setting the material
 *      water.setMaterial(waterProcessor.getMaterial());
 *
 *      //attaching the water to the root node
 *      rootNode.attachChild(water);
 * </code>
 * @author Normen Hansen and Rémy Bouquet
 */
public class SimpleWaterProcessor implements SceneProcessor {

    protected RenderManager rm;
    protected ViewPort vp;
    protected Spatial reflectionScene;
    protected ViewPort reflectionView;
    protected ViewPort refractionView;
    protected FrameBuffer reflectionBuffer;
    protected FrameBuffer refractionBuffer;
    protected Camera reflectionCam;
    protected Camera refractionCam;
    protected Texture2D reflectionTexture;
    protected Texture2D refractionTexture;
    protected Texture2D depthTexture;
    protected Texture2D normalTexture;
    protected Texture2D dudvTexture;
    protected int renderWidth = 512;
    protected int renderHeight = 512;
    protected Plane plane = new Plane(Vector3f.UNIT_Y, Vector3f.ZERO.dot(Vector3f.UNIT_Y));
    protected float speed = 0.05f;
    protected Ray ray = new Ray();
    protected Vector3f targetLocation = new Vector3f();
    protected AssetManager manager;
    protected Material material;
    protected float waterDepth = 1;
    protected float waterTransparency = 0.4f;
    protected boolean debug = false;
    private Picture dispRefraction;
    private Picture dispReflection;
    private Picture dispDepth;
    private Plane reflectionClipPlane;
    private Plane refractionClipPlane;
    private float refractionClippingOffset = 0.3f;
    private float reflectionClippingOffset = -5f;        
    private float distortionScale = 0.2f;
    private float distortionMix = 0.5f;
    private float texScale = 1f;
    private AppProfiler prof;


    /**
     * Creates a SimpleWaterProcessor
     * @param manager the asset manager
     */
    public SimpleWaterProcessor(AssetManager manager) {
        this.manager = manager;
        material = new Material(manager, "Common/MatDefs/Water/SimpleWater.j3md");
        material.setFloat("waterDepth", waterDepth);
        material.setFloat("waterTransparency", waterTransparency / 10);
        material.setColor("waterColor", ColorRGBA.White);
        material.setVector3("lightPos", new Vector3f(1, -1, 1));
        
        material.setFloat("distortionScale", distortionScale);
        material.setFloat("distortionMix", distortionMix);
        material.setFloat("texScale", texScale);
        updateClipPlanes();

    }

    @Override
    public void initialize(RenderManager rm, ViewPort vp) {
        this.rm = rm;
        this.vp = vp;

        loadTextures(manager);
        createTextures();
        applyTextures(material);

        createPreViews();

        material.setVector2("FrustumNearFar", new Vector2f(vp.getCamera().getFrustumNear(), vp.getCamera().getFrustumFar()));

        if (debug) {
            dispRefraction = new Picture("dispRefraction");
            dispRefraction.setTexture(manager, refractionTexture, false);
            dispReflection = new Picture("dispRefraction");
            dispReflection.setTexture(manager, reflectionTexture, false);
            dispDepth = new Picture("depthTexture");
            dispDepth.setTexture(manager, depthTexture, false);
        }
    }

    @Override
    public void reshape(ViewPort vp, int w, int h) {
    }

    @Override
    public boolean isInitialized() {
        return rm != null;
    }
    float time = 0;
    float savedTpf = 0;

    @Override
    public void preFrame(float tpf) {
        time = time + (tpf * speed);
        if (time > 1f) {
            time = 0;
        }
        material.setFloat("time", time);
        savedTpf = tpf;
    }

    @Override
    public void postQueue(RenderQueue rq) {
        Camera sceneCam = rm.getCurrentCamera();

        //update refraction cam
        refractionCam.setLocation(sceneCam.getLocation());
        refractionCam.setRotation(sceneCam.getRotation());
        refractionCam.setFrustum(sceneCam.getFrustumNear(),
                sceneCam.getFrustumFar(),
                sceneCam.getFrustumLeft(),
                sceneCam.getFrustumRight(),
                sceneCam.getFrustumTop(),
                sceneCam.getFrustumBottom());
        refractionCam.setParallelProjection(sceneCam.isParallelProjection());

        //update reflection cam
        WaterUtils.updateReflectionCam(reflectionCam, plane, sceneCam);
        
        //Rendering reflection and refraction
        rm.renderViewPort(reflectionView, savedTpf);
        rm.renderViewPort(refractionView, savedTpf);
        rm.getRenderer().setFrameBuffer(vp.getOutputFrameBuffer());
        rm.setCamera(sceneCam, false);

    }

    @Override
    public void postFrame(FrameBuffer out) {
        if (debug) {
            displayMap(rm.getRenderer(), dispRefraction, 64);
            displayMap(rm.getRenderer(), dispReflection, 256);
            displayMap(rm.getRenderer(), dispDepth, 448);
        }
    }

    @Override
    public void cleanup() {
    }

    @Override
    public void setProfiler(AppProfiler profiler) {
        this.prof = profiler;
    }

    //debug only : displays maps
    protected void displayMap(Renderer r, Picture pic, int left) {
        Camera cam = vp.getCamera();
        rm.setCamera(cam, true);
        int h = cam.getHeight();

        pic.setPosition(left, h / 20f);

        pic.setWidth(128);
        pic.setHeight(128);
        pic.updateGeometricState();
        rm.renderGeometry(pic);
        rm.setCamera(cam, false);
    }

    protected void loadTextures(AssetManager manager) {
        normalTexture = (Texture2D) manager.loadTexture("Common/MatDefs/Water/Textures/water_normalmap.png");
        dudvTexture = (Texture2D) manager.loadTexture("Common/MatDefs/Water/Textures/dudv_map.jpg");
        normalTexture.setWrap(WrapMode.Repeat);
        dudvTexture.setWrap(WrapMode.Repeat);
    }

    protected void createTextures() {
        reflectionTexture = new Texture2D(renderWidth, renderHeight, Format.RGBA8);
        refractionTexture = new Texture2D(renderWidth, renderHeight, Format.RGBA8);
        
        reflectionTexture.setMinFilter(Texture.MinFilter.Trilinear);
        reflectionTexture.setMagFilter(Texture.MagFilter.Bilinear);
        
        refractionTexture.setMinFilter(Texture.MinFilter.Trilinear);
        refractionTexture.setMagFilter(Texture.MagFilter.Bilinear);
        
        depthTexture = new Texture2D(renderWidth, renderHeight, Format.Depth);
    }

    protected void applyTextures(Material mat) {
        mat.setTexture("water_reflection", reflectionTexture);
        mat.setTexture("water_refraction", refractionTexture);
        mat.setTexture("water_depthmap", depthTexture);
        mat.setTexture("water_normalmap", normalTexture);
        mat.setTexture("water_dudvmap", dudvTexture);
    }

    protected void createPreViews() {
        reflectionCam = new Camera(renderWidth, renderHeight);
        refractionCam = new Camera(renderWidth, renderHeight);

        // create a pre-view. a view that is rendered before the main view
        reflectionView = new ViewPort("Reflection View", reflectionCam);
        reflectionView.setClearFlags(true, true, true);
        reflectionView.setBackgroundColor(ColorRGBA.Black);
        // create offscreen framebuffer
        reflectionBuffer = new FrameBuffer(renderWidth, renderHeight, 1);
        //setup framebuffer to use texture
        reflectionBuffer.setDepthBuffer(Format.Depth);
        reflectionBuffer.setColorTexture(reflectionTexture);

        //set viewport to render to offscreen framebuffer
        reflectionView.setOutputFrameBuffer(reflectionBuffer);
        reflectionView.addProcessor(new ReflectionProcessor(reflectionCam, reflectionBuffer, reflectionClipPlane));
        // attach the scene to the viewport to be rendered
        reflectionView.attachScene(reflectionScene);

        // create a pre-view. a view that is rendered before the main view
        refractionView = new ViewPort("Refraction View", refractionCam);
        refractionView.setClearFlags(true, true, true);
        refractionView.setBackgroundColor(ColorRGBA.Black);
        // create offscreen framebuffer
        refractionBuffer = new FrameBuffer(renderWidth, renderHeight, 1);
        //setup framebuffer to use texture
        refractionBuffer.setDepthBuffer(Format.Depth);
        refractionBuffer.setColorTexture(refractionTexture);
        refractionBuffer.setDepthTexture(depthTexture);
        //set viewport to render to offscreen framebuffer
        refractionView.setOutputFrameBuffer(refractionBuffer);
        refractionView.addProcessor(new RefractionProcessor());
        // attach the scene to the viewport to be rendered
        refractionView.attachScene(reflectionScene);
    }

    protected void destroyViews() {
        //  rm.removePreView(reflectionView);
        rm.removePreView(refractionView);
    }

    /**
     * Get the water material from this processor, apply this to your water quad.
     * @return the pre-existing Material
     */
    public Material getMaterial() {
        return material;
    }

    /**
     * Sets the reflected scene, should not include the water quad!
     * Set before adding processor.
     * @param spat
     */
    public void setReflectionScene(Spatial spat) {
        reflectionScene = spat;
    }

    /**
     * returns the width of the reflection and refraction textures
     * @return the width (in pixels)
     */
    public int getRenderWidth() {
        return renderWidth;
    }

    /**
     * returns the height of the reflection and refraction textures
     * @return the height (in pixels)
     */
    public int getRenderHeight() {
        return renderHeight;
    }

    /**
     * Set the reflection Texture render size,
     * set before adding the processor!
     * @param width
     * @param height
     */
    public void setRenderSize(int width, int height) {
        renderWidth = width;
        renderHeight = height;
    }

    /**
     * returns the water plane
     * @return the pre-existing instance
     */
    public Plane getPlane() {
        return plane;
    }

    /**
     * Set the water plane for this processor.
     * @param plane
     */
    public void setPlane(Plane plane) {
        this.plane.setConstant(plane.getConstant());
        this.plane.setNormal(plane.getNormal());
        updateClipPlanes();
    }

    /**
     * Set the water plane using an origin (location) and a normal (reflection direction).
     * @param origin Set to 0,-6,0 if your water quad is at that location for correct reflection
     * @param normal Set to 0,1,0 (Vector3f.UNIT_Y) for normal planar water
     */
    public void setPlane(Vector3f origin, Vector3f normal) {
        this.plane.setOriginNormal(origin, normal);
        updateClipPlanes();
    }

    private void updateClipPlanes() {
        reflectionClipPlane = plane.clone();
        reflectionClipPlane.setConstant(reflectionClipPlane.getConstant() + reflectionClippingOffset);
        refractionClipPlane = plane.clone();
        refractionClipPlane.setConstant(refractionClipPlane.getConstant() + refractionClippingOffset);

    }

    /**
     * Set the light Position for the processor
     * @param position
     */
    //TODO maybe we should provide a convenient method to compute position from direction
    public void setLightPosition(Vector3f position) {
        material.setVector3("lightPos", position);
    }

    /**
     * Set the color that will be added to the refraction texture.
     * @param color
     */
    public void setWaterColor(ColorRGBA color) {
        material.setColor("waterColor", color);
    }

    /**
     * Higher values make the refraction texture shine through earlier.
     * Default is 4
     * @param depth
     */
    public void setWaterDepth(float depth) {
        waterDepth = depth;
        material.setFloat("waterDepth", depth);
    }

    /**
     * return the water depth
     * @return the depth
     */
    public float getWaterDepth() {
        return waterDepth;
    }

    /**
     * returns water transparency
     * @return the transparency value
     */
    public float getWaterTransparency() {
        return waterTransparency;
    }

    /**
     * sets the water transparency default os 0.1f
     * @param waterTransparency
     */
    public void setWaterTransparency(float waterTransparency) {
        this.waterTransparency = Math.max(0, waterTransparency);
        material.setFloat("waterTransparency", waterTransparency / 10);
    }

    /**
     * Sets the speed of the wave animation, default = 0.05f.
     * @param speed
     */
    public void setWaveSpeed(float speed) {
        this.speed = speed;
    }

    /**
     * returns the speed of the wave animation.
     * @return the speed
     */
    public float getWaveSpeed(){
        return speed;
    }
    
    /**
     * Sets the scale of distortion by the normal map, default = 0.2
     */
    public void setDistortionScale(float value) {
        distortionScale  = value;
        material.setFloat("distortionScale", distortionScale);
    }

    /**
     * Sets how the normal and dudv map are mixed to create the wave effect, default = 0.5
     */
    public void setDistortionMix(float value) {
        distortionMix = value;
        material.setFloat("distortionMix", distortionMix);
    }

    /**
     * Sets the scale of the normal/dudv texture, default = 1.
     * Note that the waves should be scaled by the texture coordinates of the quad to avoid animation artifacts,
     * use mesh.scaleTextureCoordinates(Vector2f) for that.
     */
    public void setTexScale(float value) {
        texScale = value;
        material.setFloat("texScale", texScale);
    }

    /**
     * returns the scale of distortion by the normal map, default = 0.2
     *
     * @return the distortion scale
     */
    public float getDistortionScale() {
        return distortionScale;
    }

    /**
     * returns how the normal and dudv map are mixed to create the wave effect,
     * default = 0.5
     *
     * @return the distortion mix
     */
    public float getDistortionMix() {
        return distortionMix;
    }

    /**
     * returns the scale of the normal/dudv texture, default = 1. Note that the
     * waves should be scaled by the texture coordinates of the quad to avoid
     * animation artifacts, use mesh.scaleTextureCoordinates(Vector2f) for that.
     *
     * @return the textures scale
     */
    public float getTexScale() {
        return texScale;
    }


    /**
     * returns true if the waterprocessor is in debug mode
     * @return true if in debug mode, otherwise false
     */
    public boolean isDebug() {
        return debug;
    }

    /**
     * set to true to display reflection and refraction textures in the GUI for debug purpose
     * @param debug
     */
    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    /**
     * Creates a quad with the water material applied to it.
     * @param width
     * @param height
     * @return a new Geometry
     */
    public Geometry createWaterGeometry(float width, float height) {
        Quad quad = new Quad(width, height);
        Geometry geom = new Geometry("WaterGeometry", quad);
        geom.setLocalRotation(new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X));
        geom.setMaterial(material);
        return geom;
    }

    /**
     * returns the reflection clipping plane offset
     * @return the offset value
     */
    public float getReflectionClippingOffset() {
        return reflectionClippingOffset;
    }

    /**
     * sets the reflection clipping plane offset
     * set a nagetive value to lower the clipping plane for relection texture rendering.
     * @param reflectionClippingOffset
     */
    public void setReflectionClippingOffset(float reflectionClippingOffset) {
        this.reflectionClippingOffset = reflectionClippingOffset;
        updateClipPlanes();
    }

    /**
     * returns the refraction clipping plane offset
     * @return the offset value
     */
    public float getRefractionClippingOffset() {
        return refractionClippingOffset;
    }

    /**
     * Sets the refraction clipping plane offset
     * set a positive value to raise the clipping plane for refraction texture rendering
     * @param refractionClippingOffset
     */
    public void setRefractionClippingOffset(float refractionClippingOffset) {
        this.refractionClippingOffset = refractionClippingOffset;
        updateClipPlanes();
    }

    /**
     * Refraction Processor
     */
    public class RefractionProcessor implements SceneProcessor {

        RenderManager rm;
        ViewPort vp;
        private AppProfiler prof;

        @Override
        public void initialize(RenderManager rm, ViewPort vp) {
            this.rm = rm;
            this.vp = vp;
        }

        @Override
        public void reshape(ViewPort vp, int w, int h) {
        }

        @Override
        public boolean isInitialized() {
            return rm != null;
        }

        @Override
        public void preFrame(float tpf) {
            refractionCam.setClipPlane(refractionClipPlane, Plane.Side.Negative);//,-1

        }

        @Override
        public void postQueue(RenderQueue rq) {
        }

        @Override
        public void postFrame(FrameBuffer out) {
        }

        @Override
        public void cleanup() {
        }

        @Override
        public void setProfiler(AppProfiler profiler) {
            this.prof = profiler;
        }
    }
}
