using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;

namespace Mocapless
{
    /// <summary>Window > Mocapless: pick a character, type what it does, get the animated FBX in the project.</summary>
    public class MocaplessWindow : EditorWindow
    {
        const string KeyPref = "Mocapless.ApiKey";
        const string LoopPref = "Mocapless.Loop";
        const string ControllerPref = "Mocapless.Controller";
        static readonly string[] Uploadable = { ".fbx", ".glb", ".gltf", ".dae", ".zip" };

        static readonly Dictionary<string, string[]> Examples = new Dictionary<string, string[]>
        {
            ["quadruped"] = new[] { "walks forward, stops, looks around, then lies down", "trots ahead, then gallops off", "eats grass, then looks up", "runs, jumps, and keeps running" },
            ["humanoid"] = new[] { "walks forward, then waves", "runs, then jumps", "sits down", "walks forward slowly, then turns around" },
            ["bird"] = new[] { "walks forward, then takes off and flies", "flies forward, then lands" },
            ["frog"] = new[] { "hops forward, then croaks", "sits still, then snaps at a fly" },
        };

        string _key = "";
        Me _me;
        List<Character> _characters = new List<Character>();
        int _pick;
        string _prompt = "";
        Plan _plan;
        Job _job;
        MocaplessImport.Result _imported;
        string _status = "";
        string _error = "";
        bool _busy;
        bool _loop;
        bool _controller = true;
        bool _options;
        Vector2 _scroll;

        [MenuItem("Window/Mocapless")]
        public static void Open()
        {
            var w = GetWindow<MocaplessWindow>("Mocapless");
            w.minSize = new Vector2(360, 420);
        }

        void OnEnable()
        {
            _key = EditorPrefs.GetString(KeyPref, "");
            _loop = EditorPrefs.GetBool(LoopPref, false);
            _controller = EditorPrefs.GetBool(ControllerPref, true);
            if (!string.IsNullOrEmpty(_key)) Run(Connect);
        }

        void OnSelectionChange() => Repaint();

        Character Current => _pick >= 0 && _pick < _characters.Count ? _characters[_pick] : null;

        // ------------------------------------------------------------------ UI

        void OnGUI()
        {
            _scroll = EditorGUILayout.BeginScrollView(_scroll);
            EditorGUILayout.Space(6);
            EditorGUILayout.LabelField("Mocapless", EditorStyles.boldLabel);
            EditorGUILayout.LabelField("Rig and animate a character from a sentence.", EditorStyles.wordWrappedMiniLabel);
            EditorGUILayout.Space(8);

            DrawAccount();
            if (_me != null)
            {
                EditorGUILayout.Space(10);
                DrawCharacter();
                EditorGUILayout.Space(10);
                DrawMotion();
                EditorGUILayout.Space(10);
                DrawResult();
            }

            if (!string.IsNullOrEmpty(_error))
            {
                EditorGUILayout.Space(6);
                EditorGUILayout.HelpBox(_error, MessageType.Error);
            }
            if (!string.IsNullOrEmpty(_status))
            {
                EditorGUILayout.Space(4);
                EditorGUILayout.HelpBox(_status, MessageType.None);
            }
            EditorGUILayout.EndScrollView();
        }

        void DrawAccount()
        {
            EditorGUILayout.LabelField("Account", EditorStyles.boldLabel);
            if (_me == null)
            {
                EditorGUILayout.LabelField("Paste an API key from your Mocapless account. It stays on this computer.", EditorStyles.wordWrappedMiniLabel);
                _key = EditorGUILayout.PasswordField("API key", _key);
                using (new EditorGUILayout.HorizontalScope())
                {
                    using (new EditorGUI.DisabledScope(_busy || string.IsNullOrWhiteSpace(_key)))
                        if (GUILayout.Button("Connect")) Run(Connect);
                    if (GUILayout.Button("Get an API key")) Application.OpenURL(MocaplessApi.Site + "/account");
                }
                return;
            }
            using (new EditorGUILayout.HorizontalScope())
            {
                EditorGUILayout.LabelField($"{_me.email} · {_me.credits} credits", EditorStyles.wordWrappedLabel);
                if (GUILayout.Button("Top up", GUILayout.Width(64))) Application.OpenURL(MocaplessApi.Site + "/credits");
                if (GUILayout.Button("Sign out", GUILayout.Width(70)))
                {
                    EditorPrefs.DeleteKey(KeyPref);
                    _key = "";
                    _me = null;
                    _characters.Clear();
                    _job = null;
                    _imported = null;
                }
            }
        }

