﻿using System;
using System.Collections.Generic;
using System.Xml;
using Oni.Metadata;

namespace Oni.Xml
{
    internal class ObjcXmlImporter : RawXmlImporter
    {
        private readonly Dictionary<ObjectMetadata.TypeTag, Action> typeReaders = new Dictionary<ObjectMetadata.TypeTag, Action>();
        private int nextId;

        private ObjcXmlImporter(XmlReader xml, BinaryWriter writer)
            : base(xml, writer)
        {
            InitTypeReaders(typeReaders);
        }

        public static void Import(XmlReader xml, BinaryWriter writer)
        {
            var importer = new ObjcXmlImporter(xml, writer);
            importer.Import();
        }

        private void Import()
        {
            Writer.Write(39);

            nextId = 1;

            while (Xml.IsStartElement())
            {
                int objectStartPosition = Writer.Position;
                Writer.Write(0);

                BeginStruct(objectStartPosition);

                ReadObject();

                Writer.Position = Utils.Align4(Writer.Position);

                int objectSize = Writer.Position - objectStartPosition - 4;
                Writer.WriteAt(objectStartPosition, objectSize);
            }

            Writer.Write(0);
        }

        private ObjectMetadata.TypeTag ReadObject()
        {
            var id = Xml.GetAttribute("Id");
            int objectId = string.IsNullOrEmpty(id) ? nextId++ : XmlConvert.ToInt32(id);
            var objectTag = Xml.GetAttribute("Type");

            if (objectTag == null)
                objectTag = Xml.LocalName;

            var objectType = MetaEnum.Parse<ObjectMetadata.TypeTag>(objectTag);

            Xml.ReadStartElement();
            Xml.MoveToContent();

            Writer.Write((int)objectType);
            Writer.Write(objectId);
            ObjectMetadata.Header.Accept(this);
            typeReaders[objectType]();

            Xml.ReadEndElement();

            return objectType;
        }

        private void ReadCharacter()
        {
            ObjectMetadata.Character.Accept(this);
        }

        private void ReadCombatProfile()
        {
            ObjectMetadata.CombatProfile.Accept(this);
        }

        private void ReadConsole()
        {
            Xml.ReadStartElement();
            Xml.MoveToContent();

            ReadStruct(ObjectMetadata.Console);
            ReadEventList();

            Xml.ReadEndElement();
        }

        private void ReadDoor()
        {
            Xml.ReadStartElement();
            Xml.MoveToContent();

            ReadStruct(ObjectMetadata.Door);
            ReadEventList();

            Xml.ReadEndElement();
        }

        private void ReadFlag()
        {
            ObjectMetadata.Flag.Accept(this);
        }

        private void ReadFurniture()
        {
            ObjectMetadata.Furniture.Accept(this);
        }

        private struct MeleeMove
        {
            public int Type;
            public float[] Params;
        }

        private void ReadMeleeProfile()
        {
            Xml.ReadStartElement();
            Xml.MoveToContent();

            ReadStruct(ObjectMetadata.MeleeProfile);

            int countFieldsPosition = Writer.Position;
            Writer.Write(0);
            Writer.Write(0);
            Writer.Write(0);
            Writer.Write(0);

            int attackCount = 0;
            int evadeCount = 0;
            int maneuverCount = 0;
            var moves = new List<MeleeMove>();

            attackCount = ReadMeleeTechniques("Attacks", moves);
            evadeCount = ReadMeleeTechniques("Evades", moves);
            maneuverCount = ReadMeleeTechniques("Maneuvers", moves);

            foreach (var move in moves)
            {
                Writer.Write(move.Type);
                Writer.Write(move.Params);
            }

            int oldPosition = Writer.Position;
            Writer.Position = countFieldsPosition;
            Writer.Write(attackCount);
            Writer.Write(evadeCount);
            Writer.Write(maneuverCount);
            Writer.Write(moves.Count);
            Writer.Position = oldPosition;

            Xml.ReadEndElement();
        }

