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

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;

        // 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));

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

        string materialName = null;
        string heightmapName = null;

        [Category("Terrain")]
        public string Heightmap
        {
            get { return heightmapName; }
            set { heightmapName = value; Setup(value); }
        }

        [Category("Terrain")]
        public string Texture
        {
            get 
            {
                if (materialName == null)
                    materialName = new TerrainMaterial().Name;

                return ((TexturedMaterial)Engine.Services.GetService<MaterialManager>().GetMaterial(materialName)).TextureName;
            }
            set 
            {
                if (materialName == null)
                    materialName = new TerrainMaterial(value).Name;

                ((TexturedMaterial)Engine.Services.GetService<MaterialManager>().GetMaterial(materialName)).TextureName = value;
            }
        }

        // Constructors

        public Terrain()
        {
        }

        public Terrain(string HeightMap, string Texture)
            : base()
        {
            this.Heightmap = HeightMap;
            this.Texture = Texture;
        }

        void Setup(string Heightmap)
        {
            Texture2D heights = Engine.Content.Load<Texture2D>(Heightmap);
            heightmapName = Heightmap;

            // Load height data
            heightData = CreateTerrain(heights);

            // 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);
        }

        // Sets up terrain, texture, etc
        private float[,] CreateTerrain(Texture2D heightMap)
        {
            // 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));
            heightMapObject.Serialize = false;

            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()
        {
            if (string.IsNullOrEmpty(materialName))
                return;

            TerrainMaterial material = (TerrainMaterial)Engine.Services.GetService<MaterialManager>().GetMaterial(materialName);

            if (material == null || Heightmap == null || Texture == null)
                return;

            // Require the camera
            Camera camera = Engine.Services.GetService<Camera>();
            if (camera == null)
                return;

            // 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;
        }

        public override void SaveSerializationData(SerializationData Data)
        {
            base.SaveSerializationData(Data);
            Data.AddData("Terrain.Material", materialName);
            Data.AddData("Terrain.Heightmap", heightmapName);
        }

        public override void LoadFromSerializationData(SerializationData Data)
        {
            base.LoadFromSerializationData(Data);
            this.Heightmap = Data.GetData<string>("Terrain.Heightmap");
            this.materialName = Data.GetData<string>("Terrain.Material");
        }
    }

    // Material for a Terrain object
    public class TerrainMaterial : TexturedMaterial
    {
        public TerrainMaterial() : base()
        {
            SetupEffect();
        }

        public TerrainMaterial(string Texture) : base()
        {
            SetupEffect();
            this.TextureName = Texture;
        }

        // Setup BasicEffect
        private void SetupEffect()
        {
            BasicEffect basicEffect = null;

            basicEffect = (BasicEffect)this.Effect;

            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.SpecularColor = new Vector3(0, 0, 0);
        }

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

            if (Texture != null)
                ((BasicEffect)this.Effect).Texture = base.Texture;

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

        public override void LoadFromSerializationData(SerializationData Data)
        {
            base.LoadFromSerializationData(Data);
            SetupEffect();
        }
    }
}
