#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;

namespace Poi
{
    public class PoiOutlineUtilWindow : EditorWindow
    {
        private static Vector2 scrollPosition = new Vector2(0,0);
        private static GameObject avatar;
        private static readonly Dictionary<int, MeshSettings[]> meshSettings = new Dictionary<int, MeshSettings[]>(); // <instanceID, <submesh, setting>>
        private static Dictionary<Mesh, Mesh> bakedMeshes = new Dictionary<Mesh, Mesh>();
        private static int lang = -1;
        private static bool isCancelled = false;
        private static readonly Color emptyColor = new Color(0.5f, 0.5f, 1.0f, 1.0f);
        private static GUIStyle marginBox;

        private struct MeshSettings
        {
            public string name;
            public bool isBakeTarget;
            public float shrinkTipStrength;
        }

        //------------------------------------------------------------------------------------------------------------------------------
        // GUI
        [MenuItem("Poi/Outline Vertex Color Baker")]
        static void Init()
        {
            PoiOutlineUtilWindow window = (PoiOutlineUtilWindow)GetWindow(typeof(PoiOutlineUtilWindow), false, TEXT_WINDOW_NAME);
            window.Show();
        }

        void OnGUI()
        {
            scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);

            // Language
            if(lang == -1)
            {
                lang = Application.systemLanguage == SystemLanguage.Japanese ? 1 : 0;
            }
            lang = EditorGUILayout.Popup("Language", lang, TEXT_LANGUAGES);
            marginBox = new GUIStyle(EditorStyles.helpBox);
            marginBox.margin.left = 30;

            //------------------------------------------------------------------------------------------------------------------------------
            // 1. Select the mesh
            EditorGUILayout.LabelField(TEXT_STEP_SELECT_AVATAR[lang], EditorStyles.boldLabel);
            avatar = (GameObject)EditorGUILayout.ObjectField(TEXT_ITEM_DD_AVATAR[lang], avatar, typeof(GameObject), true);
            if(avatar == null)
            {
                EditorGUILayout.EndScrollView();
                return;
            }
            if(AssetDatabase.Contains(avatar))
            {
                EditorGUILayout.HelpBox(TEXT_WARN_SELECT_FROM_SCENE[lang], MessageType.Error);
                EditorGUILayout.EndScrollView();
                return;
            }
            EditorGUILayout.Space();

            //------------------------------------------------------------------------------------------------------------------------------
            // 2. Select the modify target
            EditorGUILayout.LabelField(TEXT_STEP_SELECT_SUBMESH[lang], EditorStyles.boldLabel);
            if (GUILayout.Button("Select All"))
            {
                foreach (var item in meshSettings)
                {
                    for (int i = 0; i < item.Value.Length; i++)
                    {
                        item.Value[i].isBakeTarget = true;
                    }
                }
            }
            Component[] skinnedMeshRenderers = avatar.GetComponentsInChildren(typeof(SkinnedMeshRenderer), true);
            Component[] meshRenderers = avatar.GetComponentsInChildren(typeof(MeshRenderer), true);
            DrawModifyTargetsGUI(skinnedMeshRenderers, meshRenderers);
            EditorGUILayout.Space();

            //------------------------------------------------------------------------------------------------------------------------------
            // 3. Generate the mesh, test it, then save
            GameObject bakedAvatar = FindBakedAvatar();

            EditorGUILayout.LabelField(TEXT_STEP_GENERATE_AND_SAVE[lang], EditorStyles.boldLabel);
            EditorGUILayout.BeginHorizontal();
            if(GUILayout.Button(TEXT_BUTTON_GENERATE_AND_TEST[lang]))
            {
                GenerateMeshes(bakedAvatar, skinnedMeshRenderers, meshRenderers);
            }

            if(bakedAvatar == null)
            {
                EditorGUILayout.EndHorizontal();
                EditorGUILayout.EndScrollView();
                return;
            }

