#if UNITY_EDITOR
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using UnityEditor;
using UnityEditor.Animations;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
using UnityEngine.SceneManagement;
using nadena.dev.modular_avatar.core;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Dynamics.Constraint.Components;
using VRC.Dynamics;
using VRC.SDKBase.Editor.BuildPipeline;

namespace Sakana.PropTrack
{
    // Editor tooling only. No custom scripts are uploaded on the avatar.
    [InitializeOnLoad]
    public class SetupWindow : EditorWindow
    {
        static string ScenePath => SceneManager.GetActiveScene().path;
        const string RootName = "SakanaPropTrack";
        const string Product = "PropTrack 角色設定";
        static readonly string[] Keys = { "PX", "PY", "PZ", "RY", "RX", "RZ" };
        static readonly string[] Nodes = { "PositionX", "PositionY", "PositionZ", "RotationY", "RotationX", "RotationZ" };
        static readonly string[] Properties = { "m_LocalPosition.x", "m_LocalPosition.y", "m_LocalPosition.z", "localEulerAnglesRaw.y", "localEulerAnglesRaw.x", "localEulerAnglesRaw.z" };
        static readonly float[] Ranges = { 3, 3, 3, 180, 180, 180 };
        static double nextPoll;
        static bool queued;
        static string Bridge => Path.GetFullPath("Temp/SakanaPropTrack");
        string message = "選擇角色根物件 → 建立方塊 → 驗證並上傳。測試版。";

        [Serializable] class Request { public string command; public string avatar; public string backupDirectory; public string expectedScene; public string exportPath; }
        [Serializable] class AvatarInfo { public string name; public bool active; public string head; public int baseParameterBits; public float viewHeight; }
        [Serializable] class Report
        {
            public bool ok; public string command; public string scene; public string error;
            public AvatarInfo[] avatars; public string avatar; public string propPath;
            public string controller; public int addedParameterBits; public bool saved;
            public bool animationTestPassed; public bool visibilityTestPassed;
            public bool preprocessTestPassed; public int mergedParameterBits;
            public bool cacheCleaned; public long cacheFreedBytes;
            public string backup; public string[] testedParameters;
        }
        static SetupWindow() { EditorApplication.update += Poll; }

        [MenuItem("Tools/Sakana/PropTrack")]
        public static void Open() { var window = GetWindow<SetupWindow>(Product); window.minSize = new Vector2(470, 440); window.Show(); }

        void OnGUI()
        {
            GUILayout.Label(Product, EditorStyles.boldLabel);
            EditorGUILayout.HelpBox(message, MessageType.Info);
            EditorGUILayout.LabelField("目前場景", SceneManager.GetActiveScene().path);
            VRCAvatarDescriptor current = null;
            try { current = FindAvatar(null); } catch { }
            var picked = EditorGUILayout.ObjectField("角色根物件", current ? current.gameObject : null, typeof(GameObject), true) as GameObject;
            if (picked && (!current || picked != current.gameObject)) Selection.activeGameObject = picked;
            EditorGUILayout.HelpBox("需要 VRChat Avatars SDK 與 Modular Avatar。新增 49 bits 同步參數，容量以 SDK 最終合併結果為準。", MessageType.None);
            if (GUILayout.Button("建立測試方塊", GUILayout.Height(36)))
            {
                try { var result = Setup(new Request { command = "setup", backupDirectory = Path.GetFullPath("PropTrackBackups") }); Write(result); message = "測試方塊已準備好。"; }
                catch (Exception e) { message = e.Message; }
            }
            if (GUILayout.Button("選取追蹤方塊", GUILayout.Height(32)))
            {
                try
                {
                    var avatar = FindAvatar(null);
                    var root = avatar.transform.Find(RootName);
                    if (!root) throw new Exception("尚未建立方塊。");
                    Selection.activeGameObject = FindCube(root).gameObject;
                    SceneView.lastActiveSceneView?.FrameSelected();
                }
                catch (Exception e) { message = e.Message; }
            }
            if (GUILayout.Button("驗證動畫與顯示開關", GUILayout.Height(32)))
            {
                try { var report = Validate(null); message = "動畫與開關驗證通過；硬體及 VRChat 實測待完成。"; Write(report); }
                catch (Exception e) { message = e.Message; }
            }
            EditorGUILayout.Space();
            EditorGUILayout.LabelField("下一步", "啟動本機 PropTrack，選擇物件追蹤器。", EditorStyles.wordWrappedLabel);
            EditorGUILayout.HelpBox("更換劍模型：把劍模型放在 Cube 底下，保留 Cube 作為開關容器，並關閉其 Mesh Renderer。不要刪除 Position／Rotation 節點。", MessageType.None);
        }

