/**
 * Copyright (c) 2008-2009 Ardor Labs, Inc.
 *
 * This file is part of Ardor3D.
 *
 * Ardor3D is free software: you can redistribute it and/or modify it
 * under the terms of its license which may be found in the accompanying
 * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
 */

package com.ardor3d.extension.effect.particle;

import java.io.IOException;
import java.nio.FloatBuffer;

import com.ardor3d.extension.effect.particle.ParticleSystem.ParticleType;
import com.ardor3d.math.ColorRGBA;
import com.ardor3d.math.MathUtils;
import com.ardor3d.math.Quaternion;
import com.ardor3d.math.Triangle;
import com.ardor3d.math.Vector3;
import com.ardor3d.math.type.ReadOnlyVector3;
import com.ardor3d.renderer.Camera;
import com.ardor3d.util.export.Ardor3DExporter;
import com.ardor3d.util.export.Ardor3DImporter;
import com.ardor3d.util.export.InputCapsule;
import com.ardor3d.util.export.OutputCapsule;
import com.ardor3d.util.export.Savable;
import com.ardor3d.util.geom.BufferUtils;

/**
 * <code>Particle</code> defines a single Particle of a Particle system. Generally, you would not interact with this
 * class directly.
 */
public class Particle implements Savable {

    public enum Status {
        /** Particle is dead -- not in play. */
        Dead,
        /** Particle is currently active. */
        Alive,
        /** Particle is available for spawning. */
        Available;
    }

    static final int VAL_CURRENT_SIZE = 0;
    static final int VAL_CURRENT_SPIN = 1;
    static final int VAL_CURRENT_MASS = 2;

    private int startIndex;
    private final Vector3 _position = new Vector3();
    private final Vector3 _velocity = new Vector3();
    private final ColorRGBA currColor = new ColorRGBA(ColorRGBA.BLACK);
    private Status status = Status.Available;
    private double lifeSpan;
    private final double[] values = new double[3];
    private int currentAge;
    private int currentTexIndex = -1;
    private ParticleSystem parent;
    private final Vector3 bbX = new Vector3(), bbY = new Vector3();

    // colors
    private ParticleType type = ParticleSystem.ParticleType.Quad;

    private Triangle triModel;

    /**
     * Empty constructor - mostly for use with Savable interface
     */
    public Particle() {}

    /**
     * Normal use constructor. Sets up the parent and particle type for this particle.
     * 
     * @param parent
     *            the particle collection this particle belongs to
     */
    public Particle(final ParticleSystem parent) {
        this.parent = parent;
        type = parent.getParticleType();
    }

    /**
     * Cause this particle to reset it's lifespan, velocity, color, age and size per the parent's settings. status is
     * set to Status.Available and location is set to 0,0,0. Actual geometry data is not affected by this call, only
     * particle params.
     */
    public void init() {
        init(parent.getRandomVelocity(_velocity), Vector3.ZERO, parent.getRandomLifeSpan());
    }

    /**
     * Cause this particle to reset it's color, age and size per the parent's settings. status is set to
     * Status.Available. Location, velocity and lifespan are set as given. Actual geometry data is not affected by this
     * call, only particle params.
     * 
     * @param velocity
     *            new initial particle velocity
     * @param position
     *            new initial particle position
     * @param lifeSpan
     *            new particle lifespan in ms
     */
    public void init(final ReadOnlyVector3 velocity, final ReadOnlyVector3 position, final double lifeSpan) {
        this.lifeSpan = lifeSpan;
        _velocity.set(velocity);
        _position.set(position);

        currColor.set(parent.getStartColor());
        currentAge = 0;
        status = Status.Available;
        values[VAL_CURRENT_SIZE] = parent.getStartSize();
    }

    /**
     * Reset particle conditions. Besides the passed lifespan, we also reset color, size, and spin angle to their
     * starting values (as given by parent.) Status is set to Status.Available.
     * 
     * @param lifeSpan
     *            the recreated particle's new lifespan
     */
    public void recreateParticle(final double lifeSpan) {
        this.lifeSpan = lifeSpan;

        final int verts = ParticleSystem.getVertsForParticleType(type);
        currColor.set(parent.getStartColor());
        for (int x = 0; x < verts; x++) {
            BufferUtils.setInBuffer(currColor, parent.getParticleGeometry().getMeshData().getColorBuffer(), startIndex
                    + x);
        }
        values[VAL_CURRENT_SIZE] = parent.getStartSize();
        currentAge = 0;
        values[VAL_CURRENT_MASS] = 1;
        status = Status.Available;
    }

