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

import com.jme3.bounding.BoundingBox;
import com.jme3.effect.ParticleMesh.Type;
import com.jme3.effect.influencers.DefaultParticleInfluencer;
import com.jme3.effect.influencers.ParticleInfluencer;
import com.jme3.effect.shapes.EmitterPointShape;
import com.jme3.effect.shapes.EmitterShape;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.InputCapsule;
import com.jme3.export.OutputCapsule;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Matrix3f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.Control;
import com.jme3.util.TempVars;
import java.io.IOException;

/**
 * <code>ParticleEmitter</code> is a special kind of geometry which simulates
 * a particle system.
 * <p>
 * Particle emitters can be used to simulate various kinds of phenomena,
 * such as fire, smoke, explosions and much more.
 * <p>
 * Particle emitters have many properties which are used to control the 
 * simulation. The interpretation of these properties depends on the 
 * {@link ParticleInfluencer} that has been assigned to the emitter via
 * {@link ParticleEmitter#setParticleInfluencer(com.jme3.effect.influencers.ParticleInfluencer) }.
 * By default the implementation {@link DefaultParticleInfluencer} is used.
 * 
 * @author Kirill Vainer
 */
public class ParticleEmitter extends Geometry {

    private boolean enabled = true;
    private static final EmitterShape DEFAULT_SHAPE = new EmitterPointShape(Vector3f.ZERO);
    private static final ParticleInfluencer DEFAULT_INFLUENCER = new DefaultParticleInfluencer();
    private ParticleEmitterControl control;
    private EmitterShape shape = DEFAULT_SHAPE;
    private ParticleMesh particleMesh;
    private ParticleInfluencer particleInfluencer = DEFAULT_INFLUENCER;
    private ParticleMesh.Type meshType;
    private Particle[] particles;
    private int firstUnUsed;
    private int lastUsed;
//    private int next = 0;
//    private ArrayList<Integer> unusedIndices = new ArrayList<Integer>();
    private boolean randomAngle;
    private boolean selectRandomImage;
    private boolean facingVelocity;
    private float particlesPerSec = 20;
    private float timeDifference = 0;
    private float lowLife = 3f;
    private float highLife = 7f;
    private Vector3f gravity = new Vector3f(0.0f, 0.1f, 0.0f);
    private float rotateSpeed;
    private Vector3f faceNormal = new Vector3f(Vector3f.NAN);
    private int imagesX = 1;
    private int imagesY = 1;
   
    private ColorRGBA startColor = new ColorRGBA(0.4f, 0.4f, 0.4f, 0.5f);
    private ColorRGBA endColor = new ColorRGBA(0.1f, 0.1f, 0.1f, 0.0f);
    private float startSize = 0.2f;
    private float endSize = 2f;
    private boolean worldSpace = true;
    //variable that helps with computations
    private transient Vector3f temp = new Vector3f();

    public static class ParticleEmitterControl implements Control {

        ParticleEmitter parentEmitter;

        public ParticleEmitterControl() {
        }

        public ParticleEmitterControl(ParticleEmitter parentEmitter) {
            this.parentEmitter = parentEmitter;
        }

        public Control cloneForSpatial(Spatial spatial) {
            return this; // WARNING: Sets wrong control on spatial. Will be
            // fixed automatically by ParticleEmitter.clone() method.
        }

        public void setSpatial(Spatial spatial) {
        }

        public void setEnabled(boolean enabled) {
            parentEmitter.setEnabled(enabled);
        }

        public boolean isEnabled() {
            return parentEmitter.isEnabled();
        }

        public void update(float tpf) {
            parentEmitter.updateFromControl(tpf);
        }

        public void render(RenderManager rm, ViewPort vp) {
            parentEmitter.renderFromControl(rm, vp);
        }

        public void write(JmeExporter ex) throws IOException {
        }

        public void read(JmeImporter im) throws IOException {
        }
    }

    @Override
    public ParticleEmitter clone() {
        return clone(true);
    }

