using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using GraphiXNA.Cameras;

namespace GraphiXNA.ParticleSystems
{
    public class ParticleEmitter
    {
        internal static Random Random = new Random();
        ParticleVertex[] particles = null;
        DynamicVertexBuffer vertexBuffer;
        VertexDeclaration vertexDeclaration;
        GraphicsDevice GraphicsDevice = null;

        int firstActiveParticle;
        int firstNewParticle;
        int firstFreeParticle;
        int firstRetiredParticle;

        int drawCounter;
        float currentTime;

        // Custom effect for drawing point sprite particles. This computes the particle
        // animation entirely in the vertex shader: no per-particle CPU work required!
        Effect particleEffect;


        // Shortcuts for accessing frequently changed effect parameters.
        EffectParameter effectViewParameter;
        EffectParameter effectProjectionParameter;
        EffectParameter effectViewportHeightParameter;
        EffectParameter effectTimeParameter;
        EffectParameter effectDurationRandomness = null;

        EffectParameter effectDuration = null;
        EffectParameter effectGravity = null;
        EffectParameter effectEndVelocity = null;
        EffectParameter effectMinColor = null;
        EffectParameter effectMaxColor = null;
        EffectParameter effectRotateSpeed = null;
        EffectParameter effectStartSize = null;
        EffectParameter effectEndSize = null;

        public ParticleEmitter(GraphicsDevice graphicsDevice, int bufferSize)
        {
            GraphicsDevice = graphicsDevice;
            LoadParticleEffect();
            particles = new ParticleVertex[bufferSize];
            vertexDeclaration = new VertexDeclaration(graphicsDevice,
                                          ParticleVertex.VertexElements);

            // Create a dynamic vertex buffer.
            int size = ParticleVertex.SizeInBytes * particles.Length;

            vertexBuffer = new DynamicVertexBuffer(graphicsDevice, size,
                                                   BufferUsage.WriteOnly |
                                                   BufferUsage.Points);
        }

        private void LoadParticleEffect()
        {
            // If we have several particle systems, the content manager will return
            // a single shared effect instance to them all. But we want to preconfigure
            // the effect with parameters that are specific to this particular
            // particle system. By cloning the effect, we prevent one particle system
            // from stomping over the parameter settings of another.

            particleEffect = ResourceManager.GetParticleEffect(GraphicsDevice, true);

            EffectParameterCollection parameters = particleEffect.Parameters;

            // Look up shortcuts for parameters that change every frame.
            effectViewParameter = parameters["View"];
            effectProjectionParameter = parameters["Projection"];
            effectViewportHeightParameter = parameters["ViewportHeight"];
            effectTimeParameter = parameters["CurrentTime"];
            effectDurationRandomness = parameters["DurationRandomness"];
            effectDuration = parameters["Duration"];
            effectGravity = parameters["Gravity"];
            effectEndVelocity = parameters["EndVelocity"];
            effectMinColor = parameters["MinColor"];
            effectMaxColor = parameters["MaxColor"];
            effectRotateSpeed = parameters["RotateSpeed"];
            effectStartSize = parameters["StartSize"];
            effectEndSize = parameters["EndSize"];

            effectDuration.SetValue((float)Duration.TotalSeconds);
            effectDurationRandomness.SetValue(DurationRandomness);
            effectGravity.SetValue(Gravity);
            effectEndVelocity.SetValue(EndVelocity);
            effectMinColor.SetValue(MinColor.ToVector4());
            effectMaxColor.SetValue(MaxColor.ToVector4());
            effectRotateSpeed.SetValue(new Vector2(MinRotateSpeed, MaxRotateSpeed));
            effectStartSize.SetValue(new Vector2(MinStartSize, MaxStartSize));
            effectEndSize.SetValue(new Vector2(_MinEndSize, _MaxEndSize));
        }

        private TimeSpan _Duration = TimeSpan.FromSeconds(1);
        public TimeSpan Duration
        {
            get { return _Duration; }
            set { _Duration = value; effectDuration.SetValue((float)value.TotalSeconds); }
        }



