﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Xml;
using Oni.Imaging;
using Oni.Metadata;
using Oni.Sound;

namespace Oni.Xml
{
    internal class XmlImporter : Importer
    {
        private static readonly Func<string, float> floatConverter = XmlConvert.ToSingle;
        private static readonly Func<string, byte> byteConverter = XmlConvert.ToByte;
        protected XmlReader xml;
        private readonly string[] args;
        private string baseDir;
        private string filePath;
        private bool firstInstance;
        private Dictionary<string, ImporterDescriptor> localRefs;
        private Dictionary<string, ImporterDescriptor> externalRefs;
        private ImporterDescriptor currentDescriptor;
        private BinaryWriter currentWriter;

        #region protected struct RawArray

        protected struct RawArray
        {
            private int offset;
            private int count;

            public RawArray(int offset, int count)
            {
                this.offset = offset;
                this.count = count;
            }

            public int Offset => offset;
            public int Count => count;
        }

        #endregion

        public XmlImporter(string[] args)
        {
            this.args = args;
        }

        public override void Import(string filePath, string outputDirPath)
        {
            this.filePath = filePath;

            BeginImport();

            using (xml = CreateXmlReader(filePath))
            {
                while (xml.IsStartElement())
                {
                    switch (xml.LocalName)
                    {
                        case "Objects":
                            ReadObjects();
                            break;

                        case "Texture":
                            ReadTexture();
                            break;

                        case "ImpactEffects":
                            ReadImpactEffects();
                            break;

                        case "SoundAnimation":
                            ReadSoundAnimation();
                            break;

                        case "TextureMaterials":
                            ReadTextureMaterials();
                            break;

                        case "Particle":
                            ReadParticle();
                            break;

                        case "AmbientSound":
                        case "ImpulseSound":
                        case "SoundGroup":
                            ReadSoundData();
                            break;

                        case "Animation":
                            ReadAnimation();
                            break;

                        default:
                            ReadInstance();
                            break;
                    }
                }
            }

            Write(outputDirPath);
        }

        public override void BeginImport()
        {
            base.BeginImport();

            baseDir = Path.GetDirectoryName(filePath);
            localRefs = new Dictionary<string, ImporterDescriptor>(StringComparer.Ordinal);
            externalRefs = new Dictionary<string, ImporterDescriptor>(StringComparer.Ordinal);
            firstInstance = true;
        }

        private static XmlReader CreateXmlReader(string filePath)
        {
            var settings = new XmlReaderSettings()
            {
                CloseInput = true,
                IgnoreWhitespace = true,
                IgnoreProcessingInstructions = true,
                IgnoreComments = true
            };

            var xml = XmlReader.Create(filePath, settings);

            try
            {
                if (!xml.Read())
                    throw new InvalidDataException("Not an Oni XML file");

                xml.MoveToContent();

                if (!xml.IsStartElement("Oni"))
                    throw new InvalidDataException("Not an Oni XML file");

                if (xml.IsEmptyElement)
                    throw new InvalidDataException("No instances found");

                xml.ReadStartElement();
                xml.MoveToContent();
            }
            catch
            {
#if NETCORE
                xml.Dispose();
#else
                xml.Close();
#endif
                throw;
            }

            return xml;
        }

        private void ReadInstance()
        {
            var xmlid = xml.GetAttribute("id");
            var tagName = xml.GetAttribute("type");

            if (tagName == null)
                tagName = xml.LocalName;

            var tag = (TemplateTag)Enum.Parse(typeof(TemplateTag), tagName);
            var metadata = InstanceMetadata.GetMetadata(InstanceFileHeader.OniPCTemplateChecksum);
            var template = metadata.GetTemplate(tag);

            string name = null;

            if (firstInstance)
            {
                name = Path.GetFileNameWithoutExtension(filePath);

                if (!name.StartsWith(tagName, StringComparison.Ordinal))
                    name = tagName + name;

                firstInstance = false;
            }

            var writer = BeginXmlInstance(tag, name, xmlid);
            template.Type.Accept(new XmlToBinaryVisitor(this, xml, writer));
            EndXmlInstance();
        }

        private void ReadAnimation()
        {
            var name = Path.GetFileNameWithoutExtension(filePath);

            var writer = BeginXmlInstance(TemplateTag.TRAM, name, "0");
            var animation = Totoro.AnimationXmlReader.Read(xml, Path.GetDirectoryName(filePath));
            Totoro.AnimationDatWriter.Write(animation, this, writer);
            EndXmlInstance();
        }