    /**
     * Update the vertices for this particle, taking size, spin and viewer into consideration. In the case of particle
     * type ParticleType.GeomMesh, the original triangle normal is maintained rather than rotating it to face the camera
     * or parent vectors.
     * 
     * @param cam
     *            Camera to use in determining viewer aspect. If null, or if parent is not set to camera facing,
     *            parent's left and up vectors are used.
     */
    public void updateVerts(final Camera cam) {
        final double orient = parent.getParticleOrientation() + values[VAL_CURRENT_SPIN];
        final double currSize = values[VAL_CURRENT_SIZE];

        if (type == ParticleSystem.ParticleType.GeomMesh || type == ParticleSystem.ParticleType.Point) {
            ; // nothing to do
        } else if (cam != null && parent.isCameraFacing()) {
            final ReadOnlyVector3 camUp = cam.getUp();
            final ReadOnlyVector3 camLeft = cam.getLeft();
            final ReadOnlyVector3 camDir = cam.getDirection();
            if (parent.isVelocityAligned()) {
                bbX.set(_velocity).normalizeLocal().multiplyLocal(currSize);
                camDir.cross(bbX, bbY).normalizeLocal().multiplyLocal(currSize);
            } else if (orient == 0) {
                bbX.set(camLeft).multiplyLocal(currSize);
                bbY.set(camUp).multiplyLocal(currSize);
            } else {
                final double cA = MathUtils.cos(orient) * currSize;
                final double sA = MathUtils.sin(orient) * currSize;
                bbX.set(camLeft).multiplyLocal(cA).addLocal(camUp.getX() * sA, camUp.getY() * sA, camUp.getZ() * sA);
                bbY.set(camLeft).multiplyLocal(-sA).addLocal(camUp.getX() * cA, camUp.getY() * cA, camUp.getZ() * cA);
            }
        } else {
            bbX.set(parent.getLeftVector()).multiplyLocal(0);
            bbY.set(parent.getUpVector()).multiplyLocal(0);
        }

        final Vector3 tempVec3 = Vector3.fetchTempInstance();
        final FloatBuffer vertexBuffer = parent.getParticleGeometry().getMeshData().getVertexBuffer();
        switch (type) {
            case Quad: {
                _position.subtract(bbX, tempVec3).subtractLocal(bbY);
                BufferUtils.setInBuffer(tempVec3, vertexBuffer, startIndex + 0);

                _position.subtract(bbX, tempVec3).addLocal(bbY);
                BufferUtils.setInBuffer(tempVec3, vertexBuffer, startIndex + 1);

                _position.add(bbX, tempVec3).addLocal(bbY);
                BufferUtils.setInBuffer(tempVec3, vertexBuffer, startIndex + 2);

                _position.add(bbX, tempVec3).subtractLocal(bbY);
                BufferUtils.setInBuffer(tempVec3, vertexBuffer, startIndex + 3);
                break;
            }
            case GeomMesh: {
                final Quaternion tempQuat = Quaternion.fetchTempInstance();
                final ReadOnlyVector3 norm = triModel.getNormal();
                if (orient != 0) {
                    tempQuat.fromAngleNormalAxis(orient, norm);
                }

                for (int x = 0; x < 3; x++) {
                    if (orient != 0) {
                        tempQuat.apply(triModel.get(x), tempVec3);
                    } else {
                        tempVec3.set(triModel.get(x));
                    }
                    tempVec3.multiplyLocal(currSize).addLocal(_position);
                    BufferUtils.setInBuffer(tempVec3, vertexBuffer, startIndex + x);
                }
                Quaternion.releaseTempInstance(tempQuat);
                break;
            }
            case Triangle: {
                _position.subtract(3 * bbX.getX(), 3 * bbX.getY(), 3 * bbX.getZ(), tempVec3).subtractLocal(bbY);
                BufferUtils.setInBuffer(tempVec3, vertexBuffer, startIndex + 0);

                _position.add(bbX, tempVec3).addLocal(3 * bbY.getX(), 3 * bbY.getY(), 3 * bbY.getZ());
                BufferUtils.setInBuffer(tempVec3, vertexBuffer, startIndex + 1);

                _position.add(bbX, tempVec3).subtractLocal(bbY);
                BufferUtils.setInBuffer(tempVec3, vertexBuffer, startIndex + 2);
                break;
            }
            case Line: {
                _position.subtract(bbX, tempVec3);
                BufferUtils.setInBuffer(tempVec3, vertexBuffer, startIndex);

                _position.add(bbX, tempVec3);
                BufferUtils.setInBuffer(tempVec3, vertexBuffer, startIndex + 1);
                break;
            }
            case Point: {
                BufferUtils.setInBuffer(_position, vertexBuffer, startIndex);
                break;
            }
        }
        Vector3.releaseTempInstance(tempVec3);
    }