            // Save
            bakedMeshes = new Dictionary<Mesh, Mesh>();
            GetBakedMeshes(bakedAvatar, skinnedMeshRenderers, meshRenderers);
            bool isSaved = true;
            foreach(Mesh bakedMesh in bakedMeshes.Values)
            {
                if(!isSaved) break;
                if(bakedMesh == null) continue;
                isSaved = AssetDatabase.Contains(bakedMesh);
            }

            GUIStyle saveButton = new GUIStyle(GUI.skin.button);
            if(!isSaved)
            {
                saveButton.normal.textColor = Color.red;
                saveButton.fontStyle = FontStyle.Bold;
            }

            if(GUILayout.Button(TEXT_BUTTON_SAVE[lang], saveButton))
            {
                SaveMeshes();
            }
            EditorGUILayout.EndHorizontal();

            if(!isSaved)
            {
                EditorGUILayout.HelpBox(TEXT_WARN_MESH_NOT_SAVED[lang], MessageType.Warning);
            }

            EditorGUILayout.EndScrollView();
        }

        //------------------------------------------------------------------------------------------------------------------------------
        // 2. Select the modify target
        private static void DrawModifyTargetsGUI(Component[] skinnedMeshRenderers, Component[] meshRenderers)
        {
            foreach(SkinnedMeshRenderer skinnedMeshRenderer in skinnedMeshRenderers)
            {
                EditorGUILayout.LabelField(skinnedMeshRenderer.gameObject.name, EditorStyles.boldLabel);
                int id = skinnedMeshRenderer.gameObject.GetInstanceID();
                Mesh sharedMesh = skinnedMeshRenderer.sharedMesh;
                Material[] materials = skinnedMeshRenderer.sharedMaterials;
                EditorGUI.indentLevel++;
                DrawGUIPerComponent(id, sharedMesh, materials);
                EditorGUI.indentLevel--;
            }

            foreach(MeshRenderer meshRenderer in meshRenderers)
            {
                MeshFilter meshFilter = meshRenderer.gameObject.GetComponent<MeshFilter>();
                if(meshFilter == null)
                {
                    continue;
                }
                EditorGUILayout.LabelField(meshRenderer.gameObject.name, EditorStyles.boldLabel);
                int id = meshRenderer.gameObject.GetInstanceID();
                Mesh sharedMesh = meshFilter.sharedMesh;
                Material[] materials = meshRenderer.sharedMaterials;
                EditorGUI.indentLevel++;
                DrawGUIPerComponent(id, sharedMesh, materials);
                EditorGUI.indentLevel--;
            }
        }

        private static void DrawGUIPerComponent(int id, Mesh sharedMesh, Material[] materials)
        {
            Vector3[] vertices = sharedMesh?.vertices;
            Vector3[] normals = sharedMesh?.normals;
            Vector4[] tangents = sharedMesh?.tangents;
            Color[] colors = sharedMesh?.colors;
            Vector2[] uv = sharedMesh?.uv;
            bool hasColors = colors != null && colors.Length > 2;
            bool hasUV0 = uv != null || uv.Length > 2;

            // Draw error messages
            if(sharedMesh == null)
            {
                EditorGUILayout.HelpBox(TEXT_WARN_MESH_IS_EMPTY[lang], MessageType.Error);
                return;
            }

            if(!sharedMesh.isReadable)
            {
                EditorGUILayout.HelpBox(TEXT_WARN_MESH_NOT_READABLE[lang], MessageType.Error);
                return;
            }

            if(vertices == null || vertices.Length < 2)
            {
                EditorGUILayout.HelpBox(TEXT_WARN_MESH_HAS_NO_VERT[lang], MessageType.Error);
                return;
            }

            if(normals == null && normals.Length < 2)
            {
                EditorGUILayout.HelpBox(TEXT_WARN_MESH_HAS_NO_NORM[lang], MessageType.Error);
                return;
            }

            if(tangents == null && tangents.Length < 2)
            {
                EditorGUILayout.HelpBox(TEXT_WARN_MESH_HAS_NO_TANJ[lang], MessageType.Error);
                return;
            }

            // Generate empty settings
            if(!meshSettings.ContainsKey(id)) meshSettings[id] = null;
            if(meshSettings[id] == null || meshSettings[id].Length != sharedMesh.subMeshCount)
            {
                meshSettings[id] = new MeshSettings[sharedMesh.subMeshCount];
                for(int i = 0; i < sharedMesh.subMeshCount; i++)
                {
                    meshSettings[id][i] = new MeshSettings
                    {
                        name = null,
                        isBakeTarget = false,
                        shrinkTipStrength = 0.0f
                    };
                }
            }

            // Draw settings
            for(int i = 0; i < sharedMesh.subMeshCount; i++)
            {
                if(string.IsNullOrEmpty(meshSettings[id][i].name))
                {
                    meshSettings[id][i].name = i + ": ";
                    if(i < materials.Length && materials[i] != null && !string.IsNullOrEmpty(materials[i].name))
                    {
                        meshSettings[id][i].name += materials[i].name;
                    }
                }
                DrawMeshSettingsGUI(id, i, hasColors, hasUV0);
            }
        }