        void DrawCharacter()
        {
            EditorGUILayout.LabelField("Character", EditorStyles.boldLabel);
            var ready = _characters.Where(c => c.status == "ready").ToList();
            using (new EditorGUILayout.HorizontalScope())
            {
                if (ready.Count > 0)
                {
                    var names = ready.Select(c => $"{c.filename}  ({Family(c)})").ToArray();
                    var index = Math.Max(0, ready.IndexOf(Current));
                    var picked = EditorGUILayout.Popup(index, names);
                    var chosen = ready[picked];
                    if (chosen != Current)
                    {
                        _pick = _characters.IndexOf(chosen);
                        _plan = null;
                    }
                }
                else EditorGUILayout.LabelField("No characters yet. Upload one below.", EditorStyles.wordWrappedMiniLabel);
                using (new EditorGUI.DisabledScope(_busy))
                    if (GUILayout.Button("Refresh", GUILayout.Width(64))) Run(Refresh);
            }

            var path = SelectedModelPath();
            using (new EditorGUI.DisabledScope(_busy || path == null))
            {
                var label = path == null ? "Select a model in the Project window to upload it" : $"Upload {Path.GetFileName(path)}";
                if (GUILayout.Button(label)) Run(() => UploadAndRig(path));
            }
            EditorGUILayout.LabelField("FBX, GLB, GLTF, DAE or a zip, with bones or without. Animals on four legs, people, birds.", EditorStyles.wordWrappedMiniLabel);
        }

        void DrawMotion()
        {
            var character = Current;
            EditorGUILayout.LabelField("What it does", EditorStyles.boldLabel);
            EditorGUI.BeginChangeCheck();
            _prompt = EditorGUILayout.TextArea(_prompt, EditorStyles.textArea, GUILayout.MinHeight(44));
            if (EditorGUI.EndChangeCheck()) _plan = null;

            var family = character != null ? Family(character) : "quadruped";
            if (Examples.TryGetValue(family, out var examples))
            {
                foreach (var e in examples)
                    if (GUILayout.Button(e, EditorStyles.miniButton)) { _prompt = e; _plan = null; GUI.FocusControl(null); }
            }

            if (_plan != null)
            {
                var beats = string.Join(" · ", _plan.script.segments.Select(s => $"{s.primitive.Replace("quad_", "").Replace('_', ' ')} {s.seconds:0.#} s"));
                EditorGUILayout.HelpBox($"{beats}\n{_plan.cost} credits (you have {_plan.balance})"
                                        + (_plan.notes.Count > 0 ? "\n" + string.Join("\n", _plan.notes) : ""), _plan.affordable ? MessageType.Info : MessageType.Warning);
            }

            _options = EditorGUILayout.Foldout(_options, "Import options", true);
            if (_options)
            {
                EditorGUI.indentLevel++;
                var loop = EditorGUILayout.Toggle("Loop the clip", _loop);
                var ctrl = EditorGUILayout.Toggle("Make an Animator Controller", _controller);
                if (loop != _loop) EditorPrefs.SetBool(LoopPref, _loop = loop);
                if (ctrl != _controller) EditorPrefs.SetBool(ControllerPref, _controller = ctrl);
                EditorGUI.indentLevel--;
            }

            using (new EditorGUILayout.HorizontalScope())
            using (new EditorGUI.DisabledScope(_busy || character == null || string.IsNullOrWhiteSpace(_prompt)))
            {
                if (GUILayout.Button("Check the price")) Run(CheckPrice);
                if (GUILayout.Button("Make the animation", GUILayout.Height(24))) Run(Make);
            }

            if (_busy && _job != null)
            {
                var r = EditorGUILayout.GetControlRect(false, 18);
                EditorGUI.ProgressBar(r, Mathf.Clamp01(_job.progress), Stage(_job));
            }
        }

        void DrawResult()
        {
            if (_job == null || _job.status != "succeeded") return;
            EditorGUILayout.LabelField("Result", EditorStyles.boldLabel);
            var fbx = _job.Find("fbx");
            if (fbx != null && fbx.locked)
            {
                EditorGUILayout.HelpBox("Your first two downloads are free. This one needs the character unlocked: $9, once, and every animation of it downloads, plus a full set of moves.", MessageType.Info);
                using (new EditorGUILayout.HorizontalScope())
                {
                    if (GUILayout.Button("Unlock on mocapless.com")) Application.OpenURL($"{MocaplessApi.Site}/app/animals/{_job.assetId}");
                    using (new EditorGUI.DisabledScope(_busy))
                        if (GUILayout.Button("Check again")) Run(Fetch);
                }
                return;
            }
            if (_imported == null) return;
            EditorGUILayout.LabelField(_imported.FbxPath, EditorStyles.wordWrappedMiniLabel);
            if (!string.IsNullOrEmpty(_imported.Note)) EditorGUILayout.HelpBox(_imported.Note, MessageType.Warning);
            using (new EditorGUILayout.HorizontalScope())
            {
                if (GUILayout.Button("Show in Project"))
                {
                    var obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(_imported.FbxPath);
                    Selection.activeObject = obj;
                    EditorGUIUtility.PingObject(obj);
                }
                if (GUILayout.Button("Add to scene")) MocaplessImport.AddToScene(_imported);
                if (GUILayout.Button("Watch online")) Application.OpenURL($"{MocaplessApi.Site}/app/animate?job={_job.id}");
            }
        }

