using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Unity.Collections;
using Unity.HLODSystem.Simplifier;
using Unity.HLODSystem.SpaceManager;
using Unity.HLODSystem.Streaming;
using Unity.HLODSystem.Utils;
using UnityEditor;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using Debug = UnityEngine.Debug;
using Object = UnityEngine.Object;

namespace Unity.HLODSystem
{
    public class TerrainHLODCreator
    {
        public static IEnumerator Create(TerrainHLOD hlod)
        {
            TerrainHLODCreator creator = new TerrainHLODCreator(hlod);
            yield return creator.CreateImpl();
        }
        public static IEnumerator Destroy(TerrainHLOD hlod)
        {
            var controller = hlod.GetComponent<HLODControllerBase>();
            if (controller == null)
                yield break;

            try
            {
                EditorUtility.DisplayProgressBar("Destroy HLOD", "Destroying HLOD files", 0.0f);
                var convertedPrefabObjects = hlod.ConvertedPrefabObjects;
                for (int i = 0; i < convertedPrefabObjects.Count; ++i)
                {
                    PrefabUtility.UnpackPrefabInstance(convertedPrefabObjects[i], PrefabUnpackMode.OutermostRoot,
                        InteractionMode.AutomatedAction);
                }

                
                var generatedObjects = hlod.GeneratedObjects;
                for (int i = 0; i < generatedObjects.Count; ++i)
                {
                    if (generatedObjects[i] == null)
                        continue;
                    var path = AssetDatabase.GetAssetPath(generatedObjects[i]);
                    if (string.IsNullOrEmpty(path) == false)
                    {
                        AssetDatabase.DeleteAsset(path);
                    }
                    else
                    {
                        //It means scene object.
                        //Destroy it.
                        Object.DestroyImmediate(generatedObjects[i]);
                    }

                    EditorUtility.DisplayProgressBar("Destroy HLOD", "Destroying HLOD files", (float)i / (float)generatedObjects.Count);
                }
                generatedObjects.Clear();
                Object.DestroyImmediate(controller);
            }
            finally
            {
                EditorUtility.ClearProgressBar();
            }
            
            EditorUtility.SetDirty(hlod.gameObject);
            EditorUtility.SetDirty(hlod);
        }



        class Layer : IDisposable
        {
            private NativeArray<int> m_detector = new NativeArray<int>(1, Allocator.Persistent);

            public float NormalScale => m_normalScale;
            
            public Layer(TerrainLayer layer, float chunkSize)
            {
                m_diffuseTextures = new DisposableList<WorkingTexture>();
                m_maskTextures = new DisposableList<WorkingTexture>();
                m_normalTextures = new DisposableList<WorkingTexture>();

                m_offset = layer.tileOffset;
                m_size = layer.tileSize;
                m_normalScale = layer.normalScale;

                m_chunkSize = chunkSize;

                MakeTexture(layer, layer.diffuseTexture, layer.diffuseRemapMin, layer.diffuseRemapMax, m_diffuseTextures);
                MakeTexture(layer, layer.maskMapTexture, layer.maskMapRemapMin, layer.maskMapRemapMax, m_maskTextures);
                MakeTexture(layer, layer.normalMapTexture, Vector4.zero, Vector4.one, m_normalTextures);
            }

            void MakeTexture(TerrainLayer layer, Texture2D texture, Vector4 min, Vector4 max, DisposableList<WorkingTexture> results)
            {
                bool linear = !GraphicsFormatUtility.IsSRGBFormat(texture.graphicsFormat);

                var offset = layer.tileOffset;
                var size = layer.tileSize;

                if (!linear)
                {
                    min.x = Mathf.Pow(min.x, 0.45f);
                    min.y = Mathf.Pow(min.y, 0.45f);
                    min.z = Mathf.Pow(min.z, 0.45f);

                    max.x = Mathf.Pow(max.x, 0.45f);
                    max.y = Mathf.Pow(max.y, 0.45f);
                    max.z = Mathf.Pow(max.z, 0.45f);
                }


                //make to texture readable.
                var assetImporter = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(texture));
                var textureImporter = assetImporter as TextureImporter;
                TextureImporterType type = TextureImporterType.Default;

                if (textureImporter)
                {
                    type = textureImporter.textureType;
                    textureImporter.isReadable = true;
                    textureImporter.textureType = TextureImporterType.Default;
                    textureImporter.SaveAndReimport();
                }

                try
                {
                    for (int i = 0; i < texture.mipmapCount; ++i)
                    {
                        int width = texture.width >> i;
                        int height = texture.height >> i;
                        WorkingTexture workingTexture = new WorkingTexture(Allocator.Persistent, texture.format, width, height, linear);
                        Color[] colors = texture.GetPixels(i);
                        for (int y = 0; y < height; ++y)
                        {
                            for (int x = 0; x < width; ++x)
                            {
                                workingTexture.SetPixel(x, y, colors[y * width + x]);
                            }
                        }

                        RemapTexture(workingTexture, min, max);
                        results.Add(workingTexture);
                    }
                }
                finally
                {
                    if (textureImporter)
                    {
                        textureImporter.isReadable = false;
                        textureImporter.textureType = type;
                        textureImporter.SaveAndReimport();
                    }
                }
            }
            
            public void Dispose()
            {
                m_diffuseTextures?.Dispose();
                m_maskTextures?.Dispose();
                m_normalTextures?.Dispose();
                m_detector.Dispose();
            }

