﻿using System;
using System.Collections.Generic;
using Oni.Xml;
using Oni.Metadata;

namespace Oni.Totoro
{
    internal class AnimationDatWriter
    {
        private Animation animation;
        private List<DatExtent> extents;
        private DatExtentInfo extentInfo;
        private Importer importer;
        private BinaryWriter dat;
        private BinaryWriter raw;

        #region private class DatExtent

        private class DatExtent
        {
            public readonly int Frame;
            public readonly AttackExtent Extent;

            public DatExtent(int frame, AttackExtent extent)
            {
                this.Frame = frame;
                this.Extent = extent;
            }
        }

        #endregion
        #region private class DatExtentInfo

        private class DatExtentInfo
        {
            public float MaxDistance;
            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 AnimationDatWriter()
        {
        }

        public static void Write(Animation animation, Importer importer, BinaryWriter dat)
        {
            var writer = new AnimationDatWriter
            {
                animation = animation,
                importer = importer,
                dat = dat,
                raw = importer.RawWriter
            };

            writer.WriteAnimation();
        }

        private void WriteAnimation()
        {
            extentInfo = new DatExtentInfo();
            extents = new List<DatExtent>();

            if (animation.Attacks.Count > 0)
            {
                if (animation.Attacks[0].Extents.Count == 0)
                    GenerateExtentInfo();

                foreach (var attack in animation.Attacks)
                {
                    int frame = attack.Start;

                    foreach (var extent in attack.Extents)
                        extents.Add(new DatExtent(frame++, extent));
                }

                GenerateExtentSummary();
            }

            var rotations = animation.Rotations;
            int frameSize = animation.FrameSize;

            if (frameSize == 16 && (animation.Flags & AnimationFlags.Overlay) == 0)
            {
                rotations = CompressFrames(rotations);
                frameSize = 6;
            }

            dat.Write(0);
            WriteRawArray(animation.Heights, x => raw.Write(x));
            WriteRawArray(animation.Velocities, x => raw.Write(x));
            WriteRawArray(animation.Attacks, Write);
            WriteRawArray(animation.SelfDamage, Write);
            WriteRawArray(animation.MotionBlur, Write);
            WriteRawArray(animation.Shortcuts, Write);
            WriteThrowInfo();
            WriteRawArray(animation.Footsteps, Write);
            WriteRawArray(animation.Particles, Write);
            WriteRawArray(animation.Positions, Write);
            WriteRotations(rotations, frameSize);
            WriteRawArray(animation.Sounds, Write);
            dat.Write((int)animation.Flags);

            if (!string.IsNullOrEmpty(animation.DirectAnimations[0]))
                dat.Write(importer.CreateInstance(TemplateTag.TRAM, animation.DirectAnimations[0]));
            else
                dat.Write(0);

            if (!string.IsNullOrEmpty(animation.DirectAnimations[1]))
                dat.Write(importer.CreateInstance(TemplateTag.TRAM, animation.DirectAnimations[1]));
            else
                dat.Write(0);

            dat.Write((int)animation.OverlayUsedBones);
            dat.Write((int)animation.OverlayReplacedBones);
            dat.Write(animation.FinalRotation);
            dat.Write((ushort)animation.Direction);
            dat.WriteUInt16(animation.Vocalization);
            WriteExtentInfo();
            dat.Write(animation.Impact, 16);
            dat.WriteUInt16(animation.HardPause);
            dat.WriteUInt16(animation.SoftPause);
            dat.Write(animation.Sounds.Count);
            dat.Skip(6);
            dat.WriteUInt16(60);
            dat.WriteUInt16(frameSize);
            dat.WriteUInt16((ushort)animation.Type);
            dat.WriteUInt16((ushort)animation.AimingType);
            dat.WriteUInt16((ushort)animation.FromState);
            dat.WriteUInt16((ushort)animation.ToState);
            dat.WriteUInt16(rotations.Count);
            dat.WriteUInt16(animation.Velocities.Count);
            dat.WriteUInt16(animation.Velocities.Count);
            dat.WriteUInt16((ushort)animation.Varient);
            dat.Skip(2);
            dat.WriteUInt16(animation.AtomicStart);
            dat.WriteUInt16(animation.AtomicEnd);
            dat.WriteUInt16(animation.InterpolationEnd);
            dat.WriteUInt16(animation.InterpolationMax);
            dat.WriteUInt16(animation.ActionFrame);
            dat.WriteUInt16(animation.FirstLevelAvailable);
            dat.WriteByte(animation.InvulnerableStart);
            dat.WriteByte(animation.InvulnerableEnd);
            dat.WriteByte(animation.Attacks.Count);
            dat.WriteByte(animation.SelfDamage.Count);
            dat.WriteByte(animation.MotionBlur.Count);
            dat.WriteByte(animation.Shortcuts.Count);
            dat.WriteByte(animation.Footsteps.Count);
            dat.WriteByte(animation.Particles.Count);
        }