        static void Poll()
        {
            if (queued || EditorApplication.isCompiling || EditorApplication.isUpdating || EditorApplication.isPlaying || EditorApplication.timeSinceStartup < nextPoll) return;
            nextPoll = EditorApplication.timeSinceStartup + 0.5;
            var file = Path.Combine(Bridge, "request.json");
            if (!File.Exists(file)) return;
            queued = true;
            EditorApplication.delayCall += () =>
            {
                Request request = null;
                try
                {
                    request = JsonUtility.FromJson<Request>(File.ReadAllText(file));
                    var previous = Path.Combine(Bridge, "last-request.json");
                    if (File.Exists(previous)) File.Replace(file, previous, null); else File.Move(file, previous);
                    Report report;
                    if (ScenePath != (string.IsNullOrEmpty(request.expectedScene) ? "Assets/5.unity" : request.expectedScene)) throw new Exception("Automation request does not match the open scene.");
                    switch (request.command)
                    {
                        case "inspect": report = Inspect(); break;
                        case "setup": report = Setup(request); break;
                        case "validate": report = Validate(request.avatar); break;
                        case "validate-build": report = ValidateBuild(request.avatar); break;
                        case "finalize": report = FinalizeScene(request.avatar); break;
                        case "export-installer":
                            if (string.IsNullOrWhiteSpace(request.exportPath)) throw new Exception("Export path required.");
                            AssetDatabase.ExportPackage("Assets/SakanaPropTrack/Editor/SakanaPropTrackSetup.cs", request.exportPath, ExportPackageOptions.Default);
                            report = new Report { ok = true, command = request.command, scene = ScenePath }; break;
                        case "guide-window":
                            Selection.activeGameObject = FindAvatar(request.avatar).gameObject;
                            Open(); report = new Report { ok = true, command = request.command, scene = ScenePath }; break;
                        case "guide-overview":
                            var overviewAvatar = FindAvatar(request.avatar);
                            Selection.activeGameObject = FindCube(overviewAvatar.transform.Find(RootName)).gameObject;
                            var guideView = SceneView.lastActiveSceneView;
                            if (guideView) { guideView.pivot = overviewAvatar.transform.position + Vector3.up * 0.8f; guideView.size = 1.05f; guideView.drawGizmos = false; guideView.Repaint(); }
                            report = new Report { ok = true, command = request.command, scene = ScenePath }; break;
                        case "guide-cube":
                            Selection.activeGameObject = FindCube(FindAvatar(request.avatar).transform.Find(RootName)).gameObject;
                            report = new Report { ok = true, command = request.command, scene = ScenePath }; break;
                        default: throw new Exception("Unknown PropTrack command.");
                    }
                    report.command = request.command;
                    Write(report);
                }
                catch (Exception e)
                {
                    Write(new Report { ok = false, command = request?.command, scene = SceneManager.GetActiveScene().path, error = e.ToString() });
                    Debug.LogError("[Sakana PropTrack] " + e.Message);
                }
                finally { queued = false; }
            };
        }

        static void Write(Report report)
        {
            Directory.CreateDirectory(Bridge);
            File.WriteAllText(Path.Combine(Bridge, "response.json"), JsonUtility.ToJson(report, true));
            Debug.Log("[Sakana PropTrack] " + (report.ok ? "OK: " : "FAILED: ") + report.command);
        }