        private void ReadParticle()
        {
            var name = Path.GetFileNameWithoutExtension(filePath);

            if (!name.StartsWith("BINA3RAP", StringComparison.Ordinal))
                name = "BINA3RAP" + name;

            xml.ReadStartElement();

            int rawDataOffset = RawWriter.Align32();

            RawWriter.Write((int)BinaryTag.PAR3);
            RawWriter.Write(0);

            ParticleXmlImporter.Import(xml, RawWriter);

            int rawDataLength = RawWriter.Position - rawDataOffset;
            RawWriter.WriteAt(rawDataOffset + 4, rawDataLength - 8);

            var writer = BeginXmlInstance(TemplateTag.BINA, name, "0");
            writer.Write(rawDataLength);
            writer.Write(rawDataOffset);
            EndXmlInstance();

            xml.ReadEndElement();
        }

        private void ReadSoundData()
        {
            var name = Path.GetFileNameWithoutExtension(filePath);

            int rawDataOffset = RawWriter.Align32();

            var osbdImporter = new OsbdXmlImporter(xml, RawWriter);
            osbdImporter.Import();

            int rawDataLength = RawWriter.Position - rawDataOffset;
            RawWriter.WriteAt(rawDataOffset + 4, rawDataLength - 8);

            var writer = BeginXmlInstance(TemplateTag.OSBD, name, "0");
            writer.Write(rawDataLength);
            writer.Write(rawDataOffset);
            EndXmlInstance();

            xml.ReadEndElement();
        }

        private void ReadObjects()
        {
            var name = Path.GetFileNameWithoutExtension(filePath);

            if (!name.StartsWith("BINACJBO", StringComparison.Ordinal))
                name = "BINACJBO" + name;

            xml.ReadStartElement();

            int rawDataOffset = RawWriter.Align32();

            RawWriter.Write((int)BinaryTag.OBJC);
            RawWriter.Write(0);

            ObjcXmlImporter.Import(xml, RawWriter);

            int rawDataLength = RawWriter.Position - rawDataOffset;
            RawWriter.WriteAt(rawDataOffset + 4, rawDataLength - 8);

            var writer = BeginXmlInstance(TemplateTag.BINA, name, "0");
            writer.Write(rawDataLength);
            writer.Write(rawDataOffset);
            EndXmlInstance();

            xml.ReadEndElement();
        }

        private void ReadTextureMaterials()
        {
            var name = Path.GetFileNameWithoutExtension(filePath);

            if (!name.StartsWith("BINADBMT", StringComparison.Ordinal))
                name = "BINADBMT" + name;

            xml.ReadStartElement();

            int rawDataOffset = RawWriter.Align32();

            RawWriter.Write((int)BinaryTag.TMBD);
            RawWriter.Write(0);

            TmbdXmlImporter.Import(xml, RawWriter);

            int rawDataLength = RawWriter.Position - rawDataOffset;
            RawWriter.WriteAt(rawDataOffset + 4, rawDataLength - 8);

            var writer = BeginXmlInstance(TemplateTag.BINA, name, "0");
            writer.Write(rawDataLength);
            writer.Write(rawDataOffset);
            EndXmlInstance();

            xml.ReadEndElement();
        }

        private void ReadImpactEffects()
        {
            var name = Path.GetFileNameWithoutExtension(filePath);

            if (!name.StartsWith("BINAEINO", StringComparison.Ordinal))
                name = "BINAEINO" + name;

            xml.ReadStartElement();

            int rawDataOffset = RawWriter.Align32();

            RawWriter.Write((int)BinaryTag.ONIE);
            RawWriter.Write(0);

            OnieXmlImporter.Import(xml, RawWriter);

            int rawDataLength = RawWriter.Position - rawDataOffset;
            RawWriter.WriteAt(rawDataOffset + 4, rawDataLength - 8);

            var writer = BeginXmlInstance(TemplateTag.BINA, name, "0");
            writer.Write(rawDataLength);
            writer.Write(rawDataOffset);
            EndXmlInstance();

            xml.ReadEndElement();
        }