    @Override
    public ParticleEmitter clone(boolean cloneMaterial) {
        ParticleEmitter clone = (ParticleEmitter) super.clone(cloneMaterial);
        clone.shape = shape.deepClone();

        // Reinitialize particle list
        clone.setNumParticles(particles.length);

        clone.faceNormal = faceNormal.clone();
        clone.startColor = startColor.clone();
        clone.endColor = endColor.clone();
        clone.particleInfluencer = particleInfluencer.clone();

        // remove wrong control
        clone.controls.remove(control);

        // put correct control
        clone.controls.add(new ParticleEmitterControl(clone));

        // Reinitialize particle mesh
        switch (meshType) {
            case Point:
                clone.particleMesh = new ParticlePointMesh();
                clone.setMesh(clone.particleMesh);
                break;
            case Triangle:
                clone.particleMesh = new ParticleTriMesh();
                clone.setMesh(clone.particleMesh);
                break;
            default:
                throw new IllegalStateException("Unrecognized particle type: " + meshType);
        }
        clone.particleMesh.initParticleData(clone, clone.particles.length);
        clone.particleMesh.setImagesXY(clone.imagesX, clone.imagesY);

        return clone;
    }

    public ParticleEmitter(String name, Type type, int numParticles) {
        super(name);

        // ignore world transform, unless user sets inLocalSpace
        this.setIgnoreTransform(true);

        // particles neither receive nor cast shadows
        this.setShadowMode(ShadowMode.Off);

        // particles are usually transparent
        this.setQueueBucket(Bucket.Transparent);

        meshType = type;

        // Must create clone of shape/influencer so that a reference to a static is 
        // not maintained
        shape = shape.deepClone();
        particleInfluencer = particleInfluencer.clone();

        control = new ParticleEmitterControl(this);
        controls.add(control);

        switch (meshType) {
            case Point:
                particleMesh = new ParticlePointMesh();
                this.setMesh(particleMesh);
                break;
            case Triangle:
                particleMesh = new ParticleTriMesh();
                this.setMesh(particleMesh);
                break;
            default:
                throw new IllegalStateException("Unrecognized particle type: " + meshType);
        }
        this.setNumParticles(numParticles);
//        particleMesh.initParticleData(this, particles.length);
    }

    /**
     * For serialization only. Do not use.
     */
    public ParticleEmitter() {
        super();
    }

    public void setShape(EmitterShape shape) {
        this.shape = shape;
    }

    public EmitterShape getShape() {
        return shape;
    }

    /**
     * Set the {@link ParticleInfluencer} to influence this particle emitter.
     * 
     * @param particleInfluencer the {@link ParticleInfluencer} to influence 
     * this particle emitter.
     * 
     * @see ParticleInfluencer
     */
    public void setParticleInfluencer(ParticleInfluencer particleInfluencer) {
        this.particleInfluencer = particleInfluencer;
    }

    /**
     * Returns the {@link ParticleInfluencer} that influences this 
     * particle emitter.
     * 
     * @return the {@link ParticleInfluencer} that influences this 
     * particle emitter.
     * 
     * @see ParticleInfluencer
     */
    public ParticleInfluencer getParticleInfluencer() {
        return particleInfluencer;
    }

    /**
     * Returns the mesh type used by the particle emitter.
     * 
     * 
     * @return the mesh type used by the particle emitter.
     * 
     * @see #setMeshType(com.jme3.effect.ParticleMesh.Type)
     * @see ParticleEmitter#ParticleEmitter(java.lang.String, com.jme3.effect.ParticleMesh.Type, int) 
     */
    public ParticleMesh.Type getMeshType() {
        return meshType;
    }

    /**
     * Sets the type of mesh used by the particle emitter.
     * @param meshType The mesh type to use
     */
    public void setMeshType(ParticleMesh.Type meshType) {
        this.meshType = meshType;
        switch (meshType) {
            case Point:
                particleMesh = new ParticlePointMesh();
                this.setMesh(particleMesh);
                break;
            case Triangle:
                particleMesh = new ParticleTriMesh();
                this.setMesh(particleMesh);
                break;
            default:
                throw new IllegalStateException("Unrecognized particle type: " + meshType);
        }
        this.setNumParticles(particles.length);
    }

    /**
     * Returns true if particles should spawn in world space. 
     * 
     * @return true if particles should spawn in world space. 
     * 
     * @see ParticleEmitter#setInWorldSpace(boolean) 
     */
    public boolean isInWorldSpace() {
        return worldSpace;
    }

    /**
     * Set to true if particles should spawn in world space. 
     * 
     * <p>If set to true and the particle emitter is moved in the scene,
     * then particles that have already spawned won't be effected by this
     * motion. If set to false, the particles will emit in local space
     * and when the emitter is moved, so are all the particles that
     * were emitted previously.
     * 
     * @param worldSpace true if particles should spawn in world space. 
     */
    public void setInWorldSpace(boolean worldSpace) {
        this.setIgnoreTransform(worldSpace);
        this.worldSpace = worldSpace;
    }

