﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml;
using Oni.Metadata;

namespace Oni.Xml
{
    internal class ObjcXmlExporter : RawXmlExporter
    {
        private readonly Dictionary<ObjectMetadata.TypeTag, Action> typeWriters = new Dictionary<ObjectMetadata.TypeTag, Action>();
        private int objectEndPosition;

        private ObjcXmlExporter(BinaryReader reader, XmlWriter xml)
            : base(reader, xml)
        {
            InitTypeWriters(typeWriters);
        }

        public static void Export(BinaryReader reader, XmlWriter xml)
        {
            var exporter = new ObjcXmlExporter(reader, xml);
            exporter.Export();
        }

        private void Export()
        {
            int size = Reader.ReadInt32();
            int version = Reader.ReadInt32();

            Xml.WriteStartElement("Objects");

            while (true)
            {
                int objectSize = Reader.ReadInt32();

                if (objectSize == 0)
                    break;

                int readerPosition = Reader.Position;
                objectEndPosition = readerPosition + objectSize;

                BeginStruct(Reader.Position);

                var objectType = (ObjectMetadata.TypeTag)Reader.ReadInt32();
                int objectId = Reader.ReadInt32();

                Xml.WriteStartElement(objectType.ToString());
                Xml.WriteAttributeString("Id", XmlConvert.ToString(objectId));

                Xml.WriteStartElement("Header");
                ObjectMetadata.Header.Accept(this);
                Xml.WriteEndElement();

                Xml.WriteStartElement("OSD");
                typeWriters[objectType]();
                Xml.WriteEndElement();

                Xml.WriteEndElement();

                Reader.Position = objectEndPosition;
            }

            Xml.WriteEndElement();
        }

        private void WriteCharacter()
        {
            ObjectMetadata.Character.Accept(this);
        }

        private void WriteCombatProfile()
        {
            ObjectMetadata.CombatProfile.Accept(this);
        }

        private void WriteConsole()
        {
            ObjectMetadata.Console.Accept(this);
            WriteEventList();
        }

        private void WriteDoor()
        {
            ObjectMetadata.Door.Accept(this);
            WriteEventList();
        }

        private void WriteFlag()
        {
            ObjectMetadata.Flag.Accept(this);
        }

        private void WriteFurniture()
        {
            ObjectMetadata.Furniture.Accept(this);
        }

        private void WriteMeleeProfile()
        {
            ObjectMetadata.MeleeProfile.Accept(this);

            int attackCount = Reader.ReadInt32();
            int evadeCount = Reader.ReadInt32();
            int maneuverCount = Reader.ReadInt32();
            int moveCount = Reader.ReadInt32();

            int moveTablePosition = Reader.Position + (attackCount + evadeCount + maneuverCount) * 88;

            Xml.WriteStartElement("Attacks");
            for (int i = 0; i < attackCount; i++)
                WriteMeleeTechnique(moveTablePosition);
            Xml.WriteEndElement();

            Xml.WriteStartElement("Evades");
            for (int i = 0; i < evadeCount; i++)
                WriteMeleeTechnique(moveTablePosition);
            Xml.WriteEndElement();

            Xml.WriteStartElement("Maneuvers");
            for (int i = 0; i < maneuverCount; i++)
                WriteMeleeTechnique(moveTablePosition);
            Xml.WriteEndElement();
        }

        private void WriteMeleeTechnique(int moveTablePosition)
        {
            Xml.WriteStartElement("Technique");

            ObjectMetadata.MeleeTechnique.Accept(this);

            int moveCount = Reader.ReadInt32();
            int moveStart = Reader.ReadInt32();

            int oldPosition = Reader.Position;
            Reader.Position = moveTablePosition + moveStart * 16;

            Xml.WriteStartElement("Moves");

            for (int j = 0; j < moveCount; j++)
                WriteMeleeMove();

            Xml.WriteEndElement();
            Xml.WriteEndElement();

            Reader.Position = oldPosition;
        }