        private void ReadSoundAnimation()
        {
            var name = Path.GetFileNameWithoutExtension(filePath);

            if (!name.StartsWith("BINADBAS", StringComparison.Ordinal))
                name = "BINADBAS" + name;

            int rawDataOffset = RawWriter.Align32();

            RawWriter.Write((int)BinaryTag.SABD);
            RawWriter.Write(0);

            SabdXmlImporter.Import(xml, RawWriter);

            int rawDataLength = RawWriter.Position - rawDataOffset;
            RawWriter.WriteAt(rawDataOffset + 4, rawDataLength - 8);

            var writer = BeginXmlInstance(TemplateTag.BINA, name, "0");
            writer.Write(rawDataLength);
            writer.Write(rawDataOffset);
            EndXmlInstance();
        }

        private void ReadTexture()
        {
            var textureImporter = new Motoko.TextureXmlImporter(this, xml, filePath);
            textureImporter.Import();
        }

        public BinaryWriter BeginXmlInstance(TemplateTag tag, string name, string xmlid)
        {
            if (!localRefs.TryGetValue(xmlid, out currentDescriptor))
            {
                currentDescriptor = ImporterFile.CreateInstance(tag, name);
                localRefs.Add(xmlid, currentDescriptor);
            }
            else if (currentDescriptor.Tag != tag)
            {
                throw new InvalidDataException(string.Format("{0} was expected to be of type {1} but it's type is {2}", xmlid, tag, currentDescriptor.Tag));
            }

            currentWriter = currentDescriptor.OpenWrite();

            return currentWriter;
        }

        public void EndXmlInstance()
        {
            currentWriter.Dispose();
        }

#region private class XmlToBinaryVisitor

        private class XmlToBinaryVisitor : IMetaTypeVisitor
        {
            private readonly XmlImporter importer;
            private readonly XmlReader xml;
            private readonly BinaryWriter writer;

            public XmlToBinaryVisitor(XmlImporter importer, XmlReader xml, BinaryWriter writer)
            {
                this.importer = importer;
                this.xml = xml;
                this.writer = writer;
            }

#region IMetaTypeVisitor Members

            void IMetaTypeVisitor.VisitEnum(MetaEnum type)
            {
                type.XmlToBinary(xml, writer);
            }

            void IMetaTypeVisitor.VisitByte(MetaByte type)
            {
                writer.Write(XmlConvert.ToByte(xml.ReadElementContentAsString()));
            }

            void IMetaTypeVisitor.VisitInt16(MetaInt16 type)
            {
                writer.Write(XmlConvert.ToInt16(xml.ReadElementContentAsString()));
            }

            void IMetaTypeVisitor.VisitUInt16(MetaUInt16 type)
            {
                writer.Write(XmlConvert.ToUInt16(xml.ReadElementContentAsString()));
            }

            void IMetaTypeVisitor.VisitInt32(MetaInt32 type)
            {
                writer.Write(xml.ReadElementContentAsInt());
            }

            void IMetaTypeVisitor.VisitUInt32(MetaUInt32 type)
            {
                writer.Write(XmlConvert.ToUInt32(xml.ReadElementContentAsString()));
            }

            void IMetaTypeVisitor.VisitInt64(MetaInt64 type)
            {
                writer.Write(xml.ReadElementContentAsLong());
            }

            void IMetaTypeVisitor.VisitUInt64(MetaUInt64 type)
            {
                writer.Write(XmlConvert.ToUInt64(xml.ReadElementContentAsString()));
            }

            void IMetaTypeVisitor.VisitFloat(MetaFloat type)
            {
                writer.Write(xml.ReadElementContentAsFloat());
            }

            void IMetaTypeVisitor.VisitColor(MetaColor type)
            {
                byte[] values = xml.ReadElementContentAsArray(byteConverter);

                if (values.Length > 3)
                    writer.Write(new Color(values[0], values[1], values[2], values[3]));
                else
                    writer.Write(new Color(values[0], values[1], values[2]));
            }

            void IMetaTypeVisitor.VisitVector2(MetaVector2 type)
            {
                writer.Write(xml.ReadElementContentAsVector2());
            }

            void IMetaTypeVisitor.VisitVector3(MetaVector3 type)
            {
                writer.Write(xml.ReadElementContentAsVector3());
            }

            void IMetaTypeVisitor.VisitMatrix4x3(MetaMatrix4x3 type)
            {
                writer.Write(xml.ReadElementContentAsArray(floatConverter, 12));
            }

