using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UniGLTF;
using UniGLTF.Extensions.VRMC_vrm_animation;
using UniHumanoid;
using UniJSON;
using UnityEngine;
using UniVRM10;
using VRMShaders;

namespace UniVRM10
{
    public class VrmAnimationImporter : UniGLTF.ImporterContext
    {
        VRMC_vrm_animation m_vrma;

        public VrmAnimationImporter(GltfData data,
                IReadOnlyDictionary<SubAssetKey, UnityEngine.Object> externalObjectMap = null,
                ITextureDeserializer textureDeserializer = null,
                IMaterialDescriptorGenerator materialGenerator = null)
            : base(data, externalObjectMap, textureDeserializer, materialGenerator)
        {
            InvertAxis = Axes.X;

            m_vrma = GetExtension(Data);
        }

        private static VRMC_vrm_animation GetExtension(GltfData data)
        {
            if (data.GLTF.extensions is UniGLTF.glTFExtensionImport extensions)
            {
                foreach (var kv in extensions.ObjectItems())
                {
                    if (kv.Key.GetString() == "VRMC_vrm_animation")
                    {
                        return UniGLTF.Extensions.VRMC_vrm_animation.GltfDeserializer.Deserialize(kv.Value);
                    }
                }
            }
            return null;
        }

        private static int? GetNodeIndex(UniGLTF.Extensions.VRMC_vrm_animation.Humanoid humanoid, HumanBodyBones bone)
        {
            switch (bone)
            {
                case HumanBodyBones.Hips: return humanoid.HumanBones.Hips?.Node;
                case HumanBodyBones.LeftUpperLeg: return humanoid.HumanBones.LeftUpperLeg?.Node;
                case HumanBodyBones.RightUpperLeg: return humanoid.HumanBones.RightUpperLeg?.Node;
                case HumanBodyBones.LeftLowerLeg: return humanoid.HumanBones.LeftLowerLeg?.Node;
                case HumanBodyBones.RightLowerLeg: return humanoid.HumanBones.RightLowerLeg?.Node;
                case HumanBodyBones.LeftFoot: return humanoid.HumanBones.LeftFoot?.Node;
                case HumanBodyBones.RightFoot: return humanoid.HumanBones.RightFoot?.Node;
                case HumanBodyBones.Spine: return humanoid.HumanBones.Spine?.Node;
                case HumanBodyBones.Chest: return humanoid.HumanBones.Chest?.Node;
                case HumanBodyBones.Neck: return humanoid.HumanBones.Neck?.Node;
                case HumanBodyBones.Head: return humanoid.HumanBones.Head?.Node;
                case HumanBodyBones.LeftShoulder: return humanoid.HumanBones.LeftShoulder?.Node;
                case HumanBodyBones.RightShoulder: return humanoid.HumanBones.RightShoulder?.Node;
                case HumanBodyBones.LeftUpperArm: return humanoid.HumanBones.LeftUpperArm?.Node;
                case HumanBodyBones.RightUpperArm: return humanoid.HumanBones.RightUpperArm?.Node;
                case HumanBodyBones.LeftLowerArm: return humanoid.HumanBones.LeftLowerArm?.Node;
                case HumanBodyBones.RightLowerArm: return humanoid.HumanBones.RightLowerArm?.Node;
                case HumanBodyBones.LeftHand: return humanoid.HumanBones.LeftHand?.Node;
                case HumanBodyBones.RightHand: return humanoid.HumanBones.RightHand?.Node;
                case HumanBodyBones.LeftToes: return humanoid.HumanBones.LeftToes?.Node;
                case HumanBodyBones.RightToes: return humanoid.HumanBones.RightToes?.Node;
                // case HumanBodyBones.LeftEye: return humanoid.HumanBones.LeftEye?.Node;
                // case HumanBodyBones.RightEye: return humanoid.HumanBones.RightEye?.Node;
                case HumanBodyBones.Jaw: return humanoid.HumanBones.Jaw?.Node;
                case HumanBodyBones.LeftThumbProximal: return humanoid.HumanBones.LeftThumbMetacarpal?.Node; // Metacarpal
                case HumanBodyBones.LeftThumbIntermediate: return humanoid.HumanBones.LeftThumbProximal?.Node; // Proximal
                case HumanBodyBones.LeftThumbDistal: return humanoid.HumanBones.LeftThumbDistal?.Node;
                case HumanBodyBones.LeftIndexProximal: return humanoid.HumanBones.LeftIndexProximal?.Node;
                case HumanBodyBones.LeftIndexIntermediate: return humanoid.HumanBones.LeftIndexIntermediate?.Node;
                case HumanBodyBones.LeftIndexDistal: return humanoid.HumanBones.LeftIndexDistal?.Node;
                case HumanBodyBones.LeftMiddleProximal: return humanoid.HumanBones.LeftMiddleProximal?.Node;
                case HumanBodyBones.LeftMiddleIntermediate: return humanoid.HumanBones.LeftMiddleIntermediate?.Node;
                case HumanBodyBones.LeftMiddleDistal: return humanoid.HumanBones.LeftMiddleDistal?.Node;
                case HumanBodyBones.LeftRingProximal: return humanoid.HumanBones.LeftRingProximal?.Node;
                case HumanBodyBones.LeftRingIntermediate: return humanoid.HumanBones.LeftRingIntermediate?.Node;
                case HumanBodyBones.LeftRingDistal: return humanoid.HumanBones.LeftRingDistal?.Node;
                case HumanBodyBones.LeftLittleProximal: return humanoid.HumanBones.LeftLittleProximal?.Node;
                case HumanBodyBones.LeftLittleIntermediate: return humanoid.HumanBones.LeftLittleIntermediate?.Node;
                case HumanBodyBones.LeftLittleDistal: return humanoid.HumanBones.LeftLittleDistal?.Node;
                case HumanBodyBones.RightThumbProximal: return humanoid.HumanBones.RightThumbMetacarpal?.Node; // Metacarpal
                case HumanBodyBones.RightThumbIntermediate: return humanoid.HumanBones.RightThumbProximal?.Node; // Proximal
                case HumanBodyBones.RightThumbDistal: return humanoid.HumanBones.RightThumbDistal?.Node;
                case HumanBodyBones.RightIndexProximal: return humanoid.HumanBones.RightIndexProximal?.Node;
                case HumanBodyBones.RightIndexIntermediate: return humanoid.HumanBones.RightIndexIntermediate?.Node;
                case HumanBodyBones.RightIndexDistal: return humanoid.HumanBones.RightIndexDistal?.Node;
                case HumanBodyBones.RightMiddleProximal: return humanoid.HumanBones.RightMiddleProximal?.Node;
                case HumanBodyBones.RightMiddleIntermediate: return humanoid.HumanBones.RightMiddleIntermediate?.Node;
                case HumanBodyBones.RightMiddleDistal: return humanoid.HumanBones.RightMiddleDistal?.Node;
                case HumanBodyBones.RightRingProximal: return humanoid.HumanBones.RightRingProximal?.Node;
                case HumanBodyBones.RightRingIntermediate: return humanoid.HumanBones.RightRingIntermediate?.Node;
                case HumanBodyBones.RightRingDistal: return humanoid.HumanBones.RightRingDistal?.Node;
                case HumanBodyBones.RightLittleProximal: return humanoid.HumanBones.RightLittleProximal?.Node;
                case HumanBodyBones.RightLittleIntermediate: return humanoid.HumanBones.RightLittleIntermediate?.Node;
                case HumanBodyBones.RightLittleDistal: return humanoid.HumanBones.RightLittleDistal?.Node;
                case HumanBodyBones.UpperChest: return humanoid.HumanBones.UpperChest?.Node;
            }
            return default;
        }

