﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using Oni.Metadata;
using Oni.Xml;

namespace Oni.Totoro
{
    internal class AnimationXmlReader
    {
        private const string ns = "";
        private static readonly char[] emptyChars = new char[0];
        private XmlReader xml;
        private string basePath;
        private Animation animation;
        private AnimationDaeReader daeReader;

        private AnimationXmlReader()
        {
        }

        public static Animation Read(XmlReader xml, string baseDir)
        {
            var reader = new AnimationXmlReader
            {
                xml = xml,
                basePath = baseDir,
                animation = new Animation()
            };

            var animation = reader.Read();
            animation.ValidateFrames();
            return animation;
        }

        private Animation Read()
        {
            animation.Name = xml.GetAttribute("Name");
            xml.ReadStartElement("Animation", ns);

            if (xml.IsStartElement("DaeImport") || xml.IsStartElement("Import"))
                ImportDaeAnimation();

            xml.ReadStartElement("Lookup");
            animation.Type = MetaEnum.Parse<AnimationType>(xml.ReadElementContentAsString("Type", ns));
            animation.AimingType = MetaEnum.Parse<AnimationType>(xml.ReadElementContentAsString("AimingType", ns));
            animation.FromState = MetaEnum.Parse<AnimationState>(xml.ReadElementContentAsString("FromState", ns));
            animation.ToState = MetaEnum.Parse<AnimationState>(xml.ReadElementContentAsString("ToState", ns));
            animation.Varient = MetaEnum.Parse<AnimationVarient>(xml.ReadElementContentAsString("Varient", ns));
            animation.FirstLevelAvailable = xml.ReadElementContentAsInt("FirstLevel", ns);
            ReadRawArray("Shortcuts", animation.Shortcuts, Read);
            xml.ReadEndElement();

            animation.Flags = MetaEnum.Parse<AnimationFlags>(xml.ReadElementContentAsString("Flags", ns));
            xml.ReadStartElement("Atomic", ns);
            animation.AtomicStart = xml.ReadElementContentAsInt("Start", ns);
            animation.AtomicEnd = xml.ReadElementContentAsInt("End", ns);
            xml.ReadEndElement();
            xml.ReadStartElement("Invulnerable", ns);
            animation.InvulnerableStart = xml.ReadElementContentAsInt("Start", ns);
            animation.InvulnerableEnd = xml.ReadElementContentAsInt("End", ns);
            xml.ReadEndElement();
            xml.ReadStartElement("Overlay", ns);
            animation.OverlayUsedBones = MetaEnum.Parse<BoneMask>(xml.ReadElementContentAsString("UsedBones", ns));
            animation.OverlayReplacedBones = MetaEnum.Parse<BoneMask>(xml.ReadElementContentAsString("ReplacedBones", ns));
            xml.ReadEndElement();

            xml.ReadStartElement("DirectAnimations", ns);
            animation.DirectAnimations[0] = xml.ReadElementContentAsString("Link", ns);
            animation.DirectAnimations[1] = xml.ReadElementContentAsString("Link", ns);
            xml.ReadEndElement();
            xml.ReadStartElement("Pause");
            animation.HardPause = xml.ReadElementContentAsInt("Hard", ns);
            animation.SoftPause = xml.ReadElementContentAsInt("Soft", ns);
            xml.ReadEndElement();
            xml.ReadStartElement("Interpolation", ns);
            animation.InterpolationEnd = xml.ReadElementContentAsInt("End", ns);
            animation.InterpolationMax = xml.ReadElementContentAsInt("Max", ns);
            xml.ReadEndElement();

            animation.FinalRotation = MathHelper.ToRadians(xml.ReadElementContentAsFloat("FinalRotation", ns));
            animation.Direction = MetaEnum.Parse<Direction>(xml.ReadElementContentAsString("Direction", ns));
            animation.Vocalization = xml.ReadElementContentAsInt("Vocalization", ns);
            animation.ActionFrame = xml.ReadElementContentAsInt("ActionFrame", ns);
            animation.Impact = xml.ReadElementContentAsString("Impact", ns);

            ReadRawArray("Particle", animation.Particles, Read);
            ReadRawArray("MotionBlur", animation.MotionBlur, Read);
            ReadRawArray("Footsteps", animation.Footsteps, Read);
            ReadRawArray("Sounds", animation.Sounds, Read);

            if (daeReader == null)
            {
                ReadHeights();
                ReadVelocities();
                ReadRotations();
                ReadPositions();
            }

            ReadThrowInfo();
            ReadRawArray("SelfDamage", animation.SelfDamage, Read);

            if (xml.IsStartElement("Attacks"))
            {
                ReadRawArray("Attacks", animation.Attacks, Read);
                ReadAttackRing();
            }

            xml.ReadEndElement();

            if (daeReader != null)
            {
                daeReader.Read(animation);
            }

            return animation;
        }

        private void ReadVelocities()
        {
            if (!xml.IsStartElement("Velocities"))
                return;

            if (xml.SkipEmpty())
                return;

            xml.ReadStartElement();

            while (xml.IsStartElement())
                animation.Velocities.Add(xml.ReadElementContentAsVector2());

            xml.ReadEndElement();
        }

