﻿using System;
using System.Collections.Generic;
using System.Xml;
using Oni.Imaging;
using Oni.Metadata;

namespace Oni.Xml
{
    internal class RawXmlExporter : IMetaTypeVisitor
    {
        private readonly BinaryReader reader;
        private readonly XmlWriter xml;
        private Stack<int> startOffsetStack;

        public RawXmlExporter(BinaryReader reader, XmlWriter xml)
        {
            this.reader = reader;
            this.xml = xml;

            BeginStruct(reader.Position);
        }

        protected BinaryReader Reader => reader;

        protected XmlWriter Xml => xml;

        protected void BeginStruct(int startOffset)
        {
            startOffsetStack = new Stack<int>();
            startOffsetStack.Push(startOffset);
        }

        #region IMetaTypeVisitor Members

        void IMetaTypeVisitor.VisitEnum(MetaEnum type)
        {
            type.BinaryToXml(reader, xml);
        }

        void IMetaTypeVisitor.VisitByte(MetaByte type)
        {
            xml.WriteValue(reader.ReadByte());
        }

        void IMetaTypeVisitor.VisitInt16(MetaInt16 type)
        {
            xml.WriteValue(reader.ReadInt16());
        }

        void IMetaTypeVisitor.VisitUInt16(MetaUInt16 type)
        {
            xml.WriteValue(reader.ReadUInt16());
        }

        void IMetaTypeVisitor.VisitInt32(MetaInt32 type)
        {
            xml.WriteValue(reader.ReadInt32());
        }

        void IMetaTypeVisitor.VisitUInt32(MetaUInt32 type)
        {
            xml.WriteValue(reader.ReadUInt32());
        }

        void IMetaTypeVisitor.VisitInt64(MetaInt64 type)
        {
            xml.WriteValue(reader.ReadInt64());
        }

        void IMetaTypeVisitor.VisitUInt64(MetaUInt64 type)
        {
            xml.WriteValue(XmlConvert.ToString(reader.ReadUInt64()));
        }

        void IMetaTypeVisitor.VisitFloat(MetaFloat type)
        {
            xml.WriteValue(reader.ReadSingle());
        }

        void IMetaTypeVisitor.VisitVector2(MetaVector2 type)
        {
            xml.WriteFloatArray(reader.ReadSingleArray(2));
        }

        void IMetaTypeVisitor.VisitVector3(MetaVector3 type)
        {
            xml.WriteFloatArray(reader.ReadSingleArray(3));
        }

        void IMetaTypeVisitor.VisitMatrix4x3(MetaMatrix4x3 type)
        {
            xml.WriteFloatArray(reader.ReadSingleArray(12));
        }

        void IMetaTypeVisitor.VisitPlane(MetaPlane type)
        {
            xml.WriteFloatArray(reader.ReadSingleArray(4));
        }

        void IMetaTypeVisitor.VisitQuaternion(MetaQuaternion type)
        {
            xml.WriteFloatArray(reader.ReadSingleArray(4));
        }

        void IMetaTypeVisitor.VisitBoundingSphere(MetaBoundingSphere type)
        {
            WriteFields(type.Fields);
        }

        void IMetaTypeVisitor.VisitBoundingBox(MetaBoundingBox type)
        {
            WriteFields(type.Fields);
        }

        void IMetaTypeVisitor.VisitColor(MetaColor type)
        {
            Color color = reader.ReadColor();

            if (color.A == 255)
                xml.WriteValue(string.Format("{0} {1} {2}", color.R, color.G, color.B));
            else
                xml.WriteValue(string.Format("{0} {1} {2} {3}", color.R, color.G, color.B, color.A));
        }

        void IMetaTypeVisitor.VisitRawOffset(MetaRawOffset type)
        {
            xml.WriteValue(reader.ReadInt32());
        }

        void IMetaTypeVisitor.VisitSepOffset(MetaSepOffset type)
        {
            xml.WriteValue(reader.ReadInt32());
        }

        void IMetaTypeVisitor.VisitPointer(MetaPointer type)
        {
            VisitLink(type);
        }

        void IMetaTypeVisitor.VisitString(MetaString type)
        {
            string s = reader.ReadString(type.Count);
            s = CleanupInvalidCharacters(s);
            xml.WriteValue(s);
        }

        void IMetaTypeVisitor.VisitPadding(MetaPadding type)
        {
            reader.Position += type.Count;
        }

        void IMetaTypeVisitor.VisitStruct(MetaStruct type)
        {
            WriteFields(type.Fields);
        }

        void IMetaTypeVisitor.VisitArray(MetaArray type)
        {
            WriteArray(type.ElementType, type.Count);
        }

        void IMetaTypeVisitor.VisitVarArray(MetaVarArray type)
        {
            int count;

            if (type.CountField.Type == MetaType.Int16)
                count = reader.ReadInt16();
            else
                count = reader.ReadInt32();

            WriteArray(type.ElementType, count);
        }

        #endregion

        protected virtual void VisitLink(MetaPointer link)
        {
            throw new NotSupportedException();
        }

        private void WriteFields(IEnumerable<Field> fields)
        {
            foreach (Field field in fields)
            {
                if (field.Type is MetaPadding)
                {
                    field.Type.Accept(this);
                    continue;
                }

                string name = field.Name;

                if (string.IsNullOrEmpty(name))
                {
                    int fieldOffset = reader.Position - startOffsetStack.Peek();
                    name = string.Format("Offset_{0:X4}", fieldOffset);
                }

                xml.WriteStartElement(name);
                field.Type.Accept(this);
                xml.WriteEndElement();
            }
        }

        private void WriteArray(MetaType elementType, int count)
        {
            bool simpleArray = elementType.IsBlittable;
            simpleArray = false;

            for (int i = 0; i < count; i++)
            {
                startOffsetStack.Push(reader.Position);

                if (!simpleArray)
                    xml.WriteStartElement(elementType.Name);

                elementType.Accept(this);

                if (!simpleArray)
                    xml.WriteEndElement();
                else if (i != count - 1)
                    xml.WriteWhitespace(" \n");

                startOffsetStack.Pop();
            }
        }

        private static string CleanupInvalidCharacters(string s)
        {
            char[] c = null;

            for (int i = 0; i < s.Length; i++)
            {
                if (s[i] >= ' ')
                    continue;

                switch (s[i])
                {
                    case '\t':
                    case '\n':
                    case '\r':
                        continue;
                }

                if (c == null)
                    c = s.ToCharArray();

                c[i] = ' ';
            }

            if (c == null)
                return s;

            return new string(c);
        }
    }
}