        private int ReadMeleeTechniques(string xmlName, List<MeleeMove> moves)
        {
            if (Xml.IsStartElement(xmlName) && Xml.IsEmptyElement)
            {
                Xml.Skip();
                return 0;
            }

            Xml.ReadStartElement(xmlName);
            Xml.MoveToContent();

            int techniqueCount = 0;

            for (; Xml.IsStartElement("Technique"); techniqueCount++)
            {
                Xml.ReadStartElement();
                Xml.MoveToContent();

                ReadStruct(ObjectMetadata.MeleeTechnique);
                int moveCountPosition = Writer.Position;
                Writer.Write(0);
                Writer.Write(moves.Count);

                int moveCount = 0;

                if (Xml.IsStartElement("Moves") && Xml.IsEmptyElement)
                {
                    Xml.Skip();
                }
                else
                {
                    Xml.ReadStartElement("Moves");
                    Xml.MoveToContent();

                    for (; Xml.IsStartElement(); moveCount++)
                    {
                        ReadMeleeMove(moves);
                    }

                    Xml.ReadEndElement();
                }

                Xml.ReadEndElement();

                Writer.WriteAt(moveCountPosition, moveCount);
            }

            Xml.ReadEndElement();

            return techniqueCount;
        }

        private void ReadMeleeMove(List<MeleeMove> moves)
        {
            var category = (ObjectMetadata.MeleeMoveCategory)Enum.Parse(typeof(ObjectMetadata.MeleeMoveCategory), Xml.LocalName);

            var typeText = Xml.GetAttribute("Type");
            int type;
            var moveParams = new float[3];

            switch (category)
            {
                default:
                case ObjectMetadata.MeleeMoveCategory.Attack:
                    type = Convert.ToInt32(MetaEnum.Parse<ObjectMetadata.MeleeMoveAttackType>(typeText));
                    break;
                case ObjectMetadata.MeleeMoveCategory.Evade:
                    type = Convert.ToInt32(MetaEnum.Parse<ObjectMetadata.MeleeMoveEvadeType>(typeText));
                    break;
                case ObjectMetadata.MeleeMoveCategory.Throw:
                    type = Convert.ToInt32(MetaEnum.Parse<ObjectMetadata.MeleeMoveThrowType>(typeText));
                    break;

                case ObjectMetadata.MeleeMoveCategory.Position:
                    ObjectMetadata.MeleeMovePositionType positionType = MetaEnum.Parse<ObjectMetadata.MeleeMovePositionType>(typeText);

                    if ((ObjectMetadata.MeleeMovePositionType.RunForward <= positionType
                        && positionType <= ObjectMetadata.MeleeMovePositionType.RunBack)
                            || ObjectMetadata.MeleeMovePositionType.CloseForward <= positionType)
                    {
                        moveParams[0] = XmlConvert.ToSingle(Xml.GetAttribute("MinRunInDist"));
                        moveParams[1] = XmlConvert.ToSingle(Xml.GetAttribute("MaxRunInDist"));
                        moveParams[2] = XmlConvert.ToSingle(Xml.GetAttribute("ToleranceRange"));
                    }

                    type = Convert.ToInt32(positionType);
                    break;

                case ObjectMetadata.MeleeMoveCategory.Maneuver:
                    type = Convert.ToInt32(MetaEnum.Parse<ObjectMetadata.MeleeMoveManeuverType>(typeText));
                    ObjectMetadata.MeleeMoveTypeInfo typeInfo = ObjectMetadata.MeleeMoveManeuverTypeInfo[type];

                    for (int k = 0; k < typeInfo.ParamNames.Length; k++)
                        moveParams[k] = XmlConvert.ToSingle(Xml.GetAttribute(typeInfo.ParamNames[k]));

                    break;
            }

            moves.Add(new MeleeMove()
            {
                Type = (((int)category) << 24) | (type & 0xffffff),
                Params = moveParams
            });

            Xml.Skip();
        }

        private void ReadNeutralBehavior()
        {
            Xml.ReadStartElement();
            Xml.MoveToContent();

            ReadStruct(ObjectMetadata.NeutralBehavior);
            int countFieldPosition = Writer.Position;
            Writer.WriteUInt16(0);
            ReadStruct(ObjectMetadata.NeutralBehaviorParams);

            Xml.ReadStartElement("DialogLines");
            short count = 0;

            for (; Xml.IsStartElement("DialogLine"); count++)
                ObjectMetadata.NeutralBehaviorDialogLine.Accept(this);

            Xml.ReadEndElement();
            Xml.ReadEndElement();

            Writer.WriteAt(countFieldPosition, count);
        }

        private void ReadParticle()
        {
            ObjectMetadata.Particle.Accept(this);
        }