    /**
     * <p>
     * update position (using current position and velocity), color (interpolating between start and end color), size
     * (interpolating between start and end size), spin (using parent's spin speed) and current age of particle. If this
     * particle's age is greater than its lifespan, it is set to status DEAD.
     * </p>
     * <p>
     * Note that this only changes the parameters of the Particle, not the geometry the particle is associated with.
     * </p>
     * 
     * @param secondsPassed
     *            number of seconds passed since last update.
     * @return true if this particle is not ALIVE (in other words, if it is ready to be reused.)
     */
    public boolean updateAndCheck(final double secondsPassed) {
        if (status != Status.Alive) {
            return true;
        }
        currentAge += secondsPassed * 1000; // add ms time to age
        if (currentAge > lifeSpan) {
            killParticle();
            return true;
        }

        final Vector3 temp = Vector3.fetchTempInstance();
        _position.addLocal(_velocity.multiply(secondsPassed * 1000f, temp));
        Vector3.releaseTempInstance(temp);

        // get interpolated values from appearance ramp:
        parent.getRamp().getValuesAtAge(currentAge, lifeSpan, currColor, values, parent);

        // interpolate colors
        final int verts = ParticleSystem.getVertsForParticleType(type);
        for (int x = 0; x < verts; x++) {
            BufferUtils.setInBuffer(currColor, parent.getParticleGeometry().getMeshData().getColorBuffer(), startIndex
                    + x);
        }

        // check for tex animation
        final int newTexIndex = parent.getTexAnimation().getTexIndexAtAge(currentAge, lifeSpan, parent);
        // Update tex coords if applicable
        if (currentTexIndex != newTexIndex) {
            // Only supported in Quad type for now.
            if (ParticleType.Quad.equals(parent.getParticleType())) {
                // determine side
                final float side = (float) Math.sqrt(parent.getTexQuantity());
                int index = newTexIndex;
                if (index >= parent.getTexQuantity()) {
                    index %= parent.getTexQuantity();
                }
                // figure row / col
                final float row = side - (int) (index / side) - 1;
                final float col = index % side;
                // set texcoords
                final float sU = col / side, eU = (col + 1) / side;
                final float sV = row / side, eV = (row + 1) / side;
                final FloatBuffer texs = parent.getParticleGeometry().getMeshData().getTextureCoords(0).getBuffer();
                texs.position(startIndex * 2);
                texs.put(eU).put(sV);
                texs.put(eU).put(eV);
                texs.put(sU).put(eV);
                texs.put(sU).put(sV);
                texs.clear();
            }
            currentTexIndex = newTexIndex;
        }

        return false;
    }

    public void killParticle() {
        setStatus(Status.Dead);

        final Vector3 tempVec3 = Vector3.fetchTempInstance();
        final FloatBuffer vertexBuffer = parent.getParticleGeometry().getMeshData().getVertexBuffer();
        BufferUtils.populateFromBuffer(tempVec3, vertexBuffer, startIndex);
        final int verts = ParticleSystem.getVertsForParticleType(type);
        for (int x = 1; x < verts; x++) {
            BufferUtils.setInBuffer(tempVec3, vertexBuffer, startIndex + x);
        }
        Vector3.releaseTempInstance(tempVec3);

    }

    /**
     * Resets current age to 0
     */
    public void resetAge() {
        currentAge = 0;
    }

    /**
     * @return the current age of the particle in ms
     */
    public int getCurrentAge() {
        return currentAge;
    }

