
using System;
using System.Collections.Generic;

namespace Animrig
{
    public class SpineRenderer : StateRenderer
    {
        // property
        protected Dictionary<string, Spine.Bone> BoneCacheList = new Dictionary<string, Spine.Bone>();
        public static readonly int SaveVersion = 1; 
        public Spine.AnimationState state = null;
        public Spine.Skeleton skeleton = null;
        public float AntiScaling = 1.0f;    // anti scaling. on unity, data scale x0.01.
        // public
        public SpineRenderer()
            : base()
        {
            ObjectType = ObjectType.Spine;

            Clear();
        }
        // common 
        public override void Clear()
        {
            base.Clear();

            state = null;
            skeleton = null;
            BoneCacheList.Clear();
        }
        public override void Update(double deltatime)
        {
            if (state == null) return;
            if (skeleton == null) return;

            state.Update((float)deltatime);
            state.Apply(skeleton);
            skeleton.UpdateWorldTransform();

            // physics
            var m = Misc.IdentityMatrix4;
            AddPhysicsTransform(ref m);
            UpdatePhysics(deltatime);
            ApplyPhysics();
        }
        // spine
        public override void SetAnimation(int trackindex, int usetrack, string basename, bool loop, bool addanim, MixBlend trackmix, float duration, bool seekmode)
        {
            void inSetEmpty(int index, float dt)
            {
                if (dt < 0.0) dt = state.Data.DefaultMix;

                if (addanim)
                    state.AddEmptyAnimation(index, dt, 0.0f);
                else
                    state.SetEmptyAnimation(index, dt);
            }
            void inSetAnimation(int index, string name1, string name2, float dt)
            {
                // search
                var anim = state.Data.SkeletonData.FindAnimation(name1);
                if (anim == null) anim = state.Data.SkeletonData.FindAnimation(name2);

                if (anim == null)
                {
                    inSetEmpty(index , dt);
                }
                else
                {
                    // set animation
                    Spine.TrackEntry track;
                    if (addanim)
                        track = state.AddAnimation(index, anim, loop, 0.0f);
                    else
                        track = state.SetAnimation(index, anim, loop);
                    if (track != null)
                    {
                        // mix blend
                        if ((track.MixBlend == Spine.MixBlend.Replace)||(track.MixBlend == Spine.MixBlend.Add))     // safe code
                        {
                            switch (trackmix)
                            {
                                case MixBlend.Add:
                                    track.MixBlend = Spine.MixBlend.Add;
                                    break;
                                case MixBlend.Replace:
                                    track.MixBlend = Spine.MixBlend.Replace;
                                    break;
                            }
                        }
                        // duration
                        if (dt < 0.0f)
                        {
                            // use default duration
                        }
                        else
                        {
                            track.MixDuration = Math.Max(dt, 0.0f);
                        }
                        // seek mode
                        if (seekmode)
                            track.TimeScale = 0.0f;
                        else
                            track.TimeScale = 1.0f;
                    }
                }
            }

            if (state == null) return;
            
            // layertype AnimBlend1
            if (usetrack == 1)
            {
                if (basename == "empty")
                {
                    inSetEmpty(trackindex + 0, duration);
                }
                else
                {
                    inSetAnimation(trackindex + 0, basename, basename + "@up", duration);
                }
                return;
            }
            
            // layertype AnimBlend4
            if (usetrack == 4)
            {
                if (basename == "empty")
                {
                    inSetEmpty(trackindex + 0, duration);
                    inSetEmpty(trackindex + 1, duration);
                    inSetEmpty(trackindex + 2, duration);
                    inSetEmpty(trackindex + 3, duration);
                }
                else
                {
                    inSetAnimation(trackindex + 0, basename, basename + "@up", duration);
                    inSetAnimation(trackindex + 1, basename + "@right", "", duration);
                    inSetAnimation(trackindex + 2, basename + "@down", "", duration);
                    inSetAnimation(trackindex + 3, basename + "@left", "", duration);
                }
            }
        }
        public override void SetTrackBlendAlpha(int trackindex, int usetrack, float alpha1, float alpha2, float alpha3, float alpha4, bool exists)
        {
            void inSet(int index, float alpha)
            {
                var track = state.GetCurrent(index);
                if (track == null) return;

                track.Alpha = alpha;
            }

            if (state == null) return;

            if (usetrack == 1)
            {
                inSet(trackindex + 0, alpha1);
            }
            else
            {
                inSet(trackindex + 0, alpha1);
                inSet(trackindex + 1, alpha2);
                inSet(trackindex + 2, alpha3);
                inSet(trackindex + 3, alpha4);
            }
        }
        public override void SetTrackSeekTime(int trackindex, int usetrack, float time1, float time2, float time3, float time4, bool exists)
        {
            void inSet(int index, float time)
            {
                var track = state.GetCurrent(index);
                if (track == null) return;

                track.TrackTime = time;
            }

            if (state == null) return;

            if (usetrack == 1)
            {
                inSet(trackindex + 0, time1);
            }
            else
            {
                inSet(trackindex + 0, time1);
                inSet(trackindex + 1, time2);
                inSet(trackindex + 2, time3);
                inSet(trackindex + 3, time4);
            }
        }
        public override void SetMixDuration(float duration)
        {
            if (state == null) return;

            state.Data.DefaultMix = duration;
        }
        public override void SetSkin(string skinname)
        {
            if (skeleton == null) return;

            var skin = skeleton.Data.FindSkin(skinname);
            if (skin != null)
            {
                skeleton.SetSkin(skin);
                skeleton.SetToSetupPose();
            }
        }
        public override Matrix4 GetBoneMatrix(string bonename)
        {
            if (skeleton == null) return Misc.IdentityMatrix4;
            if (state == null) return Misc.IdentityMatrix4;

            // cache
            Spine.Bone bone = null;
            bool cachehit = false;
            if (BoneCacheList.TryGetValue(bonename, out bone))
            {
                cachehit = true;
            }
            if (bone == null) bone = skeleton.FindBone(bonename);

            if (!cachehit) BoneCacheList.Add(bonename,bone);

            if (bone == null) return Misc.IdentityMatrix4;
            return ExtractWorldMatrix(bone);
        }

