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

import com.jme3.asset.AssetManager;
import com.jme3.cinematic.events.MotionEvent;
import com.jme3.export.*;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Spline;
import com.jme3.math.Spline.SplineType;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Curve;
import com.jme3.util.TempVars;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * Motion path is used to create a path between way points.
 * @author Nehon
 */
public class MotionPath implements Savable {

    private Node debugNode;
    private AssetManager assetManager;
    private List<MotionPathListener> listeners;
    private Spline spline = new Spline();
    int prevWayPoint = 0;

    /**
     * Create a motion Path
     */
    public MotionPath() {
    }

    /**
     * interpolate the path giving the time since the beginning and the motionControl
     * this methods sets the new localTranslation to the spatial of the MotionEvent control.
     * @param time the time since the animation started
     * @param control the control over the moving spatial
     * @param tpf time per frame (in seconds)
     * @return the distance travelled (in world units)
     */
    public float interpolatePath(float time, MotionEvent control, float tpf) {

        float traveledDistance = 0;
        TempVars vars = TempVars.get();
        Vector3f temp = vars.vect1;
        Vector3f tmpVector = vars.vect2;
        Vector2f v = vars.vect2d;
        //computing traveled distance according to new time
        traveledDistance = time * (getLength() / control.getInitialDuration());

        //getting waypoint index and current value from new traveled distance
        v = getWayPointIndexForDistance(traveledDistance, v);

        //setting values
        control.setCurrentWayPoint((int) v.x);
        control.setCurrentValue(v.y);

        //interpolating new position
        getSpline().interpolate(control.getCurrentValue(), control.getCurrentWayPoint(), temp);
        if (control.needsDirection()) {
            tmpVector.set(temp);
            control.setDirection(tmpVector.subtractLocal(control.getSpatial().getLocalTranslation()).normalizeLocal());
        }
        checkWayPoint(control, tpf);

        control.getSpatial().setLocalTranslation(temp);
        vars.release();
        return traveledDistance;
    }

    public void checkWayPoint(MotionEvent control, float tpf) {
        //Epsilon varies with the tpf to avoid missing a waypoint on low framerate.
        float epsilon = tpf * 4f;
        if (control.getCurrentWayPoint() != prevWayPoint) {
            if (control.getCurrentValue() >= 0f && control.getCurrentValue() < epsilon) {
                triggerWayPointReach(control.getCurrentWayPoint(), control);
                prevWayPoint = control.getCurrentWayPoint();
            }
        }
    }

    private void attachDebugNode(Node root) {
        if (debugNode == null) {
            debugNode = new Node();
            Material m = assetManager.loadMaterial("Common/Materials/RedColor.j3m");
            for (Iterator<Vector3f> it = spline.getControlPoints().iterator(); it.hasNext();) {
                Vector3f cp = it.next();
                Geometry geo = new Geometry("box", new Box(0.3f, 0.3f, 0.3f));
                geo.setLocalTranslation(cp);
                geo.setMaterial(m);
                debugNode.attachChild(geo);

            }
            switch (spline.getType()) {
                case CatmullRom:
                    debugNode.attachChild(createCatmullRomPath());
                    break;
                case Linear:
                    debugNode.attachChild(createLinearPath());
                    break;
                default:
                    debugNode.attachChild(createLinearPath());
                    break;
            }

            root.attachChild(debugNode);
        }
    }

    private Geometry createLinearPath() {

        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.getAdditionalRenderState().setWireframe(true);
        mat.setColor("Color", ColorRGBA.Blue);
        Geometry lineGeometry = new Geometry("line", new Curve(spline, 0));
        lineGeometry.setMaterial(mat);
        return lineGeometry;
    }

    private Geometry createCatmullRomPath() {

        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.getAdditionalRenderState().setWireframe(true);
        mat.setColor("Color", ColorRGBA.Blue);
        Geometry lineGeometry = new Geometry("line", new Curve(spline, 10));
        lineGeometry.setMaterial(mat);
        return lineGeometry;
    }

    @Override
    public void write(JmeExporter ex) throws IOException {
        OutputCapsule oc = ex.getCapsule(this);
        oc.write(spline, "spline", null);
    }

    @Override
    public void read(JmeImporter im) throws IOException {
        InputCapsule in = im.getCapsule(this);
        spline = (Spline) in.readSavable("spline", null);
    }

    /**
     * compute the index of the waypoint and the interpolation value according to a distance
     * returns a vector 2 containing the index in the x field and the interpolation value in the y field
     * @param distance the distance traveled on this path
     * @param store storage for the result (not null, modified)
     * @return the waypoint index and the interpolation value in a vector2
     */
    public Vector2f getWayPointIndexForDistance(float distance, Vector2f store) {
        float sum = 0;
        if (spline.getTotalLength() == 0) {
            store.set(0, 0);
            return store;
        }
        distance = distance % spline.getTotalLength();
        int i = 0;
        for (Float len : spline.getSegmentsLength()) {
            if (sum + len >= distance) {
                return new Vector2f(i, (distance - sum) / len);
            }
            sum += len;
            i++;
        }
        store.set((float) spline.getControlPoints().size() - 1, 1.0f);
        return store;
    }