        // If greater than zero, some particles will last a shorter time than others.
        private float _DurationRandomness = 0;

        public float DurationRandomness
        {
            get { return _DurationRandomness; }
            set { _DurationRandomness = value; effectDurationRandomness.SetValue(value); }
        }

        // Direction and strength of the gravity effect. Note that this can point in any
        // direction, not just down! The fire effect points it upward to make the flames
        // rise, and the smoke plume points it sideways to simulate wind.
        private Vector3 _Gravity = Vector3.Zero;

        public Vector3 Gravity
        {
            get { return _Gravity; }
            set { _Gravity = value; effectGravity.SetValue(value); }
        }

        // Controls how much particles are influenced by the velocity of the object
        // which created them. You can see this in action with the explosion effect,
        // where the flames continue to move in the same direction as the source
        // projectile. The projectile trail particles, on the other hand, set this
        // value very low so they are less affected by the velocity of the projectile.
        private float _EmitterVelocitySensitivity = 1;

        public float EmitterVelocitySensitivity
        {
            get { return _EmitterVelocitySensitivity; }
            set { _EmitterVelocitySensitivity = value; }
        }


        // Range of values controlling how much X and Z axis velocity to give each
        // particle. Values for individual particles are randomly chosen from somewhere
        // between these limits.
        public float MinHorizontalVelocity = 0;
        public float MaxHorizontalVelocity = 0;


        // Range of values controlling how much Y axis velocity to give each particle.
        // Values for individual particles are randomly chosen from somewhere between
        // these limits.
        private float _MinVerticalVelocity = 0;

        public float MinVerticalVelocity
        {
            get { return _MinVerticalVelocity; }
            set { _MinVerticalVelocity = value; }
        }

        private float _MaxVerticalVelocity = 0;

        public float MaxVerticalVelocity
        {
            get { return _MaxVerticalVelocity; }
            set { _MaxVerticalVelocity = value; }
        }





        // Controls how the particle velocity will change over their lifetime. If set
        // to 1, particles will keep going at the same speed as when they were created.
        // If set to 0, particles will come to a complete stop right before they die.
        // Values greater than 1 make the particles speed up over time.
        private float _EndVelocity = 1;

        public float EndVelocity
        {
            get { return _EndVelocity; }
            set { _EndVelocity = value; effectEndVelocity.SetValue(value); }
        }


        // Range of values controlling the particle color and alpha. Values for
        // individual particles are randomly chosen from somewhere between these limits.
        private Color _MinColor = Color.White;

        public Color MinColor
        {
            get { return _MinColor; }
            set { _MinColor = value; effectMinColor.SetValue(value.ToVector4()); }
        }
        private Color _MaxColor = Color.White;

        public Color MaxColor
        {
            get { return _MaxColor; }
            set { _MaxColor = value; effectMaxColor.SetValue(value.ToVector4()); }
        }


        // Range of values controlling how fast the particles rotate. Values for
        // individual particles are randomly chosen from somewhere between these
        // limits. If both these values are set to 0, the particle system will
        // automatically switch to an alternative shader technique that does not
        // support rotation, and thus requires significantly less GPU power. This
        // means if you don't need the rotation effect, you may get a performance
        // boost from leaving these values at 0.
        private float _MinRotateSpeed = 0;

        public float MinRotateSpeed
        {
            get { return _MinRotateSpeed; }
            set
            {
                _MinRotateSpeed = value;
                // Choose the appropriate effect technique. If these particles will never
                // rotate, we can use a simpler pixel shader that requires less GPU power.
                string techniqueName;

                if ((MinRotateSpeed == 0) && (MaxRotateSpeed == 0))
                    techniqueName = "NonRotatingParticles";
                else
                    techniqueName = "RotatingParticles";

                particleEffect.CurrentTechnique = particleEffect.Techniques[techniqueName];
            }
        }

        private float _MaxRotateSpeed = 0;