        private void WriteRotations(List<List<KeyFrame>> rotations, int frameSize)
        {
            dat.Write(raw.Align32());

            var offsets = new ushort[rotations.Count];

            offsets[0] = (ushort)(rotations.Count * 2);

            for (int i = 1; i < offsets.Length; i++)
                offsets[i] = (ushort)(offsets[i - 1] + rotations[i - 1].Count * (frameSize + 1) - 1);

            raw.Write(offsets);

            foreach (var keys in rotations)
            {
                foreach (var key in keys)
                {
                    switch (frameSize)
                    {
                        case 6:
                            raw.WriteInt16((short)(Math.Round(key.Rotation.X / 180.0f * 32767.5f)));
                            raw.WriteInt16((short)(Math.Round(key.Rotation.Y / 180.0f * 32767.5f)));
                            raw.WriteInt16((short)(Math.Round(key.Rotation.Z / 180.0f * 32767.5f)));
                            break;

                        case 16:
                            raw.Write(new Quaternion(key.Rotation));
                            break;
                    }

                    if (key != keys.Last())
                        raw.WriteByte(key.Duration);
                }
            }
        }

        private void WriteThrowInfo()
        {
            if (animation.ThrowSource == null)
            {
                dat.Write(0);
                return;
            }

            dat.Write(raw.Align32());

            raw.Write(animation.ThrowSource.Position);
            raw.Write(animation.ThrowSource.Angle);
            raw.Write(animation.ThrowSource.Distance);
            raw.WriteUInt16((ushort)animation.ThrowSource.Type);
        }

        private void WriteExtentInfo()
        {
            dat.Write(extentInfo.MaxDistance);
            dat.Write(extentInfo.MinY);
            dat.Write(extentInfo.MaxY);
            dat.Write(animation.AttackRing);
            Write(extentInfo.FirstExtent);
            Write(extentInfo.MaxExtent);
            dat.Write(0);
            dat.Write(extents.Count);
            WriteRawArray(extents, Write);
        }

        private void Write(DatExtentInfoFrame info)
        {
            dat.WriteInt16(info.Frame);
            dat.WriteByte(info.Attack);
            dat.WriteByte(info.AttackOffset);
            dat.Write(info.Location);
            dat.Write(info.Height);
            dat.Write(info.Length);
            dat.Write(info.MinY);
            dat.Write(info.MaxY);
            dat.Write(info.Angle);
        }

        private void Write(Position position)
        {
            raw.Write((short)Math.Round(position.X * 100.0f));
            raw.Write((short)Math.Round(position.Z * 100.0f));
            raw.Write((ushort)Math.Round(position.Height * 100.0f));
            raw.Write((short)Math.Round(position.YOffset * 100.0f));
        }

        private void Write(Damage damage)
        {
            raw.WriteUInt16(damage.Points);
            raw.WriteUInt16(damage.Frame);
        }

        private void Write(Shortcut shortcut)
        {
            raw.WriteUInt16((ushort)shortcut.FromState);
            raw.WriteUInt16(shortcut.Length);
            raw.Write(shortcut.ReplaceAtomic ? 1 : 0);
        }

        private void Write(Footstep footstep)
        {
            raw.WriteUInt16(footstep.Frame);
            raw.WriteUInt16((ushort)footstep.Type);
        }

        private void Write(Sound sound)
        {
            raw.Write(sound.Name, 32);
            raw.WriteUInt16(sound.Start);
        }

        private void Write(Particle particle)
        {
            raw.WriteUInt16(particle.Start);
            raw.WriteUInt16(particle.End);
            raw.Write((int)particle.Bone);
            raw.Write(particle.Name, 16);
        }

        private void Write(MotionBlur m)
        {
            raw.Write((int)m.Bones);
            raw.WriteUInt16(m.Start);
            raw.WriteUInt16(m.End);
            raw.WriteByte(m.Lifetime);
            raw.WriteByte(m.Alpha);
            raw.WriteByte(m.Interval);
            raw.WriteByte(0);
        }

