using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using UnityEditor.AssetImporters;
using UnityEngine;
using WebP;

namespace Woop.Unity.Editor
{
    [ScriptedImporter(7, "woop", AllowCaching = true)]
    public sealed class WoopImporter : ScriptedImporter
    {
        [SerializeField, Min(1f)] private float pixelsPerUnit = 100f;
        [SerializeField, Min(0f)] private float moveSpeed = 4f;
        [SerializeField] private FilterMode textureFilter = FilterMode.Bilinear;
        [SerializeField] private bool createCollider = true;

        public override void OnImportAsset(AssetImportContext context)
        {
            using FileStream stream = File.OpenRead(Path.GetFullPath(context.assetPath));
            using var archive = new ZipArchive(stream, ZipArchiveMode.Read);

            ZipArchiveEntry manifestEntry = FindEntry(archive, "woop.json")
                ?? throw new InvalidDataException("Woop 包内缺少 woop.json。");

            WoopManifest manifest = JsonUtility.FromJson<WoopManifest>(ReadText(manifestEntry));
            ValidateManifest(manifest);

            string characterName = Path.GetFileNameWithoutExtension(context.assetPath);
            var root = new GameObject(characterName);
            var visual = new GameObject("Visual");
            visual.transform.SetParent(root.transform, false);

            var metadataParts = new List<WoopRigMetadata.PartInfo>();
            var importedPartTransforms = new Dictionary<string, Transform>(StringComparer.Ordinal);

            foreach (WoopPart part in manifest.parts.OrderBy(item => item.rest.zIndex))
            {
                ZipArchiveEntry imageEntry = FindPartImage(archive, part.id)
                    ?? throw new InvalidDataException(
                        $"Woop 包内找不到部件 {part.id} 的图片。\n"
                        + $"支持 PNG、JPG 和 WebP；预期位于 parts/{part.id}/ 目录。");

                byte[] imageBytes = ReadBytes(imageEntry);
                Texture2D texture = DecodeTexture(imageBytes, imageEntry.FullName, part.id);
                texture.name = $"{part.id}_Texture";
                texture.filterMode = textureFilter;
                texture.wrapMode = TextureWrapMode.Clamp;

                var sprite = Sprite.Create(
                    texture,
                    new Rect(0f, 0f, texture.width, texture.height),
                    new Vector2(0.5f, 0.5f),
                    pixelsPerUnit,
                    0,
                    SpriteMeshType.FullRect);
                sprite.name = $"{part.id}_Sprite";

                context.AddObjectToAsset($"texture_{part.id}", texture);
                context.AddObjectToAsset($"sprite_{part.id}", sprite);

                var partObject = new GameObject(part.id);
                Transform partTransform = partObject.transform;
                partTransform.SetParent(visual.transform, false);
                ApplyRestTransform(partTransform, part, manifest.sourceSheet, pixelsPerUnit);

                var renderer = partObject.AddComponent<SpriteRenderer>();
                renderer.sprite = sprite;
                renderer.sortingOrder = part.rest.zIndex;

                importedPartTransforms.Add(part.id, partTransform);
                metadataParts.Add(new WoopRigMetadata.PartInfo
                {
                    id = part.id,
                    sourceWidth = part.width,
                    sourceHeight = part.height,
                    zIndex = part.rest.zIndex,
                    sceneTransform = partTransform
                });
            }

            WoopRigRoles rigRoles = CreateRigRoles(manifest, importedPartTransforms);

            CreateCutoutRig(
                root,
                visual.transform,
                manifest,
                importedPartTransforms,
                pixelsPerUnit);
            root.AddComponent<WoopRigMotion2D>().Configure(rigRoles);

            WoopRigMetadata.JointInfo[] metadataJoints = (manifest.joints ?? Array.Empty<WoopJoint>())
                .Select(joint => new WoopRigMetadata.JointInfo
                {
                    id = joint.id,
                    parentPartId = joint.parent?.partId,
                    parentAnchorId = joint.parent?.anchorId,
                    childPartId = joint.child?.partId,
                    childAnchorId = joint.child?.anchorId
                })
                .ToArray();

            var metadata = root.AddComponent<WoopRigMetadata>();
            metadata.Configure(
                manifest.schemaVersion,
                manifest.projectId,
                new Vector2Int(manifest.sourceSheet.width, manifest.sourceSheet.height),
                metadataParts.ToArray(),
                metadataJoints,
                rigRoles);

            var body = root.AddComponent<Rigidbody2D>();
            body.gravityScale = 0f;
            body.freezeRotation = true;
            body.interpolation = RigidbodyInterpolation2D.Interpolate;

            var controller = root.AddComponent<WoopCharacterController2D>();
            controller.Configure(visual.transform, moveSpeed);

            if (createCollider)
                AddCharacterCollider(manifest, importedPartTransforms, pixelsPerUnit);

            context.AddObjectToAsset("WoopCharacter", root);
            context.SetMainObject(root);
        }