        public float MaxRotateSpeed
        {
            get { return _MaxRotateSpeed; }
            set
            {
                _MaxRotateSpeed = value;

                // Choose the appropriate effect technique. If these particles will never
                // rotate, we can use a simpler pixel shader that requires less GPU power.
                string techniqueName;

                if ((MinRotateSpeed == 0) && (MaxRotateSpeed == 0))
                    techniqueName = "NonRotatingParticles";
                else
                    techniqueName = "RotatingParticles";

                particleEffect.CurrentTechnique = particleEffect.Techniques[techniqueName];
            }
        }


        // Range of values controlling how big the particles are when first created.
        // Values for individual particles are randomly chosen from somewhere between
        // these limits.
        private float _MinStartSize = 100;

        public float MinStartSize
        {
            get { return _MinStartSize; }
            set
            {
                _MinStartSize = value;
                effectStartSize.SetValue(new Vector2(_MinStartSize, _MaxStartSize));
            }
        }

        private float _MaxStartSize = 100;

        public float MaxStartSize
        {
            get { return _MaxStartSize; }
            set
            {
                _MaxStartSize = value;
                effectStartSize.SetValue(new Vector2(_MinStartSize, _MaxStartSize));
            }
        }


        // Range of values controlling how big particles become at the end of their
        // life. Values for individual particles are randomly chosen from somewhere
        // between these limits.
        private float _MinEndSize = 100;

        public float MinEndSize
        {
            get { return _MinEndSize; }
            set
            {
                _MinEndSize = value;
                effectEndSize.SetValue(new Vector2(_MinEndSize, _MaxEndSize));
            }
        }

        private float _MaxEndSize = 100;

        public float MaxEndSize
        {
            get { return _MaxEndSize; }
            set
            {
                _MaxEndSize = value;
                effectEndSize.SetValue(new Vector2(_MinEndSize, _MaxEndSize));
            }
        }


        // Alpha blending settings.
        private Blend _SourceBlend = Blend.SourceAlpha;

        public Blend SourceBlend
        {
            get { return _SourceBlend; }
            set { _SourceBlend = value; }
        }

        private Blend _DestinationBlend = Blend.InverseSourceAlpha;

        public Blend DestinationBlend
        {
            get { return _DestinationBlend; }
            set { _DestinationBlend = value; }
        }
        public void SetTexture(Texture2D texture)
        {
            particleEffect.Parameters["Texture"].SetValue(texture);
        }

        private void SetCamera()
        {
            effectViewParameter.SetValue(Camera._ActiveCamera._View);
            effectProjectionParameter.SetValue(Camera._ActiveCamera._Projection);
        }

        public void Update(GameTime gameTime)
        {
            if (gameTime == null)
            {
                throw new ArgumentNullException("gameTime");
            }

            currentTime += (float)gameTime.ElapsedGameTime.TotalSeconds;

            RetireActiveParticles();
            FreeRetiredParticles();

            // If we let our timer go on increasing for ever, it would eventually
            // run out of floating point precision, at which point the particles
            // would render incorrectly. An easy way to prevent this is to notice
            // that the time value doesn't matter when no particles are being drawn,
            // so we can reset it back to zero any time the active queue is empty.

            if (firstActiveParticle == firstFreeParticle)
            {
                currentTime = 0;
            }

            if (firstRetiredParticle == firstActiveParticle)
            {
                drawCounter = 0;
            }
        }