    /**
     * Returns the number of visible particles (spawned but not dead).
     * 
     * @return the number of visible particles
     */
    public int getNumVisibleParticles() {
//        return unusedIndices.size() + next;
        return lastUsed + 1;
    }

    /**
     * Set the maximum amount of particles that
     * can exist at the same time with this emitter.
     * Calling this method many times is not recommended.
     * 
     * @param numParticles the maximum amount of particles that
     * can exist at the same time with this emitter.
     */
    public final void setNumParticles(int numParticles) {
        particles = new Particle[numParticles];
        for (int i = 0; i < numParticles; i++) {
            particles[i] = new Particle();
        }
        //We have to reinit the mesh's buffers with the new size
        particleMesh.initParticleData(this, particles.length);
        particleMesh.setImagesXY(this.imagesX, this.imagesY);
        firstUnUsed = 0;
        lastUsed = -1;
    }

    public int getMaxNumParticles() {
        return particles.length;
    }

    /**
     * Returns a list of all particles (shouldn't be used in most cases).
     * 
     * <p>
     * This includes both existing and non-existing particles.
     * The size of the array is set to the <code>numParticles</code> value
     * specified in the constructor or {@link ParticleEmitter#setNumParticles(int) }
     * method. 
     * 
     * @return a list of all particles.
     */
    public Particle[] getParticles() {
        return particles;
    }

    /**
     * Get the normal which particles are facing. 
     * 
     * @return the normal which particles are facing. 
     * 
     * @see ParticleEmitter#setFaceNormal(com.jme3.math.Vector3f) 
     */
    public Vector3f getFaceNormal() {
        if (Vector3f.isValidVector(faceNormal)) {
            return faceNormal;
        } else {
            return null;
        }
    }

    /**
     * Sets the normal which particles are facing. 
     * 
     * <p>By default, particles
     * will face the camera, but for some effects (e.g shockwave) it may
     * be necessary to face a specific direction instead. To restore
     * normal functionality, provide <code>null</code> as the argument for
     * <code>faceNormal</code>.
     *
     * @param faceNormal The normals particles should face, or <code>null</code>
     * if particles should face the camera.
     */
    public void setFaceNormal(Vector3f faceNormal) {
        if (faceNormal == null || !Vector3f.isValidVector(faceNormal)) {
            this.faceNormal.set(Vector3f.NAN);
        } else {
            this.faceNormal = faceNormal;
        }
    }

    /**
     * Returns the rotation speed in radians/sec for particles.
     * 
     * @return the rotation speed in radians/sec for particles.
     * 
     * @see ParticleEmitter#setRotateSpeed(float) 
     */
    public float getRotateSpeed() {
        return rotateSpeed;
    }

    /**
     * Set the rotation speed in radians/sec for particles
     * spawned after the invocation of this method.
     * 
     * @param rotateSpeed the rotation speed in radians/sec for particles
     * spawned after the invocation of this method.
     */
    public void setRotateSpeed(float rotateSpeed) {
        this.rotateSpeed = rotateSpeed;
    }

    /**
     * Returns true if every particle spawned
     * should have a random facing angle. 
     * 
     * @return true if every particle spawned
     * should have a random facing angle. 
     * 
     * @see ParticleEmitter#setRandomAngle(boolean) 
     */
    public boolean isRandomAngle() {
        return randomAngle;
    }

    /**
     * Set to true if every particle spawned
     * should have a random facing angle. 
     * 
     * @param randomAngle if every particle spawned
     * should have a random facing angle.
     */
    public void setRandomAngle(boolean randomAngle) {
        this.randomAngle = randomAngle;
    }

    /**
     * Returns true if every particle spawned should get a random
     * image.
     * 
     * @return True if every particle spawned should get a random
     * image.
     * 
     * @see ParticleEmitter#setSelectRandomImage(boolean) 
     */
    public boolean isSelectRandomImage() {
        return selectRandomImage;
    }

    /**
     * Set to true if every particle spawned
     * should get a random image from a pool of images constructed from
     * the texture, with X by Y possible images.
     * 
     * <p>By default, X and Y are equal
     * to 1, thus allowing only 1 possible image to be selected, but if the
     * particle is configured with multiple images by using {@link ParticleEmitter#setImagesX(int) }
     * and {#link ParticleEmitter#setImagesY(int) } methods, then multiple images
     * can be selected. Setting to false will cause each particle to have an animation
     * of images displayed, starting at image 1, and going until image X*Y when
     * the particle reaches its end of life.
     * 
     * @param selectRandomImage True if every particle spawned should get a random
     * image.
     */
    public void setSelectRandomImage(boolean selectRandomImage) {
        this.selectRandomImage = selectRandomImage;
    }