        private static WoopCutoutRig2D CreateCutoutRig(
            GameObject root,
            Transform visual,
            WoopManifest manifest,
            IReadOnlyDictionary<string, Transform> partTransforms,
            float ppu)
        {
            var rigObject = new GameObject("Rig");
            Transform rigRoot = rigObject.transform;
            rigRoot.SetParent(visual, false);

            var partsById = manifest.parts.ToDictionary(part => part.id, StringComparer.Ordinal);
            var bonesByPart = new Dictionary<string, Transform>(StringComparer.Ordinal);
            var childJoints = (manifest.joints ?? Array.Empty<WoopJoint>())
                .Where(joint => joint.child != null && !string.IsNullOrEmpty(joint.child.partId))
                .GroupBy(joint => joint.child.partId, StringComparer.Ordinal)
                .ToDictionary(group => group.Key, group => group.First(), StringComparer.Ordinal);

            foreach (WoopPart part in manifest.parts)
            {
                var boneObject = new GameObject($"Bone_{part.id}");
                Transform bone = boneObject.transform;
                bone.SetParent(rigRoot, false);
                bone.localPosition = childJoints.TryGetValue(part.id, out WoopJoint incomingJoint)
                    ? GetJointPosition(incomingJoint, partsById, manifest.sourceSheet, ppu)
                    : partTransforms[part.id].localPosition;
                bonesByPart.Add(part.id, bone);
            }

            foreach (WoopJoint joint in manifest.joints ?? Array.Empty<WoopJoint>())
            {
                if (joint.parent == null || joint.child == null)
                    continue;
                if (!bonesByPart.TryGetValue(joint.parent.partId, out Transform parentBone)
                    || !bonesByPart.TryGetValue(joint.child.partId, out Transform childBone))
                    continue;

                childBone.SetParent(parentBone, true);
            }

            foreach (WoopPart part in manifest.parts)
                partTransforms[part.id].SetParent(bonesByPart[part.id], true);

            var childPartIds = new HashSet<string>(childJoints.Keys, StringComparer.Ordinal);
            WoopPart rootPart = manifest.parts
                .Where(part => !childPartIds.Contains(part.id))
                .OrderByDescending(part => part.width * part.height)
                .FirstOrDefault()
                ?? manifest.parts[0];

            WoopCutoutRig2D.BoneBinding[] bindings = manifest.parts
                .Select(part => new WoopCutoutRig2D.BoneBinding
                {
                    partId = part.id,
                    bone = bonesByPart[part.id],
                    spriteTransform = partTransforms[part.id]
                })
                .ToArray();

            var rig = root.AddComponent<WoopCutoutRig2D>();
            rig.Configure(rigRoot, bonesByPart[rootPart.id], bindings);
            return rig;
        }

        private static WoopRigRoles CreateRigRoles(
            WoopManifest manifest,
            IReadOnlyDictionary<string, Transform> partTransforms)
        {
            WoopRigRoleManifest roles = manifest.rigSemantics?.roles;
            if (roles == null)
                return default;

            return new WoopRigRoles
            {
                schemaVersion = manifest.rigSemantics.schemaVersion,
                head = ValidRole(roles.head, partTransforms),
                leftUpperArm = ValidRole(roles.leftUpperArm, partTransforms),
                leftLowerArm = ValidRole(roles.leftLowerArm, partTransforms),
                leftHand = ValidRole(roles.leftHand, partTransforms),
                rightUpperArm = ValidRole(roles.rightUpperArm, partTransforms),
                rightLowerArm = ValidRole(roles.rightLowerArm, partTransforms),
                rightHand = ValidRole(roles.rightHand, partTransforms),
                upperTorso = ValidRole(roles.upperTorso, partTransforms),
                lowerTorso = ValidRole(roles.lowerTorso, partTransforms),
                leftLeg = ValidRole(roles.leftLeg, partTransforms),
                rightLeg = ValidRole(roles.rightLeg, partTransforms)
            };
        }

        private static string ValidRole(
            string partId,
            IReadOnlyDictionary<string, Transform> partTransforms)
        {
            return !string.IsNullOrEmpty(partId) && partTransforms.ContainsKey(partId)
                ? partId
                : string.Empty;
        }