        /// <summary>
        /// Draws the particle system.
        /// </summary>
        internal void Draw()
        {
            SetCamera();
            // Restore the vertex buffer contents if the graphics device was lost.
            if (vertexBuffer.IsContentLost)
            {
                vertexBuffer.SetData(particles);
            }

            // If there are any particles waiting in the newly added queue,
            // we'd better upload them to the GPU ready for drawing.
            if (firstNewParticle != firstFreeParticle)
            {
                AddNewParticlesToVertexBuffer();
            }

            // If there are any active particles, draw them now!
            if (firstActiveParticle != firstFreeParticle)
            {
                SetParticleRenderStates(GraphicsDevice.RenderState);

                // Set an effect parameter describing the viewport size. This is needed
                // to convert particle sizes into screen space point sprite sizes.
                effectViewportHeightParameter.SetValue(GraphicsDevice.Viewport.Height);

                // Set an effect parameter describing the current time. All the vertex
                // shader particle animation is keyed off this value.
                effectTimeParameter.SetValue(currentTime);

                // Set the particle vertex buffer and vertex declaration.
                GraphicsDevice.Vertices[0].SetSource(vertexBuffer, 0,
                                             ParticleVertex.SizeInBytes);

                GraphicsDevice.VertexDeclaration = vertexDeclaration;

                // Activate the particle effect.
                particleEffect.Begin();

                foreach (EffectPass pass in particleEffect.CurrentTechnique.Passes)
                {
                    pass.Begin();

                    if (firstActiveParticle < firstFreeParticle)
                    {
                        // If the active particles are all in one consecutive range,
                        // we can draw them all in a single call.
                        GraphicsDevice.DrawPrimitives(PrimitiveType.PointList,
                                              firstActiveParticle,
                                              firstFreeParticle - firstActiveParticle);
                    }
                    else
                    {
                        // If the active particle range wraps past the end of the queue
                        // back to the start, we must split them over two draw calls.
                        GraphicsDevice.DrawPrimitives(PrimitiveType.PointList,
                                              firstActiveParticle,
                                              particles.Length - firstActiveParticle);

                        if (firstFreeParticle > 0)
                        {
                            GraphicsDevice.DrawPrimitives(PrimitiveType.PointList,
                                                  0,
                                                  firstFreeParticle);
                        }
                    }

                    pass.End();
                }

                particleEffect.End();

                // Reset a couple of the more unusual renderstates that we changed,
                // so as not to mess up any other subsequent drawing.
                GraphicsDevice.RenderState.PointSpriteEnable = false;
                GraphicsDevice.RenderState.DepthBufferWriteEnable = true;
            }

            drawCounter++;
        }

        /// <summary>
        /// Adds a new particle to the system.
        /// </summary>
        public bool AddParticle(Vector3 position, Vector3 velocity)
        {
            // Figure out where in the circular queue to allocate the new particle.
            int nextFreeParticle = firstFreeParticle + 1;

            if (nextFreeParticle >= particles.Length)
            {
                nextFreeParticle = 0;
            }

            // If there are no free particles, we just have to give up.
            if (nextFreeParticle == firstRetiredParticle)
            {
                return false;
            }

            // Adjust the input velocity based on how much
            // this particle system wants to be affected by it.
            velocity *= EmitterVelocitySensitivity;

            // Add in some random amount of horizontal velocity.
            float horizontalVelocity = MathHelper.Lerp(MinHorizontalVelocity,
                                                       MaxHorizontalVelocity,
                                                       (float)Random.NextDouble());

            double horizontalAngle = Random.NextDouble() * MathHelper.TwoPi;

            velocity.X += horizontalVelocity * (float)Math.Cos(horizontalAngle);
            velocity.Z += horizontalVelocity * (float)Math.Sin(horizontalAngle);

            // Add in some random amount of vertical velocity.
            velocity.Y += MathHelper.Lerp(MinVerticalVelocity,
                                          MaxVerticalVelocity,
                                          (float)Random.NextDouble());

            // Choose four random control values. These will be used by the vertex
            // shader to give each particle a different size, rotation, and color.
            Color randomValues = new Color((byte)Random.Next(255),
                                           (byte)Random.Next(255),
                                           (byte)Random.Next(255),
                                           (byte)Random.Next(255));

            // Fill in the particle vertex structure.
            particles[firstFreeParticle].Position = position;
            particles[firstFreeParticle].Velocity = velocity;
            particles[firstFreeParticle].Random = randomValues;
            particles[firstFreeParticle].Time = currentTime;

            firstFreeParticle = nextFreeParticle;

            return true;
        }