    /**
     * Check if particles spawned should face their velocity.
     * 
     * @return True if particles spawned should face their velocity.
     * 
     * @see ParticleEmitter#setFacingVelocity(boolean) 
     */
    public boolean isFacingVelocity() {
        return facingVelocity;
    }

    /**
     * Set to true if particles spawned should face
     * their velocity (or direction to which they are moving towards).
     * 
     * <p>This is typically used for e.g spark effects.
     * 
     * @param followVelocity True if particles spawned should face their velocity.
     * 
     */
    public void setFacingVelocity(boolean followVelocity) {
        this.facingVelocity = followVelocity;
    }

    /**
     * Get the end color of the particles spawned.
     * 
     * @return the end color of the particles spawned.
     * 
     * @see ParticleEmitter#setEndColor(com.jme3.math.ColorRGBA) 
     */
    public ColorRGBA getEndColor() {
        return endColor;
    }

    /**
     * Set the end color of the particles spawned.
     * 
     * <p>The
     * particle color at any time is determined by blending the start color
     * and end color based on the particle's current time of life relative
     * to its end of life.
     * 
     * @param endColor the end color of the particles spawned.
     */
    public void setEndColor(ColorRGBA endColor) {
        this.endColor.set(endColor);
    }

    /**
     * Get the end size of the particles spawned.
     * 
     * @return the end size of the particles spawned.
     * 
     * @see ParticleEmitter#setEndSize(float) 
     */
    public float getEndSize() {
        return endSize;
    }

    /**
     * Set the end size of the particles spawned.
     * 
     * <p>The
     * particle size at any time is determined by blending the start size
     * and end size based on the particle's current time of life relative
     * to its end of life.
     * 
     * @param endSize the end size of the particles spawned.
     */
    public void setEndSize(float endSize) {
        this.endSize = endSize;
    }

    /**
     * This method sets the gravity value of Y axis.
     * 
     * By default the Y axis is the only one to have gravity value non zero.
     * 
     * @param gravity
     *        Set the gravity of Y axis, in units/sec/sec, of particles
     *        spawned.
     * 
     * @deprecated Use {@link ParticleEmitter#setGravity(float, float, float) instead.
     */
    @Deprecated
    public void setGravity(float gravity) {
        this.gravity.y = gravity;
    }

    /**
     * Get the gravity vector.
     * 
     * @return the gravity vector.
     * 
     * @see ParticleEmitter#setGravity(com.jme3.math.Vector3f) 
     */
    public Vector3f getGravity() {
        return gravity;
    }

    /**
     * This method sets the gravity vector.
     * 
     * @param gravity the gravity vector
     */
    public void setGravity(Vector3f gravity) {
        this.gravity.set(gravity);
    }

    /**
     * Sets the gravity vector.
     * 
     * @param gravity the gravity vector
     */
    public void setGravity(float x, float y, float z) {
        this.gravity.x = x;
        this.gravity.y = y;
        this.gravity.z = z;
    }

    /**
     * Get the high value of life.
     * 
     * @return the high value of life.
     * 
     * @see ParticleEmitter#setHighLife(float) 
     */
    public float getHighLife() {
        return highLife;
    }

    /**
     * Set the high value of life.
     * 
     * <p>The particle's lifetime/expiration
     * is determined by randomly selecting a time between low life and high life.
     * 
     * @param highLife the high value of life.
     */
    public void setHighLife(float highLife) {
        this.highLife = highLife;
    }

    /**
     * Get the number of images along the X axis (width).
     * 
     * @return the number of images along the X axis (width).
     * 
     * @see ParticleEmitter#setImagesX(int) 
     */
    public int getImagesX() {
        return imagesX;
    }

    /**
     * Set the number of images along the X axis (width).
     * 
     * <p>To determine
     * how multiple particle images are selected and used, see the
     * {@link ParticleEmitter#setSelectRandomImage(boolean) } method.
     * 
     * @param imagesX the number of images along the X axis (width).
     */
    public void setImagesX(int imagesX) {
        this.imagesX = imagesX;
        particleMesh.setImagesXY(this.imagesX, this.imagesY);
    }