        // ------------------------------------------------------------- actions

        async void Run(Func<Task> action)
        {
            if (_busy) return;
            _busy = true;
            _error = "";
            Repaint();
            try { await action(); }
            catch (MocaplessException e) { _error = e.Message; if (e.Status == 401) _me = null; }
            catch (Exception e) { _error = "Something went wrong: " + e.Message; Debug.LogException(e); }
            finally
            {
                _busy = false;
                if (this != null) Repaint();
            }
        }

        async Task Connect()
        {
            _status = "Connecting…";
            _me = await MocaplessApi.GetMe(_key.Trim());
            _key = _key.Trim();
            EditorPrefs.SetString(KeyPref, _key);
            await Refresh();
            _status = "";
        }

        async Task Refresh()
        {
            var keep = Current?.id;
            _characters = await MocaplessApi.ListCharacters(_key);
            _pick = Math.Max(0, _characters.FindIndex(c => c.id == keep));
            if (keep == null) _pick = Math.Max(0, _characters.FindIndex(c => c.status == "ready"));
            _me = await MocaplessApi.GetMe(_key);
        }

        async Task UploadAndRig(string assetPath)
        {
            var full = Path.GetFullPath(assetPath);
            _status = $"Uploading {Path.GetFileName(full)}…";
            Repaint();
            var character = await MocaplessApi.Upload(_key, full);
            _status = "Building its skeleton. About a minute.";
            Repaint();
            while (character.status == "pending" || character.status == "processing")
            {
                await Task.Delay(3000);
                if (this == null) return;
                character = await MocaplessApi.GetCharacter(_key, character.id);
            }
            await Refresh();
            _pick = Math.Max(0, _characters.FindIndex(c => c.id == character.id));
            if (character.status == "ready")
                _status = $"{character.filename} is rigged as {Family(character)}. Now say what it does.";
            else
            {
                _status = "";
                _error = character.error ?? "We could not rig that model.";
            }
        }

        async Task CheckPrice()
        {
            _plan = await MocaplessApi.MakePlan(_key, _prompt.Trim(), Current.id);
        }

        async Task Make()
        {
            _imported = null;
            _status = "";
            _job = await MocaplessApi.Generate(_key, _prompt.Trim(), Current.id);
            Repaint();
            while (_job.status == "queued" || _job.status == "running")
            {
                await Task.Delay(2500);
                if (this == null) return;
                _job = await MocaplessApi.GetJob(_key, _job.id);
                Repaint();
            }
            if (_job.status != "succeeded")
            {
                _error = (_job.error ?? "The animation failed.") + (_job.creditsRefunded ? " Your credits were refunded." : "");
                return;
            }
            _me = await MocaplessApi.GetMe(_key);
            if (!string.IsNullOrEmpty(_job.fallbackFrom))
                _status = "Made with presets: the motion model was busy. Nothing extra was charged.";
            await Fetch();
        }

        async Task Fetch()
        {
            _job = await MocaplessApi.GetJob(_key, _job.id);
            var fbx = _job.Find("fbx");
            if (fbx == null || fbx.locked || string.IsNullOrEmpty(fbx.url)) return;
            var character = _characters.FirstOrDefault(c => c.id == _job.assetId) ?? Current;
            var path = MocaplessImport.PathFor(character?.filename, _job.prompt);
            _status = "Downloading…";
            Repaint();
            await MocaplessApi.Download(fbx.url, Path.GetFullPath(path));
            _imported = MocaplessImport.Setup(path, character != null ? Family(character) : "quadruped", _job.prompt, _loop, _controller);
            _status = $"Ready: {_job.durationSeconds:0.#} s, {_job.creditsCost} credits.";
            var obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
            EditorGUIUtility.PingObject(obj);
        }

        // ------------------------------------------------------------- helpers

        static string Family(Character c) => string.IsNullOrEmpty(c.family) ? "humanoid" : c.family;

        static string Stage(Job job)
        {
            switch (job.stage)
            {
                case "queued": return "Waiting for a worker";
                case "starting":
                case "preparing": return "Reading the sentence";
                case "warming": return "The motion model is waking up (about two minutes, once)";
                case "generating": return "Performing the motion";
                case "retargeting": return "Fitting it to your character";
                case "exporting": return "Making the files";
                default: return string.IsNullOrEmpty(job.stage) ? "Working" : char.ToUpper(job.stage[0]) + job.stage.Substring(1);
            }
        }

        static string SelectedModelPath()
        {
            var obj = Selection.activeObject;
            if (obj == null) return null;
            var path = AssetDatabase.GetAssetPath(obj);
            if (string.IsNullOrEmpty(path) && obj is GameObject go)
                path = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(go);
            if (string.IsNullOrEmpty(path)) return null;
            return Uploadable.Contains(Path.GetExtension(path).ToLowerInvariant()) ? path : null;
        }
    }
}
