﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;

namespace Oni.Xml
{
    internal static class XmlReaderExtensions
    {
        private static readonly char[] emptyChars = new char[0];

        [ThreadStatic]
        private static char[] charBuffer;

        private static char[] CharBuffer
        {
            get
            {
                if (charBuffer == null)
                    charBuffer = new char[16384];

                return charBuffer;
            }
        }

        public static IEnumerable<string> ReadElementContentAsList(this XmlReader xml)
        {
            if (xml.SkipEmpty())
                yield break;

            xml.ReadStartElement();

            if (xml.NodeType == XmlNodeType.EndElement)
            {
                xml.ReadEndElement();
                yield break;
            }

#if DNET
            char[] buffer = CharBuffer;
            int offset = 0;
            int length;

            while ((length = xml.ReadValueChunk(buffer, offset, buffer.Length - offset)) > 0 || offset > 0)
            {
                bool hasMore = length != 0;
                length += offset;
                offset = 0;

                for (int i = 0; i < length; i++)
                {
                    if (Char.IsWhiteSpace(buffer[i]))
                        continue;

                    int start = i;

                    do
                        i++;
                    while (i < length && !Char.IsWhiteSpace(buffer[i]));

                    if (i == length && hasMore)
                    {
                        offset = i - start;
                        Array.Copy(buffer, start, buffer, 0, offset);
                        break;
                    }

                    yield return new string(buffer, start, i - start);
                }
            }

            while (xml.NodeType != XmlNodeType.EndElement)
                xml.Read();
#else
            string buffer = xml.ReadContentAsString();

            for (int i = 0; i < buffer.Length; i++)
            {
                if (char.IsWhiteSpace(buffer[i]))
                    continue;

                int start = i;

                do
                    i++;
                while (i < buffer.Length && !char.IsWhiteSpace(buffer[i]));

                yield return buffer.Substring(start, i - start);
            }
#endif

            xml.ReadEndElement();
        }

        private static void ReadArrayCore<T>(XmlReader xml, Func<string, T> parser, List<T> list)
        {
            foreach (var text in xml.ReadElementContentAsList())
                list.Add(parser(text));
        }

        public static T[] ReadElementContentAsArray<T>(this XmlReader xml, Func<string, T> parser)
        {
            var list = new List<T>();
            ReadArrayCore<T>(xml, parser, list);
            return list.ToArray();
        }

        public static T[] ReadElementContentAsArray<T>(this XmlReader xml, Func<string, T> converter, int count)
        {
            var list = new List<T>(count);
            ReadArrayCore(xml, converter, list);
            var array = new T[count];
            list.CopyTo(0, array, 0, count);
            return array;
        }

        public static void ReadElementContentAsArray<T>(this XmlReader xml, Func<string, T> parser, T[] array)
        {
            var text = xml.ReadElementContentAsString();
            var tokens = text.Split(emptyChars, StringSplitOptions.RemoveEmptyEntries);

            for (int i = 0; i < array.Length; i++)
            {
                if (i < tokens.Length)
                    array[i] = parser(tokens[i]);
                else
                    array[i] = default(T);
            }
        }

        public static T[] ReadElementContentAsArray<T>(this XmlReader xml, Func<string, T> parser, string name)
        {
            string text;

            if (name == null)
                text = xml.ReadElementContentAsString();
            else
                text = xml.ReadElementContentAsString(name, string.Empty);

            var tokens = text.Split(emptyChars, StringSplitOptions.RemoveEmptyEntries);
            return tokens.ConvertAll(parser);
        }

        private static T[] ReadArray<T>(this XmlReader xml, Func<string, T> parser, int count)
        {
            var text = xml.ReadElementContentAsString();
            var tokens = text.Split(emptyChars, StringSplitOptions.RemoveEmptyEntries);
            var values = tokens.ConvertAll(parser);

            Array.Resize(ref values, count);

            return values;
        }

        public static T[] ReadElementContentAsArray<T>(this XmlReader xml, Func<string, T> converter, int count, string name)
        {
            var array = xml.ReadElementContentAsArray<T>(converter, name);
            Array.Resize(ref array, count);
            return array;
        }