            public Color GetColor(float u, float v, int mipLevel = 0)
            {
                u = u - Mathf.Floor(u);
                v = v - Mathf.Floor(v);

                mipLevel = Mathf.Min(mipLevel, m_diffuseTextures.Count - 1);
                
                return m_diffuseTextures[mipLevel].GetPixel(u, v);
            }

            public Vector3 GetUVByWorld(float wx, float wz, float sx, float sz)
            {
                float u = (wx + m_offset.x) / m_size.x;
                float v = (wz + m_offset.y) / m_size.y;

                float mipx = Mathf.Max(0, sx / (m_chunkSize * 2.0f) - 1);
                float mipy = Mathf.Max(0, sz / (m_chunkSize * 2.0f) - 1);

                float mip = Mathf.Max(mipx, mipy);

                return new Vector3(u, v, mip);
            }

            public Color GetColorByWorld(float wx, float wz, float sx, float sz)
            {
                Vector3 uv = GetUVByWorld(wx, wz, sx, sz);

                return GetColor(uv.x, uv.y, Mathf.RoundToInt(uv.z));
            }

            public Color GetMask(float u, float v, int mipLevel = 0)
            {
                u = u - Mathf.Floor(u);
                v = v - Mathf.Floor(v);

                mipLevel = Mathf.Min(mipLevel, m_diffuseTextures.Count - 1);

                return m_maskTextures[mipLevel].GetPixel(u, v);
            }

            public Color GetMaskByWorld(float wx, float wz, float sx, float sz)
            {
                Vector3 uv = GetUVByWorld(wx, wz, sx, sz);

                return GetMask(uv.x, uv.y, Mathf.RoundToInt(uv.z));
            }

            public Color GetNormal(float u, float v, int mipLevel = 0)
            {
                u = u - Mathf.Floor(u);
                v = v - Mathf.Floor(v);

                mipLevel = Mathf.Min(mipLevel, m_normalTextures.Count - 1);

                return m_normalTextures[mipLevel].GetPixel(u, v);
            }

            public Color GetNormalByWorld(float wx, float wz, float sx, float sz)
            {
                Vector3 uv = GetUVByWorld(wx, wz, sx, sz);

                return GetNormal(uv.x, uv.y, Mathf.RoundToInt(uv.z));
            }

            private WorkingTexture GenerateMipmap(WorkingTexture source)
            {
                int sx = Mathf.Max(source.Width >> 1, 1);
                int sy = Mathf.Max(source.Height >> 1, 1);
                
                WorkingTexture mipmap = new WorkingTexture(Allocator.Persistent, source.Format, sx, sy, source.Linear);
                mipmap.Name = source.Name;

                for (int y = 0; y < sy; ++y)
                {
                    for (int x = 0; x < sx; ++x)
                    {
                        Color color = new Color();

                        int x1 = Mathf.Min(x * 2 + 0, source.Width -1);
                        int x2 = Mathf.Min(x * 2 + 1, source.Width - 1);
                        int y1 = Mathf.Min(y * 2 + 0, source.Height - 1);
                        int y2 = Mathf.Min(y * 2 + 1, source.Height - 1);

                        color += source.GetPixel(x1, y1);
                        color += source.GetPixel(x1, y2);
                        color += source.GetPixel(x2, y1);
                        color += source.GetPixel(x2, y2);

                        color /= 4;

                        mipmap.SetPixel(x, y, color);
                    }
                }

                return mipmap;
            }

            private void RemapTexture(WorkingTexture source, Color min, Color max)
            {
                for (int y = 0; y < source.Height; ++y)
                {
                    for (int x = 0; x < source.Width; ++x)
                    {
                        var color = source.GetPixel(x, y);
                        color = color * max + min;
                        source.SetPixel(x, y, color);
                    }
                }
            }

            private DisposableList<WorkingTexture> m_diffuseTextures;
            private DisposableList<WorkingTexture> m_maskTextures;
            private DisposableList<WorkingTexture> m_normalTextures;
            private Vector2 m_offset;
            private Vector2 m_size;
            private float m_chunkSize;
            private float m_normalScale;
        }

      

        private TerrainHLOD m_hlod;

        private JobQueue m_queue;
        private Heightmap m_heightmap;

        private Vector3 m_size;
        private DisposableList<WorkingTexture> m_alphamaps;
        private DisposableList<Layer> m_layers;

        private Material m_terrainMaterial;
        private int m_terrainMaterialInstanceId;
        private string m_terrainMaterialName;

        private Material m_terrainMaterialLow;
        private int m_terrainMaterialLowInstanceId;
        private string m_terrainMaterialLowName;

        private TerrainHLODCreator(TerrainHLOD hlod)
        {
            m_hlod = hlod;
        }

        private Heightmap CreateSubHightmap(Bounds bounds)
        {
            int beginX = Mathf.RoundToInt(bounds.min.x / m_size.x * (m_heightmap.Width-1));
            int beginZ = Mathf.RoundToInt(bounds.min.z / m_size.z * (m_heightmap.Height-1));
            int endX = Mathf.RoundToInt(bounds.max.x / m_size.x * (m_heightmap.Width-1));
            int endZ = Mathf.RoundToInt(bounds.max.z / m_size.z * (m_heightmap.Height-1));

            int width = endX - beginX + 1;
            int height = endZ - beginZ + 1;

            return m_heightmap.GetHeightmap(beginX, beginZ, width, height);
        }
        private WorkingObject CreateBakedTerrain(string name, Bounds bounds, Heightmap heightmap, int distance, bool isLeaf)
        {
            WorkingObject wo = new WorkingObject(Allocator.Persistent);
            wo.Name = name;
            wo.LightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
            
            m_queue.EnqueueJob(() =>
            {
                WorkingMesh mesh = CreateBakedGeometry(name, heightmap, bounds, distance);
                wo.SetMesh(mesh);
            });

            m_queue.EnqueueJob(() =>
            {
                WorkingMaterial material = CreateBakedMaterial(name, bounds, isLeaf); 
                wo.Materials.Add(material);
            });


            return wo;
        }

