﻿using System;
using System.Globalization;
using System.Xml;
using Oni.Imaging;
using Oni.Metadata;

namespace Oni.Xml
{
    using Oni.Particles;

    internal class ParticleXmlImporter : ParticleXml
    {
        #region Private data
        private XmlReader xml;
        private Particle particle;
        #endregion

        public ParticleXmlImporter(XmlReader xml)
        {
            this.xml = xml;
            this.particle = new Particle();
        }

        public static void Import(XmlReader xml, BinaryWriter writer)
        {
            int lengthPosition = writer.Position;

            writer.WriteUInt16(0);
            writer.WriteUInt16(18);

            var reader = new ParticleXmlImporter(xml);
            reader.Read();
            reader.particle.Write(writer);

            int length = writer.Position - lengthPosition;
            writer.PushPosition(lengthPosition);
            writer.WriteUInt16(length);
            writer.PopPosition();
        }

        public void Read()
        {
            ReadOptions();
            ReadProperties();
            ReadAppearance();
            ReadAttractor();
            ReadVariables();
            ReadEmitters();
            ReadEvents();
        }

        private void ReadOptions()
        {
            if (!xml.IsStartElement("Options"))
                return;

            xml.ReadStartElement();

            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "DisableDetailLevel":
                        particle.DisableDetailLevel = MetaEnum.Parse<DisableDetailLevel>(xml.ReadElementContentAsString());
                        continue;

                    case "Lifetime":
                        particle.Lifetime = ReadValueFloat();
                        continue;

                    case "CollisionRadius":
                        particle.CollisionRadius = ReadValueFloat();
                        continue;

                    case "FlyBySoundName":
                        particle.FlyBySoundName = xml.ReadElementContentAsString();
                        continue;

                    case "AIAlertRadius":
                        particle.AIAlertRadius = xml.ReadElementContentAsFloat();
                        continue;

                    case "AIDodgeRadius":
                        particle.AIDodgeRadius = xml.ReadElementContentAsFloat();
                        continue;

                    default:
                        if (ReadFlag1() || ReadFlag2())
                            continue;
                        break;
                }

                throw new FormatException(string.Format("Unknown option {0}", xml.LocalName));
            }