        public static Vector2 ReadElementContentAsVector2(this XmlReader xml, string name = null)
        {
            var values = xml.ReadElementContentAsArray(XmlConvert.ToSingle, 2, name);

            return new Vector2(values[0], values[1]);
        }

        public static Vector3 ReadElementContentAsVector3(this XmlReader xml, string name = null)
        {
            var values = xml.ReadElementContentAsArray(XmlConvert.ToSingle, 3);

            return new Vector3(values[0], values[1], values[2]);
        }

        public static Vector4 ReadElementContentAsVector4(this XmlReader xml)
        {
            var values = xml.ReadElementContentAsArray(XmlConvert.ToSingle, 4);

            return new Vector4(values[0], values[1], values[2], values[3]);
        }

        public static Quaternion ReadElementContentAsEulerXYZ(this XmlReader xml)
        {
            var values = xml.ReadElementContentAsArray(XmlConvert.ToSingle, 3);

            return Quaternion.CreateFromEulerXYZ(values[0], values[1], values[2]);
        }

        public static Quaternion ReadElementContentAsQuaternion(this XmlReader xml, string name = null)
        {
            var quat = Quaternion.Identity;

            if (xml.IsEmptyElement)
            {
                xml.Skip();
            }
            else
            {
                if (name == null)
                    xml.ReadStartElement();
                else
                    xml.ReadStartElement(name);

                if (xml.NodeType == XmlNodeType.Text)
                {
                    var values = xml.ReadArray(XmlConvert.ToSingle, 4);

                    quat = new Quaternion(values[0], values[1], values[2], -values[3]);
                }
                else
                {
                    var transforms = new List<Quaternion>();

                    while (xml.IsStartElement())
                    {
                        switch (xml.LocalName)
                        {
                            case "rotate":
                                var rotate = xml.ReadElementContentAsVector4();
                                transforms.Add(Quaternion.CreateFromAxisAngle(rotate.XYZ, MathHelper.ToRadians(rotate.W)));
                                break;
                            case "euler":
                                transforms.Add(xml.ReadElementContentAsEulerXYZ());
                                break;
                            default:
                                throw new XmlException(string.Format("Unknown element {0}", xml.LocalName));
                        }
                    }

                    foreach (var transform in Utils.Reverse(transforms))
                        quat *= transform;
                }

                xml.ReadEndElement();
            }

            return quat;
        }

        public static Matrix ReadElementContentAsMatrix43(this XmlReader xml, string name = null)
        {
            var matrix = Matrix.Identity;

            if (xml.IsEmptyElement)
            {
                xml.Skip();
            }
            else
            {
                if (name == null)
                    xml.ReadStartElement();
                else
                    xml.ReadStartElement(name);

                if (xml.NodeType == XmlNodeType.Text)
                {
                    var values = xml.ReadArray(XmlConvert.ToSingle, 12);

                    matrix = new Matrix(
                        values[0], values[1], values[2], 0.0f,
                        values[3], values[4], values[5], 0.0f,
                        values[6], values[7], values[8], 0.0f,
                        values[9], values[10], values[11], 1.0f);
                }
                else
                {
                    var transforms = new List<Matrix>();

                    while (xml.IsStartElement())
                    {
                        switch (xml.LocalName)
                        {
                            case "translate":
                                transforms.Add(Matrix.CreateTranslation(xml.ReadElementContentAsVector3()));
                                break;
                            case "rotate":
                                var rotate = xml.ReadElementContentAsVector4();
                                transforms.Add(Matrix.CreateFromAxisAngle(rotate.XYZ, MathHelper.ToRadians(rotate.W)));
                                break;
                            case "euler":
                                transforms.Add(xml.ReadElementContentAsEulerXYZ().ToMatrix());
                                break;
                            case "scale":
                                transforms.Add(Matrix.CreateScale(xml.ReadElementContentAsVector3()));
                                break;
                            default:
                                throw new XmlException(string.Format("Unknown element {0}", xml.LocalName));
                        }
                    }

                    foreach (var transform in Utils.Reverse(transforms))
                        matrix *= transform;
                }

                xml.ReadEndElement();
            }

            return matrix;
        }
    }
}