        private static void DrawMeshSettingsGUI(int id, int i, bool hasColors, bool hasUV0)
        {
            meshSettings[id][i].isBakeTarget = EditorGUILayout.ToggleLeft(meshSettings[id][i].name, meshSettings[id][i].isBakeTarget);

            int indentCopy = EditorGUI.indentLevel;
            EditorGUI.indentLevel = 0;
            if(meshSettings[id][i].isBakeTarget)
            {
                EditorGUILayout.BeginVertical(marginBox);
                meshSettings[id][i].shrinkTipStrength = EditorGUILayout.FloatField(TEXT_ITEM_SHRINK_TIP[lang], meshSettings[id][i].shrinkTipStrength);
                EditorGUILayout.EndVertical();
            }
            EditorGUI.indentLevel = indentCopy;
        }

        //------------------------------------------------------------------------------------------------------------------------------
        // 3. Generate the mesh, test it, then save
        private static int[] GetChildIndices(GameObject root, GameObject child)
        {
            var indices = new List<int>();
            indices.Add(child.transform.GetSiblingIndex());
            Transform parent = child.transform.parent;
            while(parent != null && parent != root.transform)
            {
                indices.Add(parent.GetSiblingIndex());
                parent = parent.parent;
            }
            return indices.ToArray();
        }

        private static GameObject GetChild(GameObject root, int[] indices)
        {
            Transform current = root.transform;
            for(int i = indices.Length - 1; i >= 0; i--)
            {
                current = current.GetChild(indices[i]);
                if(current == null) return null;
            }
            return current.gameObject;
        }

        private static GameObject GetChildInstance(GameObject root, GameObject rootInstance, GameObject child)
        {
            return GetChild(rootInstance, GetChildIndices(root, child));
        }