        private static Vector3 GetJointPosition(
            WoopJoint joint,
            IReadOnlyDictionary<string, WoopPart> partsById,
            WoopSheet sheet,
            float ppu)
        {
            var positions = new List<Vector3>(2);
            AddAnchorPosition(positions, joint.parent, partsById, sheet, ppu);
            AddAnchorPosition(positions, joint.child, partsById, sheet, ppu);

            if (positions.Count == 0)
                return Vector3.zero;
            if (positions.Count == 1)
                return positions[0];
            return (positions[0] + positions[1]) * 0.5f;
        }

        private static void AddAnchorPosition(
            ICollection<Vector3> positions,
            WoopJointEnd jointEnd,
            IReadOnlyDictionary<string, WoopPart> partsById,
            WoopSheet sheet,
            float ppu)
        {
            if (jointEnd == null || !partsById.TryGetValue(jointEnd.partId, out WoopPart part))
                return;

            WoopAnchor anchor = (part.anchors ?? Array.Empty<WoopAnchor>())
                .FirstOrDefault(item => item.id == jointEnd.anchorId);
            if (anchor?.partPx == null || anchor.partPx.Length < 2)
                return;

            float[] matrix = part.rest.matrix;
            float x = matrix[0] * anchor.partPx[0] + matrix[2] * anchor.partPx[1] + matrix[4];
            float y = matrix[1] * anchor.partPx[0] + matrix[3] * anchor.partPx[1] + matrix[5];
            positions.Add(new Vector3(
                (x - sheet.width * 0.5f) / ppu,
                -(y - sheet.height * 0.5f) / ppu,
                0f));
        }

        private static void ApplyRestTransform(
            Transform target,
            WoopPart part,
            WoopSheet sheet,
            float ppu)
        {
            float[] matrix = part.rest.matrix;
            if (matrix == null || matrix.Length < 6)
                throw new InvalidDataException($"部件 {part.id} 缺少有效的 rest.matrix。");

            float a = matrix[0];
            float b = matrix[1];
            float c = matrix[2];
            float d = matrix[3];
            float e = matrix[4];
            float f = matrix[5];

            float halfWidth = part.width * 0.5f;
            float halfHeight = part.height * 0.5f;
            float centerX = a * halfWidth + c * halfHeight + e;
            float centerY = b * halfWidth + d * halfHeight + f;

            float scaleX = Mathf.Sqrt(a * a + b * b);
            float determinant = a * d - b * c;
            float scaleY = scaleX > 0.00001f ? determinant / scaleX : 1f;
            float angle = -Mathf.Atan2(b, a) * Mathf.Rad2Deg;

            target.localPosition = new Vector3(
                (centerX - sheet.width * 0.5f) / ppu,
                -(centerY - sheet.height * 0.5f) / ppu,
                0f);
            target.localRotation = Quaternion.Euler(0f, 0f, angle);
            target.localScale = new Vector3(scaleX, scaleY, 1f);
        }

        private static void AddCharacterCollider(
            WoopManifest manifest,
            IReadOnlyDictionary<string, Transform> partTransforms,
            float ppu)
        {
            var childPartIds = new HashSet<string>(
                (manifest.joints ?? Array.Empty<WoopJoint>())
                    .Where(joint => joint.child != null && !string.IsNullOrEmpty(joint.child.partId))
                    .Select(joint => joint.child.partId),
                StringComparer.Ordinal);

            WoopPart bodyPart = manifest.parts
                .Where(part => !childPartIds.Contains(part.id))
                .OrderByDescending(part => part.width * part.height)
                .FirstOrDefault()
                ?? manifest.parts.OrderByDescending(part => part.width * part.height).First();

            Transform bodyTransform = partTransforms[bodyPart.id];
            var collider = bodyTransform.gameObject.AddComponent<CapsuleCollider2D>();
            collider.direction = CapsuleDirection2D.Vertical;
            collider.offset = Vector2.zero;
            collider.size = new Vector2(
                Mathf.Max(0.1f, bodyPart.width / ppu * 0.55f),
                Mathf.Max(0.2f, bodyPart.height / ppu * 0.82f));
        }

        private static ZipArchiveEntry FindEntry(ZipArchive archive, string path)
        {
            string normalized = path.Replace('\\', '/');
            return archive.Entries.FirstOrDefault(entry =>
                string.Equals(entry.FullName.Replace('\\', '/'), normalized, StringComparison.OrdinalIgnoreCase));
        }

