module BABYLON {
    var randomNumber = (min: number, max: number): number => {
        if (min === max) {
            return (min);
        }

        var random = Math.random();

        return ((random * (max - min)) + min);
    }

    export class ParticleSystem implements IDisposable {
        // Statics
        public static BLENDMODE_ONEONE = 0;
        public static BLENDMODE_STANDARD = 1;

        // Members
        public id: string;
        public renderingGroupId = 0;
        public emitter = null;
        public emitRate = 10;
        public manualEmitCount = -1;
        public updateSpeed = 0.01;
        public targetStopDuration = 0;
        public disposeOnStop = false;

        public minEmitPower = 1;
        public maxEmitPower = 1;

        public minLifeTime = 1;
        public maxLifeTime = 1;

        public minSize = 1;
        public maxSize = 1;
        public minAngularSpeed = 0;
        public maxAngularSpeed = 0;

        public particleTexture: Texture;

        public onDispose: () => void;
        public updateFunction: (particles: Particle[]) => void;

        public blendMode = ParticleSystem.BLENDMODE_ONEONE;

        public forceDepthWrite = false;

        public gravity = Vector3.Zero();
        public direction1 = new Vector3(0, 1.0, 0);
        public direction2 = new Vector3(0, 1.0, 0);
        public minEmitBox = new Vector3(-0.5, -0.5, -0.5);
        public maxEmitBox = new Vector3(0.5, 0.5, 0.5);
        public color1 = new Color4(1.0, 1.0, 1.0, 1.0);
        public color2 = new Color4(1.0, 1.0, 1.0, 1.0);
        public colorDead = new Color4(0, 0, 0, 1.0);
        public textureMask = new Color4(1.0, 1.0, 1.0, 1.0);
        public startDirectionFunction: (emitPower: number, worldMatrix: Matrix, directionToUpdate: Vector3) => void;
        public startPositionFunction: (worldMatrix: Matrix, positionToUpdate: Vector3) => void;

        private particles = new Array<Particle>();

        private _capacity: number;
        private _scene: Scene;
        private _vertexDeclaration = [3, 4, 4];
        private _vertexStrideSize = 11 * 4; // 11 floats per particle (x, y, z, r, g, b, a, angle, size, offsetX, offsetY)
        private _stockParticles = new Array<Particle>();
        private _newPartsExcess = 0;
        private _vertexBuffer: WebGLBuffer;
        private _indexBuffer: WebGLBuffer;
        private _vertices: Float32Array;
        private _effect: Effect;
        private _customEffect: Effect;
        private _cachedDefines: string;

        private _scaledColorStep = new Color4(0, 0, 0, 0);
        private _colorDiff = new Color4(0, 0, 0, 0);
        private _scaledDirection = Vector3.Zero();
        private _scaledGravity = Vector3.Zero();
        private _currentRenderId = -1;

        private _alive: boolean;
        private _started = false;
        private _stopped = false;
        private _actualFrame = 0;
        private _scaledUpdateSpeed: number;

        constructor(public name: string, capacity: number, scene: Scene, customEffect?: Effect) {
            this.id = name;
            this._capacity = capacity;

            this._scene = scene;

            this._customEffect = customEffect;

            scene.particleSystems.push(this);

            // VBO
            this._vertexBuffer = scene.getEngine().createDynamicVertexBuffer(capacity * this._vertexStrideSize * 4);

            var indices = [];
            var index = 0;
            for (var count = 0; count < capacity; count++) {
                indices.push(index);
                indices.push(index + 1);
                indices.push(index + 2);
                indices.push(index);
                indices.push(index + 2);
                indices.push(index + 3);
                index += 4;
            }

            this._indexBuffer = scene.getEngine().createIndexBuffer(indices);

            this._vertices = new Float32Array(capacity * this._vertexStrideSize);

            // Default behaviors
            this.startDirectionFunction = (emitPower: number, worldMatrix: Matrix, directionToUpdate: Vector3): void => {
                var randX = randomNumber(this.direction1.x, this.direction2.x);
                var randY = randomNumber(this.direction1.y, this.direction2.y);
                var randZ = randomNumber(this.direction1.z, this.direction2.z);

                Vector3.TransformNormalFromFloatsToRef(randX * emitPower, randY * emitPower, randZ * emitPower, worldMatrix, directionToUpdate);
            }

            this.startPositionFunction = (worldMatrix: Matrix, positionToUpdate: Vector3): void => {
                var randX = randomNumber(this.minEmitBox.x, this.maxEmitBox.x);
                var randY = randomNumber(this.minEmitBox.y, this.maxEmitBox.y);
                var randZ = randomNumber(this.minEmitBox.z, this.maxEmitBox.z);

                Vector3.TransformCoordinatesFromFloatsToRef(randX, randY, randZ, worldMatrix, positionToUpdate);
            }

            this.updateFunction = (particles: Particle[]): void => {
                for (var index = 0; index < particles.length; index++) {
                    var particle = particles[index];
                    particle.age += this._scaledUpdateSpeed;

                    if (particle.age >= particle.lifeTime) { // Recycle by swapping with last particle
                        this.recycleParticle(particle);
                        index--;
                        continue;
                    }
                    else {
                        particle.colorStep.scaleToRef(this._scaledUpdateSpeed, this._scaledColorStep);
                        particle.color.addInPlace(this._scaledColorStep);

                        if (particle.color.a < 0)
                            particle.color.a = 0;

                        particle.angle += particle.angularSpeed * this._scaledUpdateSpeed;

                        particle.direction.scaleToRef(this._scaledUpdateSpeed, this._scaledDirection);
                        particle.position.addInPlace(this._scaledDirection);

                        this.gravity.scaleToRef(this._scaledUpdateSpeed, this._scaledGravity);
                        particle.direction.addInPlace(this._scaledGravity);
                    }
                }
            }
        }