    /**
     * @return the current position of the particle in space
     */
    public Vector3 getPosition() {
        return _position;
    }

    /**
     * Set the position of the particle in space.
     * 
     * @param position
     *            the new position in world coordinates
     */
    public void setPosition(final Vector3 position) {
        _position.set(position);
    }

    /**
     * @return the current status of this particle.
     * @see Status
     */
    public Status getStatus() {
        return status;
    }

    /**
     * Set the status of this particle.
     * 
     * @param status
     *            new status of this particle
     * @see Status
     */
    public void setStatus(final Status status) {
        this.status = status;
    }

    /**
     * @return the current velocity of this particle
     */
    public Vector3 getVelocity() {
        return _velocity;
    }

    /**
     * Set the current velocity of this particle
     * 
     * @param velocity
     *            the new velocity
     */
    public void setVelocity(final Vector3 velocity) {
        _velocity.set(velocity);
    }

    /**
     * @return the current color applied to this particle
     */
    public ColorRGBA getCurrentColor() {
        return currColor;
    }

    /**
     * @return the start index of this particle in relation to where it exists in its parent's geometry data.
     */
    public int getStartIndex() {
        return startIndex;
    }

    /**
     * Set the starting index where this particle is represented in its parent's geometry data
     * 
     * @param index
     */
    public void setStartIndex(final int index) {
        startIndex = index;
    }

    /**
     * @return the mass of this particle. Only used by ParticleInfluences such as drag.
     */
    public double getMass() {
        return values[VAL_CURRENT_MASS];
    }

    /**
     * @return the inverse mass of this particle. Often useful for skipping constant division by mass calculations. If
     *         the mass is 0, the inverse mass is considered to be positive infinity. Conversely, if the mass is
     *         positive infinity, the inverse is 0. The inverse of negative infinity is considered to be -0.
     */
    public double getInvMass() {
        final double mass = values[VAL_CURRENT_MASS];
        if (mass == 0) {
            return Float.POSITIVE_INFINITY;
        } else if (mass == Float.POSITIVE_INFINITY) {
            return 0;
        } else if (mass == Float.NEGATIVE_INFINITY) {
            return -0;
        } else {
            return 1f / mass;
        }
    }

    /**
     * Sets a triangle model to use for particle calculations when using particle type ParticleType.GeomMesh. The
     * particle will maintain the triangle's ratio and plane of orientation. It will spin (if applicable) around the
     * triangle's normal axis. The triangle should already have its center and normal fields calculated before calling
     * this method.
     * 
     * @param t
     *            the triangle to model this particle after.
     */
    public void setTriangleModel(final Triangle t) {
        triModel = t;
    }

    /**
     * @return the triangle model used by this particle
     * @see #setTriangleModel(Triangle)
     */
    public Triangle getTriangleModel() {
        return triModel;
    }

    // /////
    // Savable interface methods
    // /////

    public void write(final Ardor3DExporter e) throws IOException {
        final OutputCapsule capsule = e.getCapsule(this);
        capsule.write(startIndex, "startIndex", 0);
        capsule.write(_position, "position", new Vector3(Vector3.ZERO));
        capsule.write(status, "status", Status.Available);
        capsule.write(lifeSpan, "lifeSpan", 0);
        capsule.write(currentAge, "currentAge", 0);
        capsule.write(parent, "parent", null);
        capsule.write(_velocity, "velocity", new Vector3());
        capsule.write(type, "type", ParticleSystem.ParticleType.Quad);
    }

    public void read(final Ardor3DImporter e) throws IOException {
        final InputCapsule capsule = e.getCapsule(this);
        startIndex = capsule.readInt("startIndex", 0);
        _position.set((Vector3) capsule.readSavable("position", new Vector3(Vector3.ZERO)));
        status = capsule.readEnum("status", Status.class, Status.Available);
        lifeSpan = capsule.readDouble("lifeSpan", 0);
        currentAge = capsule.readInt("currentAge", 0);
        parent = (ParticleSystem) capsule.readSavable("parent", null);
        _velocity.set((Vector3) capsule.readSavable("velocity", new Vector3()));
        type = capsule.readEnum("type", ParticleSystem.ParticleType.class, ParticleSystem.ParticleType.Quad);
    }

    public Class<? extends Particle> getClassTag() {
        return this.getClass();
    }
}
