﻿using System;
using System.Collections.Generic;
using System.Xml;
using Oni.Imaging;
using Oni.Metadata;

namespace Oni.Xml
{
    internal class RawXmlImporter : IMetaTypeVisitor
    {
        private static readonly Func<string, float> floatConverter = XmlConvert.ToSingle;
        private static readonly Func<string, byte> byteConverter = XmlConvert.ToByte;
        private readonly XmlReader xml;
        private readonly BinaryWriter writer;
        private Stack<int> startOffsetStack;

        public RawXmlImporter(XmlReader xml, BinaryWriter writer)
        {
            this.xml = xml;
            this.writer = writer;
        }

        protected XmlReader Xml => xml;

        protected BinaryWriter Writer => writer;

        protected void BeginStruct(int startPosition)
        {
            startOffsetStack = new Stack<int>();
            startOffsetStack.Push(startPosition);
        }

        #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.ReadElementContentAsArray(floatConverter, 2));
        }

        void IMetaTypeVisitor.VisitVector3(MetaVector3 type)
        {
            writer.Write(xml.ReadElementContentAsArray(floatConverter, 3));
        }

        void IMetaTypeVisitor.VisitMatrix4x3(MetaMatrix4x3 type)
        {
            writer.WriteMatrix4x3(xml.ReadElementContentAsMatrix43());
        }

        void IMetaTypeVisitor.VisitPlane(MetaPlane type)
        {
            writer.Write(xml.ReadElementContentAsArray(floatConverter, 4));
        }

        void IMetaTypeVisitor.VisitQuaternion(MetaQuaternion type)
        {
            writer.Write(xml.ReadElementContentAsQuaternion());
        }

        void IMetaTypeVisitor.VisitBoundingSphere(MetaBoundingSphere type)
        {
            ReadFields(type.Fields);
        }

        void IMetaTypeVisitor.VisitBoundingBox(MetaBoundingBox type)
        {
            ReadFields(type.Fields);
        }

        void IMetaTypeVisitor.VisitRawOffset(MetaRawOffset type)
        {
            throw new NotImplementedException();
        }

        void IMetaTypeVisitor.VisitSepOffset(MetaSepOffset type)
        {
            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)
        {
            throw new NotImplementedException();
        }

        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 (Field field in fields)
            {
                try
                {
                    field.Type.Accept(this);
                }
                catch (Exception ex)
                {
                    var lineInfo = xml as IXmlLineInfo;
                    int line = lineInfo != null ? lineInfo.LineNumber : 0;
                    throw new InvalidOperationException(string.Format("Cannot read field '{0}' (line {1})", field.Name, line), ex);
                }
            }

            xml.ReadEndElement();
        }

        protected void ReadStruct(MetaStruct s)
        {
            foreach (Field 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();

            var localName = xml.LocalName;
            int count = 0;

            for (; count < maxCount && xml.IsStartElement(localName); count++)
            {
                startOffsetStack.Push(writer.Position);
                elementType.Accept(this);
                startOffsetStack.Pop();
            }

            xml.ReadEndElement();

            return count;
        }
    }
}
