﻿using System;
using System.Collections.Generic;
using System.Xml;
using Oni.Metadata;
using Oni.Particles;

namespace Oni.Xml
{
    internal class OnieXmlImporter : RawXmlImporter
    {
        #region Private data
        private List<ImpactEffect> impactEffects = new List<ImpactEffect>();
        private List<ImpactEffectSound> sounds = new List<ImpactEffectSound>();
        private List<ImpactEffectParticle> particles = new List<ImpactEffectParticle>();
        private Dictionary<string, int> materials = new Dictionary<string, int>();
        private List<KeyValuePair<string, int>> impactList;
        private List<KeyValuePair<string, int>> materialList;
        private List<ImpactNode> impactNodes;
        private List<MaterialNode> materialNodes;
        #endregion

        private OnieXmlImporter(XmlReader xml, BinaryWriter writer)
            : base(xml, writer)
        {
        }

        public static void Import(XmlReader xml, BinaryWriter writer)
        {
            var importer = new OnieXmlImporter(xml, writer);
            importer.Read();
            importer.Write();
        }

        #region private class ImpactLookupNode

        private class ImpactNode
        {
            private int impactIndex;
            private List<MaterialNode> materialNodes;

            public ImpactNode(int impactIndex)
            {
                this.impactIndex = impactIndex;
                this.materialNodes = new List<MaterialNode>();
            }

            public int ImpactIndex
            {
                get
                {
                    return impactIndex;
                }
            }

            public List<MaterialNode> MaterialNodes
            {
                get
                {
                    return materialNodes;
                }
            }
        }

        #endregion
        #region private class MaterialLookupNode

        private class MaterialNode
        {
            private int materialIndex;
            private List<ImpactEffect> impactEffects;

            public MaterialNode(int materialIndex)
            {
                this.materialIndex = materialIndex;
                this.impactEffects = new List<ImpactEffect>();
            }

            public int MaterialIndex
            {
                get
                {
                    return materialIndex;
                }
            }

            public List<ImpactEffect> ImpactEffects
            {
                get
                {
                    return impactEffects;
                }
            }
        }

        #endregion

        private void Read()
        {
            impactList = new List<KeyValuePair<string, int>>();

            while (Xml.IsStartElement("Impact"))
            {
                int impactIndex = impactList.Count;
                var impactName = Xml.GetAttribute("Name");
                impactList.Add(new KeyValuePair<string, int>(impactName, impactIndex));

                if (Xml.IsEmptyElement)
                {
                    Xml.ReadStartElement();
                    continue;
                }

                Xml.ReadStartElement();

                while (Xml.IsStartElement("Material"))
                {
                    var materialName = Xml.GetAttribute("Name");
                    int materialIndex;

                    if (!materials.TryGetValue(materialName, out materialIndex))
                    {
                        materialIndex = materials.Count;
                        materials.Add(materialName, materialIndex);
                    }

                    if (Xml.IsEmptyElement)
                    {
                        Xml.ReadStartElement();
                        continue;
                    }

                    Xml.ReadStartElement();

                    while (Xml.IsStartElement("ImpactEffect"))
                    {
                        Xml.ReadStartElement();

                        var impactEffect = new ImpactEffect(Xml, impactName, materialName)
                        {
                            ImpactIndex = impactIndex,
                            MaterialIndex = materialIndex
                        };

                        if (impactEffect.Sound != null)
                        {
                            impactEffect.SoundIndex = sounds.Count;
                            sounds.Add(impactEffect.Sound);
                        }
                        else
                        {
                            impactEffect.SoundIndex = -1;
                        }

                        if (impactEffect.Particles != null && impactEffect.Particles.Length > 0)
                        {
                            impactEffect.ParticleIndex = particles.Count;
                            particles.AddRange(impactEffect.Particles);
                        }
                        else
                        {
                            impactEffect.ParticleIndex = -1;
                        }

                        impactEffects.Add(impactEffect);

                        Xml.ReadEndElement();
                    }

                    Xml.ReadEndElement();
                }

                Xml.ReadEndElement();
            }

            materialList = new List<KeyValuePair<string, int>>(materials);
            materialList.Sort((x, y) => x.Value.CompareTo(y.Value));

            impactNodes = new List<ImpactNode>();
            materialNodes = new List<MaterialNode>();

            foreach (var impact in impactList)
            {
                var impactNode = new ImpactNode(impact.Value);
                impactNodes.Add(impactNode);

                foreach (var material in materialList)
                {
                    var materialNode = new MaterialNode(material.Value);

                    foreach (var effect in impactEffects)
                    {
                        if (effect.MaterialIndex == material.Value && effect.ImpactIndex == impact.Value)
                            materialNode.ImpactEffects.Add(effect);
                    }

                    if (materialNode.ImpactEffects.Count > 0)
                    {
                        impactNode.MaterialNodes.Add(materialNode);
                        materialNodes.Add(materialNode);
                    }
                }
            }
        }

        private void Write()
        {
            Writer.Write(2);
            Writer.Write(impactList.Count);
            Writer.Write(materialList.Count);
            Writer.Write(particles.Count);
            Writer.Write(sounds.Count);
            Writer.Write(impactEffects.Count);
            Writer.Write(materialNodes.Count);

            foreach (KeyValuePair<string, int> impact in impactList)
            {
                Writer.Write(impact.Key, 128);
                Writer.Write(0);
            }

            foreach (KeyValuePair<string, int> material in materialList)
            {
                Writer.Write(material.Key, 128);
                Writer.Write(0);
            }

            int currentMaterialIndex = 0;

            foreach (var impactNode in impactNodes)
            {
                Writer.WriteInt16(impactNode.ImpactIndex);
                Writer.WriteInt16(impactNode.MaterialNodes.Count);
                Writer.Write(currentMaterialIndex);

                currentMaterialIndex += impactNode.MaterialNodes.Count;
            }

            foreach (var particle in particles)
            {
                particle.Write(Writer);
            }

            foreach (var sound in sounds)
            {
                sound.Write(Writer);
            }

            foreach (var materialNode in materialNodes)
            {
                foreach (var effect in materialNode.ImpactEffects)
                    effect.Write(Writer);
            }

            int currentImpactEffectIndex = 0;

            foreach (var materialNode in materialNodes)
            {
                Writer.WriteInt16(materialNode.MaterialIndex);
                Writer.WriteInt16(materialNode.ImpactEffects.Count);
                Writer.Write(currentImpactEffectIndex);

                currentImpactEffectIndex += materialNode.ImpactEffects.Count;
            }
        }
    }
}
