using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

namespace WaterTest
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        
        Camera camera;
        Replay replay;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            IsMouseVisible = true;
        }

        // The extent of the worldmap on the xz-plane, centered around (0, 0, 0)
        private static Vector2 WorldMapExtent = new Vector2(20);

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            camera = new Camera(GraphicsDevice.PresentationParameters.BackBufferWidth, GraphicsDevice.PresentationParameters.BackBufferHeight);
            camera.LookAt = new Vector3(0, 0.1f, 0);
            camera.Position = new Vector3(-11f, 7f, -7f);
            camera.Distance = Vector3.Distance(camera.LookAt, camera.Position);
            Services.AddService(typeof(Camera), camera);

            replay = new Replay(camera);

            base.Initialize();
        }

        SpriteFont font;

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            font = Content.Load<SpriteFont>(@"Font");

            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            post = Content.Load<Model>("pole");
            postAbsoluteBoneTransforms = new Matrix[post.Bones.Count];
            post.CopyAbsoluteBoneTransformsTo(postAbsoluteBoneTransforms);

            sphere = Content.Load<Model>("sphere");
            sphereAbsoluteBoneTransforms = new Matrix[sphere.Bones.Count];
            sphere.CopyAbsoluteBoneTransformsTo(sphereAbsoluteBoneTransforms);

            heightMap = new HeightMap(this, -WorldMapExtent * 0.5f, WorldMapExtent * 0.5f);
            water = new Water(this, heightMap, camera);
            ToggleImprovements();
            ToggleVisualization();
            //flowMap = new DynamicFlowMap(this, spriteBatch, 256, 256, -WorldMapExtent * 0.5f, WorldMapExtent * 0.5f);
            flowMap = new DynamicFlowMap(this, spriteBatch, 32, 32, -WorldMapExtent * 0.5f, WorldMapExtent * 0.5f);
            terrain = new Terrain(this, heightMap, camera);
            mousePaint = new _3DMousePaint(this.GraphicsDevice, this.camera, flowMap);
            flowLines = new FlowLines(this, camera, flowMap, -WorldMapExtent * 0.5f, WorldMapExtent * 0.5f);

            brushSize = 3;
        }

        private HeightMap heightMap;
        private Water water;
        private DynamicFlowMap flowMap;
        private FlowLines flowLines;
        private Terrain terrain;
        private _3DMousePaint mousePaint;
        private Model sphere;
        private Model post;
        private Matrix[] postAbsoluteBoneTransforms;
        private Matrix[] sphereAbsoluteBoneTransforms;

        private static Vector3 sunPosition = new Vector3(0f, 2f, 2f);

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            flowLines.Dispose();
            heightMap.Dispose();
            flowMap.Dispose();
        }

        private MouseState previousMouseState;
        private MouseState currentMouseState;
        private KeyboardState previousKeyboardState;
        private KeyboardState currentKeyboardState;
        private Point dragStart;
        private Vector3 lookAtStart;
        private Vector3 directionToViewerDragStart;

        private void HandleMouse(GameTime gameTime)
        {
            currentMouseState = Mouse.GetState();

            if ((currentMouseState.RightButton == ButtonState.Pressed) &&
                (previousMouseState.RightButton != ButtonState.Pressed))
            {
                dragStart = new Point(currentMouseState.X, currentMouseState.Y);
                directionToViewerDragStart = camera.DirectionToViewer;
                lookAtStart = camera.LookAt;
            }

            Point current = new Point(currentMouseState.X, currentMouseState.Y);

            if (currentMouseState.RightButton == ButtonState.Pressed)
            {
                int delta = current.X - dragStart.X;
                int deltaUpDown = current.Y - dragStart.Y;
                if (AltDown())
                {
                    // Rotate
                    // 1000 pixels is Pi.
                    float angle = (float)delta / 1000f * MathHelper.Pi;
                    float angleUpDown = (float)deltaUpDown / 1000f * MathHelper.Pi;
                    // Rotate around lookAt
                    Vector3 toViewer = directionToViewerDragStart;
                    toViewer = Vector3.Normalize(Vector3.Transform(toViewer, Matrix.CreateRotationY(-angle)));
                    Vector3 upDownAxis = Vector3.Cross(toViewer, Vector3.Up);
                    camera.SetDirectionToViewer(Vector3.Normalize(Vector3.Transform(toViewer, Matrix.CreateFromAxisAngle(upDownAxis, angleUpDown))));
                }
                else
                {
                    Vector2 worldXZFacing = Vector2.Normalize(new Vector2(-camera.DirectionToViewer.X, -camera.DirectionToViewer.Z));
                    Vector2 worldXZFacingOrthogonal = new Vector2(worldXZFacing.Y, -worldXZFacing.X);
                    // Up moves us along this direction.
                    float forwardMovement = PanScale * deltaUpDown;
                    float sideToSideMovement = PanScale * delta;
                    Vector2 deltaMovement = worldXZFacing * forwardMovement + worldXZFacingOrthogonal * sideToSideMovement;
                    camera.SetNewPosition(lookAtStart.X + deltaMovement.X, lookAtStart.Z + deltaMovement.Y);
                }
            }

            int scrollWheelDelta = (currentMouseState.ScrollWheelValue - previousMouseState.ScrollWheelValue);
            if (scrollWheelDelta != 0)
            {
                camera.SetDistance(Math.Max(1f, camera.Distance + ((float)scrollWheelDelta * 0.01f)));
            }

            mousePaint.Update(gameTime, previousMouseState, currentMouseState);

            previousMouseState = currentMouseState;
        }

        private const float PanScale = 0.05f;

        private bool IsKeyPress(Keys key)
        {
            return currentKeyboardState.IsKeyDown(key) && !previousKeyboardState.IsKeyDown(key);
        }

        private bool AltDown()
        {
            return currentKeyboardState.IsKeyDown(Keys.LeftAlt) || currentKeyboardState.IsKeyDown(Keys.RightAlt);
        }

        private Keys[] brushSizeKeys = new Keys[]
        {
            Keys.D1,
            Keys.D2,
            Keys.D3,
            Keys.D4,
            Keys.D5,
            Keys.D6,
            Keys.D7,
            Keys.D8,
            Keys.D9,
        };

        private void HandleKeyboard(GameTime gameTime)
        {
            currentKeyboardState = Keyboard.GetState();

            if (IsKeyPress(Keys.F))
            {
                drawFlowMap = !drawFlowMap;
            }

            if (IsKeyPress(Keys.L))
            {
                drawFlowLines = !drawFlowLines;
            }

            if (IsKeyPress(Keys.I))
            {
                ToggleImprovements();
            }

            if (IsKeyPress(Keys.V))
            {
                ToggleVisualization();
            }

            if (IsKeyPress(Keys.S))
            {
                water.SkyOn = !water.SkyOn;
            }

            if (IsKeyPress(Keys.N))
            {
                water.ToggleNormalMaps();
            }
            if (IsKeyPress(Keys.D))
            {
                water.ToggleDiffuseMaps();
            }

            for (int i = 0; i < brushSizeKeys.Length; i++)
            {
                if (IsKeyPress(brushSizeKeys[i]))
                {
                    brushSize = i + 1;
                    break;
                }
            }

            if (IsKeyPress(Keys.Space))
            {
                modifyNormal_OrDiffuse = !modifyNormal_OrDiffuse;
            }

            TextureFlowInfo tfi = modifyNormal_OrDiffuse ? water.NormalTextureFlow : water.DiffuseTextureFlow;

            if (currentKeyboardState.IsKeyDown(Keys.Left))
            {
                if (modifierIndex == 0)
                {
                    tfi.CycleSeconds *= 0.97f;
                }
                else if (modifierIndex == 1)
                {
                    tfi.MaxDistortion *= 0.99f;
                }
                else if (modifierIndex == 2)
                {
                    tfi.TextureScale *= 0.97f;
                }
                else if (modifierIndex == 3)
                {
                    tfi.PulseReduction *= 0.97f;
                }
            }
            if (currentKeyboardState.IsKeyDown(Keys.Right))
            {
                if (modifierIndex == 0)
                {
                    tfi.CycleSeconds *= 1.03f;
                }
                else if (modifierIndex == 1)
                {
                    tfi.MaxDistortion *= 1.01f;
                }
                else if (modifierIndex == 2)
                {
                    tfi.TextureScale *= 1.03f;
                }
                else if (modifierIndex == 3)
                {
                    tfi.PulseReduction *= 1.03f;
                }
            }

            if (IsKeyPress(Keys.Down))
            {
                modifierIndex = Math.Min(3, modifierIndex + 1);
            }
            if (IsKeyPress(Keys.Up))
            {
                modifierIndex = Math.Max(0, modifierIndex - 1);
            }

            if (IsKeyPress(Keys.Z))
            {
                water.Reset();
            }

            // Replays
            if (IsKeyPress(Keys.R))
            {
                tookSnapshot = true;
                replay.TakeSnapshot(gameTime);
            }
            if (IsKeyPress(Keys.P))
            {
                replay.StartReplay();
            }

            previousKeyboardState = currentKeyboardState;
        }

        private bool tookSnapshot;

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            HandleMouse(gameTime);
            HandleKeyboard(gameTime);

            flowMap.BrushSizeInPixels = (brushSize - 1) * 2 + 1f;

            replay.Update(gameTime);
            camera.Update();

            water.SunPosition = sunPosition;
            water.Update(gameTime);
            terrain.SunPosition = sunPosition;

            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // TODO: Add your update logic here
            //bolt.Update(gameTime);

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            flowMap.Render();

            if (drawFlowLines)
            {
                flowLines.PreRender(spriteBatch);
            }

            water.CurrentFlowMap = flowMap.CurrentFlowMap;
            water.FlowMapTexCoordScaleAndOffset = flowMap.FlowMapTexCoordScaleAndOffset;

            GraphicsDevice.SetRenderTarget(null); // Return to back buffer.

            GraphicsDevice.Clear(Color.Black);

            // draw the table. DrawModel is a function defined below draws a model using
            // a world matrix and the model's bone transforms.
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            GraphicsDevice.BlendState = BlendState.Opaque;
            terrain.Draw();
            DrawModel(post, Matrix.CreateTranslation(-1, -0.5f, 0), postAbsoluteBoneTransforms);
            DrawModel(post, Matrix.CreateTranslation(1, -0.5f, 0), postAbsoluteBoneTransforms);
            DrawModel(sphere, Matrix.CreateTranslation(sunPosition), sphereAbsoluteBoneTransforms);

            GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;

            water.Draw(gameTime);

            if (drawFlowLines)
            {
                flowLines.Render();
            }

            if (drawFlowMap)
            {
                flowMap.DrawFlowMap();
            }

            if (!replay.InReplay)
            {
                this.IsMouseVisible = true;
                spriteBatch.Begin();

                // Left column
                int y = 0;

                DrawText(Left, y++, "(R) Improvements: " + improvement);
                DrawText(Left, y++, "Toggle (N) normals, (D) diffuse");

                DrawText(Left, y, "(Space) ");
                DrawText(Left + 60, y, "Normal ", modifyNormal_OrDiffuse ? (Color?)Color.WhiteSmoke : null);
                DrawText(Left + 130, y++, "Diffuse", modifyNormal_OrDiffuse ? null : (Color?)Color.WhiteSmoke);

                TextureFlowInfo tfi = modifyNormal_OrDiffuse ? water.NormalTextureFlow : water.DiffuseTextureFlow;

                DrawText(LeftIndent, y++, string.Format("Cycle period: {0:0.0}", tfi.CycleSeconds), (modifierIndex == 0) ? (Color?)Color.WhiteSmoke : null);
                DrawText(LeftIndent, y++, string.Format("Max water speed: {0:0.00}", tfi.MaxDistortion), (modifierIndex == 1) ? (Color?)Color.WhiteSmoke : null);
                DrawText(LeftIndent, y++, string.Format("Texture Scale: {0:0.00}", tfi.TextureScale), (modifierIndex == 2) ? (Color?)Color.WhiteSmoke : null);
                DrawText(LeftIndent, y++, string.Format("Pulse rdctn: {0:0.00}", tfi.PulseReduction), (modifierIndex == 3) ? (Color?)Color.WhiteSmoke : null);

                y = 0;

                // Right column
                DrawText(Right, y++, string.Format("(1-9) Brush size: {0}", brushSize));
                DrawText(Right, y++, "(V) Visualize: " + visualization);
                DrawText(Right, y++, "Flow (F) map, (L) lines");
                DrawText(Right, y++, "(S) Sky: " + (water.SkyOn ? "On" : "Off"));
                DrawText(Right, y++, "(R) Rec, (P) Play");

                // Instructions at bottom:
                DrawText(0f, 18, "LMB: paint flow   RMB: pan, rotate (+alt)  Scroll wheel: zoom  (Z) Reset");

                spriteBatch.End();
            }
            else
            {
                this.IsMouseVisible = false;
            }

            if (tookSnapshot)
            {
                tookSnapshot = false;
                GraphicsDevice.Clear(Color.WhiteSmoke);
            }

            base.Draw(gameTime);
        }

        private bool modifyNormal_OrDiffuse = true;
        private int modifierIndex;

        private const float Left = 10;
        private const float LeftIndent = 25;
        private const float Right = 600;
        private void DrawText(float xOffset, int yLine, string text, Color? color = null)
        {
            if (!color.HasValue)
            {
                color = Color.Goldenrod;
            }
            float y = yLine * 25f;

            spriteBatch.DrawString(font, text, new Vector2(xOffset + 1, y + 1), Color.Black);
            spriteBatch.DrawString(font, text, new Vector2(xOffset, y), color.Value);
        }

        private void ToggleImprovements()
        {
            int i = (int)improvement;
            i++;
            i %= (int)Improvement.Max;
            improvement = (Improvement)i;

            water.UseRandomNormalMapOffsets = (improvement == Improvement.Random_Tex_Offsets) || (improvement == Improvement.Random_Tex_And_Pulse_Reduction);
            water.UsePulseReduction = (improvement == Improvement.Pulse_Reduction) || (improvement == Improvement.Random_Tex_And_Pulse_Reduction);
        }

        Improvement improvement = Improvement.Random_Tex_Offsets;
        enum Improvement
        {
            None = 0,
            Random_Tex_Offsets = 1,
            Pulse_Reduction =2,
            Random_Tex_And_Pulse_Reduction = 3,
            Max = 4,
        }

        Visualization visualization = (Visualization)(-1);
        int brushSize;

        private void ToggleVisualization()
        {
            int i = (int)visualization;
            i++;
            i %= (int)Visualization.Max;
            visualization = (Visualization)i;

            water.Visualization = visualization;
        }

        private bool drawFlowMap;
        private bool drawFlowLines;

        /// <summary>
        /// DrawModel is a helper function that takes a model, world matrix, and
        /// bone transforms. It does just what its name implies, and draws the model.
        /// </summary>
        /// <param name="model">the model to draw</param>
        /// <param name="worldTransform">where to draw the model</param>
        /// <param name="absoluteBoneTransforms">the model's bone transforms. this can
        /// be calculated using the function Model.CopyAbsoluteBoneTransformsTo</param>
        private void DrawModel(Model model, Matrix worldTransform,
                               Matrix[] absoluteBoneTransforms)
        {
            // nothing tricky in here; this is the same model drawing code that we see
            // everywhere. we'll loop over all of the meshes in the model, set up their
            // effects, and then draw them.
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (Effect effect in mesh.Effects)
                {
                    BasicEffect basicEffect = effect as BasicEffect;
                    basicEffect.EnableDefaultLighting();
                    basicEffect.View = camera.View;
                    basicEffect.Projection = camera.Projection;
                    basicEffect.World = absoluteBoneTransforms[mesh.ParentBone.Index] * worldTransform;
                }
                mesh.Draw();
            }
        }
    }

    enum Visualization
    {
        Water = 0,
        PulseReductionNoise = 1,
        FlowMap = 2,
        Max = 3,
    }
}