        private void ReadPositions()
        {
            var xz = new Vector2();

            if (xml.IsStartElement("PositionOffset"))
            {
                xml.ReadStartElement();
                xz.X = xml.ReadElementContentAsFloat("X", ns);
                xz.Y = xml.ReadElementContentAsFloat("Z", ns);
                xml.ReadEndElement();
            }

            ReadRawArray("Positions", animation.Positions, ReadPosition);

            for (int i = 0; i < animation.Positions.Count; i++)
            {
                var position = animation.Positions[i];
                position.X = xz.X;
                position.Z = xz.Y;
                xz += animation.Velocities[i];
            }
        }

        private void ReadRotations()
        {
            var rotations = animation.Rotations;

            xml.ReadStartElement("Rotations");

            while (xml.IsStartElement())
            {
                xml.ReadStartElement("Bone");

                var keys = new List<KeyFrame>();

                int count = 0;

                while (xml.IsStartElement())
                {
                    string name = xml.LocalName;
                    string[] tokens = xml.ReadElementContentAsString().Split(emptyChars, StringSplitOptions.RemoveEmptyEntries);

                    var key = new KeyFrame();
                    key.Duration = XmlConvert.ToByte(tokens[0]);

                    switch (name)
                    {
                        case "EKey":
                            animation.FrameSize = 6;
                            key.Rotation.X = XmlConvert.ToSingle(tokens[1]);
                            key.Rotation.Y = XmlConvert.ToSingle(tokens[2]);
                            key.Rotation.Z = XmlConvert.ToSingle(tokens[3]);
                            break;

                        case "QKey":
                            animation.FrameSize = 16;
                            key.Rotation.X = XmlConvert.ToSingle(tokens[1]);
                            key.Rotation.Y = XmlConvert.ToSingle(tokens[2]);
                            key.Rotation.Z = XmlConvert.ToSingle(tokens[3]);
                            key.Rotation.W = -XmlConvert.ToSingle(tokens[4]);
                            break;

                        default:
                            throw new InvalidDataException(string.Format("Unknonw animation key type '{0}'", name));
                    }

                    count += key.Duration;
                    keys.Add(key);
                }

                if (count != animation.Velocities.Count)
                    throw new InvalidDataException("bad number of frames");

                rotations.Add(keys);
                xml.ReadEndElement();
            }

            xml.ReadEndElement();
        }

        private void ReadHeights()
        {
            if (!xml.IsStartElement("Heights"))
                return;

            if (xml.SkipEmpty())
                return;

            xml.ReadStartElement();

            while (xml.IsStartElement())
                animation.Heights.Add(xml.ReadElementContentAsFloat("Height", ns));

            xml.ReadEndElement();
        }

        private void ReadThrowInfo()
        {
            if (!xml.IsStartElement("ThrowSource"))
                return;

            if (xml.SkipEmpty())
                return;

            animation.ThrowSource = new ThrowInfo();
            xml.ReadStartElement("ThrowSource");
            xml.ReadStartElement("TargetAdjustment");
            animation.ThrowSource.Position = xml.ReadElementContentAsVector3("Position");
            animation.ThrowSource.Angle = xml.ReadElementContentAsFloat("Angle", ns);
            xml.ReadEndElement();
            animation.ThrowSource.Distance = xml.ReadElementContentAsFloat("Distance", ns);
            animation.ThrowSource.Type = MetaEnum.Parse<AnimationType>(xml.ReadElementContentAsString("TargetType", ns));
            xml.ReadEndElement();
        }

        private void ReadAttackRing()
        {
            if (!xml.IsStartElement("AttackRing") && !xml.IsStartElement("HorizontalExtents"))
                return;

            if (animation.Attacks.Count == 0)
            {
                Console.Error.WriteLine("Warning: AttackRing found but no attacks are present, ignoring");
                xml.Skip();
                return;
            }

            xml.ReadStartElement();

            for (int i = 0; i < 36; i++)
                animation.AttackRing[i] = xml.ReadElementContentAsFloat();

            xml.ReadEndElement();
        }

        private void ReadPosition(Position position)
        {
            xml.ReadStartElement("Position");
            position.Height = xml.ReadElementContentAsFloat("Height", ns);
            position.YOffset = xml.ReadElementContentAsFloat("YOffset", ns);
            xml.ReadEndElement();
        }

        private void Read(Particle particle)
        {
            xml.ReadStartElement("Particle");
            particle.Start = xml.ReadElementContentAsInt("Start", ns);
            particle.End = xml.ReadElementContentAsInt("End", ns);
            particle.Bone = MetaEnum.Parse<Bone>(xml.ReadElementContentAsString("Bone", ns));
            particle.Name = xml.ReadElementContentAsString("Name", ns);
            xml.ReadEndElement();
        }

        private void Read(Sound sound)
        {
            xml.ReadStartElement("Sound");
            sound.Name = xml.ReadElementContentAsString("Name", ns);
            sound.Start = xml.ReadElementContentAsInt("Start", ns);
            xml.ReadEndElement();
        }