            void IMetaTypeVisitor.VisitPlane(MetaPlane type)
            {
                writer.Write(xml.ReadElementContentAsArray(floatConverter, 4));
            }

            void IMetaTypeVisitor.VisitQuaternion(MetaQuaternion type)
            {
                writer.Write(xml.ReadElementContentAsArray(floatConverter, 4));
            }

            void IMetaTypeVisitor.VisitBoundingSphere(MetaBoundingSphere type)
            {
                ReadFields(type.Fields);
            }

            void IMetaTypeVisitor.VisitBoundingBox(MetaBoundingBox type)
            {
                ReadFields(type.Fields);
            }

            void IMetaTypeVisitor.VisitRawOffset(MetaRawOffset type)
            {
                //writer.Write(xml.ReadElementContentAsInt());
                throw new NotImplementedException();
            }

            void IMetaTypeVisitor.VisitSepOffset(MetaSepOffset type)
            {
                //writer.Write(xml.ReadElementContentAsInt());
                throw new NotImplementedException();
            }

            void IMetaTypeVisitor.VisitString(MetaString type)
            {
                writer.Write(xml.ReadElementContentAsString(), type.Count);
            }

            void IMetaTypeVisitor.VisitPadding(MetaPadding type)
            {
                writer.Write(type.FillByte, type.Count);
            }

            void IMetaTypeVisitor.VisitPointer(MetaPointer type)
            {
                var xmlid = xml.ReadElementContentAsString();

                if (xmlid != null)
                    xmlid = xmlid.Trim();

                if (string.IsNullOrEmpty(xmlid))
                {
                    writer.Write(0);
                    return;
                }

                writer.Write(importer.ResolveReference(xmlid, type.Tag));
            }

            void IMetaTypeVisitor.VisitStruct(MetaStruct type)
            {
                ReadFields(type.Fields);
            }

            void IMetaTypeVisitor.VisitArray(MetaArray type)
            {
                int count = ReadArray(type.ElementType, type.Count);

                if (count < type.Count)
                    writer.Skip((type.Count - count) * type.ElementType.Size);
            }

            void IMetaTypeVisitor.VisitVarArray(MetaVarArray type)
            {
                int countFieldPosition = writer.Position;
                int count;

                if (type.CountField.Type == MetaType.Int16)
                {
                    writer.WriteInt16(0);
                    count = ReadArray(type.ElementType, UInt16.MaxValue);
                }
                else
                {
                    writer.Write(0);
                    count = ReadArray(type.ElementType, Int32.MaxValue);
                }

                int position = writer.Position;
                writer.Position = countFieldPosition;

                if (type.CountField.Type == MetaType.Int16)
                    writer.WriteUInt16(count);
                else
                    writer.Write(count);

                writer.Position = position;
            }

#endregion

            private void ReadFields(IEnumerable<Field> fields)
            {
                xml.ReadStartElement();
                xml.MoveToContent();

                foreach (var field in fields)
                {
                    try
                    {
                        field.Type.Accept(this);
                    }
                    catch (Exception ex)
                    {
                        throw new InvalidOperationException(string.Format("Cannot read field '{0}'", field.Name), ex);
                    }
                }

                xml.ReadEndElement();
            }

            protected void ReadStruct(MetaStruct s)
            {
                foreach (var field in s.Fields)
                {
                    try
                    {
                        field.Type.Accept(this);
                    }
                    catch (Exception ex)
                    {
                        throw new InvalidOperationException(string.Format("Cannot read field '{0}'", field.Name), ex);
                    }
                }
            }

            private int ReadArray(MetaType elementType, int maxCount)
            {
                if (xml.IsEmptyElement)
                {
                    xml.Read();
                    return 0;
                }

                xml.ReadStartElement();
                xml.MoveToContent();

                string elementName = xml.LocalName;
                int count = 0;

                for (; count < maxCount && xml.IsStartElement(elementName); count++)
                    elementType.Accept(this);

                xml.ReadEndElement();

                return count;
            }

            protected int ReadRawElement(string name, MetaType elementType)
            {
                if (!xml.IsStartElement(name))
                    return 0;

                if (xml.IsEmptyElement)
                {
                    xml.ReadStartElement();
                    return 0;
                }

                int rawOffset = importer.RawWriter.Align32();

                elementType.Accept(new RawXmlImporter(xml, importer.RawWriter));

                return rawOffset;
            }