        private static void GenerateMeshes(GameObject bakedAvatar, Component[] skinnedMeshRenderers, Component[] meshRenderers)
        {
            if(bakedAvatar == null)
            {
                bakedAvatar = Instantiate(avatar);
                bakedAvatar.name = avatar.name + " (VertexColorBaked)";
                bakedAvatar.transform.parent = avatar.transform.parent;
                bakedAvatar.SetActive(true);
            }

            isCancelled = false;
            foreach(SkinnedMeshRenderer skinnedMeshRenderer in skinnedMeshRenderers)
            {
                GameObject child = GetChildInstance(avatar, bakedAvatar, skinnedMeshRenderer.gameObject);
                if(child == null)
                {
                    Debug.LogWarning($"[{TEXT_WINDOW_NAME}] Child is not found");
                    continue;
                }
                SkinnedMeshRenderer bakedSkinnedMeshRenderer = child.GetComponent<SkinnedMeshRenderer>();
                if(bakedSkinnedMeshRenderer == null)
                {
                    Debug.LogWarning($"[{TEXT_WINDOW_NAME}] Component is not found");
                    continue;
                }

                int id = skinnedMeshRenderer.gameObject.GetInstanceID();
                Mesh sharedMesh = skinnedMeshRenderer.sharedMesh;
                Mesh bakedMesh = bakedSkinnedMeshRenderer.sharedMesh;
                if(bakedMesh == null || !bakedMesh.name.Contains("(Clone)"))
                {
                    bakedMesh = Instantiate(sharedMesh);
                }
                BakeVertexColors(ref bakedMesh, sharedMesh, id);
                if(isCancelled) break;

                if(bakedMesh == null)
                {
                    Debug.LogWarning($"[{TEXT_WINDOW_NAME}] Mesh is not found");
                    continue;
                }
                bakedSkinnedMeshRenderer.sharedMesh = bakedMesh;
            }

            foreach(MeshRenderer meshRenderer in meshRenderers)
            {
                MeshFilter meshFilter = meshRenderer.gameObject.GetComponent<MeshFilter>();
                if(meshFilter == null)
                {
                    continue;
                }
                GameObject child = GetChildInstance(avatar, bakedAvatar, meshRenderer.gameObject);
                if(child == null)
                {
                    Debug.LogWarning($"[{TEXT_WINDOW_NAME}] Child is not found");
                    continue;
                }
                MeshFilter bakedMeshFilter = child.GetComponent<MeshFilter>();
                if(bakedMeshFilter == null)
                {
                    Debug.LogWarning($"[{TEXT_WINDOW_NAME}] Component is not found");
                    continue;
                }

                int id = meshRenderer.gameObject.GetInstanceID();
                Mesh sharedMesh = meshFilter.sharedMesh;
                Mesh bakedMesh = bakedMeshFilter.sharedMesh;
                if(bakedMesh == null || !bakedMesh.name.Contains("(Clone)"))
                {
                    bakedMesh = Instantiate(sharedMesh);
                }
                BakeVertexColors(ref bakedMesh, sharedMesh, id);
                if(isCancelled) break;

                if(bakedMesh == null)
                {
                    Debug.LogWarning($"[{TEXT_WINDOW_NAME}] Mesh is not found");
                    continue;
                }
                bakedMeshFilter.sharedMesh = bakedMesh;
            }
            if(!isCancelled) EditorUtility.DisplayDialog(TEXT_WINDOW_NAME, "Complete!", "OK");
        }

        private static void GetBakedMeshes(GameObject bakedAvatar, Component[] skinnedMeshRenderers, Component[] meshRenderers)
        {
            foreach(SkinnedMeshRenderer skinnedMeshRenderer in skinnedMeshRenderers)
            {
                Mesh sharedMesh = skinnedMeshRenderer.sharedMesh;
                if(sharedMesh == null)
                {
                    Debug.LogWarning($"[{TEXT_WINDOW_NAME}] Mesh is not found");
                    continue;
                }
                GameObject child = GetChildInstance(avatar, bakedAvatar, skinnedMeshRenderer.gameObject);
                if(child == null)
                {
                    Debug.LogWarning($"[{TEXT_WINDOW_NAME}] Child is not found");
                    continue;
                }
                SkinnedMeshRenderer bakedSkinnedMeshRenderer = child.GetComponent<SkinnedMeshRenderer>();
                if(bakedSkinnedMeshRenderer == null)
                {
                    Debug.LogWarning($"[{TEXT_WINDOW_NAME}] Component is not found");
                    continue;
                }
                bakedMeshes[sharedMesh] = bakedSkinnedMeshRenderer.sharedMesh;
            }

            foreach(MeshRenderer meshRenderer in meshRenderers)
            {
                MeshFilter meshFilter = meshRenderer.gameObject.GetComponent<MeshFilter>();
                if(meshFilter == null)
                {
                    Debug.LogWarning($"[{TEXT_WINDOW_NAME}] Component is not found");
                    continue;
                }
                Mesh sharedMesh = meshFilter.sharedMesh;
                if(sharedMesh == null)
                {
                    Debug.LogWarning($"[{TEXT_WINDOW_NAME}] Mesh is not found");
                    continue;
                }
                GameObject child = GetChildInstance(avatar, bakedAvatar, meshRenderer.gameObject);
                if(child == null)
                {
                    Debug.LogWarning($"[{TEXT_WINDOW_NAME}] Child is not found");
                    continue;
                }
                MeshFilter bakedMeshFilter = child.GetComponent<MeshFilter>();
                if(bakedMeshFilter == null)
                {
                    Debug.LogWarning($"[{TEXT_WINDOW_NAME}] Component is not found");
                    continue;
                }
                bakedMeshes[sharedMesh] = bakedMeshFilter.sharedMesh;
            }
        }