        private WorkingMesh CreateBakedGeometry(string name, Heightmap heightmap, Bounds bounds, int distance)
        {
            int borderWidth = CalcBorderWidth(heightmap, distance);
            int borderWidth2x = borderWidth * 2;
            
            WorkingMesh mesh =
                new WorkingMesh(Allocator.Persistent, heightmap.Width * heightmap.Height,
                    (heightmap.Width - borderWidth2x - 1) * (heightmap.Height - borderWidth2x - 1) * 6, 1, 0);

            mesh.name = name + "_Mesh";


            Vector3[] vertices =  new Vector3[(heightmap.Width - borderWidth2x) * (heightmap.Height - borderWidth2x)];
            Vector3[] normals = new Vector3[(heightmap.Width - borderWidth2x) * (heightmap.Height - borderWidth2x)];
            Vector2[] uvs = new Vector2[(heightmap.Width - borderWidth2x) * (heightmap.Height - borderWidth2x)];
            int[] triangles = new int[(heightmap.Width - borderWidth2x - 1) * (heightmap.Height - borderWidth2x - 1) * 6];


            int vi = 0;
            //except boder line
            for (int z = borderWidth; z < heightmap.Height -borderWidth; ++z)
            {
                for (int x = borderWidth; x < heightmap.Width -borderWidth; ++x)
                {
                    int index = vi++;

                    vertices[index].x = bounds.size.x * (x) / (heightmap.Width - 1) + bounds.min.x;
                    vertices[index].y = heightmap.Size.y * heightmap[z, x];
                    vertices[index].z = bounds.size.z * (z) / (heightmap.Height - 1) + bounds.min.z;

                    uvs[index].x = (float)x / (heightmap.Width - 1);
                    uvs[index].y = (float)z / (heightmap.Height - 1);
                    
                    normals[index] = heightmap.GetInterpolatedNormal(uvs[index].x, uvs[index].y);
                    
                    
                }
            }

            int ii = 0;
            for (int z = 0; z < heightmap.Height - borderWidth2x - 1; ++z)
            {
                for (int x = 0; x < heightmap.Width - borderWidth2x - 1; ++x)
                {
                    int i00 = z * (heightmap.Width -borderWidth2x)+ x;
                    int i10 = z * (heightmap.Width -borderWidth2x)+ x + 1;
                    int i01 = (z + 1) * (heightmap.Width -borderWidth2x)+ x;
                    int i11 = (z + 1) * (heightmap.Width -borderWidth2x)+ x + 1;

                    triangles[ii + 0] = i00;
                    triangles[ii + 1] = i11;
                    triangles[ii + 2] = i10;
                    triangles[ii + 3] = i11;
                    triangles[ii + 4] = i00;
                    triangles[ii + 5] = i01;
                    ii += 6;
                }
            }

            mesh.vertices = vertices;
            mesh.normals = normals;
            mesh.uv = uvs;
            mesh.SetTriangles(triangles, 0);

            return mesh;
        }

        private WorkingMaterial CreateBakedMaterial(string name, Bounds bounds, bool useHighMaterial)
        {
            int matInstanceID = useHighMaterial ? m_terrainMaterialInstanceId : m_terrainMaterialLowInstanceId;
            string matName = useHighMaterial ? m_terrainMaterialName : m_terrainMaterialLowName;

            WorkingMaterial material = new WorkingMaterial(Allocator.Persistent, matInstanceID, matName);
            material.Name = name + "_Material";

            m_queue.EnqueueJob(() =>
            {
                WorkingTexture albedo = BakeAlbedo(name, bounds, m_hlod.TextureSize);
                material.AddTexture(m_hlod.AlbedoPropertyName, albedo);
            });

            if (m_hlod.UseNormal)
            {
                m_queue.EnqueueJob(() =>
                {
                    WorkingTexture normal = BakeNormal(name, bounds, m_hlod.TextureSize);
                    material.AddTexture(m_hlod.NormalPropertyName, normal);
                });
            }

            if (m_hlod.UseMask)
            {
                m_queue.EnqueueJob(() =>
                {
                    WorkingTexture mask = BakeMask(name, bounds, m_hlod.TextureSize);
                    material.AddTexture(m_hlod.MaskPropertyName, mask);
                });
            }

            return material;
        }

        private Color UnPackNormal(Color c, float scale)
        {
            c.r = (c.r * 2 - 1) * scale;
            c.g = (c.g * 2 - 1) * scale;
            c.b = c.b * 2 - 1;
            return c;
        }

        private Color PackNormal(Color c)
        {
            c.r = c.r * 0.5f + 0.5f;
            c.g = c.g * 0.5f + 0.5f;
            c.b = c.b * 0.5f + 0.5f;
            return c;
        }

