//BSD License  
//DXGfx® - http://www.eteractions.com
//Copyright (c) 2005
//by Guillaume Randon
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software
//and associated documentation files (the "Software"), to deal in the Software without restriction,
//including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
//and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
//subject to the following conditions:
//The above copyright notice and this permission notice shall be included in all copies or substantial
//portions of the Software.
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
//INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE 
//AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
//DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
using System.Diagnostics;
using System.IO;
using Microsoft.Samples.DirectX.UtilityToolkit;

namespace DXGfxLib
{
    /// <summary>
    /// Terrain2 class manage a full 3D Terrain2. The Terrain2 will be built from a 2D grey level bitmap, which size need to
    /// be 259*259 nad which format need to be bmp. At loading time you need to provide a D3ddevice and a ref to an outdoor scene.
    /// The Terrain2 will be build of a multitude of tiles that will be registered to the outdoor scene graph. The current
    /// implementation of OutDoorScene feature a Quadtree for efficient culling of a Terrain2 outdoor scene. The quadtree struture
    /// is also used to optimize intersection methods.
    /// The Terrain2 is multi textured. The method used is to render at max  two texture per pass which should accomodate even
    /// very old 3D cards.
    /// </summary>
    public class Terrain
    {
        /// <summary>
        /// The effect file used for Terrain2 tiles. We load it once in the Terrain2 class and we pass a reference to the
        /// newly created effectGroup to all Terrain2 tiles.
        /// </summary>
        public static string terrainEffectFile = "terrainTile.fx";

        /// <summary>
        /// The bitmap used to build the terrain.
        /// </summary>
        private Bitmap terrainData;

        /// <summary>
        /// Our terrain is going to use a default texture.
        /// </summary>
        public Texture texture = null;

        /// <summary>
        /// It is also going to use a default material.
        /// </summary>
        public Material material;

        /// <summary>
        /// For some obscure reason we want to keep the filename of the bitmap used to build the terrain.
        /// </summary>
        public string heightmapFile;

        /// <summary>
        /// The filename of the 'default' texture.
        /// </summary>
        public string baseTextureFile;

        /// <summary>
        /// Filename of a second texture that will be rendered only on the areas specified through the alphamap.
        /// </summary>
        public string secondTextureFile;

        /// <summary>
        /// Out Terrain has a maximum heigth.
        /// </summary>
        public float maxTerrainHeigth;

        /// <summary>
        /// A minimal height.
        /// </summary>
        public float minTerrainHeigth;

        /// <summary>
        /// Not used. :)
        /// </summary>
        public float secondTexHeigth;

        /// <summary>
        /// The second texture texture. Will be used for areas specified through the alphaMap texture.
        /// </summary>
        public Texture secondTexture = null;

        /// <summary>
        /// Used to specify where the secondTexture should be used.
        /// </summary>
        public Texture alphaMap = null;

        /// <summary>
        /// We certainly want our terrain to have a specific width. This can be specified upon terrain creation and
        /// we keep the value in this member variable.
        /// </summary>
        public float desiredWidth;

        /// <summary>
        /// We certainly want our terrain to have a specific heigth. This can be specified upon terrain creation and
        /// we keep the value in this member variable.
        /// </summary>
        public float desiredHeigth;

        /// <summary>
        /// Number of tiles along X
        /// </summary>
        public int width;

        /// <summary>
        /// Number of tiles along Z (remember Z is looking far away, y is up and x is looking to the right).
        /// </summary>
        public int heigth;

        /// <summary>
        /// Dimension of a tile along x axis.
        /// </summary>
        public float xSpacing;

        /// <summary>
        /// Dimension of a tile along z axis.
        /// </summary>
        public float zSpacing;

        /// <summary>
        /// 
        /// </summary>
        public float stretchFactor;

        /// <summary>
        /// We keep a list of our terrain tiles
        /// </summary>
        private List<TerrainTile> tiles = null;

        public Terrain()
        {
            Initialize();
        }