            protected RawArray ReadRawArray(string name, MetaType elementType)
            {
                if (!xml.IsStartElement(name))
                    return new RawArray();

                if (xml.IsEmptyElement)
                {
                    xml.ReadStartElement();
                    return new RawArray();
                }

                xml.ReadStartElement();

                int rawOffset = importer.RawWriter.Align32();

                var rawImporter = new RawXmlImporter(xml, importer.RawWriter);
                int elementCount = 0;

                while (xml.IsStartElement(elementType.Name))
                {
                    elementType.Accept(rawImporter);
                    elementCount++;
                }

                xml.ReadEndElement();

                return new RawArray(rawOffset, elementCount);
            }
        }

#endregion

        private ImporterDescriptor ResolveReference(string xmlid, TemplateTag tag)
        {
            ImporterDescriptor descriptor;

            if (xmlid[0] == '#')
                descriptor = ResolveLocalReference(xmlid.Substring(1), tag);
            else
                descriptor = ResolveExternalReference(xmlid, tag);

            return descriptor;
        }

        private ImporterDescriptor ResolveLocalReference(string xmlid, TemplateTag tag)
        {
            ImporterDescriptor descriptor;

            if (!localRefs.TryGetValue(xmlid, out descriptor))
            {
                descriptor = ImporterFile.CreateInstance(tag);
                localRefs.Add(xmlid, descriptor);
            }
            else if (tag != TemplateTag.NONE && tag != descriptor.Tag)
            {
                throw new InvalidDataException(string.Format("{0} was expected to be of type {1} but it's type is {2}", xmlid, tag, descriptor.Tag));
            }

            return descriptor;
        }

        private ImporterDescriptor ResolveExternalReference(string xmlid, TemplateTag tag)
        {
            ImporterDescriptor descriptor;

            if (!externalRefs.TryGetValue(xmlid, out descriptor))
            {
                if (xmlid.EndsWith(".xml", StringComparison.Ordinal)
                    || xmlid.EndsWith(".dae", StringComparison.Ordinal)
                    || xmlid.EndsWith(".obj", StringComparison.Ordinal)
                    || xmlid.EndsWith(".tga", StringComparison.Ordinal))
                {
                    string filePath = Path.Combine(baseDir, xmlid);

                    if (!File.Exists(filePath))
                        throw new InvalidDataException(string.Format("Cannot find referenced file '{0}'", filePath));

                    if (tag == TemplateTag.TRCM)
                    {
                        var bodyImporter = new Totoro.BodyDaeImporter(args);
                        descriptor = bodyImporter.Import(filePath, ImporterFile);
                    }
                    else if (tag == TemplateTag.M3GM
                        && (currentDescriptor.Tag == TemplateTag.ONWC
                            || currentDescriptor.Tag == TemplateTag.CONS
                            || currentDescriptor.Tag == TemplateTag.DOOR
                            || currentDescriptor.Tag == TemplateTag.OFGA))
                    {
                        var geometryImporter = new Motoko.GeometryImporter(args);
                        descriptor = geometryImporter.Import(filePath, ImporterFile);
                    }
                    else
                    {
                        AddDependency(filePath, tag);
                        var name = Importer.DecodeFileName(Path.GetFileNameWithoutExtension(filePath));
                        descriptor = ImporterFile.CreateInstance(tag, name);
                    }
                }
                else
                {
                    if (tag != TemplateTag.NONE)
                    {
                        //
                        // If the link has a known type tag then make sure the xml id
                        // is prefixed with the tag name.
                        //

                        var typeName = tag.ToString();

                        if (!xmlid.StartsWith(typeName, StringComparison.Ordinal))
                            xmlid = typeName + xmlid;
                    }
                    else
                    {
                        //
                        // IGPG contains a link that can point to either TXMP or PSpc.
                        // In this case the xml id should be already prefixed with 
                        // the tag name because we have no way to guess what type the link is.
                        //

                        var tagName = xmlid.Substring(0, 4);
                        tag = (TemplateTag)Enum.Parse(typeof(TemplateTag), tagName);
                    }

                    descriptor = ImporterFile.CreateInstance(tag, xmlid);
                }

                externalRefs.Add(xmlid, descriptor);
            }

            return descriptor;
        }
    }
}
