// Tools > Generato > Avatars: every Generato avatar in the project, with a preview you can turn, and
// one button that adds the chosen one to the scene and sets it up for VRChat. Opens by itself when a
// Generato FBX is imported, and once when this package is first added to a project.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;

namespace Generato.AvatarTools
{
    public class GeneratoAvatarWindow : EditorWindow
    {
        const string SiteUrl = "https://generato.io/vrchat-avatar";

        class Entry
        {
            public string Path, Name, Kind;
            public GameObject Model;
            public int Triangles;
            public GameObject[] InScene = new GameObject[0];
            public bool Ready => InScene.Any(GeneratoVRChatSetup.HasDescriptor);
        }

        readonly List<Entry> entries = new List<Entry>();
        string selectedPath;
        // Our own preview: Unity's model preview draws the avatar with its real material, and the
        // VRChat Toon Lit shader (one ForwardBase pass, skipped shadow variants) shows pink there.
        // Here it is drawn with the built-in Unlit/Texture shader and the avatar's texture.
        PreviewRenderUtility stage;
        GameObject stageAvatar, stageFor;
        Material stageMaterial;
        Bounds stageBounds;
        float yaw = 20f;
        Vector2 scroll;
        bool dirty = true;

        GUIStyle heading, small, rowName;
        readonly Dictionary<string, Texture2D> thumbs = new Dictionary<string, Texture2D>();

        [MenuItem("Tools/Generato/Avatars", false, 0)]
        public static void OpenMenu() => Open();

        public static void Open(string select = null)
        {
            var w = GetWindow<GeneratoAvatarWindow>();
            w.titleContent = new GUIContent("Generato Avatars");
            w.minSize = new Vector2(340, 520);
            w.Refresh();
            if (select != null) w.selectedPath = select;
            w.Show();
            w.Focus();
        }

        // First time the package is in a project: show the window once, so nobody has to find the menu.
        [InitializeOnLoadMethod]
        static void FirstRun()
        {
            if (Application.isBatchMode) return;
            var key = "Generato.AvatarTools.Shown." + Application.dataPath.GetHashCode();
            if (EditorPrefs.GetBool(key)) return;
            EditorPrefs.SetBool(key, true);
            EditorApplication.delayCall += () => Open();
        }

        void OnEnable()
        {
            EditorApplication.hierarchyChanged += MarkDirty;
            EditorApplication.projectChanged += OnProjectChanged;
            UnityEditor.SceneManagement.EditorSceneManager.activeSceneChangedInEditMode += OnSceneChanged;
        }

        void OnDisable()
        {
            EditorApplication.hierarchyChanged -= MarkDirty;
            EditorApplication.projectChanged -= OnProjectChanged;
            UnityEditor.SceneManagement.EditorSceneManager.activeSceneChangedInEditMode -= OnSceneChanged;
            ClearStage();
            foreach (var t in thumbs.Values) if (t) DestroyImmediate(t);
            thumbs.Clear();
        }

        void OnSceneChanged(UnityEngine.SceneManagement.Scene a, UnityEngine.SceneManagement.Scene b) => MarkDirty();
        void MarkDirty() { dirty = true; Repaint(); }

        void OnProjectChanged()
        {
            // A reimport (set-up, new texture) replaces the model: draw it again.
            ClearStage();
            foreach (var t in thumbs.Values) if (t) DestroyImmediate(t);
            thumbs.Clear();
            MarkDirty();
        }

        void ClearStage()
        {
            if (stage != null) stage.Cleanup();
            stage = null;
            if (stageMaterial) DestroyImmediate(stageMaterial);
            stageAvatar = stageFor = null;
        }

        bool Stage(GameObject model)
        {
            if (stage != null && stageFor == model && stageAvatar) return true;
            ClearStage();
            if (!model) return false;
            stage = new PreviewRenderUtility();
            stage.camera.fieldOfView = 30f;
            stage.camera.nearClipPlane = 0.01f;
            stage.camera.farClipPlane = 100f;
            stage.camera.clearFlags = CameraClearFlags.SolidColor;
            stage.camera.backgroundColor = new Color(0.19f, 0.19f, 0.2f);
            stageAvatar = Instantiate(model);
            stageAvatar.hideFlags = HideFlags.HideAndDontSave;
            foreach (var a in stageAvatar.GetComponentsInChildren<Animator>()) a.enabled = false;
            var renderers = stageAvatar.GetComponentsInChildren<Renderer>();
            var tex = renderers.SelectMany(r => r.sharedMaterials).Where(m => m).Select(m => m.mainTexture).FirstOrDefault(t => t)
                ?? AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(model)).OfType<Texture2D>().FirstOrDefault();
            stageMaterial = new Material(Shader.Find("Unlit/Texture")) { hideFlags = HideFlags.HideAndDontSave, mainTexture = tex };
            stageBounds = new Bounds();
            var first = true;
            foreach (var r in renderers)
            {
                r.sharedMaterials = Enumerable.Repeat(stageMaterial, r.sharedMaterials.Length).ToArray();
                if (r is SkinnedMeshRenderer smr) smr.updateWhenOffscreen = true;
                if (first) stageBounds = r.bounds; else stageBounds.Encapsulate(r.bounds);
                first = false;
            }
            stage.AddSingleGO(stageAvatar);
            stageFor = model;
            return true;
        }