    /**
     * Get the number of images along the Y axis (height).
     * 
     * @return the number of images along the Y axis (height).
     * 
     * @see ParticleEmitter#setImagesY(int) 
     */
    public int getImagesY() {
        return imagesY;
    }

    /**
     * Set the number of images along the Y axis (height).
     * 
     * <p>To determine how multiple particle images are selected and used, see the
     * {@link ParticleEmitter#setSelectRandomImage(boolean) } method.
     * 
     * @param imagesY the number of images along the Y axis (height).
     */
    public void setImagesY(int imagesY) {
        this.imagesY = imagesY;
        particleMesh.setImagesXY(this.imagesX, this.imagesY);
    }

    /**
     * Get the low value of life.
     * 
     * @return the low value of life.
     * 
     * @see ParticleEmitter#setLowLife(float) 
     */
    public float getLowLife() {
        return lowLife;
    }

    /**
     * Set the low value of life.
     * 
     * <p>The particle's lifetime/expiration
     * is determined by randomly selecting a time between low life and high life.
     * 
     * @param lowLife the low value of life.
     */
    public void setLowLife(float lowLife) {
        this.lowLife = lowLife;
    }

    /**
     * Get the number of particles to spawn per
     * second.
     * 
     * @return the number of particles to spawn per
     * second.
     * 
     * @see ParticleEmitter#setParticlesPerSec(float) 
     */
    public float getParticlesPerSec() {
        return particlesPerSec;
    }

    /**
     * Set the number of particles to spawn per
     * second.
     * 
     * @param particlesPerSec the number of particles to spawn per
     * second.
     */
    public void setParticlesPerSec(float particlesPerSec) {
        this.particlesPerSec = particlesPerSec;
    }

    /**
     * Get the start color of the particles spawned.
     * 
     * @return the start color of the particles spawned.
     * 
     * @see ParticleEmitter#setStartColor(com.jme3.math.ColorRGBA) 
     */
    public ColorRGBA getStartColor() {
        return startColor;
    }

    /**
     * Set the start color of the particles spawned.
     * 
     * <p>The particle color at any time is determined by blending the start color
     * and end color based on the particle's current time of life relative
     * to its end of life.
     * 
     * @param startColor the start color of the particles spawned
     */
    public void setStartColor(ColorRGBA startColor) {
        this.startColor.set(startColor);
    }

    /**
     * Get the start color of the particles spawned.
     * 
     * @return the start color of the particles spawned.
     * 
     * @see ParticleEmitter#setStartSize(float) 
     */
    public float getStartSize() {
        return startSize;
    }

    /**
     * Set the start size of the particles spawned.
     * 
     * <p>The particle size at any time is determined by blending the start size
     * and end size based on the particle's current time of life relative
     * to its end of life.
     * 
     * @param startSize the start size of the particles spawned.
     */
    public void setStartSize(float startSize) {
        this.startSize = startSize;
    }

    /**
     * @deprecated Use ParticleEmitter.getParticleInfluencer().getInitialVelocity() instead.
     */
    @Deprecated
    public Vector3f getInitialVelocity() {
        return particleInfluencer.getInitialVelocity();
    }

    /**
     * @param initialVelocity Set the initial velocity a particle is spawned with,
     * the initial velocity given in the parameter will be varied according
     * to the velocity variation set in {@link ParticleEmitter#setVelocityVariation(float) }.
     * A particle will move toward its velocity unless it is effected by the
     * gravity.
     *
     * @deprecated
     * This method is deprecated. 
     * Use ParticleEmitter.getParticleInfluencer().setInitialVelocity(initialVelocity); instead.
     *
     * @see ParticleEmitter#setVelocityVariation(float) 
     * @see ParticleEmitter#setGravity(float)
     */
    @Deprecated
    public void setInitialVelocity(Vector3f initialVelocity) {
        this.particleInfluencer.setInitialVelocity(initialVelocity);
    }

    /**
     * @deprecated
     * This method is deprecated. 
     * Use ParticleEmitter.getParticleInfluencer().getVelocityVariation(); instead.
     * @return the initial velocity variation factor
     */
    @Deprecated
    public float getVelocityVariation() {
        return particleInfluencer.getVelocityVariation();
    }