        private void WriteMeleeMove()
        {
            int categoryType = Reader.ReadInt32();
            float[] moveParams = Reader.ReadSingleArray(3);

            var category = (ObjectMetadata.MeleeMoveCategory)(categoryType >> 24);
            Xml.WriteStartElement(category.ToString());

            switch (category)
            {
                default:
                case ObjectMetadata.MeleeMoveCategory.Attack:
                    Xml.WriteAttributeString("Type", ((ObjectMetadata.MeleeMoveAttackType)(categoryType & 0xffffff)).ToString());
                    break;

                case ObjectMetadata.MeleeMoveCategory.Evade:
                    Xml.WriteAttributeString("Type", ((ObjectMetadata.MeleeMoveEvadeType)(categoryType & 0xffffff)).ToString());
                    break;

                case ObjectMetadata.MeleeMoveCategory.Throw:
                    Xml.WriteAttributeString("Type", ((ObjectMetadata.MeleeMoveThrowType)(categoryType & 0xffffff)).ToString());
                    break;

                case ObjectMetadata.MeleeMoveCategory.Maneuver:
                    ObjectMetadata.MeleeMoveTypeInfo typeInfo = ObjectMetadata.MeleeMoveManeuverTypeInfo[categoryType & 0xffffff];
                    Xml.WriteAttributeString("Type", typeInfo.Type.ToString());

                    for (int k = 0; k < typeInfo.ParamNames.Length; k++)
                        Xml.WriteAttributeString(typeInfo.ParamNames[k], XmlConvert.ToString(moveParams[k]));

                    break;

                case ObjectMetadata.MeleeMoveCategory.Position:
                    ObjectMetadata.MeleeMovePositionType moveType = (ObjectMetadata.MeleeMovePositionType)(categoryType & 0xffffff);
                    Xml.WriteAttributeString("Type", moveType.ToString());

                    if ((ObjectMetadata.MeleeMovePositionType.RunForward <= moveType && moveType <= ObjectMetadata.MeleeMovePositionType.RunBack)
                        || ObjectMetadata.MeleeMovePositionType.CloseForward <= moveType)
                    {
                        Xml.WriteAttributeString("MinRunInDist", XmlConvert.ToString(moveParams[0]));
                        Xml.WriteAttributeString("MaxRunInDist", XmlConvert.ToString(moveParams[1]));
                        Xml.WriteAttributeString("ToleranceRange", XmlConvert.ToString(moveParams[2]));
                    }

                    break;
            }

            Xml.WriteEndElement();
        }

        private void WriteNeutralBehavior()
        {
            ObjectMetadata.NeutralBehavior.Accept(this);
            int lineCount = Reader.ReadInt16();
            ObjectMetadata.NeutralBehaviorParams.Accept(this);

            Xml.WriteStartElement("DialogLines");
            MetaType.Array(lineCount, ObjectMetadata.NeutralBehaviorDialogLine).Accept(this);
            Xml.WriteEndElement();
        }

        private void WriteParticle()
        {
            ObjectMetadata.Particle.Accept(this);
        }

        private void WritePatrolPath()
        {
            ObjectMetadata.PatrolPath.Accept(this);
            int length = Reader.ReadInt32();
            ObjectMetadata.PatrolPathInfo.Accept(this);

            int startPosition = Reader.Position;
            int loopStartPoint = -1;

            var pointType = (ObjectMetadata.PatrolPathPointType)Reader.ReadInt32();

            for (int i = 0; i < length; i++)
            {
                if (pointType == ObjectMetadata.PatrolPathPointType.Loop)
                    loopStartPoint = 0;
                else if (pointType == ObjectMetadata.PatrolPathPointType.LoopFrom)
                    loopStartPoint = Reader.ReadInt32();
                else
                    Reader.Position += ObjectMetadata.GetPatrolPathPointSize(pointType);

                pointType = (ObjectMetadata.PatrolPathPointType)Reader.ReadInt32();
            }

            Reader.Position = startPosition;

            Xml.WriteStartElement("Points");

            for (int i = 0; i < length; i++)
            {
                if (loopStartPoint == i)
                    Xml.WriteStartElement("Loop");

                WritePatrolPathPoint();
            }

            if (loopStartPoint != -1)
                Xml.WriteEndElement();

            Xml.WriteEndElement();
        }