        private void EnqueueBlendTextureJob(WorkingTexture texture, Bounds bounds, int resolution, System.Func<int, float, float, float, float, bool, Color> getColor, System.Func<Color, Color> packColor = null)
        {
            bool linear = texture.Linear;

            m_queue.EnqueueJob(() =>
            {
                float ustart = (bounds.min.x) / m_size.x;
                float vstart = (bounds.min.z) / m_size.z;
                float usize = (bounds.max.x - bounds.min.x) / m_size.x;
                float vsize = (bounds.max.z - bounds.min.z) / m_size.z;

                for (int y = 0; y < resolution; ++y)
                {
                    for (int x = 0; x < resolution; ++x)
                    {
                        float u = (float)x / (float)resolution * usize + ustart;
                        float v = (float)y / (float)resolution * vsize + vstart;

                        Color color = new Color(0.0f, 0.0f, 0.0f, 0.0f);

                        for (int li = 0; li < m_layers.Count; ++li)
                        {
                            float weight = 0.0f;
                            switch (li % 4)
                            {
                                case 0:
                                    weight = m_alphamaps[li / 4].GetPixel(u, v).r;
                                    break;
                                case 1:
                                    weight = m_alphamaps[li / 4].GetPixel(u, v).g;
                                    break;
                                case 2:
                                    weight = m_alphamaps[li / 4].GetPixel(u, v).b;
                                    break;
                                case 3:
                                    weight = m_alphamaps[li / 4].GetPixel(u, v).a;
                                    break;
                            }

                            //optimize to skip not effect pixels.
                            if (weight < 0.01f)
                                continue;

                            float wx = (float)x / (float)resolution * bounds.size.x + bounds.min.x;
                            float wy = (float)y / (float)resolution * bounds.size.z + bounds.min.z;

                            Color c = getColor(li, wx, wy, bounds.size.x, bounds.size.z, linear);

                            // blend in linear space.
                            color.r += c.r * weight;
                            color.g += c.g * weight;
                            color.b += c.b * weight;
                            color.a += c.a * weight;
                        }

                        if (packColor != null)
                        {
                            color = packColor(color);
                        }

                        if (!linear)
                        {
                            color = color.gamma;
                        }

                        texture.SetPixel(x, y, color);
                    }
                }
            });
        }

        private WorkingTexture BakeAlbedo(string name, Bounds bounds, int resolution)
        {
            WorkingTexture albedoTexture = new WorkingTexture(Allocator.Persistent, TextureFormat.RGB24, resolution, resolution, false);
            albedoTexture.Name = name + "_Albedo";
            albedoTexture.WrapMode = TextureWrapMode.Clamp;

            EnqueueBlendTextureJob(albedoTexture, bounds, resolution, (layer, wx, wz, sx, sz, linear) => 
            {
                Color c = m_layers[layer].GetColorByWorld(wx, wz, sx, sz);
                if (!linear)
                    c = c.linear;
                return c;
            });

            
            return albedoTexture;
        }

        private WorkingTexture BakeMask(string name, Bounds bounds, int resolution)
        {
            WorkingTexture maskTexture = new WorkingTexture(Allocator.Persistent, TextureFormat.ARGB32, resolution, resolution, false);
            maskTexture.Name = name + "_Mask";
            maskTexture.WrapMode = TextureWrapMode.Clamp;

            EnqueueBlendTextureJob(maskTexture, bounds, resolution, (layer, wx, wz, sx, sz, linear) =>
            {
                Color c = m_layers[layer].GetMaskByWorld(wx, wz, sx, sz);
                if (!linear)
                    c = c.linear;
                return c;
            });

            return maskTexture;
        }

        private WorkingTexture BakeNormal(string name, Bounds bounds, int resolution)
        {
            WorkingTexture normalTexture = new WorkingTexture(Allocator.Persistent, TextureFormat.RGB24, resolution, resolution, true);
            normalTexture.Name = name + "_Normal";
            normalTexture.WrapMode = TextureWrapMode.Clamp;

            EnqueueBlendTextureJob(normalTexture, bounds, resolution, (layer, wx, wz, sx, sz, linear) =>
            {
                Color c = m_layers[layer].GetNormalByWorld(wx, wz, sx, sz);
                c = UnPackNormal(c, m_layers[layer].NormalScale);
                return c;
            },(c) =>
            {
                Vector3 n = new Vector3(c.r, c.g, c.b);
                n.Normalize();
                c = new Color(n.x, n.y, n.z);
                return PackNormal(c);
            });
            
            return normalTexture;
        }

        private List<Vector2Int> GetEdgeList(List<int> tris)
        {
            HashSet<Vector2Int> candidates = new HashSet<Vector2Int>();

            int trisCount = tris.Count / 3;

            for (int i = 0; i < trisCount; ++i)
            {
                Vector2Int[] edges = new[]
                {
                    new Vector2Int(tris[i * 3 + 0], tris[i * 3 + 1]),
                    new Vector2Int(tris[i * 3 + 1], tris[i * 3 + 2]),
                    new Vector2Int(tris[i * 3 + 2], tris[i * 3 + 0])
                };

                for (int ei = 0; ei < edges.Length; ++ei)
                {
                    Vector2Int otherSideEdge = new Vector2Int(edges[ei].y, edges[ei].x);
                    if (candidates.Contains(otherSideEdge) == true)
                    {
                        candidates.Remove(otherSideEdge);
                    }
                    else
                    {
                        candidates.Add(edges[ei]);
                    }
                }
            }
            

            return candidates.ToList();
        }

        struct BorderVertex
        {
            public Vector3 Pos;
            public int ClosestIndex;
        }