        /// <summary>
        /// Helper for uploading new particles from our managed
        /// array to the GPU vertex buffer.
        /// </summary>
        void AddNewParticlesToVertexBuffer()
        {
            int stride = ParticleVertex.SizeInBytes;

            if (firstNewParticle < firstFreeParticle)
            {
                // If the new particles are all in one consecutive range,
                // we can upload them all in a single call.
                vertexBuffer.SetData(firstNewParticle * stride, particles,
                                     firstNewParticle,
                                     firstFreeParticle - firstNewParticle,
                                     stride, SetDataOptions.NoOverwrite);
            }
            else
            {
                // If the new particle range wraps past the end of the queue
                // back to the start, we must split them over two upload calls.
                vertexBuffer.SetData(firstNewParticle * stride, particles,
                                     firstNewParticle,
                                     particles.Length - firstNewParticle,
                                     stride, SetDataOptions.NoOverwrite);

                if (firstFreeParticle > 0)
                {
                    vertexBuffer.SetData(0, particles,
                                         0, firstFreeParticle,
                                         stride, SetDataOptions.NoOverwrite);
                }
            }

            // Move the particles we just uploaded from the new to the active queue.
            firstNewParticle = firstFreeParticle;
        }


        /// <summary>
        /// Helper for setting the renderstates used to draw particles.
        /// </summary>
        private void SetParticleRenderStates(RenderState renderState)
        {
            // Enable point sprites.
            renderState.PointSpriteEnable = true;
            renderState.PointSizeMax = 256;

            // Set the alpha blend mode.
            renderState.AlphaBlendEnable = true;
            renderState.AlphaBlendOperation = BlendFunction.Add;

            // Set the alpha test mode.
            renderState.AlphaTestEnable = true;
            renderState.AlphaFunction = CompareFunction.Greater;
            renderState.ReferenceAlpha = 0;

            // Enable the depth buffer (so particles will not be visible through
            // solid objects like the ground plane), but disable depth writes
            // (so particles will not obscure other particles).
            renderState.DepthBufferEnable = true;
            renderState.DepthBufferWriteEnable = false;


            renderState.SourceBlend = SourceBlend;
            renderState.DestinationBlend = DestinationBlend;
        }

        /// <summary>
        /// Helper for checking when active particles have reached the end of
        /// their life. It moves old particles from the active area of the queue
        /// to the retired section.
        /// </summary>
        void RetireActiveParticles()
        {
            float particleDuration = (float)Duration.TotalSeconds;

            while (firstActiveParticle != firstNewParticle)
            {
                // Is this particle old enough to retire?
                float particleAge = currentTime - particles[firstActiveParticle].Time;

                if (particleAge < particleDuration)
                {
                    break;
                }

                // Remember the time at which we retired this particle.
                particles[firstActiveParticle].Time = drawCounter;

                // Move the particle from the active to the retired queue.
                firstActiveParticle++;

                if (firstActiveParticle >= particles.Length)
                {
                    firstActiveParticle = 0;
                }
            }
        }


        /// <summary>
        /// Helper for checking when retired particles have been kept around long
        /// enough that we can be sure the GPU is no longer using them. It moves
        /// old particles from the retired area of the queue to the free section.
        /// </summary>
        void FreeRetiredParticles()
        {
            while (firstRetiredParticle != firstActiveParticle)
            {
                // Has this particle been unused long enough that
                // the GPU is sure to be finished with it?
                int age = drawCounter - (int)particles[firstRetiredParticle].Time;

                // The GPU is never supposed to get more than 2 frames behind the CPU.
                // We add 1 to that, just to be safe in case of buggy drivers that
                // might bend the rules and let the GPU get further behind.
                if (age < 3)
                    break;

                // Move the particle from the retired to the free queue.
                firstRetiredParticle++;

                if (firstRetiredParticle >= particles.Length)
                    firstRetiredParticle = 0;
            }
        }
    }
}