        public Dictionary<HumanBodyBones, Transform> GetHumanMap()
        {
            var humanMap = new Dictionary<HumanBodyBones, Transform>();
            if (m_vrma is UniGLTF.Extensions.VRMC_vrm_animation.VRMC_vrm_animation animation && animation.Humanoid != null)
            {
                foreach (HumanBodyBones bone in UniGLTF.Utils.CachedEnum.GetValues<HumanBodyBones>())
                {
                    // Debug.Log($"{bone} => {index}");
                    var node = GetNodeIndex(animation.Humanoid, bone);
                    if (node.HasValue)
                    {
                        humanMap.Add(bone, Nodes[node.Value]);
                    }
                }
            }
            return humanMap;
        }

        class ExpressionInfo
        {
            public ExpressionKey Key;
            public int ChannelIndex;
            public string PropertyName;
            public glTFAnimationChannel Channel;
        }

        ExpressionInfo GetExpression(glTFAnimation animation, ExpressionKey key, string propertyName, Expression expression)
        {
            if (expression == null)
            {
                return null;
            }
            if (!expression.Node.HasValue)
            {
                return null;
            }

            for (int i = 0; i < animation.channels.Count; ++i)
            {
                var channel = animation.channels[i];
                if (channel.target.node == expression.Node.Value)
                {
                    return new ExpressionInfo
                    {
                        Key = key,
                        ChannelIndex = i,
                        // 全部小文字
                        PropertyName = propertyName.ToLower(),
                        Channel = channel,
                    };
                }
            }

            return null;
        }