        private List<BorderVertex> GenerateBorderVertices(Heightmap heightmap, int borderCount)
        {
            //generate border vertices
            List<BorderVertex> borderVertices = new List<BorderVertex>((heightmap.Width + heightmap.Height) * 2);

            int xBorderOffset = Mathf.Max((heightmap.Width - 1) / borderCount, 1 );    //< avoid 0
            int yBorderOffset = Mathf.Max((heightmap.Height - 1) / borderCount, 1);    //< avoid 0
            
            //upside
            for (int i = 0; i < heightmap.Width-1; i += xBorderOffset)
            {
                float h = heightmap[0, i];

                BorderVertex v;
                v.Pos.x = (heightmap.Size.x * i) / (heightmap.Width-1);
                v.Pos.y = (heightmap.Size.y * h);
                v.Pos.z = 0.0f;
                v.Pos += heightmap.Offset;

                v.ClosestIndex = -1;

                borderVertices.Add(v);
            }

            //rightside
            for (int i = 0; i < heightmap.Height-1; i += yBorderOffset)
            {
                float h = heightmap[i, heightmap.Width - 1];

                BorderVertex v;
                v.Pos.x = heightmap.Size.x;
                v.Pos.y = (heightmap.Size.y * h);
                v.Pos.z = (heightmap.Size.z * i) / (heightmap.Height-1);
                v.Pos += heightmap.Offset;

                v.ClosestIndex = -1;

                borderVertices.Add(v);
            }

            //downside
            for (int i = heightmap.Width-1; i > 0; i -= xBorderOffset)
            {
                float h = heightmap[heightmap.Height - 1, i];

                BorderVertex v;
                v.Pos.x = (heightmap.Size.x * i) / (heightmap.Width-1);
                v.Pos.y = (heightmap.Size.y * h);
                v.Pos.z = heightmap.Size.z;
                v.Pos += heightmap.Offset;

                v.ClosestIndex = -1;

                borderVertices.Add(v);
            }

            //leftside
            for (int i = heightmap.Height - 1; i > 0; i -= yBorderOffset)
            {
                float h = heightmap[i, 0];

                BorderVertex v;
                v.Pos.x = 0.0f;
                v.Pos.y = (heightmap.Size.y * h);
                v.Pos.z = (heightmap.Size.z * i) / (heightmap.Height-1);
                v.Pos += heightmap.Offset;

                v.ClosestIndex = -1;

                borderVertices.Add(v);
            }

            return borderVertices;
        }
        

        private WorkingMesh MakeBorder(WorkingMesh source, Heightmap heightmap, int borderCount)
        {
            List<Vector3> vertices = source.vertices.ToList();
            List<Vector3> normals = source.normals.ToList();
            List<Vector2> uvs = source.uv.ToList();
            List<int[]> subMeshTris = new List<int[]>();

            int maxTris = 0;

            for (int si = 0; si < source.subMeshCount; ++si)
            {
                List<int> tris = source.GetTriangles(si).ToList();
                List<Vector2Int> edges = GetEdgeList(tris);
                HashSet<int> vertexIndces = new HashSet<int>();
                List<BorderVertex> edgeVertices = new List<BorderVertex>();
                
                for (int ei = 0; ei < edges.Count; ++ei)
                {
                    vertexIndces.Add(edges[ei].x);
                    vertexIndces.Add(edges[ei].y);
                }
                
                List<BorderVertex> borderVertices = GenerateBorderVertices(heightmap, borderCount);
                
                //calculate closest vertex from border vertices.
                for (int i = 0; i < borderVertices.Count; ++i)
                {
                    float closestDistance = Single.MaxValue;
                    BorderVertex v = borderVertices[i];
                    foreach (var index in vertexIndces)
                    {
                        Vector3 pos = vertices[index];
                        float dist = Vector3.SqrMagnitude(pos - borderVertices[i].Pos);
                        if (dist < closestDistance)
                        {
                            closestDistance = dist;
                            v.ClosestIndex = index;
                        }
                    }

                    borderVertices[i] = v;
                }
                
                //generate tris
                int startAddIndex = vertices.Count;
                for (int bi = 0; bi < borderVertices.Count; ++bi)
                {
                    int next = (bi == borderVertices.Count - 1) ? 0 : bi + 1;
                    
                    tris.Add(bi + startAddIndex);
                    tris.Add(borderVertices[bi].ClosestIndex);
                    tris.Add(next + startAddIndex);

                    Vector2 uv;
                    uv.x = (borderVertices[bi].Pos.x - heightmap.Offset.x) / heightmap.Size.x;
                    uv.y = (borderVertices[bi].Pos.z - heightmap.Offset.z) / heightmap.Size.z;
                    vertices.Add(borderVertices[bi].Pos);
                    
                    normals.Add(heightmap.GetInterpolatedNormal(uv.x, uv.y));
                    
                    uvs.Add(uv);
                    
                    if (borderVertices[bi].ClosestIndex == borderVertices[next].ClosestIndex)
                        continue;
                    
                    tris.Add(borderVertices[bi].ClosestIndex);
                    tris.Add(borderVertices[next].ClosestIndex);
                    tris.Add(next + startAddIndex);


                }

                maxTris += tris.Count;
                subMeshTris.Add(tris.ToArray());
            }

            WorkingMesh mesh = new WorkingMesh(Allocator.Persistent, vertices.Count, maxTris, subMeshTris.Count, 0);
            mesh.name = source.name;
            mesh.vertices = vertices.ToArray();
            mesh.normals = normals.ToArray();
            mesh.uv = uvs.ToArray();

            for (int i = 0; i < subMeshTris.Count; ++i)
            {
                mesh.SetTriangles(subMeshTris[i], i);
            }

            return mesh;
        }