        private static void SaveMeshes()
        {
            foreach(KeyValuePair<Mesh, Mesh> bakedMesh in bakedMeshes)
            {
                if(bakedMesh.Value == null || string.IsNullOrEmpty(bakedMesh.Value.name)) continue;

                string path = AssetDatabase.GetAssetPath(bakedMesh.Value);
                if(string.IsNullOrEmpty(path))
                {
                    path = AssetDatabase.GetAssetPath(bakedMesh.Key);
                    if(string.IsNullOrEmpty(path) || !path.StartsWith("Assets/"))
                    {
                        path = "Assets/BakedMeshes/" + bakedMesh.Value.name + ".asset";
                    }
                    else
                    {
                        path = Path.GetDirectoryName(path) + "/BakedMeshes/" + bakedMesh.Value.name + ".asset";
                    }
                    path = GetUniqueName(path);
                }

                string saveDirectory = Path.GetDirectoryName(path);
                if(!Directory.Exists(saveDirectory))
                {
                    Directory.CreateDirectory(saveDirectory);
                }
                if(!File.Exists(path))
                {
                    Debug.Log($"[{TEXT_WINDOW_NAME}] Create asset to: " + path);
                    AssetDatabase.CreateAsset(bakedMesh.Value, path);
                }
                else
                {
                    Debug.Log($"[{TEXT_WINDOW_NAME}] Overwrite mesh to: " + path);
                }
            }
            AssetDatabase.SaveAssets();
            EditorUtility.DisplayDialog(TEXT_WINDOW_NAME, "Complete!", "OK");
        }

        private static string GetUniqueName(string path)
        {
            if(!File.Exists(path)) return path;

            string baseName = Path.GetDirectoryName(path) + "/" + Path.GetFileNameWithoutExtension(path);
            string outPath;
            int i = 1;
            while(true)
            {
                outPath = baseName + " " + i.ToString() + ".asset";
                if(!File.Exists(outPath)) return outPath;
                i++;
            }
        }

        //------------------------------------------------------------------------------------------------------------------------------
        // Mesh Generator
        private static void BakeVertexColors(ref Mesh mesh, Mesh sharedMesh, int id)
        {
            if(isCancelled || sharedMesh == null || !sharedMesh.isReadable) return;
            Vector3[] vertices = sharedMesh.vertices;
            Vector3[] normals = sharedMesh.normals;
            Vector4[] tangents = sharedMesh.tangents;

            if(vertices == null || vertices.Length < 2 ||
               normals == null && normals.Length < 2 ||
               tangents == null && tangents.Length < 2)
            {
                return;
            }

            Color[] outColors = Enumerable.Repeat(Color.white, vertices.Length).ToArray();

            isCancelled = false;
            for(int mi = 0; mi < sharedMesh.subMeshCount; mi++)
            {
                if(!meshSettings[id][mi].isBakeTarget) continue;
                int[] sharedIndices = GetOptIndices(sharedMesh, mi);
                BakeNormalAverage(ref outColors, sharedIndices, meshSettings[id][mi], vertices, normals, tangents);
                EditorUtility.ClearProgressBar();
                if(isCancelled) return;
            }

            FixIllegalDatas(ref outColors);
            mesh.SetColors(outColors);
            EditorUtility.SetDirty(mesh);
        }