        IEnumerable<ExpressionInfo> IterateExpressions()
        {
            if (m_vrma is UniGLTF.Extensions.VRMC_vrm_animation.VRMC_vrm_animation animation && animation.Expressions != null)
            {
                var gltfAnimation = Data.GLTF.animations[0];
                if (animation.Expressions.Preset is Preset preset)
                {
                    { if (GetExpression(gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.happy), nameof(Vrm10AnimationInstance.preset_happy), preset.Happy) is ExpressionInfo info) yield return info; }
                    { if (GetExpression(gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.angry), nameof(Vrm10AnimationInstance.preset_angry), preset.Angry) is ExpressionInfo info) yield return info; }
                    { if (GetExpression(gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.sad), nameof(Vrm10AnimationInstance.preset_sad), preset.Sad) is ExpressionInfo info) yield return info; }
                    { if (GetExpression(gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.relaxed), nameof(Vrm10AnimationInstance.preset_relaxed), preset.Relaxed) is ExpressionInfo info) yield return info; }
                    { if (GetExpression(gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.surprised), nameof(Vrm10AnimationInstance.preset_surprised), preset.Surprised) is ExpressionInfo info) yield return info; }
                    { if (GetExpression(gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.aa), nameof(Vrm10AnimationInstance.preset_aa), preset.Aa) is ExpressionInfo info) yield return info; }
                    { if (GetExpression(gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.ih), nameof(Vrm10AnimationInstance.preset_ih), preset.Ih) is ExpressionInfo info) yield return info; }
                    { if (GetExpression(gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.ou), nameof(Vrm10AnimationInstance.preset_ou), preset.Ou) is ExpressionInfo info) yield return info; }
                    { if (GetExpression(gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.ee), nameof(Vrm10AnimationInstance.preset_ee), preset.Ee) is ExpressionInfo info) yield return info; }
                    { if (GetExpression(gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.oh), nameof(Vrm10AnimationInstance.preset_oh), preset.Oh) is ExpressionInfo info) yield return info; }
                    { if (GetExpression(gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.blink), nameof(Vrm10AnimationInstance.preset_blink), preset.Blink) is ExpressionInfo info) yield return info; }
                    { if (GetExpression(gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.blinkLeft), nameof(Vrm10AnimationInstance.preset_blinkleft), preset.BlinkLeft) is ExpressionInfo info) yield return info; }
                    { if (GetExpression(gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.blinkRight), nameof(Vrm10AnimationInstance.preset_blinkright), preset.BlinkRight) is ExpressionInfo info) yield return info; }
                    { if (GetExpression(gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.lookUp), "preset_lookup", preset.LookUp) is ExpressionInfo info) yield return info; }
                    { if (GetExpression(gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.lookDown), "preset_lookdown", preset.LookDown) is ExpressionInfo info) yield return info; }
                    { if (GetExpression(gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.lookLeft), "preset_lookleft", preset.LookLeft) is ExpressionInfo info) yield return info; }
                    { if (GetExpression(gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.lookRight), "preset_lookright", preset.LookRight) is ExpressionInfo info) yield return info; }
                    { if (GetExpression(gltfAnimation, ExpressionKey.CreateFromPreset(ExpressionPreset.neutral), nameof(Vrm10AnimationInstance.preset_neutral), preset.Neutral) is ExpressionInfo info) yield return info; }
                }
                if (animation.Expressions.Custom != null)
                {
                    int customIndex = 0;
                    foreach (var (k, v) in animation.Expressions.Custom.OrderBy(kv => kv.Value.Node))
                    {
                        var info = GetExpression(gltfAnimation, ExpressionKey.CreateCustom(k), $"custom_{customIndex:D2}", v);
                        ++customIndex;
                        if (info != null)
                        {
                            yield return info;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// xyz translation カーブから x だけの カーブを生成する
        /// </summary>
        /// <param name="input"></param>
        /// <param name="output"></param>
        /// <returns></returns>
        static AnimationCurve CreateCurve(
            float[] input,
            float[] output)
        {
            var keyframes = new List<Keyframe>();
            for (var inputIndex = 0; inputIndex < input.Length; ++inputIndex)
            {
                var time = input[inputIndex];
                // translation
                var value = output[inputIndex * 3];
                keyframes.Add(new Keyframe(time, value, 0, 0));
                if (keyframes.Count > 0)
                {
                    AnimationImporterUtil.CalculateTangent(keyframes, keyframes.Count - 1);
                }
            }

            var curve = new AnimationCurve();
            foreach (var keyFrame in keyframes)
            {
                curve.AddKey(keyFrame);
            }
            return curve;
        }

        public override async Task<RuntimeGltfInstance> LoadAsync(IAwaitCaller awaitCaller, Func<string, IDisposable> measureTime = null)
        {
            // Expression は AnimationClip を分ける。
            // glTFData から関連 Animation を取り除いて、取っておく。
            var expressions = IterateExpressions().ToArray();
            foreach (var channelIndex in expressions.Select(x => x.ChannelIndex).OrderByDescending(x => x))
            {
                var nodeIndex = Data.GLTF.animations[0].channels[channelIndex].target.node;
                Data.GLTF.nodes.RemoveAt(nodeIndex);
                // 後ろから順に channel を除去
                Data.GLTF.animations[0].channels.RemoveAt(channelIndex);

                Debug.Log($"remove: {channelIndex}");
            }
            Data.GLTF.scenes[0].nodes = Data.GLTF.scenes[0].nodes.Take(1).ToArray();

            // Humanoid Animation が Gltf アニメーションとしてロードされる
            var instance = await base.LoadAsync(awaitCaller, measureTime);

            // setup humanoid
            var humanMap = GetHumanMap();
            if (humanMap.Count > 0)
            {
                var description = AvatarDescription.Create(humanMap);
                //
                // avatar
                //
                var avatar = description.CreateAvatar(instance.Root.transform);
                avatar.name = "Avatar";
                // AvatarDescription = description;
                var animator = instance.gameObject.AddComponent<Animator>();
                animator.avatar = avatar;
            }

            if (expressions.Length > 0)
            {
                var animation = instance.GetComponent<Animation>();
                var clip = animation.clip;

                // Expression の float カーブを追加する
                // VrmAnimationInstance の "preset_xx" field に連動する
                var gltfAnimation = Data.GLTF.animations[0];
                foreach (var expression in expressions)
                {
                    var channel = expression.Channel;
                    var sampler = gltfAnimation.samplers[channel.sampler];
                    var input = Data.GetArrayFromAccessor<float>(sampler.input);
                    var output = Data.FlatternFloatArrayFromAccessor(sampler.output);
                    var curve = CreateCurve(
                        input.ToArray(),
                        output.ToArray());
                    clip.SetCurve("", typeof(Vrm10AnimationInstance), expression.PropertyName, curve);
                }
            }

            // VRMA-animation solver
            var animationInstance = instance.gameObject.AddComponent<Vrm10AnimationInstance>();
            animationInstance.Initialize(expressions.Select(x => x.Key));

            return instance;
        }
    }
}
