﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Xml;
using Oni.Collections;
using Oni.Metadata;
using Oni.Sound;

namespace Oni.Xml
{
    internal sealed class XmlExporter : Exporter
    {
        private bool noAnimation;
        private bool recursive;
        private Totoro.Body animBody;
        private bool mergeAnimations;
        private Dae.Node animBodyNode;
        private Totoro.Animation mergedAnim;
        private string animDaeFileName;
        private readonly Dictionary<InstanceDescriptor, string> externalChildren = new Dictionary<InstanceDescriptor, string>();
        private readonly Set<InstanceDescriptor> queued = new Set<InstanceDescriptor>();
        private readonly Queue<InstanceDescriptor> exportQueue = new Queue<InstanceDescriptor>();
        private InstanceDescriptor mainDescriptor;
        private string baseFileName;
        private XmlWriter xml;

        public XmlExporter(InstanceFileManager fileManager, string outputDirPath)
            : base(fileManager, outputDirPath)
        {
        }

        public bool NoAnimation
        {
            get { return noAnimation; }
            set { noAnimation = value; }
        }

        public bool Recursive
        {
            get { return recursive; }
            set { recursive = value; }
        }

        public Totoro.Body AnimationBody
        {
            get { return animBody; }
            set
            {
                animBody = value;
                animBodyNode = null;
            }
        }

        public bool MergeAnimations
        {
            get { return mergeAnimations; }
            set { mergeAnimations = value; }
        }

        protected override void ExportInstance(InstanceDescriptor descriptor)
        {
            exportQueue.Enqueue(descriptor);

            mainDescriptor = descriptor;

            string filePath = baseFileName = CreateFileName(descriptor, ".xml");
            baseFileName = Path.GetFileNameWithoutExtension(filePath);

            if (recursive && animBody == null && descriptor.Template.Tag == TemplateTag.ONCC)
                animBody = Totoro.BodyDatReader.Read(descriptor);

            using (xml = CreateXmlWriter(filePath))
                ExportDescriptors(xml);
        }

        private void ExportChild(InstanceDescriptor descriptor)
        {
            if (descriptor.Template.Tag == TemplateTag.TRCM && mainDescriptor.Template.Tag == TemplateTag.TRBS)
            {
                xml.WriteValue(WriteBody(descriptor));
                return;
            }

            if (descriptor.Template.Tag == TemplateTag.M3GM)
            {
                if (!descriptor.IsPlaceholder)
                {
                    xml.WriteValue(WriteGeometry(descriptor));
                    return;
                }

                if (recursive)
                {
                    var m3gmFile = InstanceFileManager.FindInstance(descriptor.FullName, descriptor.File);

                    if (m3gmFile != null && m3gmFile.Descriptors[0].Template.Tag == TemplateTag.M3GM && m3gmFile.Descriptors[0].Name == descriptor.Name)
                    {
                        xml.WriteValue(WriteGeometry(m3gmFile.Descriptors[0]));
                        return;
                    }
                }
            }

            if (!recursive || !descriptor.HasName)
            {
                if (descriptor.HasName)
                {
                    xml.WriteValue(descriptor.FullName);
                }
                else
                {
                    xml.WriteValue(string.Format(CultureInfo.InvariantCulture, "#{0}", descriptor.Index));

                    if (queued.Add(descriptor))
                        exportQueue.Enqueue(descriptor);
                }

                return;
            }

            var childFile = InstanceFileManager.FindInstance(descriptor.FullName, descriptor.File);

            if (childFile == null || childFile == mainDescriptor.File)
            {
                xml.WriteValue(descriptor.FullName);
                return;
            }

            string fileName;

            if (!externalChildren.TryGetValue(descriptor, out fileName))
            {
                var exporter = new XmlExporter(InstanceFileManager, OutputDirPath)
                {
                    recursive = recursive,
                    animBody = animBody,
                    mergeAnimations = mergeAnimations
                };

                exporter.ExportFiles(new[] { childFile.FilePath });

                fileName = Path.GetFileName(CreateFileName(descriptor, ".xml"));

                externalChildren.Add(descriptor, fileName);
            }

            xml.WriteValue(fileName);
        }

        private static XmlWriter CreateXmlWriter(string filePath)
        {
            var settings = new XmlWriterSettings
            {
                CloseOutput = true,
                Indent = true,
                IndentChars = "    "
            };

            var stream = File.Create(filePath);
            var writer = XmlWriter.Create(stream, settings);

            try
            {
                writer.WriteStartElement("Oni");
            }
            catch
            {
#if NETCORE
                writer.Dispose();
#else
                writer.Close();
#endif
                throw;
            }

            return writer;
        }

        private void ExportDescriptors(XmlWriter writer)
        {
            while (exportQueue.Count > 0)
            {
                var descriptor = exportQueue.Dequeue();

                if (descriptor.IsPlaceholder || (descriptor.HasName && descriptor != mainDescriptor))
                    continue;

                switch (descriptor.Template.Tag)
                {
                    case TemplateTag.TRAM:
                        WriteAnimation(descriptor);
                        break;

                    case TemplateTag.BINA:
                        WriteBinaryObject(descriptor);
                        break;

                    case TemplateTag.TXMP:
                        //
                        // Only export TXMP instances that have a name, the others
                        // are animation frames and they're exported as part of named textures
                        //

                        if (descriptor.HasName)
                            Oni.Motoko.TextureXmlExporter.Export(descriptor, writer, OutputDirPath, baseFileName);

                        break;

                    case TemplateTag.TXAN:
                        //
                        // Do nothing: TXAN instances are exported as part of TXMP instances
                        //
                        break;

                    case TemplateTag.OSBD:
                        WriteBinarySound(descriptor);
                        break;

                    default:
                        GenericXmlWriter.Write(xml, ExportChild, descriptor);
                        break;
                }
            }
        }

