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

import java.io.IOException;

import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.math.FastMath;
import com.jme3.math.Matrix3f;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;

public class BillboardControl extends AbstractControl {

    private Matrix3f orient;
    private Vector3f look;
    private Vector3f left;
    private Alignment alignment;

    /**
     * Determines how the billboard is aligned to the screen/camera.
     */
    public enum Alignment {
        /**
         * Aligns this Billboard to the screen.
         */
        Screen,

        /**
         * Aligns this Billboard to the camera position.
         */
        Camera,

        /**
          * Aligns this Billboard to the screen, but keeps the Y axis fixed.
          */
        AxialY,

        /**
         * Aligns this Billboard to the screen, but keeps the Z axis fixed.
         */
        AxialZ;
    }

    public BillboardControl() {
        super();
        orient = new Matrix3f();
        look = new Vector3f();
        left = new Vector3f();
        alignment = Alignment.Screen;
    }

    public Control cloneForSpatial(Spatial spatial) {
        BillboardControl control = new BillboardControl();
        control.alignment = this.alignment;
        control.setSpatial(spatial);
        return control;
    }

    @Override
    protected void controlUpdate(float tpf) {
    }

    @Override
    protected void controlRender(RenderManager rm, ViewPort vp) {
        Camera cam = vp.getCamera();
        rotateBillboard(cam);
    }
    
    private void fixRefreshFlags(){
        // force transforms to update below this node
        spatial.updateGeometricState();
        
        // force world bound to update
        Spatial rootNode = spatial;
        while (rootNode.getParent() != null){
            rootNode = rootNode.getParent();
        }
        rootNode.getWorldBound(); 
    }

    /**
     * rotate the billboard based on the type set
     *
     * @param cam
     *            Camera
     */
    private void rotateBillboard(Camera cam) {
        switch (alignment) {
            case AxialY:
                rotateAxial(cam, Vector3f.UNIT_Y);
                break;
            case AxialZ:
                rotateAxial(cam, Vector3f.UNIT_Z);
                break;
            case Screen:
                rotateScreenAligned(cam);
                break;
            case Camera:
                rotateCameraAligned(cam);
                break;
        }
    }

    /**
     * Aligns this Billboard so that it points to the camera position.
     *
     * @param camera
     *            Camera
     */
    private void rotateCameraAligned(Camera camera) {
        look.set(camera.getLocation()).subtractLocal(
                spatial.getWorldTranslation());
        // coopt left for our own purposes.
        Vector3f xzp = left;
        // The xzp vector is the projection of the look vector on the xz plane
        xzp.set(look.x, 0, look.z);

        // check for undefined rotation...
        if (xzp.equals(Vector3f.ZERO)) {
            return;
        }

        look.normalizeLocal();
        xzp.normalizeLocal();
        float cosp = look.dot(xzp);

        // compute the local orientation matrix for the billboard
        orient.set(0, 0, xzp.z);
        orient.set(0, 1, xzp.x * -look.y);
        orient.set(0, 2, xzp.x * cosp);
        orient.set(1, 0, 0);
        orient.set(1, 1, cosp);
        orient.set(1, 2, look.y);
        orient.set(2, 0, -xzp.x);
        orient.set(2, 1, xzp.z * -look.y);
        orient.set(2, 2, xzp.z * cosp);

        // The billboard must be oriented to face the camera before it is
        // transformed into the world.
        spatial.setLocalRotation(orient);
        fixRefreshFlags();
    }

    /**
     * Rotate the billboard so it points directly opposite the direction the
     * camera's facing
     *
     * @param camera
     *            Camera
     */
    private void rotateScreenAligned(Camera camera) {
        // coopt diff for our in direction:
        look.set(camera.getDirection()).negateLocal();
        // coopt loc for our left direction:
        left.set(camera.getLeft()).negateLocal();
        orient.fromAxes(left, camera.getUp(), look);
        Node parent = spatial.getParent();
        Quaternion rot=new Quaternion().fromRotationMatrix(orient);
        if ( parent != null ) {
            rot =  parent.getWorldRotation().inverse().multLocal(rot);
            rot.normalizeLocal();
        }
        spatial.setLocalRotation(rot);
        fixRefreshFlags();
    }