        void Aim(float aspect)
        {
            var c = stageBounds.center;
            var h = Mathf.Max(stageBounds.size.y, stageBounds.size.x / Mathf.Max(aspect, 0.1f), 0.1f);
            var dist = h * 0.5f / Mathf.Tan(15f * Mathf.Deg2Rad) * 1.12f;
            var cam = stage.camera;
            cam.transform.position = c + Quaternion.Euler(4f, yaw, 0f) * Vector3.forward * dist;
            cam.transform.LookAt(c);
        }

        /// <summary>A still of the avatar as the preview draws it (thumbnails, tests).</summary>
        internal Texture2D RenderStill(GameObject model, int width, int height, float angle)
        {
            if (!Stage(model)) return null;
            var keep = yaw;
            yaw = angle;
            stage.BeginStaticPreview(new Rect(0, 0, width, height));
            Aim(width / (float)height);
            stage.Render(true);
            var t = stage.EndStaticPreview();
            yaw = keep;
            return t;
        }

        void DrawStage(Rect r)
        {
            var ev = Event.current;
            if (ev.type == EventType.MouseDrag && r.Contains(ev.mousePosition))
            {
                yaw += ev.delta.x * 0.8f;
                ev.Use();
                Repaint();
            }
            if (ev.type != EventType.Repaint) return;
            stage.BeginPreview(r, GUIStyle.none);
            Aim(r.width / Mathf.Max(r.height, 1f));
            stage.Render(true);
            stage.EndAndDrawPreview(r);
        }

        void Update()
        {
            if (AssetPreview.IsLoadingAssetPreviews()) Repaint();
        }

        public void Refresh()
        {
            dirty = false;
            entries.Clear();
            foreach (var path in AssetDatabase.FindAssets("t:Model").Select(AssetDatabase.GUIDToAssetPath).Distinct())
            {
                if (!GeneratoAvatarPostprocessor.IsGeneratoModel(path)) continue;
                var model = AssetDatabase.LoadAssetAtPath<GameObject>(path);
                if (!model) continue;
                var tris = 0;
                foreach (var mesh in AssetDatabase.LoadAllAssetsAtPath(path).OfType<Mesh>())
                    for (var i = 0; i < mesh.subMeshCount; i++) tris += (int)(mesh.GetIndexCount(i) / 3);
                var file = System.IO.Path.GetFileNameWithoutExtension(path);
                entries.Add(new Entry
                {
                    Path = path,
                    Model = model,
                    Triangles = tris,
                    Name = Pretty(file),
                    Kind = tris <= 10000 ? "Quest + PC" : tris <= 70000 ? "PC" : "PC (too many triangles for Quest)",
                    InScene = GeneratoVRChatSetup.FindInScene(path),
                });
            }
            if (entries.Count > 0 && entries.All(e => e.Path != selectedPath)) selectedPath = entries[0].Path;
        }

        static string Pretty(string file)
        {
            var s = file.StartsWith("generato-avatar-") ? file.Substring("generato-avatar-".Length) : file;
            return s.Length > 0 ? s : file;
        }

        void Styles()
        {
            if (heading != null) return;
            heading = new GUIStyle(EditorStyles.boldLabel) { fontSize = 15 };
            small = new GUIStyle(EditorStyles.miniLabel) { wordWrap = true };
            rowName = new GUIStyle(EditorStyles.boldLabel) { fontSize = 12 };
        }

        void OnGUI()
        {
            Styles();
            if (dirty) Refresh();

            GUILayout.Space(6);
            GUILayout.Label("Generato Avatars", heading);
            GUILayout.Label("Pick an avatar, then add it to the scene. The VRChat set-up is done for you.", small);
            GUILayout.Space(4);

            if (!GeneratoVRChatSetup.HasVRChatSdk)
                EditorGUILayout.HelpBox("The VRChat SDK isn't in this project. Make the project with the VRChat Creator Companion (Avatars template) to upload to VRChat.", MessageType.Warning);

            if (entries.Count == 0)
            {
                Empty();
                return;
            }

            var listHeight = Mathf.Min(entries.Count, 3) * 64f + 4f;
            scroll = EditorGUILayout.BeginScrollView(scroll, GUILayout.Height(listHeight));
            foreach (var e in entries) Row(e);
            EditorGUILayout.EndScrollView();

            var sel = entries.FirstOrDefault(e => e.Path == selectedPath);
            if (sel != null) Selected(sel);

            GUILayout.Space(4);
            using (new EditorGUILayout.HorizontalScope())
            {
                if (GUILayout.Button("Import another FBX…", EditorStyles.miniButton)) ImportFbx();
                if (GUILayout.Button("Help", EditorStyles.miniButton, GUILayout.Width(50))) Application.OpenURL(SiteUrl);
            }
            GUILayout.Space(4);
        }