        /// <summary>
        /// Default constructor for Terrain. After that you won't have much until you effectively load a bitmap to really
        /// build the terrain.
        /// </summary>
        /// <param name="desiredWidth"></param>
        /// <param name="desiredHeigth"></param>
        public Terrain(float desiredWidth, float desiredHeigth)
        {
            Initialize(desiredWidth, desiredHeigth);
        }

        public void Initialize()
        {
            Initialize(1024, 1024);
        }

        public void Initialize(float desiredWidth, float desiredHeigth)
        {
            // This means that we have 129*129 vertices. Our bitmap should also be
            // 129*129
            //bitmapWidth = 128;
            //bitmapHeigth = 128;
            //spacing = 10;
            this.desiredWidth = desiredWidth;
            this.desiredHeigth = desiredHeigth;

            stretchFactor = 70.0f;

            maxTerrainHeigth = -10000.0f;
            minTerrainHeigth = 10000.0f;

            // Default material
            material = new Material();
            material.Ambient = Color.Aqua;
            material.Diffuse = Color.Azure;
            material.Emissive = Color.Blue;
            material.Specular = Color.Brown;

            secondTexHeigth = 0.0f;

            tiles = new List<TerrainTile>();
        }

        /// <summary>
        /// If your device has changed you need to call this one.
        /// </summary>
        /// <param name="d3ddevice"></param>
        /// <param name="scene"></param>
        public void DeviceChanged(Device d3ddevice, OutDoorScene scene)
        {
            Load(d3ddevice, scene, heightmapFile, baseTextureFile, secondTextureFile);
        }

        /// <summary>
        /// This method is the most CPU intensive method of this class. It will use the bitmap to generate the terrain
        /// tiles geometry. Also each newly generated tile is going to be attached to the provided Outdoorscene. With our
        /// base implementation of Ourdoorscene this means that position in the quadtree will be determine for each newly
        /// created tile.
        /// </summary>
        /// <param name="d3ddevice"></param>
        /// <param name="scene"></param>
        /// <param name="ressourceName"></param>
        public void Load(Device d3ddevice, OutDoorScene scene, string ressourceName)
        {
            LoadFromFile(d3ddevice, scene, ressourceName);
        }

        /// <summary>
        /// Load the terrain from a bitmap image used as an heigthmap. In Eter 3D Control the Terrain
        /// class will then load two texture grass.bmp and snow.bmp as well as an alphamap lightmap.dds and used those
        /// to texture the terrain. As in your specific situation I expect that your textured might be named
        /// differently, you should consider using the other load method.
        /// The lightmap.dds is used only for alpha value to know where on the terrain the snow texture
        /// should be applied. The grass texture is applied everywhere.
        /// </summary>
        /// <param name="fileName">The bitmap used as heigthmap</param>
        public void LoadFromFile(Device d3ddevice, OutDoorScene scene, string fileName)
        {
            string path = StringUtil.GetPath(fileName);
            if (path == null)
            {
                Load(d3ddevice, scene, fileName, "newbase.jpg", "grass.bmp");
            }
            else
            {
                if (path.Length != 0)
                {
                    Load(d3ddevice, scene, fileName, path + "\\newbase.jpg", path + "\\grass.bmp");
                }
                else
                {
                    Load(d3ddevice, scene, fileName, "newbase.jpg", "grass.bmp");
                }
            }
        }
        /// <summary>
        /// Load the terrain from the bitmap in the filename file. base texture will be loaded then and
        /// applied everywhere. Then the secondtexture will also be tiled on the terrain geometry based on the
        /// alpha information in the alphamap dds file. As far as I am concerned I made up the dds file with
        /// the texture tool in the directx sdk. 
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="baseTexture"></param>
        /// <param name="secondTexture"></param>
        /// <param name="alphaMap"></param>
        public void Load(Device d3ddevice, OutDoorScene scene, string fileName, string baseTextureFile, string secondTextureFile)
        {
            this.heightmapFile = fileName;
            this.baseTextureFile = baseTextureFile;
            this.secondTextureFile = secondTextureFile;

            if (File.Exists(baseTextureFile))
            {
                texture = ResourceCache.GetGlobalInstance().CreateTextureFromFile(d3ddevice, baseTextureFile);
            }
            else
            {
                Trace.TraceError("File for base texture does not exist!");
                throw new ArgumentException("File :" + baseTextureFile + " for base texture does not exist!");
            }

            if (File.Exists(secondTextureFile))
            {
                secondTexture = ResourceCache.GetGlobalInstance().CreateTextureFromFile(d3ddevice, secondTextureFile);
            }
            else
            {
                Trace.TraceError("File for second texture file does not exist!");
                throw new ArgumentException("File :" + secondTextureFile + " for second texture does not exist!");
            }

            if (File.Exists(heightmapFile))
            {
                this.terrainData = new Bitmap(heightmapFile);
            }
            else
            {
                Trace.TraceError("File for heigthmap does not exist!");
                throw new ArgumentException("File :" + heightmapFile + " for heigthmap does not exist!");
            }

            width = terrainData.Width - 1;
            heigth = terrainData.Height - 1;

            xSpacing = this.desiredWidth / width;
            zSpacing = this.desiredHeigth / heigth;

            TerrainVertex[] vertices = BuildVerticesFromBitMap(width, heigth, xSpacing,
                zSpacing, stretchFactor, terrainData);

            BuildTilesFromVertices(d3ddevice, scene, vertices);

            terrainData.Dispose();
        }

