// Generato Avatar Tools: imports Generato avatar FBX files ready for VRChat (PC and Quest).
//
// 1. Automatically, when a file named generato-avatar-*.fbx is imported: Humanoid rig, Read/Write
//    on, legacy blendshape normals (the VRChat SDK's checks), looping clips; textures it extracts get
//    streaming mip maps and a Kaiser mip filter.
// 2. Tools > Generato > Avatars opens a window that lists every Generato avatar in the project with a
//    preview; one button adds it to the scene and sets it up. It opens by itself when a Generato FBX
//    is imported. The set-up (also Tools > Generato > Set Up Avatar for VRChat): extracts the
//    texture, makes a "VRChat/Mobile/Toon Lit" material (allowed on PC and Quest, and it shows the
//    photo's baked-in lighting as captured), places the avatar in the open scene and adds a VRC
//    Avatar Descriptor with the view position measured between the eyes.
//
// Then: VRChat SDK > Show Control Panel > Builder > Build & Publish (switch the build target to
// Android for Quest). https://generato.io/vrchat-avatar
using System;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
#if VRC_SDK_VRCSDK3
using VRC.SDK3.Avatars.Components;
using VRC.SDKBase;
#endif

namespace Generato.AvatarTools
{
    public class GeneratoAvatarPostprocessor : AssetPostprocessor
    {
        const string Marker = "generato-avatar-tools:1";
        // Clips that should loop when played on their own (the one-shots hold their last frame).
        static readonly string[] OneShots = { "A-Pose" };

        internal static bool IsGeneratoModel(string path)
        {
            if (string.IsNullOrEmpty(path) || !path.EndsWith(".fbx", StringComparison.OrdinalIgnoreCase)) return false;
            if (Path.GetFileName(path).StartsWith("generato-avatar", StringComparison.OrdinalIgnoreCase)) return true;
            // A renamed file that was imported as a Generato avatar keeps the marker.
            return AssetImporter.GetAtPath(path) is ModelImporter m && m.userData == Marker;
        }

        void OnPreprocessModel()
        {
            if (!IsGeneratoModel(assetPath)) return;
            var importer = (ModelImporter)assetImporter;
            if (importer.userData == Marker) return; // set once: later changes by the user stay
            ApplyModelSettings(importer);
            JustImported = assetPath; // first import: show it in the Generato window
        }

        static string JustImported;

        static void OnPostprocessAllAssets(string[] imported, string[] deleted, string[] moved, string[] movedFrom)
        {
            if (JustImported == null) return;
            var path = JustImported;
            JustImported = null;
            if (Application.isBatchMode) return;
            EditorApplication.delayCall += () => GeneratoAvatarWindow.Open(path);
        }

        void OnPreprocessAnimation()
        {
            if (!IsGeneratoModel(assetPath)) return;
            var importer = (ModelImporter)assetImporter;
            if (importer.clipAnimations != null && importer.clipAnimations.Length > 0) return;
            var clips = importer.defaultClipAnimations;
            if (clips == null || clips.Length == 0) return;
            foreach (var clip in clips)
            {
                var loop = !OneShots.Contains(clip.name);
                clip.loopTime = loop;
                clip.loopPose = loop;
                clip.lockRootRotation = true;
                clip.lockRootHeightY = false;
                clip.keepOriginalPositionY = true;
                clip.keepOriginalOrientation = true;
            }
            importer.clipAnimations = clips;
        }

        void OnPreprocessTexture()
        {
            if (!assetPath.Replace('\\', '/').StartsWith(GeneratoVRChatSetup.Root + "/")) return;
            ApplyTextureSettings((TextureImporter)assetImporter);
        }

        internal static void ApplyModelSettings(ModelImporter importer)
        {
            importer.animationType = ModelImporterAnimationType.Human;
            importer.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel;
            importer.isReadable = true;
            importer.importBlendShapes = false;
            importer.importBlendShapeNormals = ModelImporterNormals.None;
            importer.materialImportMode = ModelImporterMaterialImportMode.ImportStandard;
            var so = new SerializedObject(importer);
            var legacy = so.FindProperty("legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes");
            if (legacy != null)
            {
                legacy.boolValue = true;
                so.ApplyModifiedPropertiesWithoutUndo();
            }
            importer.userData = Marker;
        }