    /**
     * @param variation Set the variation by which the initial velocity
     * of the particle is determined. <code>variation</code> should be a value
     * from 0 to 1, where 0 means particles are to spawn with exactly
     * the velocity given in {@link ParticleEmitter#setStartVel(com.jme3.math.Vector3f) },
     * and 1 means particles are to spawn with a completely random velocity.
     * 
     * @deprecated
     * This method is deprecated. 
     * Use ParticleEmitter.getParticleInfluencer().setVelocityVariation(variation); instead.
     */
    @Deprecated
    public void setVelocityVariation(float variation) {
        this.particleInfluencer.setVelocityVariation(variation);
    }

//    private int newIndex(){
//        liveParticles ++;
//        return unusedIndices.remove(0);
//        if (unusedIndices.size() > 0){
//            liveParticles++;
//            return unusedIndices.remove(0);
//        }else if (next < particles.length){
//            liveParticles++;
//            return next++;
//        }else{
//            return -1;
//        }
//    }
//    private void freeIndex(int index){
//        liveParticles--;
//        if (index == next-1)
//            next--;
//        else
//        assert !unusedIndices.contains(index);
//        unusedIndices.add(index);
//    }
    private Particle emitParticle(Vector3f min, Vector3f max) {
//        int idx = newIndex();
//        if (idx == -1)
//            return false;
        int idx = lastUsed + 1;
        if (idx >= particles.length) {
            return null;
        }

        Particle p = particles[idx];
        if (selectRandomImage) {
            p.imageIndex = FastMath.nextRandomInt(0, imagesY - 1) * imagesX + FastMath.nextRandomInt(0, imagesX - 1);
        }

        p.startlife = lowLife + FastMath.nextRandomFloat() * (highLife - lowLife);
        p.life = p.startlife;
        p.color.set(startColor);
        p.size = startSize;
        //shape.getRandomPoint(p.position);
        particleInfluencer.influenceParticle(p, shape);
        if (worldSpace) {
            worldTransform.transformVector(p.position, p.position);
            worldTransform.getRotation().mult(p.velocity, p.velocity);
            // TODO: Make scale relevant somehow??
        }
        if (randomAngle) {
            p.angle = FastMath.nextRandomFloat() * FastMath.TWO_PI;
        }
        if (rotateSpeed != 0) {
            p.rotateSpeed = rotateSpeed * (0.2f + (FastMath.nextRandomFloat() * 2f - 1f) * .8f);
        }

        temp.set(p.position).addLocal(p.size, p.size, p.size);
        max.maxLocal(temp);
        temp.set(p.position).subtractLocal(p.size, p.size, p.size);
        min.minLocal(temp);

        ++lastUsed;
        firstUnUsed = idx + 1;
        return p;
    }

    /**
     * Instantly emits all the particles possible to be emitted. Any particles
     * which are currently inactive will be spawned immediately.
     */
    public void emitAllParticles() {
        // Force world transform to update
        this.getWorldTransform();

        TempVars vars = TempVars.get();

        BoundingBox bbox = (BoundingBox) this.getMesh().getBound();

        Vector3f min = vars.vect1;
        Vector3f max = vars.vect2;

        bbox.getMin(min);
        bbox.getMax(max);

        if (!Vector3f.isValidVector(min)) {
            min.set(Vector3f.POSITIVE_INFINITY);
        }
        if (!Vector3f.isValidVector(max)) {
            max.set(Vector3f.NEGATIVE_INFINITY);
        }

        while (emitParticle(min, max) != null);

        bbox.setMinMax(min, max);
        this.setBoundRefresh();

        vars.release();
    }

    /**
     * Instantly kills all active particles, after this method is called, all
     * particles will be dead and no longer visible.
     */
    public void killAllParticles() {
        for (int i = 0; i < particles.length; ++i) {
            if (particles[i].life > 0) {
                this.freeParticle(i);
            }
        }
    }

    private void freeParticle(int idx) {
        Particle p = particles[idx];
        p.life = 0;
        p.size = 0f;
        p.color.set(0, 0, 0, 0);
        p.imageIndex = 0;
        p.angle = 0;
        p.rotateSpeed = 0;

//        freeIndex(idx);

        if (idx == lastUsed) {
            while (lastUsed >= 0 && particles[lastUsed].life == 0) {
                lastUsed--;
            }
        }
        if (idx < firstUnUsed) {
            firstUnUsed = idx;
        }
    }

    private void swap(int idx1, int idx2) {
        Particle p1 = particles[idx1];
        particles[idx1] = particles[idx2];
        particles[idx2] = p1;
    }