        public recycleParticle(particle: Particle): void {
            var lastParticle = this.particles.pop();

            if (lastParticle !== particle) {
                lastParticle.copyTo(particle);
                this._stockParticles.push(lastParticle);
            }
        }

        public getCapacity(): number {
            return this._capacity;
        }

        public isAlive(): boolean {
            return this._alive;
        }

        public isStarted(): boolean {
            return this._started;
        }

        public start(): void {
            this._started = true;
            this._stopped = false;
            this._actualFrame = 0;
        }

        public stop(): void {
            this._stopped = true;
        }

        public _appendParticleVertex(index: number, particle: Particle, offsetX: number, offsetY: number): void {
            var offset = index * 11;
            this._vertices[offset] = particle.position.x;
            this._vertices[offset + 1] = particle.position.y;
            this._vertices[offset + 2] = particle.position.z;
            this._vertices[offset + 3] = particle.color.r;
            this._vertices[offset + 4] = particle.color.g;
            this._vertices[offset + 5] = particle.color.b;
            this._vertices[offset + 6] = particle.color.a;
            this._vertices[offset + 7] = particle.angle;
            this._vertices[offset + 8] = particle.size;
            this._vertices[offset + 9] = offsetX;
            this._vertices[offset + 10] = offsetY;
        }

        private _update(newParticles: number): void {
            // Update current
            this._alive = this.particles.length > 0;

            this.updateFunction(this.particles);

            // Add new ones
            var worldMatrix;

            if (this.emitter.position) {
                worldMatrix = this.emitter.getWorldMatrix();
            } else {
                worldMatrix = Matrix.Translation(this.emitter.x, this.emitter.y, this.emitter.z);
            }

            for (var index = 0; index < newParticles; index++) {
                if (this.particles.length === this._capacity) {
                    break;
                }

                if (this._stockParticles.length !== 0) {
                    var particle = this._stockParticles.pop();
                    particle.age = 0;
                } else {
                    particle = new Particle();
                }
                this.particles.push(particle);

                var emitPower = randomNumber(this.minEmitPower, this.maxEmitPower);

                this.startDirectionFunction(emitPower, worldMatrix, particle.direction);

                particle.lifeTime = randomNumber(this.minLifeTime, this.maxLifeTime);

                particle.size = randomNumber(this.minSize, this.maxSize);
                particle.angularSpeed = randomNumber(this.minAngularSpeed, this.maxAngularSpeed);

                this.startPositionFunction(worldMatrix, particle.position);

                var step = randomNumber(0, 1.0);

                Color4.LerpToRef(this.color1, this.color2, step, particle.color);

                this.colorDead.subtractToRef(particle.color, this._colorDiff);
                this._colorDiff.scaleToRef(1.0 / particle.lifeTime, particle.colorStep);
            }
        }

        private _getEffect(): Effect {
            if (this._customEffect) {
                return this._customEffect;
            };

            var defines = [];

            if (this._scene.clipPlane) {
                defines.push("#define CLIPPLANE");
            }

            // Effect
            var join = defines.join("\n");
            if (this._cachedDefines !== join) {
                this._cachedDefines = join;

                this._effect = this._scene.getEngine().createEffect(
                    "particles",
                    ["position", "color", "options"],
                    ["invView", "view", "projection", "vClipPlane", "textureMask"],
                    ["diffuseSampler"], join);
            }

            return this._effect;
        }