        static VRCAvatarDescriptor[] Avatars()
        {
            var scene = SceneManager.GetActiveScene();
            if (!scene.IsValid() || string.IsNullOrEmpty(scene.path)) throw new Exception("請先儲存角色所在的場景，再建立方塊。");
            return scene.GetRootGameObjects().SelectMany(x => x.GetComponentsInChildren<VRCAvatarDescriptor>(true)).ToArray();
        }

        static Report Inspect()
        {
            return new Report { ok = true, command = "inspect", scene = SceneManager.GetActiveScene().path,
                avatars = Avatars().Select(a => new AvatarInfo { name = a.name, active = a.gameObject.activeInHierarchy,
                    head = a.GetComponent<Animator>()?.GetBoneTransform(HumanBodyBones.Head)?.name,
                    baseParameterBits = a.expressionParameters ? a.expressionParameters.CalcTotalCost() : 0,
                    viewHeight = a.ViewPosition.y }).ToArray() };
        }

        static VRCAvatarDescriptor FindAvatar(string name)
        {
            if (string.IsNullOrEmpty(name) && Selection.activeGameObject)
            {
                var selected = Selection.activeGameObject.GetComponentInParent<VRCAvatarDescriptor>(true);
                if (selected && selected.gameObject.scene == SceneManager.GetActiveScene()) return selected;
            }
            var avatars = Avatars().Where(a => string.IsNullOrEmpty(name) ? a.gameObject.activeInHierarchy : a.name == name).ToArray();
            if (avatars.Length != 1) throw new Exception("需要指定唯一角色；目前符合條件的角色數：" + avatars.Length);
            return avatars[0];
        }

        static Transform Child(Transform parent, string name)
        {
            var go = new GameObject(name);
            Undo.RegisterCreatedObjectUndo(go, "Add PropTrack node");
            go.transform.SetParent(parent, false);
            return go.transform;
        }

        static string PathOf(Transform item, Transform root) => AnimationUtility.CalculateTransformPath(item, root);
        static Transform FindCube(Transform root) => root.Find(string.Join("/", Nodes) + "/Cube");