    private void updateParticle(Particle p, float tpf, Vector3f min, Vector3f max){
        // applying gravity
        p.velocity.x -= gravity.x * tpf;
        p.velocity.y -= gravity.y * tpf;
        p.velocity.z -= gravity.z * tpf;
        temp.set(p.velocity).multLocal(tpf);
        p.position.addLocal(temp);

        // affecting color, size and angle
        float b = (p.startlife - p.life) / p.startlife;
        p.color.interpolate(startColor, endColor, b);
        p.size = FastMath.interpolateLinear(b, startSize, endSize);
        p.angle += p.rotateSpeed * tpf;

        // Computing bounding volume
        temp.set(p.position).addLocal(p.size, p.size, p.size);
        max.maxLocal(temp);
        temp.set(p.position).subtractLocal(p.size, p.size, p.size);
        min.minLocal(temp);

        if (!selectRandomImage) {
            p.imageIndex = (int) (b * imagesX * imagesY);
        }
    }
    
    private void updateParticleState(float tpf) {
        // Force world transform to update
        this.getWorldTransform();

        TempVars vars = TempVars.get();

        Vector3f min = vars.vect1.set(Vector3f.POSITIVE_INFINITY);
        Vector3f max = vars.vect2.set(Vector3f.NEGATIVE_INFINITY);

        for (int i = 0; i < particles.length; ++i) {
            Particle p = particles[i];
            if (p.life == 0) { // particle is dead
//                assert i <= firstUnUsed;
                continue;
            }

            p.life -= tpf;
            if (p.life <= 0) {
                this.freeParticle(i);
                continue;
            }

            updateParticle(p, tpf, min, max);

            if (firstUnUsed < i) {
                this.swap(firstUnUsed, i);
                if (i == lastUsed) {
                    lastUsed = firstUnUsed;
                }
                firstUnUsed++;
            }
        }
        
        // Spawns particles within the tpf timeslot with proper age
        float interval = 1f / particlesPerSec;
        tpf += timeDifference;
        while (tpf > interval){
            tpf -= interval;
            Particle p = emitParticle(min, max);
            if (p != null){
                p.life -= tpf;
                if (p.life <= 0){
                    freeParticle(lastUsed);
                }else{
                    updateParticle(p, tpf, min, max);
                }
            }
        }
        timeDifference = tpf;

        BoundingBox bbox = (BoundingBox) this.getMesh().getBound();
        bbox.setMinMax(min, max);
        this.setBoundRefresh();

        vars.release();
    }

    /**
     * Set to enable or disable the particle emitter
     * 
     * <p>When a particle is
     * disabled, it will be "frozen in time" and not update.
     * 
     * @param enabled True to enable the particle emitter
     */
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    /**
     * Check if a particle emitter is enabled for update.
     * 
     * @return True if a particle emitter is enabled for update.
     * 
     * @see ParticleEmitter#setEnabled(boolean) 
     */
    public boolean isEnabled() {
        return enabled;
    }

    /**
     * Callback from Control.update(), do not use.
     * @param tpf 
     */
    public void updateFromControl(float tpf) {
        if (enabled) {
            this.updateParticleState(tpf);
        }
    }

    /**
     * Callback from Control.render(), do not use.
     * 
     * @param rm
     * @param vp 
     */
    private void renderFromControl(RenderManager rm, ViewPort vp) {
        Camera cam = vp.getCamera();

        if (meshType == ParticleMesh.Type.Point) {
            float C = cam.getProjectionMatrix().m00;
            C *= cam.getWidth() * 0.5f;

            // send attenuation params
            this.getMaterial().setFloat("Quadratic", C);
        }

        Matrix3f inverseRotation = Matrix3f.IDENTITY;
        TempVars vars = null;
        if (!worldSpace) {
            vars = TempVars.get();

            inverseRotation = this.getWorldRotation().toRotationMatrix(vars.tempMat3).invertLocal();
        }
        particleMesh.updateParticleData(particles, cam, inverseRotation);
        if (!worldSpace) {
            vars.release();
        }
    }

    public void preload(RenderManager rm, ViewPort vp) {
        this.updateParticleState(0);
        particleMesh.updateParticleData(particles, vp.getCamera(), Matrix3f.IDENTITY);
    }