        private void ReadPatrolPath()
        {
            Xml.ReadStartElement();
            Xml.MoveToContent();

            ReadStruct(ObjectMetadata.PatrolPath);
            int lengthFieldPosition = Writer.Position;
            Writer.Write(0);
            ReadStruct(ObjectMetadata.PatrolPathInfo);

            int count = 0;
            bool isEmpty = Xml.IsEmptyElement;

            Xml.ReadStartElement("Points");

            if (!isEmpty)
            {
                int loopStart = -1;

                for (; Xml.IsStartElement(); count++)
                {
                    if (ReadPatrolPathPoint())
                        loopStart = count;
                }

                if (loopStart != -1)
                {
                    if (loopStart == 0)
                    {
                        Writer.Write((int)ObjectMetadata.PatrolPathPointType.Loop);
                    }
                    else
                    {
                        Writer.Write((int)ObjectMetadata.PatrolPathPointType.LoopFrom);
                        Writer.Write(loopStart);
                    }

                    if (Xml.NodeType == XmlNodeType.EndElement && Xml.LocalName == "Loop")
                        Xml.ReadEndElement();
                }

                Xml.ReadEndElement();
            }

            Xml.ReadEndElement();

            Writer.WriteAt(lengthFieldPosition, count);
        }

        private bool ReadPatrolPathPoint()
        {
            var pointType = MetaEnum.Parse<ObjectMetadata.PatrolPathPointType>(Xml.LocalName);

            switch (pointType)
            {
                case ObjectMetadata.PatrolPathPointType.Loop:
                    if (Xml.IsEmptyElement)
                    {
                        Xml.Skip();
                    }
                    else
                    {
                        Xml.ReadStartElement();
                        Xml.MoveToContent();
                    }
                    return true;
            }

            Writer.Write((int)pointType);

            switch (pointType)
            {
                case ObjectMetadata.PatrolPathPointType.Stop:
                case ObjectMetadata.PatrolPathPointType.StopLooking:
                case ObjectMetadata.PatrolPathPointType.StopScanning:
                case ObjectMetadata.PatrolPathPointType.FreeFacing:
                    break;

                case ObjectMetadata.PatrolPathPointType.IgnorePlayer:
                    Writer.WriteByte(Xml.GetAttribute("Value") == "Yes" ? 1 : 0);
                    break;

                case ObjectMetadata.PatrolPathPointType.ForkScript:
                case ObjectMetadata.PatrolPathPointType.CallScript:
                    Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("ScriptId")));
                    break;