        static Report Setup(Request request)
        {
            var avatar = FindAvatar(request.avatar);
            var scene = avatar.gameObject.scene;
            if (avatar.transform.Find(RootName)) return Validate(avatar.name);
            var head = avatar.GetComponent<Animator>()?.GetBoneTransform(HumanBodyBones.Head);
            if (!head) throw new Exception("找不到 Humanoid Head 骨骼。");
            if (string.IsNullOrEmpty(request.backupDirectory)) throw new Exception("Backup directory required.");
            Directory.CreateDirectory(request.backupDirectory);
            var backup = Path.Combine(request.backupDirectory, Path.GetFileNameWithoutExtension(scene.path) + "-before-proptrack-" + DateTime.Now.ToString("yyyyMMdd-HHmmss") + ".unity.gz");
            using (var source = File.OpenRead(scene.path))
            using (var target = File.Create(backup))
            using (var gzip = new GZipStream(target, System.IO.Compression.CompressionLevel.Optimal)) source.CopyTo(gzip);
            // Preserve any changes already in the editor instead of reopening the disk copy.
            if (scene.isDirty && !EditorSceneManager.SaveScene(scene)) throw new Exception("Could not save the current scene before setup.");
            Undo.IncrementCurrentGroup();
            int group = Undo.GetCurrentGroup();
            Undo.SetCurrentGroupName("Add Sakana PropTrack test cube");
            try
            {
                var propRoot = Child(avatar.transform, RootName);
                propRoot.position = avatar.transform.TransformPoint(avatar.ViewPosition);
                propRoot.rotation = avatar.transform.rotation;
                // Keep meshes out of the head hierarchy so first-person head hiding doesn't hide the prop.
                var constraint = Undo.AddComponent<VRCParentConstraint>(propRoot.gameObject);
                constraint.Sources.Add(new VRCConstraintSource(head, 1));
                constraint.GlobalWeight = 1;
                constraint.ActivateConstraint();
                constraint.ApplyConfigurationChanges();
                var axisNodes = new Transform[6];
                var parent = propRoot;
                for (int i = 0; i < Nodes.Length; i++) { axisNodes[i] = Child(parent, Nodes[i]); parent = axisNodes[i]; }
                axisNodes[0].localPosition = new Vector3(0.35f, 0, 0);
                axisNodes[1].localPosition = new Vector3(0, -0.2f, 0);
                axisNodes[2].localPosition = new Vector3(0, 0, 0.5f);
                var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
                cube.name = "Cube";
                Undo.RegisterCreatedObjectUndo(cube, "Add tracked cube");
                cube.transform.SetParent(parent, false);
                cube.transform.localScale = Vector3.one * 0.12f;
                UnityEngine.Object.DestroyImmediate(cube.GetComponent<BoxCollider>());
                var generated = "Assets/SakanaPropTrack/Generated/Setup-" + DateTime.Now.ToString("yyyyMMdd-HHmmss");
                Directory.CreateDirectory(generated);
                AssetDatabase.Refresh();
                var material = new Material(Shader.Find("Standard")) { name = "PropTrack Blue", color = new Color(0.25f, 0.65f, 1f) };
                AssetDatabase.CreateAsset(material, generated + "/Cube.mat");
                cube.GetComponent<MeshRenderer>().sharedMaterial = material;
                var controller = AnimatorController.CreateAnimatorControllerAtPath(generated + "/PropTrack.controller");
                controller.layers = new AnimatorControllerLayer[0];
                foreach (var key in Keys) controller.AddParameter("SPT/" + key, AnimatorControllerParameterType.Float);
                controller.AddParameter("SPT/Enabled", AnimatorControllerParameterType.Bool);
                for (int i = 0; i < Keys.Length; i++)
                {
                    controller.AddLayer("SPT " + Keys[i]);
                    var layers = controller.layers;
                    var layer = layers[layers.Length - 1]; layer.defaultWeight = 1;
                    controller.layers = layers;
                    var state = layer.stateMachine.AddState("Track " + Keys[i]);
                    state.writeDefaultValues = false;
                    var tree = new BlendTree { name = Keys[i], blendType = BlendTreeType.Simple1D, blendParameter = "SPT/" + Keys[i], useAutomaticThresholds = false };
                    AssetDatabase.AddObjectToAsset(tree, controller);
                    // Unity blends quaternion rotations, not raw Euler angles. Small intervals
                    // keep nlerp close to the intended angle (15 degrees per interval).
                    int steps = i < 3 ? 1 : 12;
                    for (int value = -steps; value <= steps; value++)
                    {
                        float threshold = value / (float)steps;
                        var clip = new AnimationClip { name = Keys[i] + "_" + value, frameRate = 60 };
                        AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve(PathOf(axisNodes[i], propRoot), typeof(Transform), Properties[i]), AnimationCurve.Constant(0, 1, threshold * Ranges[i]));
                        AssetDatabase.AddObjectToAsset(clip, controller);
                        tree.AddChild(clip, threshold);
                    }
                    state.motion = tree;
                    layer.stateMachine.defaultState = state;
                }
                controller.AddLayer("SPT Visibility");
                var visibilityLayers = controller.layers;
                var visibility = visibilityLayers[visibilityLayers.Length - 1]; visibility.defaultWeight = 1;
                controller.layers = visibilityLayers;
                AnimatorState hidden = null, shown = null;
                foreach (bool active in new[] { false, true })
                {
                    var state = visibility.stateMachine.AddState(active ? "Shown" : "Hidden");
                    state.writeDefaultValues = false;
                    var clip = new AnimationClip { name = active ? "Show Cube" : "Hide Cube" };
                    AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve(PathOf(cube.transform, propRoot), typeof(GameObject), "m_IsActive"), AnimationCurve.Constant(0, 1, active ? 1 : 0));
                    AssetDatabase.AddObjectToAsset(clip, controller);
                    state.motion = clip;
                    if (active) shown = state; else hidden = state;
                }
                visibility.stateMachine.defaultState = hidden;
                var showTransition = hidden.AddTransition(shown); showTransition.hasExitTime = false; showTransition.duration = 0;
                showTransition.AddCondition(AnimatorConditionMode.If, 0, "SPT/Enabled");
                var hideTransition = shown.AddTransition(hidden); hideTransition.hasExitTime = false; hideTransition.duration = 0;
                hideTransition.AddCondition(AnimatorConditionMode.IfNot, 0, "SPT/Enabled");
                var merge = Undo.AddComponent<ModularAvatarMergeAnimator>(propRoot.gameObject);
                merge.animator = controller; merge.layerType = VRCAvatarDescriptor.AnimLayerType.FX;
                merge.pathMode = MergeAnimatorPathMode.Relative;
                merge.relativePathRoot.Set(propRoot.gameObject);
                merge.matchAvatarWriteDefaults = true;
                var parameters = Undo.AddComponent<ModularAvatarParameters>(propRoot.gameObject);
                foreach (string key in Keys) parameters.parameters.Add(new ParameterConfig { nameOrPrefix = "SPT/" + key, syncType = ParameterSyncType.Float, localOnly = false, saved = false, hasExplicitDefaultValue = true, defaultValue = 0 });
                parameters.parameters.Add(new ParameterConfig { nameOrPrefix = "SPT/Enabled", syncType = ParameterSyncType.Bool, localOnly = false, saved = false, hasExplicitDefaultValue = true, defaultValue = 0 });
                AssetDatabase.SaveAssetIfDirty(controller);
                AssetDatabase.SaveAssetIfDirty(material);
                var report = Validate(avatar.name);
                if (!report.ok) throw new Exception(report.error);
                EditorSceneManager.MarkSceneDirty(scene);
                if (!EditorSceneManager.SaveScene(scene)) throw new Exception("Scene save failed.");
                report.saved = true; report.backup = backup;
                Undo.CollapseUndoOperations(group);
                Selection.activeGameObject = cube;
                return report;
            }
            catch { Undo.RevertAllDownToGroup(group); throw; }
        }

        static Report Validate(string name)
        {
            var avatar = FindAvatar(name);
            var root = avatar.transform.Find(RootName);
            if (!root) throw new Exception("找不到追蹤方塊。");
            var merge = root.GetComponent<ModularAvatarMergeAnimator>();
            if (!merge || !(merge.animator is AnimatorController controller)) throw new Exception("找不到追蹤動畫控制器。");
            var constraint = root.GetComponent<VRCParentConstraint>();
            if (!constraint || constraint.Sources.Count != 1 || !constraint.IsActive) throw new Exception("Head reference constraint is not active.");
            var expected = new Vector3(0.6f, -0.45f, 0.9f);
            var preview = EditorSceneManager.NewPreviewScene();
            GameObject copy = null;
            var graph = PlayableGraph.Create("Sakana PropTrack validation");
            try
            {
                copy = UnityEngine.Object.Instantiate(root.gameObject);
                SceneManager.MoveGameObjectToScene(copy, preview);
                copy.GetComponent<VRCParentConstraint>().enabled = false;
                copy.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
                copy.transform.localScale = Vector3.one;
                var animator = copy.AddComponent<Animator>();
                animator.cullingMode = AnimatorCullingMode.AlwaysAnimate;
                var playable = AnimatorControllerPlayable.Create(graph, controller);
                AnimationPlayableOutput.Create(graph, "PropTrack", animator).SetSourcePlayable(playable);
                graph.Play();
                graph.Evaluate(0.02f);
                if (FindCube(copy.transform).gameObject.activeSelf) throw new Exception("Default hidden state failed.");
                playable.SetFloat("SPT/PX", 0.2f); playable.SetFloat("SPT/PY", -0.15f); playable.SetFloat("SPT/PZ", 0.3f);
                playable.SetFloat("SPT/RX", 0.1f); playable.SetFloat("SPT/RY", -0.25f); playable.SetFloat("SPT/RZ", 0.05f);
                playable.SetBool("SPT/Enabled", true);
                for (int i = 0; i < 3; i++) graph.Evaluate(0.02f);
                var cube = FindCube(copy.transform);
                if (!cube.gameObject.activeSelf) throw new Exception("Enable animation failed.");
                if (Vector3.Distance(cube.position, expected) > 0.005f) throw new Exception("Position animation failed: " + cube.position);
                if (Quaternion.Angle(cube.rotation, Quaternion.Euler(18, -45, 9)) > 0.2f) throw new Exception("Rotation animation failed: " + cube.rotation.eulerAngles);
                playable.SetBool("SPT/Enabled", false);
                for (int i = 0; i < 3; i++) graph.Evaluate(0.02f);
                if (cube.gameObject.activeSelf) throw new Exception("Disable animation failed.");
                return new Report { ok = true, command = "validate", scene = ScenePath, avatar = avatar.name,
                    propPath = PathOf(root, avatar.transform), controller = AssetDatabase.GetAssetPath(controller),
                    addedParameterBits = 49, animationTestPassed = true, visibilityTestPassed = true,
                    testedParameters = new[] { "SPT/PX", "SPT/PY", "SPT/PZ", "SPT/RX", "SPT/RY", "SPT/RZ", "SPT/Enabled" } };
            }
            finally
            {
                if (graph.IsValid()) graph.Destroy();
                if (copy) UnityEngine.Object.DestroyImmediate(copy);
                EditorSceneManager.ClosePreviewScene(preview);
            }
        }

        static Report ValidateBuild(string name)
        {
            var avatar = FindAvatar(name);
            var preview = EditorSceneManager.NewPreviewScene();
            GameObject clone = null;
            try
            {
                clone = UnityEngine.Object.Instantiate(avatar.gameObject);
                SceneManager.MoveGameObjectToScene(clone, preview);
                clone.name = avatar.name + " (PropTrack validation only)";
                using (new nadena.dev.ndmf.OverrideTemporaryDirectoryScope(null))
                    if (!VRCBuildPipelineCallbacks.OnPreprocessAvatar(clone)) throw new Exception("Avatar preprocessing was rejected. Check the existing avatar build errors.");
                var descriptor = clone.GetComponent<VRCAvatarDescriptor>();
                if (!descriptor.expressionParameters) throw new Exception("Merged expression parameters are missing.");
                int cost = descriptor.expressionParameters.CalcTotalCost();
                foreach (var key in new[] { "PX", "PY", "PZ", "RX", "RY", "RZ", "Enabled" })
                {
                    var parameter = descriptor.expressionParameters.parameters.FirstOrDefault(p => p.name == "SPT/" + key);
                    if (parameter == null || !parameter.networkSynced) throw new Exception("Synced parameter missing: SPT/" + key);
                }
                if (cost > 256) throw new Exception("The merged avatar uses " + cost + " parameter bits, exceeding 256.");
                return new Report { ok = true, command = "validate-build", scene = ScenePath, avatar = avatar.name,
                    addedParameterBits = 49, mergedParameterBits = cost, preprocessTestPassed = true };
            }
            finally
            {
                if (clone) UnityEngine.Object.DestroyImmediate(clone);
                EditorSceneManager.ClosePreviewScene(preview);
            }
        }

        static Report FinalizeScene(string name)
        {
            var avatar = FindAvatar(name);
            var root = avatar.transform.Find(RootName);
            if (!root || !FindCube(root)) throw new Exception("The source cube is missing.");
            if (!EditorSceneManager.SaveScene(avatar.gameObject.scene)) throw new Exception("Scene save failed.");
            Selection.activeGameObject = FindCube(root).gameObject;
            return new Report { ok = true, command = "finalize", scene = ScenePath, avatar = avatar.name,
                saved = true, addedParameterBits = 49 };
        }
    }
}
#endif
