﻿using System;
using System.Collections.Generic;

namespace Oni.Totoro
{
    internal class AnimationDatReader
    {
        private readonly Animation animation = new Animation();
        private readonly InstanceDescriptor tram;
        private readonly BinaryReader dat;

        #region private class DatExtent

        private class DatExtent
        {
            public int Frame;
            public readonly AttackExtent Extent = new AttackExtent();
        }

        #endregion
        #region private class DatExtentInfo

        private class DatExtentInfo
        {
            public float MaxHorizontal;
            public float MinY = 1e09f;
            public float MaxY = -1e09f;
            public readonly DatExtentInfoFrame FirstExtent = new DatExtentInfoFrame();
            public readonly DatExtentInfoFrame MaxExtent = new DatExtentInfoFrame();
        }

        #endregion
        #region private class DatExtentInfoFrame

        private class DatExtentInfoFrame
        {
            public int Frame = -1;
            public int Attack;
            public int AttackOffset;
            public Vector2 Location;
            public float Height;
            public float Length;
            public float MinY;
            public float MaxY;
            public float Angle;
        }

        #endregion

        private AnimationDatReader(InstanceDescriptor tram, BinaryReader dat)
        {
            this.tram = tram;
            this.dat = dat;
        }

        public static Animation Read(InstanceDescriptor tram)
        {
            using (var dat = tram.OpenRead())
            {
                var reader = new AnimationDatReader(tram, dat);
                reader.ReadAnimation();
                return reader.animation;
            }
        }

        private void ReadAnimation()
        {
            dat.Skip(4);
            int heightOffset = dat.ReadInt32();
            int velocityOffset = dat.ReadInt32();
            int attackOffset = dat.ReadInt32();
            int damageOffset = dat.ReadInt32();
            int motionBlurOffset = dat.ReadInt32();
            int shortcutOffset = dat.ReadInt32();
            ReadOptionalThrowInfo();
            int footstepOffset = dat.ReadInt32();
            int particleOffset = dat.ReadInt32();
            int positionOffset = dat.ReadInt32();
            int rotationOffset = dat.ReadInt32();
            int soundOffset = dat.ReadInt32();
            animation.Flags = (AnimationFlags)dat.ReadInt32();

            var directAnimations = dat.ReadLinkArray(2);
            for (int i = 0; i < directAnimations.Length; i++)
                animation.DirectAnimations[i] = (directAnimations[i] != null ? directAnimations[i].FullName : null);

            animation.OverlayUsedBones = (BoneMask)dat.ReadInt32();
            animation.OverlayReplacedBones = (BoneMask)dat.ReadInt32();
            animation.FinalRotation = dat.ReadSingle();
            animation.Direction = (Direction)dat.ReadUInt16();
            animation.Vocalization = dat.ReadUInt16();
            var extents = ReadExtentInfo();
            animation.Impact = dat.ReadString(16);
            animation.HardPause = dat.ReadUInt16();
            animation.SoftPause = dat.ReadUInt16();
            int soundCount = dat.ReadInt32();
            dat.Skip(6);
            int fps = dat.ReadUInt16();
            animation.FrameSize = dat.ReadUInt16();
            animation.Type = (AnimationType)dat.ReadUInt16();
            animation.AimingType = (AnimationType)dat.ReadUInt16();
            animation.FromState = (AnimationState)dat.ReadUInt16();
            animation.ToState = (AnimationState)dat.ReadUInt16();
            int boneCount = dat.ReadUInt16();
            int frameCount = dat.ReadUInt16();
            int duration = dat.ReadInt16();
            animation.Varient = (AnimationVarient)dat.ReadUInt16();
            dat.Skip(2);
            animation.AtomicStart = dat.ReadUInt16();
            animation.AtomicEnd = dat.ReadUInt16();
            animation.InterpolationEnd = dat.ReadUInt16();
            animation.InterpolationMax = dat.ReadUInt16();
            animation.ActionFrame = dat.ReadUInt16();
            animation.FirstLevelAvailable = dat.ReadUInt16();
            animation.InvulnerableStart = dat.ReadByte();
            animation.InvulnerableEnd = dat.ReadByte();
            int attackCount = dat.ReadByte();
            int damageCount = dat.ReadByte();
            int motionBlurCount = dat.ReadByte();
            int shortcutCount = dat.ReadByte();
            int footstepCount = dat.ReadByte();
            int particleCount = dat.ReadByte();
            ReadRawArray(heightOffset, frameCount, animation.Heights, r => r.ReadSingle());
            ReadRawArray(velocityOffset, frameCount, animation.Velocities, r => r.ReadVector2());
            ReadRotations(rotationOffset, boneCount, frameCount);
            ReadRawArray(positionOffset, frameCount, animation.Positions, ReadPosition);
            ReadRawArray(shortcutOffset, shortcutCount, animation.Shortcuts, ReadShortcut);
            ReadRawArray(damageOffset, damageCount, animation.SelfDamage, ReadDamage);
            ReadRawArray(particleOffset, particleCount, animation.Particles, ReadParticle);
            ReadRawArray(footstepOffset, footstepCount, animation.Footsteps, ReadFootstep);
            ReadRawArray(soundOffset, soundCount, animation.Sounds, ReadSound);
            ReadRawArray(motionBlurOffset, motionBlurCount, animation.MotionBlur, ReadMotionBlur);
            ReadRawArray(attackOffset, attackCount, animation.Attacks, ReadAttack);

            foreach (var attack in animation.Attacks)
            {
                for (int i = attack.Start; i <= attack.End; i++)
                {
                    var extent = extents.FirstOrDefault(e => e.Frame == i);

                    if (extent != null)
                        attack.Extents.Add(extent.Extent);
                }
            }
        }