        void Empty()
        {
            GUILayout.Space(12);
            EditorGUILayout.HelpBox("No Generato avatar in this project yet.\n\nOn generato.io, open your avatar's download menu and pick \"For VRChat on Quest\" (or \"For Unity and VRChat on PC\"). Then import the .fbx here.", MessageType.Info);
            GUILayout.Space(8);
            if (GUILayout.Button("Import Avatar FBX…", GUILayout.Height(34))) ImportFbx();
            GUILayout.Space(4);
            if (GUILayout.Button("Make an avatar on generato.io")) Application.OpenURL(SiteUrl);
        }

        void Row(Entry e)
        {
            var isSel = e.Path == selectedPath;
            var rect = EditorGUILayout.BeginHorizontal(EditorStyles.helpBox, GUILayout.Height(56));
            if (isSel) EditorGUI.DrawRect(rect, new Color(0.24f, 0.48f, 0.9f, 0.25f));
            var thumb = (Texture)Thumb(e) ?? AssetPreview.GetMiniThumbnail(e.Model);
            GUILayout.Label(thumb, GUILayout.Width(48), GUILayout.Height(48));
            using (new EditorGUILayout.VerticalScope())
            {
                GUILayout.Label(e.Name, rowName);
                GUILayout.Label($"{e.Kind} · {e.Triangles:N0} triangles", small);
                GUILayout.Label(Status(e), small);
            }
            EditorGUILayout.EndHorizontal();
            if (Event.current.type == EventType.MouseDown && rect.Contains(Event.current.mousePosition))
            {
                selectedPath = e.Path;
                if (Event.current.clickCount == 2) EditorGUIUtility.PingObject(e.Model);
                Event.current.Use();
                Repaint();
            }
        }

        // A small render of the avatar itself (Unity's own asset preview is often just a model icon).
        Texture2D Thumb(Entry e)
        {
            if (thumbs.TryGetValue(e.Path, out var t) && t) return t;
            t = RenderStill(e.Model, 96, 96, 20f);
            if (t) thumbs[e.Path] = t;
            return t;
        }

        static string Status(Entry e) =>
            e.Ready ? "✓ In the scene, ready for VRChat"
            : e.InScene.Length > 0 ? "In the scene, not set up yet"
            : "Not in the scene";

        void Selected(Entry e)
        {
            GUILayout.Space(6);
            // A preview you can drag to turn.
            var r = GUILayoutUtility.GetRect(10, 10000, 200, 10000, GUILayout.ExpandHeight(true));
            if (Stage(e.Model)) DrawStage(r);
            GUILayout.Label("Drag to turn the preview.", EditorStyles.centeredGreyMiniLabel);

            GUILayout.Space(4);
            var label = e.InScene.Length == 0 ? "Add to Scene and Set Up for VRChat"
                : e.Ready ? "Set Up Again"
                : "Set Up for VRChat";
            var old = GUI.backgroundColor;
            if (!e.Ready) GUI.backgroundColor = new Color(0.45f, 0.75f, 1f);
            if (GUILayout.Button(label, GUILayout.Height(36)))
            {
                var avatar = GeneratoVRChatSetup.SetUp(e.Path, dialog: false);
                Refresh();
                ShowNotification(new GUIContent(GeneratoVRChatSetup.HasVRChatSdk ? "Ready for VRChat" : "Added to the scene"));
                Selection.activeGameObject = avatar;
                GUIUtility.ExitGUI();
            }
            GUI.backgroundColor = old;

            if (e.InScene.Length > 0)
                using (new EditorGUILayout.HorizontalScope())
                {
                    if (GUILayout.Button("Select in Scene"))
                    {
                        Selection.activeGameObject = e.InScene[0];
                        SceneView.lastActiveSceneView?.FrameSelected();
                    }
                    if (e.Ready && GUILayout.Button("Open VRChat SDK"))
                        if (!EditorApplication.ExecuteMenuItem("VRChat SDK/Show Control Panel"))
                            EditorUtility.DisplayDialog("Generato", "Open VRChat SDK > Show Control Panel from the menu bar.", "OK");
                }

            if (e.Ready)
                EditorGUILayout.HelpBox("Next: VRChat SDK > Show Control Panel > Builder > Build & Publish. Pick Android to publish for Quest too.", MessageType.None);
        }

        void ImportFbx()
        {
            var src = EditorUtility.OpenFilePanel("Import Generato avatar", Downloads(), "fbx");
            if (string.IsNullOrEmpty(src)) return;
            var dest = AssetDatabase.GenerateUniqueAssetPath("Assets/" + Path.GetFileName(src));
            File.Copy(src, dest);
            AssetDatabase.ImportAsset(dest);
            // Renamed files still count as Generato avatars once imported through here.
            if (AssetImporter.GetAtPath(dest) is ModelImporter m && !GeneratoAvatarPostprocessor.IsGeneratoModel(dest))
            {
                GeneratoAvatarPostprocessor.ApplyModelSettings(m);
                m.SaveAndReimport();
            }
            selectedPath = dest;
            Refresh();
            Repaint();
        }

        static string Downloads()
        {
            var home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile);
            var d = Path.Combine(home, "Downloads");
            return Directory.Exists(d) ? d : home;
        }
    }
}