        private void ReampUV(WorkingMesh mesh, Heightmap heightmap)
        {
            var vertices = mesh.vertices;
            var uvs = mesh.uv;

            for (int i = 0; i < mesh.vertexCount; ++i)
            {
                Vector2 uv;
                uv.x = (vertices[i].x - heightmap.Offset.x) / heightmap.Size.x;
                uv.y = (vertices[i].z - heightmap.Offset.z) / heightmap.Size.z;
                uvs[i] = uv;
                //vertices[i].
            }

            mesh.uv = uvs;
        }
        private int CalcBorderWidth(Heightmap heightmap, int distance)
        {
            if (m_hlod.SimplifierType == typeof(Simplifier.None))
            {
                return 1;
            }
            dynamic options = m_hlod.SimplifierOptions;
            
            int maxPolygonCount = options.SimplifyMaxPolygonCount;
            int minPolygonCount = options.SimplifyMinPolygonCount;
            float polygonRatio = options.SimplifyPolygonRatio;
            int triangleCount = (heightmap.Width - 1) * (heightmap.Height - 1) * 2;

            float maxQuality = Mathf.Min((float) maxPolygonCount / (float) triangleCount, polygonRatio);
            float minQuality = Mathf.Max((float) minPolygonCount / (float) triangleCount, 0.0f);
            
            var ratio = maxQuality * Mathf.Pow(polygonRatio, distance);
            ratio = Mathf.Max(ratio, minQuality);

            int expectPolygonCount = (int)(triangleCount * ratio);

            float areaSize = (heightmap.Size.x * heightmap.Size.z); 
            float sourceSizePerTri = areaSize/  triangleCount;
            float targetSizePerTri = areaSize / expectPolygonCount;
            float sizeRatio = targetSizePerTri / sourceSizePerTri;
            float sizeRatioSqrt = Mathf.Sqrt(sizeRatio);
            
            //sizeRatioSqrt is little bit big i think.
            //So I adjust the value by divide 2.
            return Mathf.Max((int) sizeRatioSqrt / 2, 1);
        }
        

        public class EdgeGroup
        {
            public int Begin = -1;
            public int End = -1;
            public List<Vector2Int> EdgeList = new List<Vector2Int>();
        }

        private WorkingMesh MakeFillHoleMesh(WorkingMesh source)
        {
            int totalTris = 0;
            List<int[]> newTris = new List<int[]>();
            
            for (int si = 0; si < source.subMeshCount; ++si)
            {
                List<int> tris = source.GetTriangles(si).ToList();
                List<Vector2Int> edgeList = GetEdgeList(tris);
                
                
                List<EdgeGroup> groups = new List<EdgeGroup>();
                for (int i = 0; i < edgeList.Count; ++i)
                {
                    EdgeGroup group = new EdgeGroup();
                    group.Begin = edgeList[i].x;
                    group.End = edgeList[i].y;
                    group.EdgeList.Add(edgeList[i]);
                
                    groups.Add(group);
                }

                bool isFinish = false;

                while (isFinish == false)
                {
                    isFinish = true;

                    for (int gi1 = 0; gi1 < groups.Count; ++gi1)
                    {
                        for (int gi2 = gi1 + 1; gi2 < groups.Count; ++gi2)
                        {
                            EdgeGroup g1 = groups[gi1];
                            EdgeGroup g2 = groups[gi2];

                            if (g1.End == g2.Begin)
                            {
                                g1.End = g2.End;
                                g1.EdgeList.AddRange(g2.EdgeList);

                                groups[gi2] = groups[groups.Count - 1];
                                groups.RemoveAt(groups.Count - 1);

                                gi2 -= 1;
                                isFinish = false;
                            }
                            else if (g1.Begin == g2.End)
                            {
                                g2.End = g1.End;
                                g2.EdgeList.AddRange(g1.EdgeList);

                                groups[gi1] = groups[gi2];
                                groups[gi2] = groups[groups.Count - 1];
                                groups.RemoveAt(groups.Count - 1);

                                gi2 -= 1;
                                isFinish = false;
                            }
                        }
                    }
                }

                for (int gi = 0; gi < groups.Count; ++gi)
                {
                    EdgeGroup group = groups[gi];
                    for (int ei1 = 1; ei1 < group.EdgeList.Count-1; ++ei1)
                    {
                        for (int ei2 = ei1 + 1; ei2 < group.EdgeList.Count; ++ei2)
                        {
                            if (group.EdgeList[ei1].x == group.EdgeList[ei2].y)
                            {
                                EdgeGroup ng = new EdgeGroup();
                                ng.Begin = group.EdgeList[ei1].x;
                                ng.End = group.EdgeList[ei2].y;

                                for (int i = ei1; i <= ei2; ++i)
                                {
                                    ng.EdgeList.Add(group.EdgeList[i]);
                                }

                                for (int i = ei2; i >= ei1; --i)
                                {
                                    group.EdgeList.RemoveAt(i);
                                }
                                
                                groups.Add(ng);

                                ei1 = 0; // goto first
                                break;
                            }
                        }
                    }
                }

                if (groups.Count == 0)
                    continue;
                
                groups.Sort((g1, g2) => { return g2.EdgeList.Count - g1.EdgeList.Count; });
                
                //first group( longest group ) is outline. 
                for (int i = 1; i < groups.Count; ++i)
                {
                    EdgeGroup group = groups[i];
                    for (int ei = 1; ei < group.EdgeList.Count - 1; ++ei)
                    {
                        tris.Add(group.Begin);
                        tris.Add(group.EdgeList[ei].y);
                        tris.Add(group.EdgeList[ei].x);
                    }
                
                }

                totalTris += tris.Count;
                newTris.Add(tris.ToArray());
            }
            
            WorkingMesh mesh = new WorkingMesh(Allocator.Persistent, source.vertexCount, totalTris, source.subMeshCount, 0);
            mesh.name = source.name;
            mesh.vertices = source.vertices;
            mesh.normals = source.normals;
            mesh.uv = source.uv;

            for (int i = 0; i < newTris.Count; ++i)
            {
                mesh.SetTriangles(newTris[i], i);
            }

            return mesh;
        }