        private void ReadRotations(int offset, int boneCount, int frameCount)
        {
            using (var raw = tram.GetRawReader(offset))
            {
                int basePosition = raw.Position;
                var boneOffsets = raw.ReadUInt16Array(boneCount);

                foreach (int boneOffset in boneOffsets)
                {
                    raw.Position = basePosition + boneOffset;

                    var keys = new List<KeyFrame>();
                    int time = 0;

                    do
                    {
                        var key = new KeyFrame();

                        if (animation.FrameSize == 6)
                        {
                            key.Rotation.X = raw.ReadInt16() * 180.0f / 32767.5f;
                            key.Rotation.Y = raw.ReadInt16() * 180.0f / 32767.5f;
                            key.Rotation.Z = raw.ReadInt16() * 180.0f / 32767.5f;
                        }
                        else if (animation.FrameSize == 16)
                        {
                            key.Rotation = raw.ReadQuaternion().ToVector4();
                        }

                        if (time == frameCount - 1)
                            key.Duration = 1;
                        else
                            key.Duration = raw.ReadByte();

                        time += key.Duration;
                        keys.Add(key);

                    } while (time < frameCount);

                    animation.Rotations.Add(keys);
                }
            }
        }

        private List<DatExtent> ReadExtentInfo()
        {
            var info = new DatExtentInfo
            {
                MaxHorizontal = dat.ReadSingle(),
                MinY = dat.ReadSingle(),
                MaxY = dat.ReadSingle()
            };

            for (int i = 0; i < animation.AttackRing.Length; i++)
                animation.AttackRing[i] = dat.ReadSingle();

            info.FirstExtent.Frame = dat.ReadUInt16();
            info.FirstExtent.Attack = dat.ReadByte();
            info.FirstExtent.AttackOffset = dat.ReadByte();
            info.FirstExtent.Location = dat.ReadVector2();
            info.FirstExtent.Height = dat.ReadSingle();
            info.FirstExtent.Length = dat.ReadSingle();
            info.FirstExtent.MinY = dat.ReadSingle();
            info.FirstExtent.MaxY = dat.ReadSingle();
            info.FirstExtent.Angle = dat.ReadSingle();

            info.MaxExtent.Frame = dat.ReadUInt16();
            info.MaxExtent.Attack = dat.ReadByte();
            info.MaxExtent.AttackOffset = dat.ReadByte();
            info.MaxExtent.Location = dat.ReadVector2();
            info.MaxExtent.Height = dat.ReadSingle();
            info.MaxExtent.Length = dat.ReadSingle();
            info.MaxExtent.MinY = dat.ReadSingle();
            info.MaxExtent.MaxY = dat.ReadSingle();
            info.MaxExtent.Angle = dat.ReadSingle();

            dat.Skip(4);

            int extentCount = dat.ReadInt32();
            int extentOffset = dat.ReadInt32();

            var extents = new List<DatExtent>();
            ReadRawArray(extentOffset, extentCount, extents, ReadExtent);

            foreach (var datExtent in extents)
            {
                var attackExtent = datExtent.Extent;

                if (datExtent.Frame == info.FirstExtent.Frame)
                {
                    attackExtent.Angle = MathHelper.ToDegrees(info.FirstExtent.Angle);
                    attackExtent.Length = info.FirstExtent.Length;
                    attackExtent.MinY = info.FirstExtent.MinY;
                    attackExtent.MaxY = info.FirstExtent.MaxY;
                }
                else if (datExtent.Frame == info.MaxExtent.Frame)
                {
                    attackExtent.Angle = MathHelper.ToDegrees(info.MaxExtent.Angle);
                    attackExtent.Length = info.MaxExtent.Length;
                    attackExtent.MinY = info.MaxExtent.MinY;
                    attackExtent.MaxY = info.MaxExtent.MaxY;
                }

                if (Math.Abs(attackExtent.MinY - info.MinY) < 0.01f)
                    attackExtent.MinY = info.MinY;

                if (Math.Abs(attackExtent.MaxY - info.MaxY) < 0.01f)
                    attackExtent.MaxY = info.MaxY;
            }

            return extents;
        }