        internal static void ApplyTextureSettings(TextureImporter importer)
        {
            importer.mipmapEnabled = true;
            importer.streamingMipmaps = true;
            importer.mipmapFilter = TextureImporterMipFilter.KaiserFilter;
        }
    }

    public static class GeneratoVRChatSetup
    {
        internal const string Root = "Assets/Generato";
        const string MenuTools = "Tools/Generato/Set Up Avatar for VRChat";
        const string MenuAssets = "Assets/Generato/Set Up Avatar for VRChat";

        [MenuItem(MenuAssets, true)]
        static bool CanSetUp() => SelectedModelPath() != null;

        // Always enabled: with a Generato avatar selected (the FBX or the avatar in the scene) it sets
        // that one up; otherwise it opens the Generato window to pick one.
        [MenuItem(MenuTools, false, 10)]
        [MenuItem(MenuAssets, false, 1000)]
        static void SetUpSelected()
        {
            var path = SelectedModelPath();
            if (path != null) SetUp(path);
            else GeneratoAvatarWindow.Open();
        }

        static string SelectedModelPath()
        {
            var obj = Selection.activeObject;
            if (!obj) return null;
            var path = AssetDatabase.GetAssetPath(obj);
            if (string.IsNullOrEmpty(path) && obj is GameObject go && PrefabUtility.IsPartOfPrefabInstance(go))
                path = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(go);
            return GeneratoAvatarPostprocessor.IsGeneratoModel(path) ? path : null;
        }

        /// <summary>The avatars from this FBX that are in the open scenes.</summary>
        public static GameObject[] FindInScene(string fbxPath) =>
            UnityEngine.Object.FindObjectsOfType<Animator>(true)
                .Select(a => a.gameObject)
                .Where(g => PrefabUtility.IsOutermostPrefabInstanceRoot(g)
                    && PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(g) == fbxPath)
                .ToArray();

        public static bool HasDescriptor(GameObject avatar)
        {
#if VRC_SDK_VRCSDK3
            return avatar && avatar.GetComponent<VRCAvatarDescriptor>() != null;
#else
            return false;
#endif
        }

        public static readonly bool HasVRChatSdk =
#if VRC_SDK_VRCSDK3
            true;
#else
            false;
#endif

        /// <summary>Sets up one Generato FBX for VRChat and returns the avatar in the scene: the one
        /// already there, or a new one at the origin.</summary>
        public static GameObject SetUp(string fbxPath, bool dialog = true)
        {
            var importer = (ModelImporter)AssetImporter.GetAtPath(fbxPath);
            var name = Safe(Path.GetFileNameWithoutExtension(fbxPath));
            var folder = $"{Root}/{name}";
            EnsureFolder(folder);
            GeneratoAvatarPostprocessor.ApplyModelSettings(importer);
            importer.SaveAndReimport();

            // The texture is embedded in the FBX: extract it next to the material.
            importer.ExtractTextures(folder);
            AssetDatabase.Refresh();
            var texture = AssetDatabase.FindAssets("t:Texture2D", new[] { folder })
                .Select(AssetDatabase.GUIDToAssetPath)
                .Select(AssetDatabase.LoadAssetAtPath<Texture2D>)
                .FirstOrDefault(t => t != null);
            if (texture != null)
            {
                var texImporter = (TextureImporter)AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(texture));
                GeneratoAvatarPostprocessor.ApplyTextureSettings(texImporter);
                texImporter.SaveAndReimport();
            }

            // One material for PC and Quest: VRChat's mobile Toon Lit shader is allowed on both, and it
            // shows the texture (which already holds the photo's lighting) as captured.
            var shader = Shader.Find("VRChat/Mobile/Toon Lit") ?? Shader.Find("Unlit/Texture") ?? Shader.Find("Standard");
            var materialPath = $"{folder}/{name}.mat";
            var material = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
            if (material == null)
            {
                material = new Material(shader);
                AssetDatabase.CreateAsset(material, materialPath);
            }
            material.shader = shader;
            material.mainTexture = texture;
            EditorUtility.SetDirty(material);
            foreach (var embedded in AssetDatabase.LoadAllAssetsAtPath(fbxPath).OfType<Material>())
                importer.AddRemap(new AssetImporter.SourceAssetIdentifier(typeof(Material), embedded.name), material);
            importer.SaveAndReimport();
            AssetDatabase.SaveAssets();