        /// <summary>
        /// This method return the heigth of the terrain at the specified x and z coordinate. Remember
        /// that the z coordinate is going far away with directx default convention. The y is pointing up.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="z"></param>
        /// <returns>the heigth of the terrain at the specified x and z position.</returns>
        public float GetHeigth(OutDoorScene scene, float x, float z)
        {
            TerrainTile tile = FindTile(scene, x, z);
            return tile.GetHeigth(x, z);
        }

        /// <summary>
        /// This method return the heigth of the terrain at the specified x and z coordinate and below the y
        /// value provided. In DX the ray intersection with a mesh is sensible to the distance between the point
        /// from which the ray is cast and the mesh here using an y value allow us to have a better precision than
        /// by cast the ray from a very high point in the sky :).
        /// This will also be consistant with a world getheigth that would be able to deal with houses with several level.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="z"></param>
        /// <returns></returns>
        public float GetHeigth(OutDoorScene scene, float x, float y, float z)
        {
            TerrainTile tile = FindTile(scene, x, z);
            return tile.GetHeigth(x, y, z);
        }


        /// <summary>
        /// Get the heigth of the tile under the point given in parameter. If the point is under the terrain
        /// level at that x and z 65536 is returned.
        /// </summary>
        /// <param name="underPoint"></param>
        /// <returns></returns>
        public float GetHeigth(OutDoorScene scene, Vector3 underPoint)
        {
            TerrainTile tile = FindTile(scene, underPoint.X, underPoint.Z);
            return tile.GetHeigth(underPoint);
        }

        /// <summary>
        /// This method can be used to determine which terrain tile is associated with position x, z. This is not part
        /// of the ITerrain interface so if you want to use that method you might have to cast your ITerrain ref to
        /// a Terrain ref.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="z"></param>
        /// <returns></returns>
        internal TerrainTile FindTile(OutDoorScene scene, float x, float z)
        {
            int maxLevel;
            Vector3 extents;

            if (CheckForQuadTree(scene, out maxLevel, out extents))
            {
                if (x > extents.X * 0.5f)
                {
                    throw new ArgumentOutOfRangeException("x value is greater than terrain x limit");
                }
                if (z > extents.Z * 0.5f)
                {
                    throw new ArgumentOutOfRangeException("z value is greater than terrain z limit");
                }

                // Position 0,0 doesn't match with tile 0,0
                // we need to add half our extents value to x and z
                x += extents.X * 0.5f;
                z += extents.Z * 0.5f;

                int numTilesSQRT = (int)System.Math.Pow(2, maxLevel);


                float tileExtentX = extents.X / numTilesSQRT;
                float tileExtentZ = extents.Z / numTilesSQRT;

                int coll = (int)System.Math.Floor(x / tileExtentX);
                int row = (int)System.Math.Floor(z / tileExtentZ);

                return (TerrainTile)tiles[coll + row * numTilesSQRT];
            }
            return null;
        }