    @Override
    public void write(JmeExporter ex) throws IOException {
        super.write(ex);
        OutputCapsule oc = ex.getCapsule(this);
        oc.write(shape, "shape", DEFAULT_SHAPE);
        oc.write(meshType, "meshType", ParticleMesh.Type.Triangle);
        oc.write(enabled, "enabled", true);
        oc.write(particles.length, "numParticles", 0);
        oc.write(particlesPerSec, "particlesPerSec", 0);
        oc.write(lowLife, "lowLife", 0);
        oc.write(highLife, "highLife", 0);
        oc.write(gravity, "gravity", null);
        oc.write(imagesX, "imagesX", 1);
        oc.write(imagesY, "imagesY", 1);

        oc.write(startColor, "startColor", null);
        oc.write(endColor, "endColor", null);
        oc.write(startSize, "startSize", 0);
        oc.write(endSize, "endSize", 0);
        oc.write(worldSpace, "worldSpace", false);
        oc.write(facingVelocity, "facingVelocity", false);
        oc.write(faceNormal, "faceNormal", new Vector3f(Vector3f.NAN));
        oc.write(selectRandomImage, "selectRandomImage", false);
        oc.write(randomAngle, "randomAngle", false);
        oc.write(rotateSpeed, "rotateSpeed", 0);

        oc.write(particleInfluencer, "influencer", DEFAULT_INFLUENCER);
    }

    @Override
    public void read(JmeImporter im) throws IOException {
        super.read(im);
        InputCapsule ic = im.getCapsule(this);
        shape = (EmitterShape) ic.readSavable("shape", DEFAULT_SHAPE);

        if (shape == DEFAULT_SHAPE) {
            // Prevent reference to static
            shape = shape.deepClone();
        }

        meshType = ic.readEnum("meshType", ParticleMesh.Type.class, ParticleMesh.Type.Triangle);
        int numParticles = ic.readInt("numParticles", 0);


        enabled = ic.readBoolean("enabled", true);
        particlesPerSec = ic.readFloat("particlesPerSec", 0);
        lowLife = ic.readFloat("lowLife", 0);
        highLife = ic.readFloat("highLife", 0);
        gravity = (Vector3f) ic.readSavable("gravity", null);
        imagesX = ic.readInt("imagesX", 1);
        imagesY = ic.readInt("imagesY", 1);

        startColor = (ColorRGBA) ic.readSavable("startColor", null);
        endColor = (ColorRGBA) ic.readSavable("endColor", null);
        startSize = ic.readFloat("startSize", 0);
        endSize = ic.readFloat("endSize", 0);
        worldSpace = ic.readBoolean("worldSpace", false);
        this.setIgnoreTransform(worldSpace);
        facingVelocity = ic.readBoolean("facingVelocity", false);
        faceNormal = (Vector3f)ic.readSavable("faceNormal", new Vector3f(Vector3f.NAN));
        selectRandomImage = ic.readBoolean("selectRandomImage", false);
        randomAngle = ic.readBoolean("randomAngle", false);
        rotateSpeed = ic.readFloat("rotateSpeed", 0);

        switch (meshType) {
            case Point:
                particleMesh = new ParticlePointMesh();
                this.setMesh(particleMesh);
                break;
            case Triangle:
                particleMesh = new ParticleTriMesh();
                this.setMesh(particleMesh);
                break;
            default:
                throw new IllegalStateException("Unrecognized particle type: " + meshType);
        }
        this.setNumParticles(numParticles);
//        particleMesh.initParticleData(this, particles.length);
//        particleMesh.setImagesXY(imagesX, imagesY);

        particleInfluencer = (ParticleInfluencer) ic.readSavable("influencer", DEFAULT_INFLUENCER);
        if (particleInfluencer == DEFAULT_INFLUENCER) {
            particleInfluencer = particleInfluencer.clone();
        }

        if (im.getFormatVersion() == 0) {
            // compatibility before the control inside particle emitter
            // was changed:
            // find it in the controls and take it out, then add the proper one in
            for (int i = 0; i < controls.size(); i++) {
                Object obj = controls.get(i);
                if (obj instanceof ParticleEmitter) {
                    controls.remove(i);
                    // now add the proper one in
                    controls.add(new ParticleEmitterControl(this));
                    break;
                }
            }

            // compatability before gravity was not a vector but a float
            if (gravity == null) {
                gravity = new Vector3f();
                gravity.y = ic.readFloat("gravity", 0);
            }
        } else {
            // since the parentEmitter is not loaded, it must be 
            // loaded separately
            control = getControl(ParticleEmitterControl.class);
            control.parentEmitter = this;

        }
    }
}