        private void WritePatrolPathPoint()
        {
            var pointType = (ObjectMetadata.PatrolPathPointType)Reader.ReadInt32();

            switch (pointType)
            {
                case ObjectMetadata.PatrolPathPointType.Loop:
                    return;

                case ObjectMetadata.PatrolPathPointType.LoopFrom:
                    Reader.Skip(4);
                    return;
            }

            Xml.WriteStartElement(pointType.ToString());

            switch (pointType)
            {
                case ObjectMetadata.PatrolPathPointType.Stop:
                case ObjectMetadata.PatrolPathPointType.StopLooking:
                case ObjectMetadata.PatrolPathPointType.StopScanning:
                case ObjectMetadata.PatrolPathPointType.FreeFacing:
                    break;

                case ObjectMetadata.PatrolPathPointType.IgnorePlayer:
                    Xml.WriteAttributeString("Value", Reader.ReadBoolean() ? "Yes" : "No");
                    break;

                case ObjectMetadata.PatrolPathPointType.MoveToFlag:
                case ObjectMetadata.PatrolPathPointType.LookAtFlag:
                case ObjectMetadata.PatrolPathPointType.MoveAndFaceFlag:
                    Xml.WriteAttributeString("FlagId", XmlConvert.ToString(Reader.ReadInt16()));
                    break;

                case ObjectMetadata.PatrolPathPointType.ForkScript:
                case ObjectMetadata.PatrolPathPointType.CallScript:
                    Xml.WriteAttributeString("ScriptId", XmlConvert.ToString(Reader.ReadInt16()));
                    break;

                case ObjectMetadata.PatrolPathPointType.Pause:
                    Xml.WriteAttributeString("Frames", XmlConvert.ToString(Reader.ReadInt32()));
                    break;

                case ObjectMetadata.PatrolPathPointType.MovementMode:
                    Xml.WriteAttributeString("Mode", ((ObjectMetadata.PatrolPathMovementMode)Reader.ReadInt32()).ToString());
                    break;

                case ObjectMetadata.PatrolPathPointType.LockFacing:
                    Xml.WriteAttributeString("Facing", ((ObjectMetadata.PatrolPathFacing)Reader.ReadInt32()).ToString());
                    break;

                case ObjectMetadata.PatrolPathPointType.MoveThroughFlag:
                case ObjectMetadata.PatrolPathPointType.MoveNearFlag:
                    Xml.WriteAttributeString("FlagId", XmlConvert.ToString(Reader.ReadInt16()));
                    Xml.WriteAttributeString("Distance", XmlConvert.ToString(Reader.ReadSingle()));
                    break;

                case ObjectMetadata.PatrolPathPointType.GlanceAtFlagFor:
                    Xml.WriteAttributeString("FlagId", XmlConvert.ToString(Reader.ReadInt16()));
                    Xml.WriteAttributeString("Frames", XmlConvert.ToString(Reader.ReadInt32()));
                    break;

                case ObjectMetadata.PatrolPathPointType.Scan:
                    Xml.WriteAttributeString("Frames", XmlConvert.ToString(Reader.ReadInt16()));
                    Xml.WriteAttributeString("Rotation", XmlConvert.ToString(Reader.ReadSingle()));
                    break;

                case ObjectMetadata.PatrolPathPointType.MoveToFlagLookAndWait:
                    Xml.WriteAttributeString("Frames", XmlConvert.ToString(Reader.ReadInt16()));
                    Xml.WriteAttributeString("FlagId", XmlConvert.ToString(Reader.ReadInt16()));
                    Xml.WriteAttributeString("Rotation", XmlConvert.ToString(Reader.ReadSingle()));
                    break;

                case ObjectMetadata.PatrolPathPointType.FaceToFlagAndFire:
                    Xml.WriteAttributeString("FlagId", XmlConvert.ToString(Reader.ReadInt16()));
                    Xml.WriteAttributeString("Frames", XmlConvert.ToString(Reader.ReadInt16()));
                    Xml.WriteAttributeString("Spread", XmlConvert.ToString(Reader.ReadSingle()));
                    break;

                case ObjectMetadata.PatrolPathPointType.LookAtPoint:
                case ObjectMetadata.PatrolPathPointType.MoveToPoint:
                    Xml.WriteAttributeString("X", XmlConvert.ToString(Reader.ReadSingle()));
                    Xml.WriteAttributeString("Y", XmlConvert.ToString(Reader.ReadSingle()));
                    Xml.WriteAttributeString("Z", XmlConvert.ToString(Reader.ReadSingle()));
                    break;

                case ObjectMetadata.PatrolPathPointType.MoveThroughPoint:
                    Xml.WriteAttributeString("X", XmlConvert.ToString(Reader.ReadSingle()));
                    Xml.WriteAttributeString("Y", XmlConvert.ToString(Reader.ReadSingle()));
                    Xml.WriteAttributeString("Z", XmlConvert.ToString(Reader.ReadSingle()));
                    Xml.WriteAttributeString("Distance", XmlConvert.ToString(Reader.ReadSingle()));
                    break;

                default:
                    throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Unsupported path point type {0}", pointType));
            }

            Xml.WriteEndElement();
        }