        /// <summary>
        /// This private method is in charge of generating the geometry for each tile. This is called from the load method.
        /// </summary>
        /// <param name="d3ddevice"></param>
        /// <param name="scene"></param>
        /// <param name="vertices"></param>
        private void BuildTilesFromVertices(Device d3ddevice, OutDoorScene scene, TerrainVertex[] vertices)
        {
            // Create an effect group that we are going to use for our terrain tiles.
            EffectGroup terrainEG = new EffectGroup();
            terrainEG.Set(d3ddevice, terrainEffectFile);

            int maxLevel;
            Vector3 extents;

            if (CheckForQuadTree(scene, out maxLevel, out extents))
            {
                int numTilesSQRT = (int)System.Math.Pow(2, maxLevel);


                for (int j = 0; j < numTilesSQRT; j++)
                {
                    for (int i = 0; i < numTilesSQRT; i++)
                    {

                        TerrainTile tile = new TerrainTile();
                        tile.AssociatedEffectGroup = terrainEG;
                        tile.Populate(d3ddevice, (width / numTilesSQRT), (heigth / numTilesSQRT),
                            i * (width / numTilesSQRT), j * (heigth / numTilesSQRT),
                            width, heigth,
                            vertices);

                        tile.texture = this.texture;
                        tile.material = this.material;
                        tile.secondTexture = this.secondTexture;

                        tiles.Add(tile);
                        scene.nodes[maxLevel][i + j * numTilesSQRT].AddChild(tile);
                    }
                }
            }
            else
            {
                TerrainTile tile = new TerrainTile();
                tile.AssociatedEffectGroup = terrainEG;
                tile.Populate(d3ddevice, width, heigth, 0, 0, width, heigth, vertices);
                tiles.Add(tile);
                scene.Add(tile);
            }
        }


        private bool CheckForQuadTree(OutDoorScene scene, out int maxLevel, out Vector3 extents)
        {

            maxLevel = scene.depth;
            extents = scene.Extents;

            return true;
        }