        //------------------------------------------------------------------------------------------------------------------------------
        // Bake normal to color
        private static void BakeNormalAverage(ref Color[] outColors, int[] sharedIndices, MeshSettings settings, Vector3[] vertices, Vector3[] normals, Vector4[] tangents)
        {
            var normalAverages = NormalGatherer.GetNormalAveragesFast(sharedIndices, vertices, normals);
            string message = "Run bake in " + settings.name;

            for(int i = 0; i < sharedIndices.Length; ++i)
            {
                int index = sharedIndices[i];
                float width = 1.0f;
                Vector3 normal = normals[index];
                Vector4 tangent = tangents[index];
                Vector3 bitangent = Vector3.Cross(normal, tangent) * tangent.w;
                if(IsIllegalTangent(normal, tangent))
                {
                    outColors[index].r = 0.5f;
                    outColors[index].g = 0.5f;
                    outColors[index].b = 1.0f;
                    outColors[index].a = 1.0f;
                    continue;
                }
                Vector3 normalAverage = NormalGatherer.GetClosestNormal(normalAverages, vertices[index]);
                if(settings.shrinkTipStrength > 0) width *= Mathf.Pow(Mathf.Clamp01(Vector3.Dot(normal,normalAverage)), settings.shrinkTipStrength);
                outColors[index].r = Vector3.Dot(normalAverage, tangent) * 0.5f + 0.5f;
                outColors[index].g = Vector3.Dot(normalAverage, bitangent) * 0.5f + 0.5f;
                outColors[index].b = Vector3.Dot(normalAverage, normal) * 0.5f + 0.5f;
                outColors[index].a = width;
                if(DrawProgress(message, i, (float)i / (float)sharedIndices.Length)) return;
            }
        }

        public static bool DrawProgress(string message, int i, float progress)
        {
            if((i & 0b11111111) == 0b11111111) return isCancelled = isCancelled || EditorUtility.DisplayCancelableProgressBar(TEXT_WINDOW_NAME, message, progress);
            return isCancelled;
        }

        private static int[] GetOptIndices(Mesh mesh, int mi)
        {
            return mesh.GetIndices(mi).ToList().Distinct().ToArray();
        }

        private static bool IsIllegalTangent(Vector3 normal, Vector4 tangent)
        {
            return normal.x == tangent.x && normal.y == tangent.y && normal.z == tangent.z;
        }

        //------------------------------------------------------------------------------------------------------------------------------
        // Utils

        private static GameObject FindBakedAvatar()
        {
            if(avatar.transform.parent != null)
            {
                for(int i = 0; i < avatar.transform.parent.childCount; i++)
                {
                    GameObject childObject = avatar.transform.parent.GetChild(i).gameObject;
                    if(childObject.name.Contains(avatar.name + " (VertexColorBaked)"))
                    {
                        return childObject;
                    }
                }
            }

            return GameObject.Find(avatar.name + " (VertexColorBaked)");
        }

        private static void FixIllegalDatas(ref Color[] outColors)
        {
            for(int i = 0; i < outColors.Length; i++)
            {
                Color color = outColors[i];
                if(
                    color.r >= 0 && color.r <= 1 &&
                    color.g >= 0 && color.g <= 1 &&
                    color.b >= 0 && color.b <= 1 &&
                    color.a >= 0 && color.a <= 1
                )
                {
                    continue;
                }

                outColors[i] = emptyColor;
            }
        }

        //------------------------------------------------------------------------------------------------------------------------------
        // Languages
        private const string TEXT_WINDOW_NAME = "PoiOutlineUtil";

        private static readonly string[] TEXT_LANGUAGES                 = new[] {"English", "Japanese"};