        private void Write(DatExtent extent)
        {
            raw.WriteInt16(extent.Frame);
            raw.Write((short)Math.Round(extent.Extent.Angle * 65535.0f / 360.0f));
            raw.Write((ushort)Math.Round(extent.Extent.Length * 100.0f));
            raw.WriteInt16(0);
            raw.Write((short)Math.Round(extent.Extent.MinY * 100.0f));
            raw.Write((short)Math.Round(extent.Extent.MaxY * 100.0f));
        }

        private void Write(Attack attack)
        {
            raw.Write((int)attack.Bones);
            raw.Write(attack.Knockback);
            raw.Write((int)attack.Flags);
            raw.WriteInt16(attack.HitPoints);
            raw.WriteInt16(attack.Start);
            raw.WriteInt16(attack.End);
            raw.WriteInt16((short)attack.HitType);
            raw.WriteInt16(attack.HitLength);
            raw.WriteInt16(attack.StunLength);
            raw.WriteInt16(attack.StaggerLength);
            raw.WriteInt16(0);
            raw.Write(0);
        }

        private void WriteRawArray<T>(List<T> list, Action<T> writeElement)
        {
            if (list.Count == 0)
            {
                dat.Write(0);
                return;
            }

            dat.Write(raw.Align32());

            foreach (T t in list)
                writeElement(t);
        }

        private void GenerateExtentInfo()
        {
            float[] attackRing = animation.AttackRing;

            Array.Clear(attackRing, 0, attackRing.Length);

            foreach (var attack in animation.Attacks)
            {
                attack.Extents.Clear();

                for (int frame = attack.Start; frame <= attack.End; frame++)
                {
                    var position = animation.Positions[frame].XZ;
                    var framePoints = animation.AllPoints[frame];

                    for (int j = 0; j < framePoints.Count / 8; j++)
                    {
                        if ((attack.Bones & (BoneMask)(1 << j)) == 0)
                            continue;

                        for (int k = j * 8; k < (j + 1) * 8; k++)
                        {
                            var point = framePoints[k];
                            var delta = point.XZ - animation.Positions[0].XZ;

                            float distance = delta.Length();
                            float angle = FMath.Atan2(delta.X, delta.Y);

                            if (angle < 0.0f)
                                angle += MathHelper.TwoPi;

                            for (int r = 0; r < attackRing.Length; r++)
                            {
                                float ringAngle = r * MathHelper.TwoPi / attackRing.Length;

                                if (Math.Abs(ringAngle - angle) < MathHelper.ToRadians(30.0f))
                                    attackRing[r] = Math.Max(attackRing[r], distance);
                            }
                        }
                    }

                    float minHeight = +1e09f;
                    float maxHeight = -1e09f;
                    float maxDistance = -1e09f;
                    float maxAngle = 0.0f;

                    for (int j = 0; j < framePoints.Count / 8; j++)
                    {
                        if ((attack.Bones & (BoneMask)(1 << j)) == 0)
                            continue;

                        for (int k = j * 8; k < (j + 1) * 8; k++)
                        {
                            var point = framePoints[k];
                            var delta = point.XZ - position;

                            float distance;

                            switch (animation.Direction)
                            {
                                case Direction.Forward:
                                    distance = delta.Y;
                                    break;
                                case Direction.Left:
                                    distance = delta.X;
                                    break;
                                case Direction.Right:
                                    distance = -delta.X;
                                    break;
                                case Direction.Backward:
                                    distance = -delta.Y;
                                    break;
                                default:
                                    distance = delta.Length();
                                    break;
                            }

                            if (distance > maxDistance)
                            {
                                maxDistance = distance;
                                maxAngle = FMath.Atan2(delta.X, delta.Y);
                            }

                            minHeight = Math.Min(minHeight, point.Y);
                            maxHeight = Math.Max(maxHeight, point.Y);
                        }
                    }

                    maxDistance = Math.Max(maxDistance, 0.0f);

                    if (maxAngle < 0)
                        maxAngle += MathHelper.TwoPi;

                    attack.Extents.Add(new AttackExtent
                    {
                        Angle = MathHelper.ToDegrees(maxAngle),
                        Length = maxDistance,
                        MinY = minHeight,
                        MaxY = maxHeight
                    });
                }
            }
        }