        private void WriteAnimation(InstanceDescriptor tram)
        {
            var anim = Totoro.AnimationDatReader.Read(tram);

            if (animBody == null)
            {
                Totoro.AnimationXmlWriter.Write(anim, xml, null, 0, 0);
            }
            else
            {
                if (animBodyNode == null)
                {
                    var textureWriter = new Motoko.TextureDaeWriter(OutputDirPath);
                    var geometryWriter = new Motoko.GeometryDaeWriter(textureWriter);
                    var bodyWriter = new Totoro.BodyDaeWriter(geometryWriter);

                    animBodyNode = bodyWriter.Write(animBody, false, null);
                }

                if (mergeAnimations)
                {
                    if (mergedAnim == null)
                    {
                        mergedAnim = new Totoro.Animation();
                        animDaeFileName = tram.FullName + ".dae";
                    }

                    var startFrame = mergedAnim.Heights.Count;
                    Totoro.AnimationDaeWriter.AppendFrames(mergedAnim, anim);
                    var endFrame = mergedAnim.Heights.Count;

                    Totoro.AnimationXmlWriter.Write(anim, xml, animDaeFileName, startFrame, endFrame);
                }
                else
                {
                    var fileName = tram.FullName + ".dae";

                    Totoro.AnimationDaeWriter.Write(animBodyNode, anim);

                    Dae.Writer.WriteFile(Path.Combine(OutputDirPath, fileName), new Dae.Scene
                    {
                        Nodes = { animBodyNode }
                    });

                    Totoro.AnimationXmlWriter.Write(anim, xml, fileName, 0, 0);
                }
            }
        }

        protected override void Flush()
        {
            if (mergedAnim != null)
            {
                Totoro.AnimationDaeWriter.Write(animBodyNode, mergedAnim);

                Dae.Writer.WriteFile(Path.Combine(OutputDirPath, animDaeFileName), new Dae.Scene
                {
                    Nodes = { animBodyNode }
                });

                mergedAnim = null;
            }
        }

        private string WriteBody(InstanceDescriptor descriptor)
        {
            string fileName;

            if (!externalChildren.TryGetValue(descriptor, out fileName))
            {
                var body = Totoro.BodyDatReader.Read(descriptor);

                var textureWriter = new Motoko.TextureDaeWriter(OutputDirPath);
                var geometryWriter = new Motoko.GeometryDaeWriter(textureWriter);
                var bodyWriter = new Totoro.BodyDaeWriter(geometryWriter);

                var pelvis = bodyWriter.Write(body, noAnimation, null);

                fileName = string.Format("{0}_TRCM{1}.dae", mainDescriptor.FullName, descriptor.Index);

                Dae.Writer.WriteFile(Path.Combine(OutputDirPath, fileName), new Dae.Scene
                {
                    Nodes = { pelvis }
                });

                externalChildren.Add(descriptor, fileName);
            }

            return fileName;
        }

        private string WriteGeometry(InstanceDescriptor descriptor)
        {
            string fileName;

            if (!externalChildren.TryGetValue(descriptor, out fileName))
            {
                var geometry = Motoko.GeometryDatReader.Read(descriptor);

                var textureWriter = new Motoko.TextureDaeWriter(OutputDirPath);
                var geometryWriter = new Motoko.GeometryDaeWriter(textureWriter);

                if (descriptor.HasName)
                    fileName = descriptor.FullName + ".dae";
                else
                    fileName = string.Format("{0}_{1}.dae", mainDescriptor.Name, descriptor.Index);

                var node = geometryWriter.WriteNode(geometry, geometry.Name);

                Dae.Writer.WriteFile(Path.Combine(OutputDirPath, fileName), new Dae.Scene
                {
                    Nodes = { node }
                });

                externalChildren.Add(descriptor, fileName);
            }

            return fileName;
        }

        private void WriteBinarySound(InstanceDescriptor descriptor)
        {
            int dataSize, dataOffset;

            using (var reader = descriptor.OpenRead())
            {
                dataSize = reader.ReadInt32();
                dataOffset = reader.ReadInt32();
            }

            using (var rawReader = descriptor.GetRawReader(dataOffset))
            {
                OsbdXmlExporter.Export(rawReader, xml);
            }
        }

        private void WriteBinaryObject(InstanceDescriptor descriptor)
        {
            int dataSize, dataOffset;

            using (var reader = descriptor.OpenRead())
            {
                dataSize = reader.ReadInt32();
                dataOffset = reader.ReadInt32();
            }

            using (var rawReader = descriptor.GetRawReader(dataOffset))
            {
                var tag = (BinaryTag)rawReader.ReadInt32();

                switch (tag)
                {
                    case BinaryTag.OBJC:
                        ObjcXmlExporter.Export(rawReader, xml);
                        break;

                    case BinaryTag.PAR3:
                        ParticleXmlExporter.Export(descriptor.FullName.Substring(8), rawReader, xml);
                        break;

                    case BinaryTag.TMBD:
                        TmbdXmlExporter.Export(rawReader, xml);
                        break;

                    case BinaryTag.ONIE:
                        OnieXmlExporter.Export(rawReader, xml);
                        break;

                    case BinaryTag.SABD:
                        SabdXmlExporter.Export(rawReader, xml);
                        break;

                    default:
                        throw new NotSupportedException(string.Format("Unsupported BINA type '{0}'", Utils.TagToString((int)tag)));
                }
            }
        }
    }
}