        private DisposableList<HLODBuildInfo> CreateBuildInfo(TerrainData data, SpaceNode root)
        {
            DisposableList<HLODBuildInfo> results = new DisposableList<HLODBuildInfo>();
            Queue<SpaceNode> trevelQueue = new Queue<SpaceNode>();
            Queue<int> parentQueue = new Queue<int>();
            Queue<string> nameQueue = new Queue<string>();
            Queue<int> depthQueue = new Queue<int>();

            int maxDepth = 0;

            trevelQueue.Enqueue(root);
            parentQueue.Enqueue(-1);
            nameQueue.Enqueue("HLOD");
            depthQueue.Enqueue(0);
            

            while (trevelQueue.Count > 0)
            {
                int currentNodeIndex = results.Count;
                string name = nameQueue.Dequeue();
                SpaceNode node = trevelQueue.Dequeue();
                int depth = depthQueue.Dequeue();
                HLODBuildInfo info = new HLODBuildInfo
                {
                    Name = name,
                    ParentIndex = parentQueue.Dequeue(),
                    Target = node,
                };

                for (int i = 0; i < node.GetChildCount(); ++i)
                {
                    trevelQueue.Enqueue(node.GetChild(i));
                    parentQueue.Enqueue(currentNodeIndex);
                    nameQueue.Enqueue(name + "_" + (i + 1));
                    depthQueue.Enqueue(depth + 1);
                }
                
                info.Heightmap = CreateSubHightmap(node.Bounds);
                info.WorkingObjects.Add(CreateBakedTerrain(name, node.Bounds, info.Heightmap, depth, node.GetChildCount() == 0));
                info.Distances.Add(depth);
                results.Add(info);
                
                if (depth > maxDepth)
                    maxDepth = depth;
            }

            //convert depth to distance
            for (int i = 0; i < results.Count; ++i)
            {
                HLODBuildInfo info = results[i];
                for (int di = 0; di < info.Distances.Count; ++di)
                {
                    info.Distances[di] = maxDepth - info.Distances[di];
                }
            }

            return results;
        }
        
