using System;
using JigLibX.Geometry;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Innovation
{
    // A 3D heightmap terrain generated from a heightmap texture
    public class Terrain : Component, I3DComponent
    {
        // Height representation
        public float[,] heightData;

        // Physics height representation
        HeightMapInfo heightMapInfo;

        // Physics object
        HeightMapObject heightMapObject;

        // Material
        public TerrainMaterial Material;

        // Vertex and index buffers
        VertexDeclaration myVertexDeclaration;
        VertexBuffer terrainVertexBuffer;
        IndexBuffer terrainIndexBuffer;

        // I3DComponent values
        Vector3 position = Vector3.Zero;
        Matrix rotation = Matrix.Identity;
        Vector3 scale = new Vector3(1, 1, -1);
        BoundingBox boundingBox = new BoundingBox(new Vector3(-1), new Vector3(1));

        public Vector3 Position { get { return position; } set { position = value; } }
        public Vector3 EulerRotation 
        { 
            get { return MathUtil.MatrixToVector3(Rotation); } 
            set { this.Rotation = MathUtil.Vector3ToMatrix(value); } 
        }
        public Matrix Rotation { get { return rotation; } set { rotation = value; } }
        public Vector3 Scale { get { return scale; } set { scale = value; } }
        public BoundingBox BoundingBox { get { return boundingBox; } }

        // Constructors

        public Terrain(Texture2D HeightMap, Texture2D Texture)
            : base()
        {
            Setup(HeightMap, Texture);
        }

        public Terrain(Texture2D HeightMap, Texture2D Texture, GameScreen Parent)
            : base(Parent)
        {
            Setup(HeightMap, Texture);
        }

        void Setup(Texture2D Heightmap, Texture2D Texture)
        {
            // Load height data
            heightData = CreateTerrain(Heightmap, Texture);

            // Create vertex and index buffers
            myVertexDeclaration = new VertexDeclaration(Engine.GraphicsDevice, 
                VertexPositionNormalTexture.VertexElements);
            VertexPositionNormalTexture[] terrainVertices = CreateVertices();
            int[] terrainIndices = CreateIndices();
            terrainVertices = GenerateNormalsForTriangleStrip(terrainVertices, 
                terrainIndices);
            CreateBuffers(terrainVertices, terrainIndices);

            Material = new TerrainMaterial(Texture);
        }

        // Sets up terrain, texture, etc
        private float[,] CreateTerrain(Texture2D heightMap, Texture2D texture)
        {
            // Minimum and maximum heights for terrain
            float minimumHeight = 0;
            float maximumHeight = 255;

            // Width and height of terrain (from heightmap)
            int width = heightMap.Width;
            int height = heightMap.Height;

            // Setup bounding box with width and height
            boundingBox = new BoundingBox(
                new Vector3(-width / 2, maximumHeight - minimumHeight, -height / 2), 
                new Vector3(width / 2, maximumHeight - minimumHeight, height / 2));

            // Get data from heightmap
            Color[] heightMapColors = new Color[width * height];
            heightMap.GetData<Color>(heightMapColors);

            // Setup height data from heightmap data

            float[,] heightData = new float[width, height];
            for (int x = 0; x < width; x++)
                for (int y = 0; y < height; y++)
                {
                    heightData[x, y] = heightMapColors[x + y * width].R;
                    if (heightData[x, y] < minimumHeight) 
                        minimumHeight = heightData[x, y];
                    if (heightData[x, y] > maximumHeight) 
                        maximumHeight = heightData[x, y];
                }

            for (int x = 0; x < width; x++)
                for (int y = 0; y < height; y++)
                    heightData[x, y] = (heightData[x, y] - minimumHeight) 
                        / (maximumHeight - minimumHeight) * 30.0f;

            // Setup physics

            heightMapInfo = new HeightMapInfo(heightData, 1);

            if (heightMapObject != null)
            {
                heightMapObject.DisableComponent();
                heightMapObject = null;
            }

            heightMapObject = new HeightMapObject(heightMapInfo, new Vector2(
                heightMapInfo.Width / 2, 
                -heightMapInfo.Height / 2 + heightMapInfo.Height));

            return heightData;
        }

        // Set up vertices
        private VertexPositionNormalTexture[] CreateVertices()
        {
            // Get width and height and create new vertex array
            int width = heightData.GetLength(0);
            int height = heightData.GetLength(1);
            VertexPositionNormalTexture[] terrainVertices = 
                new VertexPositionNormalTexture[width * height];

            // Calculate position, normal, and texcoords for vertices
            int i = 0;
            for (int z = 0; z < height; z++)
                for (int x = 0; x < width; x++)
                {
                    Vector3 position = new Vector3(x, heightData[x, z], -z);
                    Vector3 normal = new Vector3(0, 0, 1);
                    Vector2 texCoord = new Vector2((float)x / 30.0f, 
                        (float)z / 30.0f);

                    terrainVertices[i++] = new VertexPositionNormalTexture(
                        position, normal, texCoord);
                }

            return terrainVertices;
        }

        // Set up indices
        private int[] CreateIndices()
        {
            // Get width and height and create new index array
            int width = heightData.GetLength(0);
            int height = heightData.GetLength(1);
            int[] terrainIndices = new int[(width) * 2 * (height - 1)];
            
            // Calculate indices for triangle
            int i = 0;
            int z = 0;
            while (z < height - 1)
            {
                for (int x = 0; x < width; x++)
                {
                    terrainIndices[i++] = x + z * width;
                    terrainIndices[i++] = x + (z + 1) * width;
                }
                z++;

                if (z < height - 1)
                {
                    for (int x = width - 1; x >= 0; x--)
                    {
                        terrainIndices[i++] = x + (z + 1) * width;
                        terrainIndices[i++] = x + z * width;
                    }
                }
                z++;
            }

            return terrainIndices;
        }

        // Generates normals for a group of triangles
        private VertexPositionNormalTexture[] GenerateNormalsForTriangleStrip(
            VertexPositionNormalTexture[] vertices, int[] indices)
        {
            for (int i = 0; i < vertices.Length; i++)
                vertices[i].Normal = new Vector3(0, 0, 0);

            bool swappedWinding = false;
            for (int i = 2; i < indices.Length; i++)
            {
                Vector3 firstVec = vertices[indices[i - 1]].Position 
                    - vertices[indices[i]].Position;
                Vector3 secondVec = vertices[indices[i - 2]].Position 
                    - vertices[indices[i]].Position;
                Vector3 normal = Vector3.Cross(firstVec, secondVec);
                normal.Normalize();

                if (swappedWinding)
                    normal *= -1;

                if (!float.IsNaN(normal.X))
                {
                    vertices[indices[i]].Normal += normal;
                    vertices[indices[i - 1]].Normal += normal;
                    vertices[indices[i - 2]].Normal += normal;
                }

                swappedWinding = !swappedWinding;
            }

            for (int i = 0; i < vertices.Length; i++)
                vertices[i].Normal.Normalize();

            return vertices;
        }

        // Sets up vertex and index buffers used for drawing
        private void CreateBuffers(VertexPositionNormalTexture[] vertices, 
            int[] indices)
        {
            terrainVertexBuffer = new VertexBuffer(Engine.GraphicsDevice, 
                VertexPositionNormalTexture.SizeInBytes * vertices.Length, 
                BufferUsage.WriteOnly);
            terrainVertexBuffer.SetData(vertices);

            terrainIndexBuffer = new IndexBuffer(Engine.GraphicsDevice, 
                typeof(int), indices.Length, BufferUsage.WriteOnly);
            terrainIndexBuffer.SetData(indices);
        }

        // Draw the terrain
        public override void Draw()
        {
            // Require the camera
            Camera camera = Engine.Services.GetService<Camera>();
            if (camera == null)
                throw new Exception("The engine services does not contain a "
                + "camera service. The terrain requires a camera to draw.");

            // Set effect values
            Material.Prepare3DDraw(MathUtil.CreateWorldMatrix(
                Position, Rotation, Scale));

            // Get width and height
            int width = heightData.GetLength(0);
            int height = heightData.GetLength(1);

            // Terrain uses different vertex winding than normal models, 
            // so set the new one
            Engine.GraphicsDevice.RenderState.CullMode = CullMode.CullClockwiseFace;

            // Start the effect
            Material.Effect.Begin();

            // For each pass..
            foreach (EffectPass pass in Material.Effect.CurrentTechnique.Passes)
            {
                // Begin the pass
                pass.Begin();

                // Draw the terrain vertices and indices
                Engine.GraphicsDevice.Vertices[0].SetSource(terrainVertexBuffer, 0, 
                    VertexPositionNormalTexture.SizeInBytes);
                Engine.GraphicsDevice.Indices = terrainIndexBuffer;
                Engine.GraphicsDevice.VertexDeclaration = myVertexDeclaration;
                Engine.GraphicsDevice.DrawIndexedPrimitives(
                    Microsoft.Xna.Framework.Graphics.PrimitiveType.TriangleStrip, 
                    0, 0, width * height, 0, width * 2 * (height - 1) - 2);

                // End the pass
                pass.End();
            }

            // End the effect
            Material.Effect.End();

            // Set the vertex winding back
            Engine.GraphicsDevice.RenderState.CullMode = 
                CullMode.CullCounterClockwiseFace;
        }
    }

    // Material for a Terrain object
    public class TerrainMaterial : TexturedMaterial
    {
        public TerrainMaterial(Texture2D Texture)
            : base()
        {
            this.Effect = new BasicEffect(Engine.GraphicsDevice, null);
            this.Texture = Texture;

            SetupEffect();
        }

        // Setup BasicEffect
        private void SetupEffect()
        {
            BasicEffect basicEffect = (BasicEffect)this.Effect;

            basicEffect.Texture = Texture;
            basicEffect.TextureEnabled = true;
            basicEffect.EnableDefaultLighting();
            basicEffect.DirectionalLight0.Direction = new Vector3(1, -1, 1);
            basicEffect.DirectionalLight0.Enabled = true;
            basicEffect.AmbientLightColor = new Vector3(0.3f, 0.3f, 0.3f);
            basicEffect.DirectionalLight1.Enabled = false;
            basicEffect.DirectionalLight2.Enabled = false;
            basicEffect.SpecularColor = new Vector3(0, 0, 0);
        }

        public override void Prepare3DDraw(Matrix World)
        {
            base.Prepare3DDraw(World);

            Engine.GraphicsDevice.SamplerStates[0].AddressU = TextureAddressMode.Wrap;
            Engine.GraphicsDevice.SamplerStates[0].AddressV = TextureAddressMode.Wrap;
        }
    }
}