        protected Matrix4 ExtractWorldMatrix(Spine.Bone bone)
        {
            Matrix4 r = Misc.IdentityMatrix4;
            r._11 = bone.A;
            r._12 = bone.C;
            r._21 = bone.B;
            r._22 = bone.D;
            r._41 = bone.WorldX * AntiScaling;
            r._42 = bone.WorldY * AntiScaling;
            return r;
        }
    }

    public class SpinePhysicsRenderer : SpineRenderer
    {
        public override void AddPhysicsTransform(ref Matrix4 externalmatrix)
        {
            foreach (var g in PhysicsGroups)
            {
                if (g.GroupType == PhysicsGroupType.Spring)
                {
                    var gs = g as PhysicsGroupSpringBone;
                    var m = ExtractWorldMatrix(gs.RootUserObject as Spine.Bone);
                    Misc.m4PushMatrix(ref m, ref TransMatrix);
                    gs.RootMatrix = m;
                    gs.ExternalMatrix = externalmatrix;
                }
            }
        }
        public override void AddPhysicsForce(string target, double x, double y, double z)
        {
            // safe check
            if (state == null) return;
            if (skeleton == null) return;

            if (target == "none") return;

            var v = new PhysicsVector3(x, y, z);
            foreach (var g in PhysicsGroups)
            {
                if ((g.AliasName == target) || (target == "all")) g.AddForce(ref v);
            }
        }
        public override void SetupPhysics(List<string> setting)
        {
            // get setting value
            double inGetSettingFloat(Dictionary<string, string> dic, int index, string key, double def_, double min_, double max_)
            {
                string s = "";
                try
                {
                    if (dic.ContainsKey(key)) s = dic[key];
                }
                catch
                {
                    s = "";
                }

                var sl = Misc.DivideString(',', s);
                if (index < sl.Count) s = sl[index];

                var f = Misc.ToFloatDef(s, (float)def_);
                return Math.Min(Math.Max(f, min_), max_);
            }
            string inGetSettingString(Dictionary<string, string> dic, string key, string def_)
            {
                string s = "";
                try
                {
                    if (dic.ContainsKey(key)) s = dic[key];
                }
                catch
                {
                    s = "";
                }
                if (s == "") s = def_;
                return s;
            }
            // bone
            void inAddBone(PhysicsGroupSpringBone g, Spine.Bone bone, PhysicsNodeBone parent)
            {
                if (parent != null)
                {
                    var s = bone.Data.Name;
                    if (s.IndexOf("@end") > -1) return;  // terminate code
                    if (s.IndexOf("@") > -1) return;     // when other code exists, safe exit.
                }

                var b = new PhysicsNodeBone(parent);
                b.UserObject = bone;
                b.LocalAngle = bone.Data.Rotation / 360.0;
                b.LocalLength = bone.Data.Length * AntiScaling;
                b.LocalPosition.x = bone.Data.X * AntiScaling;
                b.LocalPosition.y = bone.Data.Y * AntiScaling;
                g.AddBone(b);

                foreach (var c in bone.Children)
                {
                    inAddBone(g, c, b);
                }

                // create terminate bone
                if (bone.Children.Count == 0)
                {
                    parent = b;
                    b = new PhysicsNodeBone(parent);
                    b.UserObject = null;
                    b.LocalAngle = 0;
                    b.LocalLength = 100;
                    b.LocalPosition.x = parent.LocalLength;
                    b.LocalPosition.y = 0;
                    g.AddBone(b);
                }
            }
            // spring
            void inAddSpring(PhysicsGroupSpringBone g, PhysicsNodeBone bone)
            {
                foreach (var b in bone.Children)
                {
                    var s = new PhysicsSpringBone(g, bone, b);
                    g.AddSpring(s);
                    inAddSpring(g, b);
                }
            }
            // spring group
            void inAddSpringGroup(Dictionary<string, string> dic, string name, string alias, Spine.Bone bone)
            {
                // need has parent
                var root = bone.Parent;
                if (root == null) return;

                if (alias == "") alias = name;

                // enable / disable
                if (inGetSettingString(dic , alias + ".enable", "true") == "false") return;

                var g = new PhysicsGroupSpringBone();
                g.Name = name;
                g.AliasName = alias;
                g.RootUserObject = root;

                var s = inGetSettingString(dic, alias + ".mode", "bone");
                if (s == "strpoint") g.Mode = PhysicsGroupSpringBoneMode.StrPoint;
                if (s == "point") g.Mode = PhysicsGroupSpringBoneMode.Point;
                if (s == "strbone") g.Mode = PhysicsGroupSpringBoneMode.StrBone;
                if (s == "bone") g.Mode = PhysicsGroupSpringBoneMode.Bone;

                g.K = inGetSettingFloat(dic, 0, alias + ".k", g.K, 0.1, 100 * 100);
                g.Damping = inGetSettingFloat(dic, 0, alias + ".damping", g.Damping, 0.1, 100 * 100);
                g.Mass = inGetSettingFloat(dic, 0, alias + ".mass", g.Mass, 0.001, 1000);
                g.LimitVelocity = inGetSettingFloat(dic, 0, alias + ".limitvelocity", g.LimitVelocity, 1, 0xffff);   // 100 = physics frame
                g.Gravity = inGetSettingFloat(dic, 0, alias + ".gravity", g.Gravity, 0, 10000 * 100);
                g.GravityVector.x = inGetSettingFloat(dic, 0, alias + ".gravityvector", g.GravityVector.x, -1000, +1000);
                g.GravityVector.y = inGetSettingFloat(dic, 1, alias + ".gravityvector", g.GravityVector.y, -1000, +1000);
                g.GravityVector.z = inGetSettingFloat(dic, 2, alias + ".gravityvector", g.GravityVector.z, -1000, +1000);
                g.GravityVector.Normalize();
                g.LimitAngleMin = inGetSettingFloat(dic, 0, alias + ".limitangle", g.LimitAngleMin, -0xff, 0);
                g.LimitAngleMax = inGetSettingFloat(dic, 1, alias + ".limitangle", g.LimitAngleMax, 0, +0xff);
                g.LimitMoveRatio = inGetSettingFloat(dic, 0, alias + ".limitmove", g.LimitMoveRatio, 0, +0xff);
                // private
                g.ForceRotateRatio = inGetSettingFloat(dic, 0, alias + ".forcerotate", g.ForceRotateRatio, 0.0, 10.0);
                g.ForceRatio = inGetSettingFloat(dic, 0, alias + ".force", g.ForceRatio, 0.0, 10.0);

                inAddBone(g, bone, null);
                inAddSpring(g, g.RootBone);

                g.RootMatrix = ExtractWorldMatrix(root);
                g.SetupNatural();

                PhysicsGroups.Add(g);
            }

            // do not forget to clear PhysicsGroups
            base.SetupPhysics(setting);

            if (state == null) return;
            if (skeleton == null) return;

            var settingdic = Misc.ConvertKeyValue(setting);

            skeleton.UpdateWorldTransform();

            foreach(var b in skeleton.Bones)
            {
                var s = b.Data.Name;

                var idx = s.IndexOf("@");
                if (idx == -1) continue;

                // name @ spring,aliasname
                var op = "";
                var alias = "";
                var name = s.Substring(0,idx);
                var sl = Misc.DivideString(',', s.Substring(idx + 1));

                if (sl.Count > 0) op = sl[0];
                if (sl.Count > 1) alias = sl[1];

                if (op == "spring") inAddSpringGroup(settingdic, name, alias, b);
            }
        }
        public override void ApplyPhysics()
        {
            void inUpdateWorldMatrix(Spine.Bone bone)
            {
                if (bone == null) return;

                bone.UpdateWorldTransform();

                foreach (var b in bone.Children)
                {
                    inUpdateWorldMatrix(b);
                }
            }
            void inApplyBone(PhysicsGroupSpringBone g, Spine.Skeleton skeleton)
            {
                foreach (var n in g.Nodes)
                {
                    var b = n as PhysicsNodeBone;
                    if (b.UserObject != null)
                    {
                        var sb = b.UserObject as Spine.Bone;
                        switch (g.Mode)
                        {
                            case PhysicsGroupSpringBoneMode.Bone:
                                sb.Rotation = (float)(b.aLocalAngle * 360.0);
                                break;
                            case PhysicsGroupSpringBoneMode.StrBone:
                                sb.Rotation = (float)(b.aLocalAngle * 360.0);
                                sb.X = (float)b.aLocalPosition.x / AntiScaling;
                                sb.Y = (float)b.aLocalPosition.y / AntiScaling;
                                break;
                            case PhysicsGroupSpringBoneMode.Point:
                            case PhysicsGroupSpringBoneMode.StrPoint:
                                sb.X = (float)b.aLocalPosition.x / AntiScaling;
                                sb.Y = (float)b.aLocalPosition.y / AntiScaling;
                                break;
                        }
                    }
                }
                //inUpdateWorldMatrix((Spine.Bone)(g.RootBone.UserObject));
            }
            // safe check
            if (state == null) return;
            if (skeleton == null) return;

            foreach (var g in PhysicsGroups)
            {
                g.Apply();
                if (g.GroupType == PhysicsGroupType.Spring)
                {
                    inApplyBone(g as PhysicsGroupSpringBone, skeleton);
                }
            }
            skeleton.UpdateWorldTransform();
        }
        public override void AppeasePhysics()
        {
            if (state == null) return;
            if (skeleton == null) return;

            foreach (var g in PhysicsGroups)
            {
                g.Appease();
            }
        }
    }
}