    /**
     * Rotate the billboard towards the camera, but keeping a given axis fixed.
     *
     * @param camera
     *            Camera
     */
    private void rotateAxial(Camera camera, Vector3f axis) {
        // Compute the additional rotation required for the billboard to face
        // the camera. To do this, the camera must be inverse-transformed into
        // the model space of the billboard.
        look.set(camera.getLocation()).subtractLocal(
                spatial.getWorldTranslation());   
        spatial.getParent().getWorldRotation().mult(look, left); // coopt left for our own
        // purposes.
        left.x *= 1.0f / spatial.getWorldScale().x;
        left.y *= 1.0f / spatial.getWorldScale().y;
        left.z *= 1.0f / spatial.getWorldScale().z;

        // squared length of the camera projection in the xz-plane
        float lengthSquared = left.x * left.x + left.z * left.z;
        if (lengthSquared < FastMath.FLT_EPSILON) {
            // camera on the billboard axis, rotation not defined
            return;
        }

        // unitize the projection
        float invLength = FastMath.invSqrt(lengthSquared);
        if (axis.y == 1) {
            left.x *= invLength;
            left.y = 0.0f;
            left.z *= invLength;

            // compute the local orientation matrix for the billboard
            orient.set(0, 0, left.z);
            orient.set(0, 1, 0);
            orient.set(0, 2, left.x);
            orient.set(1, 0, 0);
            orient.set(1, 1, 1);
            orient.set(1, 2, 0);
            orient.set(2, 0, -left.x);
            orient.set(2, 1, 0);
            orient.set(2, 2, left.z);
        } else if (axis.z == 1) {
            left.x *= invLength;
            left.y *= invLength;
            left.z = 0.0f;

            // compute the local orientation matrix for the billboard
            orient.set(0, 0, left.y);
            orient.set(0, 1, left.x);
            orient.set(0, 2, 0);
            orient.set(1, 0, -left.y);
            orient.set(1, 1, left.x);
            orient.set(1, 2, 0);
            orient.set(2, 0, 0);
            orient.set(2, 1, 0);
            orient.set(2, 2, 1);
        }

        // The billboard must be oriented to face the camera before it is
        // transformed into the world.
        spatial.setLocalRotation(orient);
        fixRefreshFlags();
    }

    /**
     * Returns the alignment this Billboard is set too.
     *
     * @return The alignment of rotation, AxialY, AxialZ, Camera or Screen.
     */
    public Alignment getAlignment() {
        return alignment;
    }

    /**
     * Sets the type of rotation this Billboard will have. The alignment can
     * be Camera, Screen, AxialY, or AxialZ. Invalid alignments will
     * assume no billboard rotation.
     */
    public void setAlignment(Alignment alignment) {
        this.alignment = alignment;
    }

    @Override
    public void write(JmeExporter e) throws IOException {
        super.write(e);
        OutputCapsule capsule = e.getCapsule(this);
        capsule.write(orient, "orient", null);
        capsule.write(look, "look", null);
        capsule.write(left, "left", null);
        capsule.write(alignment, "alignment", Alignment.Screen);
    }

    @Override
    public void read(JmeImporter e) throws IOException {
        super.read(e);
        InputCapsule capsule = e.getCapsule(this);
        orient = (Matrix3f) capsule.readSavable("orient", null);
        look = (Vector3f) capsule.readSavable("look", null);
        left = (Vector3f) capsule.readSavable("left", null);
        alignment = capsule.readEnum("alignment", Alignment.class, Alignment.Screen);
    }
}