        public IEnumerator CreateImpl()
        {
            try
            {
                using (m_queue = new JobQueue(8))
                {
                    Stopwatch sw = new Stopwatch();

                    AssetDatabase.Refresh();
                    AssetDatabase.SaveAssets();

                    sw.Reset();
                    sw.Start();

                    EditorUtility.DisplayProgressBar("Bake HLOD", "Initialize Bake", 0.0f);


                    TerrainData data = m_hlod.TerrainData;

                    m_size = data.size;

                    m_heightmap = new Heightmap(data.heightmapResolution, data.heightmapResolution, data.size,
                        data.GetHeights(0, 0, data.heightmapResolution, data.heightmapResolution));

                    string materialPath = AssetDatabase.GUIDToAssetPath(m_hlod.MaterialGUID);
                    m_terrainMaterial = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
                    if (m_terrainMaterial == null)
                    {
                        m_terrainMaterial = new Material(Shader.Find("Standard"));
                    }

                    m_terrainMaterialInstanceId = m_terrainMaterial.GetInstanceID();
                    m_terrainMaterialName = m_terrainMaterial.name;

                    materialPath = AssetDatabase.GUIDToAssetPath(m_hlod.MaterialLowGUID);
                    m_terrainMaterialLow = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
                    if (m_terrainMaterialLow == null)
                    {
                        m_terrainMaterialLow = new Material(Shader.Find("Standard"));
                    }

                    m_terrainMaterialLowInstanceId = m_terrainMaterialLow.GetInstanceID();
                    m_terrainMaterialLowName = m_terrainMaterialLow.name;

                    using (m_alphamaps = new DisposableList<WorkingTexture>())
                    using (m_layers = new DisposableList<Layer>())
                    {
                        for (int i = 0; i < data.alphamapTextures.Length; ++i)
                        {
                            m_alphamaps.Add(new WorkingTexture(Allocator.Persistent, data.alphamapTextures[i]));
                        }

                        for (int i = 0; i < data.terrainLayers.Length; ++i)
                        {
                            m_layers.Add(new Layer(data.terrainLayers[i], m_hlod.ChunkSize));
                        }


                        QuadTreeSpaceSplitter splitter = new QuadTreeSpaceSplitter(null);

                        List<SpaceNode> rootNodeList = splitter.CreateSpaceTree(m_hlod.GetBounds(), m_hlod.ChunkSize * 2.0f,
                        m_hlod.transform, null, progress => { });

                        EditorUtility.DisplayProgressBar("Bake HLOD", "Create mesh", 0.0f);
                        foreach (var rootNode in rootNodeList)
                        {
                            using (DisposableList<HLODBuildInfo> buildInfos = CreateBuildInfo(data, rootNode))
                            {
                                yield return m_queue.WaitFinish();
                                //Write material & textures

                                for (int i = 0; i < buildInfos.Count; ++i)
                                {
                                    int curIndex = i;
                                    m_queue.EnqueueJob(() =>
                                    {
                                        ISimplifier simplifier = (ISimplifier)Activator.CreateInstance(
                                            m_hlod.SimplifierType,
                                            new object[] { m_hlod.SimplifierOptions });
                                        simplifier.SimplifyImmidiate(buildInfos[curIndex]);
                                    });
                                }

                                EditorUtility.DisplayProgressBar("Bake HLOD", "Simplify meshes", 0.0f);
                                yield return m_queue.WaitFinish();

                                Debug.Log("[TerrainHLOD] Simplify: " + sw.Elapsed.ToString("g"));
                                sw.Reset();
                                sw.Start();
                                EditorUtility.DisplayProgressBar("Bake HLOD", "Make border", 0.0f);

                                for (int i = 0; i < buildInfos.Count; ++i)
                                {
                                    HLODBuildInfo info = buildInfos[i];
                                    m_queue.EnqueueJob(() =>
                                    {
                                        for (int oi = 0; oi < info.WorkingObjects.Count; ++oi)
                                        {
                                            WorkingObject o = info.WorkingObjects[oi];
                                            int borderVertexCount = m_hlod.BorderVertexCount *
                                                                    Mathf.RoundToInt(Mathf.Pow(2.0f,
                                                                        (float)info.Distances[oi]));
                                            using (WorkingMesh m = MakeBorder(o.Mesh, info.Heightmap,
                                                       borderVertexCount))
                                            {
                                                ReampUV(m, info.Heightmap);
                                                o.SetMesh(MakeFillHoleMesh(m));
                                            }
                                        }
                                    });
                                }

                                yield return m_queue.WaitFinish();

                                Debug.Log("[TerrainHLOD] Make Border: " + sw.Elapsed.ToString("g"));
                                sw.Reset();
                                sw.Start();


                                for (int i = 0; i < buildInfos.Count; ++i)
                                {
                                    SpaceNode node = buildInfos[i].Target;
                                    HLODBuildInfo info = buildInfos[i];
                                    if (node.HasChild() == false)
                                    {
                                        SpaceNode parent = node.ParentNode;
                                        node.ParentNode = null;

                                        GameObject go = new GameObject(buildInfos[i].Name);

                                        for (int wi = 0; wi < info.WorkingObjects.Count; ++wi)
                                        {
                                            WorkingObject wo = info.WorkingObjects[wi];
                                            GameObject targetGO = null;
                                            if (wi == 0)
                                            {
                                                targetGO = go;
                                            }
                                            else
                                            {
                                                targetGO = new GameObject(wi.ToString());
                                                targetGO.transform.SetParent(go.transform, false);
                                            }

                                            List<Material> materials = new List<Material>();
                                            for (int mi = 0; mi < wo.Materials.Count; ++mi)
                                            {

                                                WorkingMaterial wm = wo.Materials[mi];
                                                if (wm.NeedWrite() == false)
                                                {
                                                    materials.Add(wm.ToMaterial());
                                                    continue;
                                                }

                                                Material mat = new Material(wm.ToMaterial());
                                                string[] textureNames = wm.GetTextureNames();
                                                for (int ti = 0; ti < textureNames.Length; ++ti)
                                                {
                                                    WorkingTexture wt = wm.GetTexture(textureNames[ti]);
                                                    Texture2D tex = wt.ToTexture();
                                                    tex.wrapMode = wt.WrapMode;
                                                    mat.name = targetGO.name + "_Mat";
                                                    mat.SetTexture(textureNames[ti], tex);
                                                }

                                                mat.EnableKeyword("_NORMALMAP");
                                                materials.Add(mat);
                                            }

                                            targetGO.AddComponent<MeshFilter>().sharedMesh = wo.Mesh.ToMesh();

                                            var mr = targetGO.AddComponent<MeshRenderer>();
                                            mr.sharedMaterials = materials.ToArray();
                                            mr.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
                                        }

                                        go.transform.SetParent(m_hlod.transform, false);
                                        m_hlod.AddGeneratedResource(go);

                                        parent.Objects.Add(go);
                                        buildInfos.RemoveAt(i);
                                        i -= 1;
                                    }
                                }

                                //controller
                                IStreamingBuilder builder = (IStreamingBuilder)Activator.CreateInstance(m_hlod.StreamingType,
                                        new object[] { m_hlod, m_hlod.StreamingOptions });

                                builder.Build(rootNode, buildInfos, m_hlod.gameObject, m_hlod.CullDistance,
                                    m_hlod.LODDistance, true, false,
                                    progress =>
                                    {
                                        EditorUtility.DisplayProgressBar("Bake HLOD", "Storing results.",
                                            0.75f + progress * 0.25f);
                                    });

                                Debug.Log("[TerrainHLOD] Build: " + sw.Elapsed.ToString("g"));

                            }
                        }
                    }

                    EditorUtility.SetDirty(m_hlod.gameObject);
                }
            }
            finally
            {
                EditorUtility.ClearProgressBar();
                GC.Collect();
            }
        }

        
    }

}