        private void WritePowerUp()
        {
            ObjectMetadata.PowerUp.Accept(this);
        }

        private void WriteSound()
        {
            ObjectMetadata.Sound.Accept(this);

            var volumeType = (ObjectMetadata.SoundVolumeType)Reader.ReadInt32();

            switch (volumeType)
            {
                case ObjectMetadata.SoundVolumeType.Box:
                    Xml.WriteStartElement("Box");
                    MetaType.BoundingBox.Accept(this);
                    Xml.WriteEndElement();
                    break;

                case ObjectMetadata.SoundVolumeType.Sphere:
                    Xml.WriteStartElement("Sphere");
                    ObjectMetadata.SoundSphere.Accept(this);
                    Xml.WriteEndElement();
                    break;
            }

            ObjectMetadata.SoundParams.Accept(this);
        }

        private void WriteTriggerVolume()
        {
            ObjectMetadata.TriggerVolume.Accept(this);
        }

        private void WriteTrigger()
        {
            ObjectMetadata.Trigger.Accept(this);
            WriteEventList();
        }

        private void WriteTurret()
        {
            ObjectMetadata.Turret.Accept(this);
        }

        private void WriteWeapon()
        {
            ObjectMetadata.Weapon.Accept(this);
        }

        private void WriteEventList()
        {
            Xml.WriteStartElement("Events");

            int count = Reader.ReadInt16();

            for (int i = 0; i < count; i++)
            {
                var eventType = (ObjectMetadata.EventType)Reader.ReadInt16();

                Xml.WriteStartElement(eventType.ToString());

                switch (eventType)
                {
                    case ObjectMetadata.EventType.None:
                        break;
                    case ObjectMetadata.EventType.Script:
                        Xml.WriteAttributeString("Function", Reader.ReadString(32));
                        break;
                    default:
                        Xml.WriteAttributeString("TargetId", XmlConvert.ToString(Reader.ReadInt16()));
                        break;
                }

                Xml.WriteEndElement();
            }

            Xml.WriteEndElement();
        }

        private void InitTypeWriters(Dictionary<ObjectMetadata.TypeTag, Action> typeWriters)
        {
            typeWriters.Add(ObjectMetadata.TypeTag.CHAR, WriteCharacter);
            typeWriters.Add(ObjectMetadata.TypeTag.CMBT, WriteCombatProfile);
            typeWriters.Add(ObjectMetadata.TypeTag.CONS, WriteConsole);
            typeWriters.Add(ObjectMetadata.TypeTag.DOOR, WriteDoor);
            typeWriters.Add(ObjectMetadata.TypeTag.FLAG, WriteFlag);
            typeWriters.Add(ObjectMetadata.TypeTag.FURN, WriteFurniture);
            typeWriters.Add(ObjectMetadata.TypeTag.MELE, WriteMeleeProfile);
            typeWriters.Add(ObjectMetadata.TypeTag.NEUT, WriteNeutralBehavior);
            typeWriters.Add(ObjectMetadata.TypeTag.PART, WriteParticle);
            typeWriters.Add(ObjectMetadata.TypeTag.PATR, WritePatrolPath);
            typeWriters.Add(ObjectMetadata.TypeTag.PWRU, WritePowerUp);
            typeWriters.Add(ObjectMetadata.TypeTag.SNDG, WriteSound);
            typeWriters.Add(ObjectMetadata.TypeTag.TRGV, WriteTriggerVolume);
            typeWriters.Add(ObjectMetadata.TypeTag.TRIG, WriteTrigger);
            typeWriters.Add(ObjectMetadata.TypeTag.TURR, WriteTurret);
            typeWriters.Add(ObjectMetadata.TypeTag.WEAP, WriteWeapon);
        }
    }
}