        public animate(): void {
            if (!this._started)
                return;

            var effect = this._getEffect();

            // Check
            if (!this.emitter || !effect.isReady() || !this.particleTexture || !this.particleTexture.isReady())
                return;

            if (this._currentRenderId === this._scene.getRenderId()) {
                return;
            }

            this._currentRenderId = this._scene.getRenderId();

            this._scaledUpdateSpeed = this.updateSpeed * this._scene.getAnimationRatio();

            // determine the number of particles we need to create   
            var emitCout;

            if (this.manualEmitCount > -1) {
                emitCout = this.manualEmitCount;
                this.manualEmitCount = 0;
            } else {
                emitCout = this.emitRate;
            }

            var newParticles = ((emitCout * this._scaledUpdateSpeed) >> 0);
            this._newPartsExcess += emitCout * this._scaledUpdateSpeed - newParticles;

            if (this._newPartsExcess > 1.0) {
                newParticles += this._newPartsExcess >> 0;
                this._newPartsExcess -= this._newPartsExcess >> 0;
            }

            this._alive = false;

            if (!this._stopped) {
                this._actualFrame += this._scaledUpdateSpeed;

                if (this.targetStopDuration && this._actualFrame >= this.targetStopDuration)
                    this.stop();
            } else {
                newParticles = 0;
            }

            this._update(newParticles);

            // Stopped?
            if (this._stopped) {
                if (!this._alive) {
                    this._started = false;
                    if (this.disposeOnStop) {
                        this._scene._toBeDisposed.push(this);
                    }
                }
            }

            // Update VBO
            var offset = 0;
            for (var index = 0; index < this.particles.length; index++) {
                var particle = this.particles[index];

                this._appendParticleVertex(offset++, particle, 0, 0);
                this._appendParticleVertex(offset++, particle, 1, 0);
                this._appendParticleVertex(offset++, particle, 1, 1);
                this._appendParticleVertex(offset++, particle, 0, 1);
            }
            var engine = this._scene.getEngine();
            engine.updateDynamicVertexBuffer(this._vertexBuffer, this._vertices);
        }

        public render(): number {
            var effect = this._getEffect();

            // Check
            if (!this.emitter || !effect.isReady() || !this.particleTexture || !this.particleTexture.isReady() || !this.particles.length)
                return 0;

            var engine = this._scene.getEngine();

            // Render
            engine.enableEffect(effect);
            engine.setState(false);

            var viewMatrix = this._scene.getViewMatrix();
            effect.setTexture("diffuseSampler", this.particleTexture);
            effect.setMatrix("view", viewMatrix);
            effect.setMatrix("projection", this._scene.getProjectionMatrix());
            effect.setFloat4("textureMask", this.textureMask.r, this.textureMask.g, this.textureMask.b, this.textureMask.a);

            if (this._scene.clipPlane) {
                var clipPlane = this._scene.clipPlane;
                var invView = viewMatrix.clone();
                invView.invert();
                effect.setMatrix("invView", invView);
                effect.setFloat4("vClipPlane", clipPlane.normal.x, clipPlane.normal.y, clipPlane.normal.z, clipPlane.d);
            }

            // VBOs
            engine.bindBuffers(this._vertexBuffer, this._indexBuffer, this._vertexDeclaration, this._vertexStrideSize, effect);

            // Draw order
            if (this.blendMode === ParticleSystem.BLENDMODE_ONEONE) {
                engine.setAlphaMode(Engine.ALPHA_ADD);
            } else {
                engine.setAlphaMode(Engine.ALPHA_COMBINE);
            }

            if (this.forceDepthWrite) {
                engine.setDepthWrite(true);
            }

            engine.draw(true, 0, this.particles.length * 6);
            engine.setAlphaMode(Engine.ALPHA_DISABLE);

            return this.particles.length;
        }

        public dispose(): void {
            if (this._vertexBuffer) {
                this._scene.getEngine()._releaseBuffer(this._vertexBuffer);
                this._vertexBuffer = null;
            }

            if (this._indexBuffer) {
                this._scene.getEngine()._releaseBuffer(this._indexBuffer);
                this._indexBuffer = null;
            }

            if (this.particleTexture) {
                this.particleTexture.dispose();
                this.particleTexture = null;
            }

            // Remove from scene
            var index = this._scene.particleSystems.indexOf(this);
            this._scene.particleSystems.splice(index, 1);

            // Callback
            if (this.onDispose) {
                this.onDispose();
            }
        }

        // Clone
        public clone(name: string, newEmitter: any): ParticleSystem {
            var result = new ParticleSystem(name, this._capacity, this._scene);

            Tools.DeepCopy(this, result, ["particles"], ["_vertexDeclaration", "_vertexStrideSize"]);

            if (newEmitter === undefined) {
                newEmitter = this.emitter;
            }

            result.emitter = newEmitter;
            if (this.particleTexture) {
                result.particleTexture = new Texture(this.particleTexture.url, this._scene);
            }

            result.start();

            return result;
        }
    }
}  