        private void Read(Shortcut shortcut)
        {
            xml.ReadStartElement("Shortcut");
            shortcut.FromState = MetaEnum.Parse<AnimationState>(xml.ReadElementContentAsString("FromState", ns));
            shortcut.Length = xml.ReadElementContentAsInt("Length", ns);
            shortcut.ReplaceAtomic = (xml.ReadElementContentAsString("ReplaceAtomic", ns) == "yes");
            xml.ReadEndElement();
        }

        private void Read(Footstep footstep)
        {
            xml.ReadStartElement("Footstep");

            string frame = xml.GetAttribute("Frame");

            if (frame != null)
            {
                footstep.Frame = XmlConvert.ToInt32(frame);
                footstep.Type = MetaEnum.Parse<FootstepType>(xml.GetAttribute("Type"));
            }
            else
            {
                footstep.Frame = xml.ReadElementContentAsInt("Frame", ns);
                footstep.Type = MetaEnum.Parse<FootstepType>(xml.ReadElementContentAsString("Type", ns));
            }

            xml.ReadEndElement();
        }

        private void Read(Damage damage)
        {
            xml.ReadStartElement("Damage");
            damage.Points = xml.ReadElementContentAsInt("Points", ns);
            damage.Frame = xml.ReadElementContentAsInt("Frame", ns);
            xml.ReadEndElement();
        }

        private void Read(MotionBlur d)
        {
            xml.ReadStartElement("MotionBlur");
            d.Bones = MetaEnum.Parse<BoneMask>(xml.ReadElementContentAsString("Bones", ns));
            d.Start = xml.ReadElementContentAsInt("Start", ns);
            d.End = xml.ReadElementContentAsInt("End", ns);
            d.Lifetime = xml.ReadElementContentAsInt("Lifetime", ns);
            d.Alpha = xml.ReadElementContentAsInt("Alpha", ns);
            d.Interval = xml.ReadElementContentAsInt("Interval", ns);
            xml.ReadEndElement();
        }

        private void Read(Attack attack)
        {
            xml.ReadStartElement("Attack");
            attack.Start = xml.ReadElementContentAsInt("Start", ns);
            attack.End = xml.ReadElementContentAsInt("End", ns);
            attack.Bones = MetaEnum.Parse<BoneMask>(xml.ReadElementContentAsString("Bones", ns));
            attack.Flags = MetaEnum.Parse<AttackFlags>(xml.ReadElementContentAsString("Flags", ns));
            attack.Knockback = xml.ReadElementContentAsFloat("Knockback", ns);
            attack.HitPoints = xml.ReadElementContentAsInt("HitPoints", ns);
            attack.HitType = MetaEnum.Parse<AnimationType>(xml.ReadElementContentAsString("HitType", ns));
            attack.HitLength = xml.ReadElementContentAsInt("HitLength", ns);
            attack.StunLength = xml.ReadElementContentAsInt("StunLength", ns);
            attack.StaggerLength = xml.ReadElementContentAsInt("StaggerLength", ns);

            if (xml.IsStartElement("Extents"))
            {
                ReadRawArray("Extents", attack.Extents, Read);

                if (attack.Extents.Count != attack.End - attack.Start + 1)
                    Console.Error.WriteLine("Error: Attack starting at frame {0} has an incorrect number of extents ({1})", attack.Start, attack.Extents.Count);
            }

            xml.ReadEndElement();
        }

        private void Read(AttackExtent extent)
        {
            xml.ReadStartElement("Extent");
            extent.Angle = xml.ReadElementContentAsFloat("Angle", ns);
            extent.Length = xml.ReadElementContentAsFloat("Length", ns);
            extent.MinY = xml.ReadElementContentAsFloat("MinY", ns);
            extent.MaxY = xml.ReadElementContentAsFloat("MaxY", ns);
            xml.ReadEndElement();
        }

        private void ReadRawArray<T>(string name, List<T> list, Action<T> elementReader)
            where T : new()
        {
            if (xml.SkipEmpty())
                return;

            xml.ReadStartElement();

            while (xml.IsStartElement())
            {
                T t = new T();
                elementReader(t);
                list.Add(t);
            }

            xml.ReadEndElement();
        }

        private void ImportDaeAnimation()
        {
            string filePath = xml.GetAttribute("Path");
            bool empty = xml.SkipEmpty();

            if (!empty)
            {
                xml.ReadStartElement();

                if (filePath == null)
                    filePath = xml.ReadElementContentAsString("Path", ns);
            }

            filePath = Path.Combine(basePath, filePath);

            if (!File.Exists(filePath))
            {
                Console.Error.WriteLine("Could not find animation import source file '{0}'", filePath);
                return;
            }

            Console.WriteLine("Importing {0}", filePath);

            daeReader = new AnimationDaeReader();
            daeReader.Scene = Dae.Reader.ReadFile(filePath);

            if (!empty)
            {
                if (xml.IsStartElement("Start"))
                    daeReader.StartFrame = xml.ReadElementContentAsInt("Start", ns);

                if (xml.IsStartElement("End"))
                    daeReader.EndFrame = xml.ReadElementContentAsInt("End", ns);

                xml.ReadEndElement();
            }
        }
    }
}