        /// <summary>
        /// This method will generate the geometry based on the bitmap. Gray level of the bitmap are used to determine
        /// what should be the eigth of the vertex!
        /// </summary>
        /// <param name="width"></param>
        /// <param name="heigth"></param>
        /// <param name="xSpacing"></param>
        /// <param name="zSpacing"></param>
        /// <param name="stretchFactor"></param>
        /// <param name="bitmapToUse"></param>
        /// <returns></returns>
        private TerrainVertex[] BuildVerticesFromBitMap(int width, int heigth,
            float xSpacing, float zSpacing, float stretchFactor,
            Bitmap bitmapToUse)
        {

            TerrainVertex[] tmpVertices = new TerrainVertex[(width + 1) * (heigth + 1)];
            if (bitmapToUse != null)
            {
                float x = -(width * xSpacing) / 2;
                float z = -(heigth * zSpacing) / 2;

                float texv = 0.0f;

                for (int j = 0; j <= heigth; j++)
                {
                    float texu = 0.0f;

                    for (int i = 0; i <= width; i++)
                    {
                        tmpVertices[i + j * (width + 1)].X = x;
                        tmpVertices[i + j * (width + 1)].Z = z;

                        if ((i == 0) || (j == 0) || (i == width) || (j == heigth))
                            tmpVertices[i + j * (width + 1)].Y = -1000.0f;
                        else
                            tmpVertices[i + j * (width + 1)].Y = ((bitmapToUse.GetPixel(i, j)).ToArgb() / (100000)) + 100.0f;

                        if (tmpVertices[i + j * (width + 1)].Y < minTerrainHeigth)
                            minTerrainHeigth = tmpVertices[i + j * (width + 1)].Y;
                        if (tmpVertices[i + j * (width + 1)].Y > maxTerrainHeigth)
                            maxTerrainHeigth = tmpVertices[i + j * (width + 1)].Y;


                        // set the texture coordinates
                        tmpVertices[i + j * (width + 1)].Tu1 = (float)i / (float)width;
                        tmpVertices[i + j * (width + 1)].Tv1 = ((float)j / (float)heigth);
                        tmpVertices[i + j * (width + 1)].Tu2 = (((float)(texu) / (float)(width)));
                        tmpVertices[i + j * (width + 1)].Tv2 = (((float)texv / (float)heigth));


                        if (texu < width)
                            texu += stretchFactor;
                        else
                            texu = 0.0f;

                        // calc the normal from the last pixel and row
                        Vector3 rowVector = new Vector3(1.0f, 0.0f, 0.0f);
                        Vector3 collumnVector = new Vector3(0.0f, 1.0f, 0.0f);
                        Vector3 tempNorm = new Vector3(0.0f, 1.0f, 0.0f);
                        if (i > 0)
                        {
                            rowVector.X = tmpVertices[(i - 1) + j * (width + 1)].X - tmpVertices[i + j * (width + 1)].X;
                            rowVector.Y = tmpVertices[(i - 1) + j * (width + 1)].Y - tmpVertices[i + j * (width + 1)].Y;
                            rowVector.Z = tmpVertices[(i - 1) + j * (width + 1)].Z - tmpVertices[i + j * (width + 1)].Z;
                        }
                        if (j > 0)
                        {
                            collumnVector.X = tmpVertices[(i) + (j - 1) * (width + 1)].X - tmpVertices[i + j * (width + 1)].X;
                            collumnVector.Y = tmpVertices[(i) + (j - 1) * (width + 1)].Y - tmpVertices[i + j * (width + 1)].Y;
                            collumnVector.Z = tmpVertices[(i) + (j - 1) * (width + 1)].Z - tmpVertices[i + j * (width + 1)].Z;
                        }
                        tempNorm = Vector3.Cross(rowVector, collumnVector);
                        tempNorm = Vector3.Normalize(tempNorm);

                        tmpVertices[i + j * (width + 1)].Nx = tempNorm.X;
                        tmpVertices[i + j * (width + 1)].Ny = tempNorm.Y;
                        tmpVertices[i + j * (width + 1)].Nz = tempNorm.Z;

                        x += xSpacing;
                    }

                    if (texv < heigth)
                        texv += stretchFactor;
                    else
                        texv = 0.0f;

                    z += zSpacing;
                    x = -(width * xSpacing) / 2;
                }
            }

            return tmpVertices;
        }

        public void AddInstances(OutDoorScene scene, FrameworkMesh mesh, InstanceInfo[] instancesInfo)
        {
            Hashtable tileInstanceAssociations = new Hashtable();

            foreach (InstanceInfo info in instancesInfo)
            {
                TerrainTile tile = FindTile(scene, info.positionRot.X, info.positionRot.Z);

                if (tileInstanceAssociations.ContainsKey(tile))
                {
                    List<InstanceInfo> infos = (List<InstanceInfo>)tileInstanceAssociations[tile];
                    infos.Add(info);
                }
                else
                {
                    List<InstanceInfo> infos = new List<InstanceInfo>();
                    infos.Add(info);
                    tileInstanceAssociations.Add(tile, infos);
                }
            }

            IDictionaryEnumerator enumerator = tileInstanceAssociations.GetEnumerator();

            while (enumerator.MoveNext())
            {
                List<InstanceInfo> infos = (List<InstanceInfo>)enumerator.Value;
                InstanceInfo[] instInfos = new InstanceInfo[infos.Count];
                infos.CopyTo(instInfos);

                TerrainTile tile = (TerrainTile)enumerator.Key;
                tile.AddInstances(mesh, instInfos);
            }
        }
    }
}