        private static ZipArchiveEntry FindPartImage(ZipArchive archive, string partId)
        {
            string prefix = $"parts/{partId}/";
            return archive.Entries
                .Where(entry =>
                {
                    string path = entry.FullName.Replace('\\', '/');
                    string extension = Path.GetExtension(path);
                    return path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
                        && (extension.Equals(".png", StringComparison.OrdinalIgnoreCase)
                            || extension.Equals(".jpg", StringComparison.OrdinalIgnoreCase)
                            || extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase)
                            || extension.Equals(".webp", StringComparison.OrdinalIgnoreCase));
                })
                .OrderBy(entry => GetImagePriority(entry.FullName))
                .ThenBy(entry => entry.FullName, StringComparer.OrdinalIgnoreCase)
                .FirstOrDefault();
        }

        private static int GetImagePriority(string path)
        {
            string fileName = Path.GetFileName(path);
            if (fileName.Equals("00.png", StringComparison.OrdinalIgnoreCase))
                return 0;
            if (fileName.Equals("00.webp", StringComparison.OrdinalIgnoreCase))
                return 1;
            if (fileName.StartsWith("00.", StringComparison.OrdinalIgnoreCase))
                return 2;
            return 3;
        }

        private static Texture2D DecodeTexture(byte[] bytes, string path, string partId)
        {
            if (Path.GetExtension(path).Equals(".webp", StringComparison.OrdinalIgnoreCase))
            {
                Texture2D webpTexture = Texture2DExt.CreateTexture2DFromWebP(
                    bytes,
                    lMipmaps: false,
                    lLinear: false,
                    lError: out WebP.Error error,
                    makeNoLongerReadable: false);

                if (error != WebP.Error.Success || webpTexture == null)
                    throw new InvalidDataException($"无法解码部件 {partId} 的 WebP：{error}。");

                return webpTexture;
            }

            var texture = new Texture2D(2, 2, TextureFormat.RGBA32, false);
            if (!texture.LoadImage(bytes, false))
            {
                UnityEngine.Object.DestroyImmediate(texture);
                throw new InvalidDataException($"无法解码部件 {partId} 的图片：{path}。");
            }

            return texture;
        }

        private static string ReadText(ZipArchiveEntry entry)
        {
            using Stream stream = entry.Open();
            using var reader = new StreamReader(stream);
            return reader.ReadToEnd();
        }

        private static byte[] ReadBytes(ZipArchiveEntry entry)
        {
            using Stream input = entry.Open();
            using var output = new MemoryStream();
            input.CopyTo(output);
            return output.ToArray();
        }

        private static void ValidateManifest(WoopManifest manifest)
        {
            if (manifest == null)
                throw new InvalidDataException("woop.json 不是有效的 JSON。");
            if (manifest.sourceSheet == null || manifest.sourceSheet.width <= 0 || manifest.sourceSheet.height <= 0)
                throw new InvalidDataException("woop.json 缺少有效的 sourceSheet。");
            if (manifest.parts == null || manifest.parts.Length == 0)
                throw new InvalidDataException("woop.json 中没有任何 parts。");
        }

        [Serializable]
        private sealed class WoopManifest
        {
            public string schemaVersion;
            public string projectId;
            public WoopSheet sourceSheet;
            public WoopPart[] parts;
            public WoopJoint[] joints;
            public WoopRigSemanticsManifest rigSemantics;
        }

        [Serializable]
        private sealed class WoopRigSemanticsManifest
        {
            public string schemaVersion;
            public WoopRigRoleManifest roles;
        }

        [Serializable]
        private sealed class WoopRigRoleManifest
        {
            public string head;
            public string leftUpperArm;
            public string leftLowerArm;
            public string leftHand;
            public string rightUpperArm;
            public string rightLowerArm;
            public string rightHand;
            public string upperTorso;
            public string lowerTorso;
            public string leftLeg;
            public string rightLeg;
        }

        [Serializable]
        private sealed class WoopSheet
        {
            public int width;
            public int height;
        }

        [Serializable]
        private sealed class WoopPart
        {
            public string id;
            public int width;
            public int height;
            public WoopRest rest;
            public WoopAnchor[] anchors;
        }

        [Serializable]
        private sealed class WoopAnchor
        {
            public string id;
            public float[] partPx;
        }

        [Serializable]
        private sealed class WoopRest
        {
            public float[] matrix;
            public int zIndex;
        }

        [Serializable]
        private sealed class WoopJoint
        {
            public string id;
            public WoopJointEnd parent;
            public WoopJointEnd child;
        }

        [Serializable]
        private sealed class WoopJointEnd
        {
            public string partId;
            public string anchorId;
        }
    }
}
