/*
 * 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.water;

import com.jme3.asset.AssetManager;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Plane;
import com.jme3.math.Quaternion;
import com.jme3.math.Ray;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.post.SceneProcessor;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.Renderer;
import com.jme3.renderer.ViewPort;
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.FrameBuffer;
import com.jme3.texture.Image.Format;
import com.jme3.texture.Texture.WrapMode;
import com.jme3.texture.Texture2D;
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 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 & 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 Vector3f vect1 = new Vector3f();
    private Vector3f vect2 = new Vector3f();
    private Vector3f vect3 = new Vector3f();

    /**
     * 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.setColor("distortionScale", new ColorRGBA(0.2f, 0.2f, 0.2f, 0.2f));
        material.setColor("distortionMix", new ColorRGBA(0.5f, 0.5f, 0.5f, 0.5f));
        material.setColor("texScale", new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
        updateClipPlanes();

    }

    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);
        }
    }

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

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

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

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

        //update ray
        ray.setOrigin(sceneCam.getLocation());
        ray.setDirection(sceneCam.getDirection());

        //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());

        //update reflection cam
        boolean inv = false;
        if (!ray.intersectsWherePlane(plane, targetLocation)) {
            ray.setDirection(ray.getDirection().negateLocal());
            ray.intersectsWherePlane(plane, targetLocation);
            inv = true;
        }
        Vector3f loc = plane.reflect(sceneCam.getLocation(), new Vector3f());
        reflectionCam.setLocation(loc);
        reflectionCam.setFrustum(sceneCam.getFrustumNear(),
                sceneCam.getFrustumFar(),
                sceneCam.getFrustumLeft(),
                sceneCam.getFrustumRight(),
                sceneCam.getFrustumTop(),
                sceneCam.getFrustumBottom());
        // tempVec and calcVect are just temporary vector3f objects
        vect1.set(sceneCam.getLocation()).addLocal(sceneCam.getUp());
        float planeDistance = plane.pseudoDistance(vect1);
        vect2.set(plane.getNormal()).multLocal(planeDistance * 2.0f);
        vect3.set(vect1.subtractLocal(vect2)).subtractLocal(loc).normalizeLocal().negateLocal();
        // now set the up vector
        reflectionCam.lookAt(targetLocation, vect3);
        if (inv) {
            reflectionCam.setAxes(reflectionCam.getLeft().negateLocal(), reflectionCam.getUp(), reflectionCam.getDirection().negateLocal());
        }

        //Rendering reflection and refraction
        rm.renderViewPort(reflectionView, savedTpf);
        rm.renderViewPort(refractionView, savedTpf);
        rm.getRenderer().setFrameBuffer(vp.getOutputFrameBuffer());
        rm.setCamera(sceneCam, false);

    }

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

    public void cleanup() {
    }

    //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.dds");
        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);
        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
     */
    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 
     */
    public int getRenderWidth() {
        return renderWidth;
    }

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

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

    /**
     * returns the water plane
     * @return 
     */
    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 
     */
    public float getWaterDepth() {
        return waterDepth;
    }

    /**
     * returns water transparency
     * @return 
     */
    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;
    }

    /**
     * Sets the scale of distortion by the normal map, default = 0.2
     */
    public void setDistortionScale(float value) {
        material.setColor("distortionScale", new ColorRGBA(value, value, value, value));
    }

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

    /**
     * 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) {
        material.setColor("texScale", new ColorRGBA(value, value, value, value));
    }

    /**
     * retruns true if the waterprocessor is in debug mode
     * @return 
     */
    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
     */
    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
     */
    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
     */
    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;

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

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

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

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

        }

        public void postQueue(RenderQueue rq) {
        }

        public void postFrame(FrameBuffer out) {
        }

        public void cleanup() {
        }
    }
}