        private static readonly string[] TEXT_STEP_SELECT_AVATAR        = new[] {"1. Select the avatar",                        "1. アバターを選択"};
        private static readonly string[] TEXT_STEP_SELECT_SUBMESH       = new[] {"2. Select the modify target",                 "2. 編集対象を選択"};
        private static readonly string[] TEXT_STEP_GENERATE_AND_SAVE    = new[] {"3. Generate the mesh, test it, then save",    "3. メッシュを生成・テスト・保存"};

        private static readonly string[] TEXT_WARN_SELECT_FROM_SCENE    = new[] {"Please select from the scene (hierarchy)",            "シーン（ヒエラルキー）から選択してください"};
        private static readonly string[] TEXT_WARN_MESH_NOT_READABLE    = new[] {"The selected mesh is not set to \"Read/Write\" on.",  "選択されたメッシュは\"Read/Write\"がオンになっていません。"};
        private static readonly string[] TEXT_WARN_MESH_IS_EMPTY        = new[] {"The selected mesh is empty!",         "選択したメッシュは空です"};
        private static readonly string[] TEXT_WARN_MESH_HAS_NO_VERT     = new[] {"The selected mesh has no vertices!",  "選択したメッシュは頂点がありません。"};
        private static readonly string[] TEXT_WARN_MESH_HAS_NO_NORM     = new[] {"The selected mesh has no normals!",   "選択したメッシュは法線がありません。"};
        private static readonly string[] TEXT_WARN_MESH_HAS_NO_TANJ     = new[] {"The selected mesh has no tangents!",  "選択したメッシュはタンジェントがありません。"};
        private static readonly string[] TEXT_WARN_MESH_NOT_SAVED       = new[] {"Generated mesh is not saved!",        "生成されたメッシュが保存されていません。"};

        private static readonly string[] TEXT_ITEM_DD_AVATAR            = new[] {"Avatar (D&D from scene)", "アバター (シーンからD&D)"};
        private static readonly string[] TEXT_ITEM_SHRINK_TIP           = new[] {"Shrink the tip",          "先端を細くする度合い"};

        private static readonly string[] TEXT_BUTTON_GENERATE_AND_TEST  = new[] {"Generate & Test",         "生成 & テスト"};
        private static readonly string[] TEXT_BUTTON_SAVE               = new[] {"Save",                    "保存"};
    }

    public class NormalGatherer
    {
        public static Dictionary<Vector3, Vector3> GetNormalAveragesFast(int[] sharedIndices, Vector3[] vertices, Vector3[] normals)
        {
            var normalAverages = new Dictionary<Vector3, Vector3>();
            string message = "Generating averages";

            for(int i = 0; i < sharedIndices.Length; i++)
            {
                int index = sharedIndices[i];
                Vector3 pos = vertices[index];
                if(!normalAverages.ContainsKey(pos))
                {
                    normalAverages[pos] = normals[index];
                    continue;
                }
                normalAverages[pos] += normals[index];
                if(PoiOutlineUtilWindow.DrawProgress(message, i, (float)i / (float)vertices.Length)) return normalAverages;
            }

            var keys = normalAverages.Keys.ToArray();
            for(int j = 0; j < keys.Length; j++)
            {
                normalAverages[keys[j]] = Vector3.Normalize(normalAverages[keys[j]]);
            }

            return normalAverages;
        }

        public static Vector3 GetClosestNormal(Dictionary<Vector3, Vector3> normalAverages, Vector3 pos)
        {
            if(normalAverages.ContainsKey(pos)) return normalAverages[pos];

            float closestDist = 1000.0f;
            Vector3 closestNormal = new Vector3(0,0,0);
            foreach(KeyValuePair<Vector3, Vector3> normalAverage in normalAverages)
            {
                float dist = Vector3.Distance(pos, normalAverage.Key);
                closestDist = dist < closestDist ? dist : closestDist;
                closestNormal = dist < closestDist ? normalAverage.Value : closestNormal;
            }

            return closestNormal;
        }
    }

}
#endif