            xml.ReadEndElement();
        }

        private void ReadProperties()
        {
            if (!xml.IsStartElement("Properties"))
                return;

            xml.ReadStartElement();

            while (xml.IsStartElement())
            {
                if (ReadFlag1())
                    continue;

                throw new FormatException(string.Format("Unknown property {0}", xml.LocalName));
            }

            xml.ReadEndElement();
        }

        private void ReadAppearance()
        {
            if (!xml.IsStartElement("Appearance"))
                return;

            Appearance appearance = particle.Appearance;

            xml.ReadStartElement();

            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "DisplayType":
                        string text = xml.ReadElementContentAsString();

                        switch (text)
                        {
                            case "Geometry":
                                particle.Flags1 |= ParticleFlags1.Geometry;
                                break;
                            case "Vector":
                                particle.Flags2 |= ParticleFlags2.Vector;
                                break;
                            case "Decal":
                                particle.Flags2 |= ParticleFlags2.Decal;
                                break;
                            default:
                                particle.SpriteType = MetaEnum.Parse<SpriteType>(text);
                                break;
                        }

                        continue;

                    case "TexGeom":
                        appearance.TextureName = xml.ReadElementContentAsString();
                        continue;

                    case "Scale":
                        appearance.Scale = ReadValueFloat();
                        continue;
                    case "YScale":
                        appearance.YScale = ReadValueFloat();
                        continue;
                    case "Rotation":
                        appearance.Rotation = ReadValueFloat();
                        continue;
                    case "Alpha":
                        appearance.Alpha = ReadValueFloat();
                        continue;
                    case "XOffset":
                        appearance.XOffset = ReadValueFloat();
                        continue;
                    case "XShorten":
                        appearance.XShorten = ReadValueFloat();
                        continue;
                    case "Tint":
                        appearance.Tint = ReadValueColor();
                        continue;
                    case "EdgeFadeMin":
                        appearance.EdgeFadeMin = ReadValueFloat();
                        continue;
                    case "EdgeFadeMax":
                        appearance.EdgeFadeMax = ReadValueFloat();
                        continue;
                    case "MaxContrailDistance":
                        appearance.MaxContrail = ReadValueFloat();
                        continue;
                    case "LensFlareDistance":
                        appearance.LensFlareDistance = ReadValueFloat();
                        continue;
                    case "LensFlareFadeInFrames":
                        appearance.LensFlareFadeInFrames = xml.ReadElementContentAsInt();
                        continue;
                    case "LensFlareFadeOutFrames":
                        appearance.LensFlareFadeOutFrames = xml.ReadElementContentAsInt();
                        continue;
                    case "MaxDecals":
                        appearance.MaxDecals = xml.ReadElementContentAsInt();
                        continue;
                    case "DecalFadeFrames":
                        appearance.DecalFadeFrames = xml.ReadElementContentAsInt();
                        continue;
                    case "DecalWrapAngle":
                        appearance.DecalWrapAngle = ReadValueFloat();
                        continue;
                    default:
                        if (ReadFlag1() || ReadFlag2())
                            continue;
                        break;
                }

                throw new FormatException(string.Format("Unknown appearance property {0}", xml.LocalName));
            }

            xml.ReadEndElement();
        }

        private void ReadAttractor()
        {
            if (!xml.IsStartElement("Attractor"))
                return;

            xml.ReadStartElement();

            Attractor attractor = particle.Attractor;

            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "Target":
                        attractor.Target = (AttractorTarget)Enum.Parse(typeof(AttractorTarget), xml.ReadElementContentAsString());
                        continue;
                    case "Selector":
                        attractor.Selector = (AttractorSelector)Enum.Parse(typeof(AttractorSelector), xml.ReadElementContentAsString());
                        continue;
                    case "Class":
                        attractor.ClassName = xml.ReadElementContentAsString();
                        continue;
                    case "MaxDistance":
                        attractor.MaxDistance = ReadValueFloat();
                        continue;
                    case "MaxAngle":
                        attractor.MaxAngle = ReadValueFloat();
                        continue;
                    case "AngleSelectMax":
                        attractor.AngleSelectMax = ReadValueFloat();
                        continue;
                    case "AngleSelectMin":
                        attractor.AngleSelectMin = ReadValueFloat();
                        continue;
                    case "AngleSelectWeight":
                        attractor.AngleSelectWeight = ReadValueFloat();
                        continue;
                }

                throw new FormatException(string.Format("Unknown attractor property {0}", xml.LocalName));
            }

            xml.ReadEndElement();
        }

        private void ReadVariables()
        {
            if (!xml.IsStartElement("Variables"))
                return;

            if (xml.IsEmptyElement)
            {
                xml.ReadStartElement();
            }
            else
            {
                xml.ReadStartElement();

                while (xml.IsStartElement())
                    particle.Variables.Add(ReadVariable());

                xml.ReadEndElement();
            }
        }

        private Variable ReadVariable()
        {
            string typeName = xml.LocalName;
            string name = xml.GetAttribute("Name");

            switch (typeName)
            {
                case "Float":
                    return new Variable(name, StorageType.Float, ReadValueFloat());

                case "Color":
                    return new Variable(name, StorageType.Color, ReadValueColor());

                case "PingPongState":
                    return new Variable(name, StorageType.PingPongState, ReadValueInt());

                default:
                    throw new XmlException(string.Format("Unknown variable type '{0}'", typeName));

            }
        }

        private void ReadEmitters()
        {
            if (!xml.IsStartElement("Emitters"))
                return;

            if (xml.IsEmptyElement)
            {
                xml.ReadStartElement();
            }
            else
            {
                xml.ReadStartElement();

                while (xml.IsStartElement())
                    particle.Emitters.Add(ReadEmitter());

                xml.ReadEndElement();
            }
        }

        private Emitter ReadEmitter()
        {
            xml.ReadStartElement();

            Emitter emitter = new Emitter();

            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "Class":
                        emitter.ParticleClass = xml.ReadElementContentAsString();
                        continue;

                    case "Flags":
                        emitter.Flags = MetaEnum.Parse<EmitterFlags>(xml.ReadElementContentAsString());
                        continue;

                    case "TurnOffTreshold":
                        emitter.TurnOffTreshold = xml.ReadElementContentAsInt();
                        continue;

                    case "Probability":
                        emitter.Probability = (int)(xml.ReadElementContentAsFloat() * 65535.0f);
                        continue;

                    case "Copies":
                        emitter.Copies = xml.ReadElementContentAsFloat();
                        continue;

                    case "LinkTo":
                        string text = xml.ReadElementContentAsString();
                        if (!string.IsNullOrEmpty(text))
                        {
                            if (text == "this")
                                emitter.LinkTo = 1;
                            else if (text == "link")
                                emitter.LinkTo = 10;
                            else
                                emitter.LinkTo = int.Parse(text, CultureInfo.InvariantCulture) + 2;
                        }
                        continue;

                    case "Rate":
                        xml.ReadStartElement();
                        ReadEmitterRate(emitter);
                        xml.ReadEndElement();
                        continue;

                    case "Position":
                        xml.ReadStartElement();
                        ReadEmitterPosition(emitter);
                        xml.ReadEndElement();
                        continue;

                    case "Speed":
                        xml.ReadStartElement();
                        ReadEmitterSpeed(emitter);
                        xml.ReadEndElement();
                        continue;

                    case "Direction":
                        xml.ReadStartElement();
                        ReadEmitterDirection(emitter);
                        xml.ReadEndElement();
                        continue;

                    case "Orientation":
                        emitter.OrientationDir = MetaEnum.Parse<EmitterOrientation>(xml.ReadElementContentAsString());
                        continue;

                    case "OrientationUp":
                        emitter.OrientationUp = MetaEnum.Parse<EmitterOrientation>(xml.ReadElementContentAsString());
                        continue;
                }

                throw new FormatException(string.Format("Unknown emitter property '{0}'", xml.LocalName));
            }

            xml.ReadEndElement();

            return emitter;
        }

        private void ReadEmitterRate(Emitter emitter)
        {
            emitter.Rate = MetaEnum.Parse<EmitterRate>(xml.LocalName);

            if (xml.IsEmptyElement)
            {
                xml.ReadStartElement();
            }
            else
            {
                xml.ReadStartElement();

                switch (emitter.Rate)
                {
                    case EmitterRate.Continous:
                        emitter.Parameters[0] = ReadValueFloat("Interval");
                        break;
                    case EmitterRate.Random:
                        emitter.Parameters[0] = ReadValueFloat("MinInterval");
                        emitter.Parameters[1] = ReadValueFloat("MaxInterval");
                        break;
                    case EmitterRate.Distance:
                        emitter.Parameters[0] = ReadValueFloat("Distance");
                        break;
                    case EmitterRate.Attractor:
                        emitter.Parameters[0] = ReadValueFloat("RechargeTime");
                        emitter.Parameters[1] = ReadValueFloat("CheckInterval");
                        break;

                }

                xml.ReadEndElement();
            }
        }

        private void ReadEmitterPosition(Emitter emitter)
        {
            emitter.Position = MetaEnum.Parse<EmitterPosition>(xml.LocalName);

            if (xml.IsEmptyElement)
            {
                xml.ReadStartElement();
            }
            else
            {
                xml.ReadStartElement();

                switch (emitter.Position)
                {
                    case EmitterPosition.Line:
                        emitter.Parameters[2] = ReadValueFloat("Radius");
                        break;
                    case EmitterPosition.Circle:
                    case EmitterPosition.Sphere:
                        emitter.Parameters[2] = ReadValueFloat("InnerRadius");
                        emitter.Parameters[3] = ReadValueFloat("OuterRadius");
                        break;
                    case EmitterPosition.Offset:
                        emitter.Parameters[2] = ReadValueFloat("X");
                        emitter.Parameters[3] = ReadValueFloat("Y");
                        emitter.Parameters[4] = ReadValueFloat("Z");
                        break;
                    case EmitterPosition.Cylinder:
                        emitter.Parameters[2] = ReadValueFloat("Height");
                        emitter.Parameters[3] = ReadValueFloat("InnerRadius");
                        emitter.Parameters[4] = ReadValueFloat("OuterRadius");
                        break;
                    case EmitterPosition.BodySurface:
                    case EmitterPosition.BodyBones:
                        emitter.Parameters[2] = ReadValueFloat("OffsetRadius");
                        break;
                }

                xml.ReadEndElement();
            }
        }

        private void ReadEmitterDirection(Emitter emitter)
        {
            emitter.Direction = MetaEnum.Parse<EmitterDirection>(xml.LocalName);

            if (xml.IsEmptyElement)
            {
                xml.ReadStartElement();
            }
            else
            {
                xml.ReadStartElement();

                switch (emitter.Direction)
                {
                    case EmitterDirection.Cone:
                        emitter.Parameters[5] = ReadValueFloat("Angle");
                        emitter.Parameters[6] = ReadValueFloat("CenterBias");
                        break;
                    case EmitterDirection.Ring:
                        emitter.Parameters[5] = ReadValueFloat("Angle");
                        emitter.Parameters[6] = ReadValueFloat("Offset");
                        break;
                    case EmitterDirection.Offset:
                        emitter.Parameters[5] = ReadValueFloat("X");
                        emitter.Parameters[6] = ReadValueFloat("Y");
                        emitter.Parameters[7] = ReadValueFloat("Z");
                        break;
                    case EmitterDirection.Inaccurate:
                        emitter.Parameters[5] = ReadValueFloat("BaseAngle");
                        emitter.Parameters[6] = ReadValueFloat("Inaccuracy");
                        emitter.Parameters[7] = ReadValueFloat("CenterBias");
                        break;
                }

                xml.ReadEndElement();
            }
        }

        private void ReadEmitterSpeed(Emitter emitter)
        {
            emitter.Speed = MetaEnum.Parse<EmitterSpeed>(xml.LocalName);

            if (xml.IsEmptyElement)
            {
                xml.ReadStartElement();
            }
            else
            {
                xml.ReadStartElement();

                switch (emitter.Speed)
                {
                    case EmitterSpeed.Uniform:
                        emitter.Parameters[8] = ReadValueFloat("Speed");
                        break;
                    case EmitterSpeed.Stratified:
                        emitter.Parameters[8] = ReadValueFloat("Speed1");
                        emitter.Parameters[9] = ReadValueFloat("Speed2");
                        break;
                }

                xml.ReadEndElement();
            }
        }

        private void ReadEvents()
        {
            if (!xml.IsStartElement("Events"))
                return;

            if (xml.IsEmptyElement)
            {
                xml.ReadStartElement();
            }
            else
            {
                xml.ReadStartElement();

                while (xml.IsStartElement())
                    ReadEvent();

                xml.ReadEndElement();
            }
        }

        private void ReadEvent()
        {
            Event e = new Event((EventType)Enum.Parse(typeof(EventType), xml.LocalName));

            if (xml.IsEmptyElement)
            {
                xml.ReadStartElement();
            }
            else
            {
                xml.ReadStartElement();

                while (xml.IsStartElement())
                    e.Actions.Add(ReadEventAction());

                xml.ReadEndElement();
            }

            particle.Events.Add(e);
        }

        private EventAction ReadEventAction()
        {
            EventAction action = new EventAction((EventActionType)Enum.Parse(typeof(EventActionType), xml.LocalName));
            EventActionInfo info = eventActionInfoTable[(int)action.Type];

            if (xml.IsEmptyElement)
            {
                xml.ReadStartElement();
            }
            else
            {
                xml.ReadStartElement();

                for (int i = 0; xml.IsStartElement(); i++)
                {
                    if (i < info.OutCount)
                    {
                        action.Variables.Add(new VariableReference(xml.ReadElementContentAsString()));
                        continue;
                    }

                    if (i >= info.Parameters.Length)
                        throw new XmlException(string.Format("Too many arguments for action '{0}'", action.Type));

                    switch (info.Parameters[i].Type)
                    {
                        case StorageType.Float:
                        case StorageType.BlastFalloff:
                            action.Parameters.Add(ReadValueFloat());
                            continue;

                        case StorageType.Color:
                            action.Parameters.Add(ReadValueColor());
                            continue;

                        case StorageType.ActionIndex:
                        case StorageType.Emitter:
                        case StorageType.CoordFrame:
                        case StorageType.CollisionOrient:
                        case StorageType.Boolean:
                        case StorageType.PingPongState:
                        case StorageType.ImpactModifier:
                        case StorageType.DamageType:
                        case StorageType.Direction:
                            action.Parameters.Add(ReadValueInt());
                            continue;

                        case StorageType.ImpulseSoundName:
                        case StorageType.AmbientSoundName:
                        case StorageType.ImpactName:
                            action.Parameters.Add(ReadValueInstance());
                            continue;
                    }
                }

                xml.ReadEndElement();
            }

            return action;
        }

        private Value ReadValueInstance()
        {
            return new Value(ValueType.InstanceName, xml.ReadElementContentAsString());
        }

        private Value ReadValueInt()
        {
            Value value = null;
            xml.ReadStartElement();

            string text = xml.ReadElementContentAsString();
            int i;

            if (int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out i))
                value = new Value(i);
            else
                value = new Value(ValueType.Variable, text.Trim());

            xml.ReadEndElement();
            return value;
        }

        private Value ReadValueFloat(string name)
        {
            if (xml.LocalName != name)
                throw new XmlException(string.Format(CultureInfo.CurrentCulture, "Unexpected '{0}' element found at line {1}", xml.LocalName, 0));

            return ReadValueFloat();
        }

        private Value ReadValueFloat()
        {
            if (xml.IsEmptyElement)
            {
                xml.Read();
                return new Value(0.0f);
            }

            Value value = null;

            xml.ReadStartElement();

            if (xml.NodeType == XmlNodeType.Text)
            {
                string text = xml.ReadElementContentAsString();
                float f;

                if (float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out f))
                    value = new Value(f);
                else
                    value = new Value(ValueType.Variable, text.Trim());
            }
            else if (xml.NodeType == XmlNodeType.Element)
            {
                string name = xml.LocalName;

                if (name == "Random")
                {
                    float min = float.Parse(xml.GetAttribute("Min"), CultureInfo.InvariantCulture);
                    float max = float.Parse(xml.GetAttribute("Max"), CultureInfo.InvariantCulture);
                    value = new Value(ValueType.FloatRandom, min, max);
                }
                else if (name == "BellCurve")
                {
                    float mean = float.Parse(xml.GetAttribute("Mean"), CultureInfo.InvariantCulture);
                    float stddev = float.Parse(xml.GetAttribute("StdDev"), CultureInfo.InvariantCulture);
                    value = new Value(ValueType.FloatBellCurve, mean, stddev);
                }
                else
                {
                    throw new XmlException(string.Format(CultureInfo.CurrentCulture, "Unknown value type '{0}'", name));
                }

                xml.ReadStartElement();
            }

            xml.ReadEndElement();
            return value;
        }

        private Value ReadValueColor()
        {
            Value value = null;

            xml.ReadStartElement();

            if (xml.NodeType == XmlNodeType.Text)
            {
                string text = xml.ReadElementContentAsString();
                Color c;

                if (Color.TryParse(text, out c))
                    value = new Value(c);
                else
                    value = new Value(ValueType.Variable, text.Trim());
            }
            else if (xml.NodeType == XmlNodeType.Element)
            {
                string name = xml.LocalName;

                if (name == "Random")
                {
                    Color min = Color.Parse(xml.GetAttribute("Min"));
                    Color max = Color.Parse(xml.GetAttribute("Max"));
                    value = new Value(ValueType.ColorRandom, min, max);
                }
                else if (name == "BellCurve")
                {
                    Color mean = Color.Parse(xml.GetAttribute("Mean"));
                    Color stddev = Color.Parse(xml.GetAttribute("StdDev"));
                    value = new Value(ValueType.ColorBellCurve, mean, stddev);
                }
                else
                {
                    throw new XmlException(string.Format(CultureInfo.CurrentCulture, "Unknown value type '{0}'", name));
                }

                xml.ReadStartElement();
            }

            xml.ReadEndElement();
            return value;
        }

        private bool ReadFlag1()
        {
            ParticleFlags1 flag;

            try
            {
                flag = (ParticleFlags1)Enum.Parse(typeof(ParticleFlags1), xml.LocalName);
            }
            catch
            {
                return false;
            }

            if (ReadFlagValue())
                particle.Flags1 |= flag;

            return true;
        }

        private bool ReadFlag2()
        {
            ParticleFlags2 flag;

            try
            {
                flag = (ParticleFlags2)Enum.Parse(typeof(ParticleFlags2), xml.LocalName);
            }
            catch
            {
                return false;
            }

            if (ReadFlagValue())
                particle.Flags2 |= flag;

            return true;
        }

        private bool ReadFlagValue()
        {
            string text = xml.ReadElementContentAsString();

            switch (text)
            {
                case "false":
                    return false;
                case "true":
                    return true;
                default:
                    throw new FormatException(string.Format(CultureInfo.CurrentCulture, "Unknown value '{0}'", text));
            }
        }
    }
}