    /**
     * Add a waypoint to the path
     * @param wayPoint a position in world space
     */
    public void addWayPoint(Vector3f wayPoint) {
        spline.addControlPoint(wayPoint);
    }

    /**
     * Return the length of the path in world units
     * @return the length
     */
    public float getLength() {
        return spline.getTotalLength();
    }

    /**
     * returns the waypoint at the given index
     * @param i the index
     * @return returns the waypoint position
     */
    public Vector3f getWayPoint(int i) {
        return spline.getControlPoints().get(i);
    }

    /**
     * remove the waypoint from the path
     * @param wayPoint the waypoint to remove
     */
    public void removeWayPoint(Vector3f wayPoint) {
        spline.removeControlPoint(wayPoint);
    }

    /**
     * remove the waypoint at the given index from the path
     * @param i the index of the waypoint to remove
     */
    public void removeWayPoint(int i) {
        removeWayPoint(spline.getControlPoints().get(i));
    }

    /**
     * returns an iterator on the waypoints collection
     * @return an iterator
     */
    public Iterator<Vector3f> iterator() {
        return spline.getControlPoints().iterator();
    }

    /**
     * return the type of spline used for the path interpolation for this path
     * @return the path interpolation spline type
     */
    public SplineType getPathSplineType() {
        return spline.getType();
    }

    /**
     * sets the type of spline used for the path interpolation for this path
     * @param pathSplineType the desired type
     */
    public void setPathSplineType(SplineType pathSplineType) {
        spline.setType(pathSplineType);
        if (debugNode != null) {
            Node parent = debugNode.getParent();
            debugNode.removeFromParent();
            debugNode.detachAllChildren();
            debugNode = null;
            attachDebugNode(parent);
        }
    }

    /**
     * disable the display of the path and the waypoints
     */
    public void disableDebugShape() {

        debugNode.detachAllChildren();
        debugNode = null;
        assetManager = null;
    }

    /**
     * enable the display of the path and the waypoints
     * @param manager the assetManager
     * @param rootNode the node where the debug shapes must be attached
     */
    public void enableDebugShape(AssetManager manager, Node rootNode) {
        assetManager = manager;
        // computeTotalLength();
        attachDebugNode(rootNode);
    }

    /**
     * Adds a motion pathListener to the path
     * @param listener the MotionPathListener to attach
     */
    public void addListener(MotionPathListener listener) {
        if (listeners == null) {
            listeners = new ArrayList<MotionPathListener>();
        }
        listeners.add(listener);
    }

    /**
     * remove the given listener
     * @param listener the listener to remove
     */
    public void removeListener(MotionPathListener listener) {
        if (listeners != null) {
            listeners.remove(listener);
        }
    }

    /**
     * return the number of waypoints of this path
     * @return the count (&ge;0)
     */
    public int getNbWayPoints() {
        return spline.getControlPoints().size();
    }

    public void triggerWayPointReach(int wayPointIndex, MotionEvent control) {
        if (listeners != null) {
            for (Iterator<MotionPathListener> it = listeners.iterator(); it.hasNext();) {
                MotionPathListener listener = it.next();
                listener.onWayPointReach(control, wayPointIndex);
            }
        }
    }

    /**
     * Returns the curve tension
     * @return the curve tension
     */
    public float getCurveTension() {
        return spline.getCurveTension();
    }

    /**
     * sets the tension of the curve (only for catmull rom) 0.0 will give a linear curve, 1.0 a round curve
     *
     * @param curveTension the desired value
     */
    public void setCurveTension(float curveTension) {
        spline.setCurveTension(curveTension);
        if (debugNode != null) {
            Node parent = debugNode.getParent();
            debugNode.removeFromParent();
            debugNode.detachAllChildren();
            debugNode = null;
            attachDebugNode(parent);
        }
    }

    public void clearWayPoints() {
        spline.clearControlPoints();
    }

    /**
     * Sets the path to be a cycle
     *
     * @param cycle true for a cycle, false for a non-cycle
     */
    public void setCycle(boolean cycle) {

        spline.setCycle(cycle);
        if (debugNode != null) {
            Node parent = debugNode.getParent();
            debugNode.removeFromParent();
            debugNode.detachAllChildren();
            debugNode = null;
            attachDebugNode(parent);
        }

    }

    /**
     * returns true if the path is a cycle
     * @return true if the path is a cycle
     */
    public boolean isCycle() {
        return spline.isCycle();
    }

    public Spline getSpline() {
        return spline;
    }
}