            var avatar = FindInScene(fbxPath).FirstOrDefault();
            if (avatar == null)
            {
                var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(fbxPath);
                avatar = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
                Undo.RegisterCreatedObjectUndo(avatar, "Add Generato Avatar");
                avatar.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
            }
            avatar.SetActive(true);

#if VRC_SDK_VRCSDK3
            var descriptor = avatar.GetComponent<VRCAvatarDescriptor>();
            if (descriptor == null) descriptor = Undo.AddComponent<VRCAvatarDescriptor>(avatar);
            Undo.RecordObject(descriptor, "Set Up Generato Avatar");
            descriptor.ViewPosition = EstimateViewPosition(avatar);
            descriptor.lipSync = VRC_AvatarDescriptor.LipSyncStyle.Default;
            EditorUtility.SetDirty(descriptor);
            var report = "Ready for VRChat. Open VRChat SDK > Show Control Panel > Builder and Build & Publish.\n"
                + "For Quest: switch the build target to Android first (File > Build Settings).";
#else
            var report = "Imported and placed in the scene. Add the VRChat SDK (Creator Companion) to upload it to VRChat.";
#endif
            Selection.activeGameObject = avatar;
            SceneView.lastActiveSceneView?.FrameSelected();
            UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(avatar.scene);
            Debug.Log($"[Generato] {name}: {report}", avatar);
            if (dialog && !Application.isBatchMode) EditorUtility.DisplayDialog("Generato avatar", report, "OK");
            return avatar;
        }

        /// <summary>Between the eyes, measured on the avatar: eye height below the top of the head, just
        /// behind the most forward point of the face at that height (avatars face +Z).</summary>
        internal static Vector3 EstimateViewPosition(GameObject avatar)
        {
            var renderer = avatar.GetComponentInChildren<SkinnedMeshRenderer>();
            var animator = avatar.GetComponent<Animator>();
            var head = animator != null && animator.isHuman ? animator.GetBoneTransform(HumanBodyBones.Head) : null;
            if (renderer == null || head == null) return new Vector3(0f, 1.6f, 0.08f);

            var baked = new Mesh();
            renderer.BakeMesh(baked, true);
            var toWorld = renderer.transform.localToWorldMatrix;
            var points = baked.vertices.Select(v => toWorld.MultiplyPoint3x4(v)).ToArray();
            UnityEngine.Object.DestroyImmediate(baked);

            var h = head.position;
            var height = points.Max(p => p.y) - points.Min(p => p.y);
            var k = height / 1.7f; // proportions measured on a 1.7 m adult
            var headPoints = points.Where(p => p.y > h.y - 0.02f * k && Mathf.Abs(p.x - h.x) < 0.12f * k).ToArray();
            if (headPoints.Length == 0) return avatar.transform.InverseTransformPoint(h + new Vector3(0f, 0.1f * k, 0.08f * k));
            var top = headPoints.Max(p => p.y);
            var eyeY = top - 0.115f * k;
            var band = headPoints.Where(p => Mathf.Abs(p.y - eyeY) < 0.02f * k && Mathf.Abs(p.x - h.x) < 0.05f * k).ToArray();
            var front = band.Length > 0 ? band.Max(p => p.z) : h.z + 0.1f * k;
            return avatar.transform.InverseTransformPoint(new Vector3(h.x, eyeY, front - 0.02f * k));
        }

        static void EnsureFolder(string folder)
        {
            var parts = folder.Split('/');
            var current = parts[0];
            for (var i = 1; i < parts.Length; i++)
            {
                var next = $"{current}/{parts[i]}";
                if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(current, parts[i]);
                current = next;
            }
        }

        static string Safe(string name) => new string(name.Select(c => char.IsLetterOrDigit(c) || c == '-' || c == '_' ? c : '_').ToArray());
    }
}