                case ObjectMetadata.PatrolPathPointType.MoveToFlag:
                case ObjectMetadata.PatrolPathPointType.LookAtFlag:
                case ObjectMetadata.PatrolPathPointType.MoveAndFaceFlag:
                    Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("FlagId")));
                    break;

                case ObjectMetadata.PatrolPathPointType.MovementMode:
                    Writer.Write(Convert.ToInt32(MetaEnum.Parse<ObjectMetadata.PatrolPathMovementMode>(Xml.GetAttribute("Mode"))));
                    break;

                case ObjectMetadata.PatrolPathPointType.LockFacing:
                    Writer.Write(Convert.ToInt32(MetaEnum.Parse<ObjectMetadata.PatrolPathFacing>(Xml.GetAttribute("Facing"))));
                    break;

                case ObjectMetadata.PatrolPathPointType.Pause:
                    Writer.Write(XmlConvert.ToInt32(Xml.GetAttribute("Frames")));
                    break;

                case ObjectMetadata.PatrolPathPointType.GlanceAtFlagFor:
                    Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("FlagId")));
                    Writer.Write(XmlConvert.ToInt32(Xml.GetAttribute("Frames")));
                    break;

                case ObjectMetadata.PatrolPathPointType.Scan:
                    Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("Frames")));
                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("Rotation")));
                    break;

                case ObjectMetadata.PatrolPathPointType.MoveThroughFlag:
                case ObjectMetadata.PatrolPathPointType.MoveNearFlag:
                    Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("FlagId")));
                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("Distance")));
                    break;

                case ObjectMetadata.PatrolPathPointType.MoveToFlagLookAndWait:
                    Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("Frames")));
                    Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("FlagId")));
                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("Rotation")));
                    break;

                case ObjectMetadata.PatrolPathPointType.FaceToFlagAndFire:
                    Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("FlagId")));
                    Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("Frames")));
                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("Spread")));
                    break;

                case ObjectMetadata.PatrolPathPointType.LookAtPoint:
                case ObjectMetadata.PatrolPathPointType.MoveToPoint:
                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("X")));
                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("Y")));
                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("Z")));
                    break;

                case ObjectMetadata.PatrolPathPointType.MoveThroughPoint:
                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("X")));
                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("Y")));
                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("Z")));
                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("Distance")));
                    break;

                default:
                    throw new NotSupportedException(string.Format("Unsupported path point type {0}", pointType));
            }

            Xml.Skip();

            return false;
        }

        private void ReadPowerUp()
        {
            ObjectMetadata.PowerUp.Accept(this);
        }

        private void ReadSound()
        {
            Xml.ReadStartElement();
            Xml.MoveToContent();

            ReadStruct(ObjectMetadata.Sound);

            var volumeType = MetaEnum.Parse<ObjectMetadata.SoundVolumeType>(Xml.LocalName);

            Writer.Write((int)volumeType);

            switch (volumeType)
            {
                case ObjectMetadata.SoundVolumeType.Box:
                    MetaType.BoundingBox.Accept(this);
                    break;

                case ObjectMetadata.SoundVolumeType.Sphere:
                    ObjectMetadata.SoundSphere.Accept(this);
                    break;
            }

            ReadStruct(ObjectMetadata.SoundParams);

            if (volumeType == ObjectMetadata.SoundVolumeType.Sphere)
                Writer.Skip(16);

            Xml.ReadEndElement();
        }

        private void ReadTriggerVolume()
        {
            ObjectMetadata.TriggerVolume.Accept(this);
        }

        private void ReadTrigger()
        {
            Xml.ReadStartElement();
            Xml.MoveToContent();

            ReadStruct(ObjectMetadata.Trigger);
            ReadEventList();

            Xml.ReadEndElement();
        }

        private void ReadTurret()
        {
            ObjectMetadata.Turret.Accept(this);
        }

        private void ReadWeapon()
        {
            ObjectMetadata.Weapon.Accept(this);
        }

        private void ReadEventList()
        {
            int countFieldPosition = Writer.Position;
            Writer.WriteUInt16(0);

            if (Xml.IsStartElement("Events") && Xml.IsEmptyElement)
            {
                Xml.ReadStartElement();
                return;
            }

            Xml.ReadStartElement("Events");
            Xml.MoveToContent();

            short eventCount = 0;

            while (Xml.IsStartElement())
            {
                var eventType = MetaEnum.Parse<ObjectMetadata.EventType>(Xml.Name);

                Writer.Write((short)eventType);

                switch (eventType)
                {
                    case ObjectMetadata.EventType.None:
                        break;
                    case ObjectMetadata.EventType.Script:
                        Writer.Write(Xml.GetAttribute("Function"), 32);
                        break;
                    default:
                        Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("TargetId")));
                        break;
                }

                eventCount++;
                Xml.Skip();
            }

            Writer.WriteAt(countFieldPosition, eventCount);
            Xml.ReadEndElement();
        }

        private void InitTypeReaders(Dictionary<ObjectMetadata.TypeTag, Action> typeReaders)
        {
            typeReaders.Add(ObjectMetadata.TypeTag.CHAR, ReadCharacter);
            typeReaders.Add(ObjectMetadata.TypeTag.CMBT, ReadCombatProfile);
            typeReaders.Add(ObjectMetadata.TypeTag.CONS, ReadConsole);
            typeReaders.Add(ObjectMetadata.TypeTag.DOOR, ReadDoor);
            typeReaders.Add(ObjectMetadata.TypeTag.FLAG, ReadFlag);
            typeReaders.Add(ObjectMetadata.TypeTag.FURN, ReadFurniture);
            typeReaders.Add(ObjectMetadata.TypeTag.MELE, ReadMeleeProfile);
            typeReaders.Add(ObjectMetadata.TypeTag.NEUT, ReadNeutralBehavior);
            typeReaders.Add(ObjectMetadata.TypeTag.PART, ReadParticle);
            typeReaders.Add(ObjectMetadata.TypeTag.PATR, ReadPatrolPath);
            typeReaders.Add(ObjectMetadata.TypeTag.PWRU, ReadPowerUp);
            typeReaders.Add(ObjectMetadata.TypeTag.SNDG, ReadSound);
            typeReaders.Add(ObjectMetadata.TypeTag.TRGV, ReadTriggerVolume);
            typeReaders.Add(ObjectMetadata.TypeTag.TRIG, ReadTrigger);
            typeReaders.Add(ObjectMetadata.TypeTag.TURR, ReadTurret);
            typeReaders.Add(ObjectMetadata.TypeTag.WEAP, ReadWeapon);
        }
    }
}