        private void ReadOptionalThrowInfo()
        {
            int offset = dat.ReadInt32();

            if (offset != 0)
            {
                using (var raw = tram.GetRawReader(offset))
                    animation.ThrowSource = ReadThrowInfo(raw);
            }
        }

        private ThrowInfo ReadThrowInfo(BinaryReader raw)
        {
            return new ThrowInfo
            {
                Position = raw.ReadVector3(),
                Angle = raw.ReadSingle(),
                Distance = raw.ReadSingle(),
                Type = (AnimationType)raw.ReadUInt16()
            };
        }

        private Shortcut ReadShortcut(BinaryReader raw)
        {
            return new Shortcut
            {
                FromState = (AnimationState)raw.ReadUInt16(),
                Length = raw.ReadUInt16(),
                ReplaceAtomic = (raw.ReadInt32() != 0)
            };
        }

        private Footstep ReadFootstep(BinaryReader raw)
        {
            return new Footstep
            {
                Frame = raw.ReadUInt16(),
                Type = (FootstepType)raw.ReadUInt16()
            };
        }

        private Sound ReadSound(BinaryReader raw)
        {
            return new Sound
            {
                Name = raw.ReadString(32),
                Start = raw.ReadUInt16()
            };
        }

        private MotionBlur ReadMotionBlur(BinaryReader raw)
        {
            var motionBlur = new MotionBlur
            {
                Bones = (BoneMask)raw.ReadInt32(),
                Start = raw.ReadUInt16(),
                End = raw.ReadUInt16(),
                Lifetime = raw.ReadByte(),
                Alpha = raw.ReadByte(),
                Interval = raw.ReadByte()
            };

            raw.Skip(1);

            return motionBlur;
        }

        private Particle ReadParticle(BinaryReader raw)
        {
            return new Particle
            {
                Start = raw.ReadUInt16(),
                End = raw.ReadUInt16(),
                Bone = (Bone)raw.ReadInt32(),
                Name = raw.ReadString(16)
            };
        }

        private Damage ReadDamage(BinaryReader raw)
        {
            return new Damage
            {
                Points = raw.ReadUInt16(),
                Frame = raw.ReadUInt16()
            };
        }

        private Position ReadPosition(BinaryReader raw)
        {
            return new Position
            {
                X = raw.ReadInt16() * 0.01f,
                Z = raw.ReadInt16() * 0.01f,
                Height = raw.ReadUInt16() * 0.01f,
                YOffset = raw.ReadInt16() * 0.01f
            };
        }

        private Attack ReadAttack(BinaryReader raw)
        {
            var attack = new Attack
            {
                Bones = (BoneMask)raw.ReadInt32(),
                Knockback = raw.ReadSingle(),
                Flags = (AttackFlags)raw.ReadInt32(),
                HitPoints = raw.ReadInt16(),
                Start = raw.ReadUInt16(),
                End = raw.ReadUInt16(),
                HitType = (AnimationType)raw.ReadUInt16(),
                HitLength = raw.ReadUInt16(),
                StunLength = raw.ReadUInt16(),
                StaggerLength = raw.ReadUInt16()
            };

            raw.Skip(6);

            return attack;
        }

        private DatExtent ReadExtent(BinaryReader raw)
        {
            var extent = new DatExtent();
            extent.Frame = raw.ReadInt16();
            extent.Extent.Angle = raw.ReadUInt16() * 360.0f / 65535.0f;
            extent.Extent.Length = (raw.ReadUInt32() & 0xffffu) * 0.01f;
            extent.Extent.MinY = raw.ReadInt16() * 0.01f;
            extent.Extent.MaxY = raw.ReadInt16() * 0.01f;
            return extent;
        }

        private void ReadRawArray<T>(int offset, int count, List<T> list, Func<BinaryReader, T> readElement)
        {
            if (offset == 0 || count == 0)
                return;

            using (var raw = tram.GetRawReader(offset))
            {
                for (int i = 0; i < count; i++)
                    list.Add(readElement(raw));
            }
        }
    }
}