        private void GenerateExtentSummary()
        {
            if (extents.Count == 0)
                return;

            var positions = animation.Positions;
            var attacks = animation.Attacks;
            var heights = animation.Heights;

            float minY = float.MaxValue, maxY = float.MinValue;

            foreach (var datExtent in extents)
            {
                minY = Math.Min(minY, datExtent.Extent.MinY);
                maxY = Math.Max(maxY, datExtent.Extent.MaxY);
            }

            var firstExtent = extents[0];
            var maxExtent = firstExtent;

            foreach (var datExtent in extents)
            {
                if (datExtent.Extent.Length + positions[datExtent.Frame].Z > maxExtent.Extent.Length + positions[maxExtent.Frame].Z)
                    maxExtent = datExtent;
            }

            int maxAttackIndex = 0, maxAttackOffset = 0;

            for (int i = 0; i < attacks.Count; i++)
            {
                var attack = attacks[i];

                if (attack.Start <= maxExtent.Frame && maxExtent.Frame <= attack.End)
                {
                    maxAttackIndex = i;
                    maxAttackOffset = maxExtent.Frame - attack.Start;
                    break;
                }
            }

            extentInfo.MaxDistance = animation.AttackRing.Max();
            extentInfo.MinY = minY;
            extentInfo.MaxY = maxY;

            extentInfo.FirstExtent.Frame = firstExtent.Frame;
            extentInfo.FirstExtent.Attack = 0;
            extentInfo.FirstExtent.AttackOffset = 0;
            extentInfo.FirstExtent.Location.X = positions[firstExtent.Frame].X;
            extentInfo.FirstExtent.Location.Y = -positions[firstExtent.Frame].Z;
            extentInfo.FirstExtent.Height = heights[firstExtent.Frame];
            extentInfo.FirstExtent.Angle = MathHelper.ToRadians(firstExtent.Extent.Angle);
            extentInfo.FirstExtent.Length = firstExtent.Extent.Length;
            extentInfo.FirstExtent.MinY = FMath.Round(firstExtent.Extent.MinY, 2);
            extentInfo.FirstExtent.MaxY = firstExtent.Extent.MaxY;

            if ((animation.Flags & AnimationFlags.ThrowTarget) == 0)
            {
                extentInfo.MaxExtent.Frame = maxExtent.Frame;
                extentInfo.MaxExtent.Attack = maxAttackIndex;
                extentInfo.MaxExtent.AttackOffset = maxAttackOffset;
                extentInfo.MaxExtent.Location.X = positions[maxExtent.Frame].X;
                extentInfo.MaxExtent.Location.Y = -positions[maxExtent.Frame].Z;
                extentInfo.MaxExtent.Height = heights[maxExtent.Frame];
                extentInfo.MaxExtent.Angle = MathHelper.ToRadians(maxExtent.Extent.Angle);
                extentInfo.MaxExtent.Length = maxExtent.Extent.Length;
                extentInfo.MaxExtent.MinY = maxExtent.Extent.MinY;
                extentInfo.MaxExtent.MaxY = FMath.Round(maxExtent.Extent.MaxY, 2);
            }
        }

        private List<List<KeyFrame>> CompressFrames(List<List<KeyFrame>> tracks)
        {
            float tolerance = 0.5f;
            float cosTolerance = FMath.Cos(MathHelper.ToRadians(tolerance) * 0.5f);
            var newTracks = new List<List<KeyFrame>>();

            foreach (var keys in tracks)
            {
                var newFrames = new List<KeyFrame>(keys.Count);

                for (int i = 0; i < keys.Count;)
                {
                    var key = keys[i];

                    int duration = key.Duration;
                    var q0 = new Quaternion(key.Rotation);

                    if (duration == 1)
                    {
                        for (int j = i + 2; j < keys.Count; j++)
                        {
                            if (!IsLinearRange(keys, i, j, cosTolerance))
                                break;

                            duration = j - i;
                        }
                    }

                    var eulerXYZ = q0.ToEulerXYZ();

                    newFrames.Add(new KeyFrame
                    {
                        Duration = duration,
                        Rotation = {
                            X = eulerXYZ.X,
                            Y = eulerXYZ.Y,
                            Z = eulerXYZ.Z
                        }
                    });

                    i += duration;
                }

                newTracks.Add(newFrames);
            }

            return newTracks;
        }

        private static bool IsLinearRange(List<KeyFrame> frames, int first, int last, float tolerance)
        {
            var q0 = new Quaternion(frames[first].Rotation);
            var q1 = new Quaternion(frames[last].Rotation);
            float length = last - first;

            for (int i = first + 1; i < last; ++i)
            {
                float t = (i - first) / length;

                var linear = Quaternion.Lerp(q0, q1, t);
                var real = new Quaternion(frames[i].Rotation);
                var error = Quaternion.Conjugate(linear) * real;

                if (Math.Abs(error.W) < tolerance)
                    return false;
            }

            return true;
        }
    }
}
