using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;

using BYTES = System.ArraySegment<System.Byte>;

using DIMENSIONS = SharpGLTF.Schema2.DimensionType;
using ENCODING = SharpGLTF.Schema2.EncodingType;

namespace SharpGLTF.Memory
{
    /// <summary>
    /// Defines the memory encoding pattern for an arbitrary <see cref="BYTES"/>.
    /// </summary>
    [System.Diagnostics.DebuggerDisplay("{_GetDebuggerDisplay(),nq}")]
    public struct MemoryAccessInfo
    {
        #region debug

        internal readonly string _GetDebuggerDisplay()
        {
            return Diagnostics.DebuggerDisplay.ToReport(this);
        }

        #endregion

        #region constructor

        public static MemoryAccessInfo[] Create(params string[] attributes)
        {
            return attributes.Select(item => CreateDefaultElement(item)).ToArray();
        }

        public static MemoryAccessInfo CreateDefaultElement(string attribute)
        {
            switch (attribute)
            {
                case "INDEX": return new MemoryAccessInfo("INDEX", 0, 0, 0, DIMENSIONS.SCALAR, ENCODING.UNSIGNED_INT, false);

                case "POSITION": return new MemoryAccessInfo("POSITION", 0, 0, 0, DIMENSIONS.VEC3);
                case "NORMAL": return new MemoryAccessInfo("NORMAL", 0, 0, 0, DIMENSIONS.VEC3);
                case "TANGENT": return new MemoryAccessInfo("TANGENT", 0, 0, 0, DIMENSIONS.VEC4);

                case "TEXCOORD_0": return new MemoryAccessInfo("TEXCOORD_0", 0, 0, 0, DIMENSIONS.VEC2);
                case "TEXCOORD_1": return new MemoryAccessInfo("TEXCOORD_1", 0, 0, 0, DIMENSIONS.VEC2);
                case "TEXCOORD_2": return new MemoryAccessInfo("TEXCOORD_2", 0, 0, 0, DIMENSIONS.VEC2);
                case "TEXCOORD_3": return new MemoryAccessInfo("TEXCOORD_3", 0, 0, 0, DIMENSIONS.VEC2);

                case "COLOR_0": return new MemoryAccessInfo("COLOR_0", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE, true);
                case "COLOR_1": return new MemoryAccessInfo("COLOR_1", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE, true);
                case "COLOR_2": return new MemoryAccessInfo("COLOR_2", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE, true);
                case "COLOR_3": return new MemoryAccessInfo("COLOR_3", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE, true);

                case "JOINTS_0": return new MemoryAccessInfo("JOINTS_0", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE);
                case "JOINTS_1": return new MemoryAccessInfo("JOINTS_1", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE);

                case "WEIGHTS_0": return new MemoryAccessInfo("WEIGHTS_0", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE, true);
                case "WEIGHTS_1": return new MemoryAccessInfo("WEIGHTS_1", 0, 0, 0, DIMENSIONS.VEC4, ENCODING.UNSIGNED_BYTE, true);
            }

            throw new NotImplementedException();
        }

        public MemoryAccessInfo(string name, int byteOffset, int itemsCount, int byteStride, AttributeFormat format)
        {
            this.Name = name;
            this.ByteOffset = byteOffset;
            this.ItemsCount = itemsCount;
            this.ByteStride = byteStride;
            this.Dimensions = format.Dimensions;
            this.Encoding = format.Encoding;
            this.Normalized = format.Normalized;
        }

        public MemoryAccessInfo(string name, int byteOffset, int itemsCount, int byteStride, DIMENSIONS dimensions, ENCODING encoding = ENCODING.FLOAT, Boolean normalized = false)
        {
            this.Name = name;
            this.ByteOffset = byteOffset;
            this.ItemsCount = itemsCount;
            this.ByteStride = byteStride;
            this.Dimensions = dimensions;
            this.Encoding = encoding;
            this.Normalized = normalized;
        }

        public readonly MemoryAccessInfo Slice(int itemStart, int itemCount)
        {
            var stride = _GetRowByteLength();

            var clone = this;
            clone.ByteOffset += itemStart * stride;
            clone.ItemsCount = Math.Min(clone.ItemsCount, itemCount);

            return clone;
        }

        #endregion

        #region data

        /// <summary>
        /// If set, it can be used to identify the data with an attribute name: POSITION, NORMAL, etc
        /// </summary>
        public String Name;

        /// <summary>
        /// number of bytes to advance to the beginning of the first item.
        /// </summary>
        public int ByteOffset;

        /// <summary>
        /// Total number of items
        /// </summary>
        public int ItemsCount;

        /// <summary>
        /// number of bytes to advance to the beginning of the next item
        /// </summary>
        public int ByteStride;

        /// <summary>
        /// number of sub-elements of each item.
        /// </summary>
        public DIMENSIONS Dimensions;

        /// <summary>
        /// byte encoding of sub-elements of each item.
        /// </summary>
        public ENCODING Encoding;

        /// <summary>
        /// normalization of sub-elements of each item.
        /// </summary>
        public Boolean Normalized;

        #endregion

        #region properties

        /// <summary>
        /// number of bytes to advance to the next item.
        /// </summary>
        public readonly int StepByteLength => _GetRowByteLength();

        public readonly int ItemByteLength => _GetItemByteLength();

        public readonly Boolean IsValidVertexAttribute
        {
            get
            {
                if (this.ItemsCount < 0) return false;

                if (this.ByteOffset < 0) return false;
                if (!this.ByteOffset.IsMultipleOf(4)) return false;

                if (this.ByteStride < 0) return false;
                if (!this.ByteStride.IsMultipleOf(4)) return false;

                if (this.ByteStride > 0 && this.ByteStride < this.StepByteLength) return false;

                return true;
            }
        }

        /// <summary>
        /// returns true if this type can be used as a joint index.
        /// </summary>
        public readonly Boolean IsValidIndexer
        {
            get
            {
                if (this.ByteOffset < 0) return false;
                if (this.ItemsCount < 0) return false;
                if (this.ByteStride < 0) return false;
                if (this.Dimensions != DIMENSIONS.SCALAR) return false;
                if (this.Normalized) return false;
                if (this.ByteStride == 0) return true;
                if (this.ByteStride == 1) return true;
                if (this.ByteStride == 2) return true;
                if (this.ByteStride == 4) return true;
                return false;
            }
        }

        #endregion

        #region API

        private readonly int _GetItemByteLength()
        {
            var xlen = Encoding.ByteLength();

            if (Dimensions != DIMENSIONS.SCALAR || Name != "INDEX")
            {
                xlen *= this.Dimensions.DimCount();
                xlen = xlen.WordPadded();
            }

            return xlen;
        }

        private readonly int _GetRowByteLength()
        {
            return Math.Max(ByteStride, _GetItemByteLength());
        }

        /// <summary>
        /// Assuming that <paramref name="attributes"/> are sequential and adyacent,
        /// it modifies the <see cref="ByteOffset"/> of each item of <paramref name="attributes"/> to ensure
        /// the offsets are sequential.
        /// </summary>
        /// <param name="attributes">A list of attributes to fix.</param>
        /// <param name="byteOffset">The initial byteoffset.</param>
        /// <param name="itemsCount">the default items count.</param>
        /// <returns>The byte stride.</returns>
        public static int SetInterleavedInfo(MemoryAccessInfo[] attributes, int byteOffset, int itemsCount)
        {
            Guard.NotNull(attributes, nameof(attributes));

            var byteStride = 0;

            for (int i = 0; i < attributes.Length; ++i)
            {
                var a = attributes[i];

                a.ByteOffset = byteOffset;
                a.ItemsCount = itemsCount;

                var step = a.StepByteLength;

                byteStride += step;
                byteOffset += step;

                attributes[i] = a;
            }

            for (int i = 0; i < attributes.Length; ++i)
            {
                var a = attributes[i];
                a.ByteStride = byteStride;
                attributes[i] = a;
            }

            return byteStride;
        }

        public static MemoryAccessInfo[] Slice(MemoryAccessInfo[] attributes, int start, int count)
        {
            Guard.NotNull(attributes, nameof(attributes));

            var dst = new MemoryAccessInfo[attributes.Length];

            for (int i = 0; i < dst.Length; ++i)
            {
                dst[i] = attributes[i].Slice(start, count);
            }

            return dst;
        }

        #endregion

        #region nested types

        internal static IComparer<string> NameComparer { get; private set; } = new AttributeComparer();

        /// <summary>
        /// Comparer used to sort attribute names in a friendly order.
        /// </summary>
        private class AttributeComparer : IComparer<String>
        {
            public int Compare(string x, string y)
            {
                var xx = _GetSortingScore(x);
                var yy = _GetSortingScore(y);

                return xx.CompareTo(yy);
            }

            private static int _GetSortingScore(string attribute)
            {
                switch (attribute)
                {
                    case "POSITION": return 0;
                    case "NORMAL": return 1;
                    case "TANGENT": return 2;

                    case "COLOR_0": return 10;
                    case "COLOR_1": return 11;
                    case "COLOR_2": return 12;
                    case "COLOR_3": return 13;

                    case "TEXCOORD_0": return 20;
                    case "TEXCOORD_1": return 21;
                    case "TEXCOORD_2": return 22;
                    case "TEXCOORD_3": return 23;

                    case "JOINTS_0": return 50;
                    case "JOINTS_1": return 51;
                    case "WEIGHTS_0": return 50;
                    case "WEIGHTS_1": return 51;
                    default: return 100;
                }
            }
        }

        #endregion
    }

    /// <summary>
    /// Wraps a <see cref="BYTES"/> decoding it and exposing its content as arrays of different types.
    /// </summary>
    [System.Diagnostics.DebuggerDisplay("{_GetDebuggerDisplay(),nq}")]
    public sealed partial class MemoryAccessor
    {
        #region debug

        internal string _GetDebuggerDisplay()
        {
            return _Slicer._GetDebuggerDisplay();
        }

        #endregion

        #region constructor

        public MemoryAccessor(BYTES data, MemoryAccessInfo info)
        {
            this._Slicer = info;
            this._Data = data;
        }

        public MemoryAccessor(MemoryAccessInfo info)
        {
            this._Slicer = info;
            this._Data = default;
        }

        public static IList<Single> CreateScalarSparseArray(MemoryAccessor bottom, IntegerArray topKeys, MemoryAccessor topValues)
        {
            Guard.NotNull(bottom, nameof(bottom));
            Guard.NotNull(topValues, nameof(topValues));
            Guard.IsTrue(bottom._Slicer.Dimensions == topValues._Slicer.Dimensions, nameof(topValues));
            Guard.IsTrue(topKeys.Count <= bottom._Slicer.ItemsCount, nameof(topKeys));
            Guard.IsTrue(topKeys.Count == topValues._Slicer.ItemsCount, nameof(topValues));
            Guard.IsTrue(topKeys.All(item => item < (uint)bottom._Slicer.ItemsCount), nameof(topKeys));

            return new SparseArray<Single>(bottom.AsScalarArray(), topValues.AsScalarArray(), topKeys);
        }

        public static IList<Vector2> CreateVector2SparseArray(MemoryAccessor bottom, IntegerArray topKeys, MemoryAccessor topValues)
        {
            Guard.NotNull(bottom, nameof(bottom));
            Guard.NotNull(topValues, nameof(topValues));
            Guard.IsTrue(bottom._Slicer.Dimensions == topValues._Slicer.Dimensions, nameof(topValues));
            Guard.IsTrue(topKeys.Count <= bottom._Slicer.ItemsCount, nameof(topKeys));
            Guard.IsTrue(topKeys.Count == topValues._Slicer.ItemsCount, nameof(topValues));
            Guard.IsTrue(topKeys.All(item => item < (uint)bottom._Slicer.ItemsCount), nameof(topKeys));

            return new SparseArray<Vector2>(bottom.AsVector2Array(), topValues.AsVector2Array(), topKeys);
        }

        public static IList<Vector3> CreateVector3SparseArray(MemoryAccessor bottom, IntegerArray topKeys, MemoryAccessor topValues)
        {
            Guard.NotNull(bottom, nameof(bottom));
            Guard.NotNull(topValues, nameof(topValues));
            Guard.IsTrue(bottom._Slicer.Dimensions == topValues._Slicer.Dimensions, nameof(topValues));
            Guard.IsTrue(topKeys.Count <= bottom._Slicer.ItemsCount, nameof(topKeys));
            Guard.IsTrue(topKeys.Count == topValues._Slicer.ItemsCount, nameof(topValues));
            Guard.IsTrue(topKeys.All(item => item < (uint)bottom._Slicer.ItemsCount), nameof(topKeys));

            return new SparseArray<Vector3>(bottom.AsVector3Array(), topValues.AsVector3Array(), topKeys);
        }

        public static IList<Vector4> CreateVector4SparseArray(MemoryAccessor bottom, IntegerArray topKeys, MemoryAccessor topValues)
        {
            Guard.NotNull(bottom, nameof(bottom));
            Guard.NotNull(topValues, nameof(topValues));
            Guard.IsTrue(bottom._Slicer.Dimensions == topValues._Slicer.Dimensions, nameof(topValues));
            Guard.IsTrue(topKeys.Count <= bottom._Slicer.ItemsCount, nameof(topKeys));
            Guard.IsTrue(topKeys.Count == topValues._Slicer.ItemsCount, nameof(topValues));
            Guard.IsTrue(topKeys.All(item => item < (uint)bottom._Slicer.ItemsCount), nameof(topKeys));

            return new SparseArray<Vector4>(bottom.AsVector4Array(), topValues.AsVector4Array(), topKeys);
        }

        public static IList<Vector4> CreateColorSparseArray(MemoryAccessor bottom, IntegerArray topKeys, MemoryAccessor topValues, Single defaultW = 1)
        {
            Guard.NotNull(bottom, nameof(bottom));
            Guard.NotNull(topValues, nameof(topValues));
            Guard.IsTrue(bottom._Slicer.Dimensions == topValues._Slicer.Dimensions, nameof(topValues));
            Guard.IsTrue(topKeys.Count <= bottom._Slicer.ItemsCount, nameof(topKeys));
            Guard.IsTrue(topKeys.Count == topValues._Slicer.ItemsCount, nameof(topValues));
            Guard.IsTrue(topKeys.All(item => item < (uint)bottom._Slicer.ItemsCount), nameof(topKeys));

            return new SparseArray<Vector4>(bottom.AsColorArray(defaultW), topValues.AsColorArray(defaultW), topKeys);
        }

        #endregion

        #region data

        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
        private MemoryAccessInfo _Slicer;

        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
        private BYTES _Data;

        #endregion

        #region properties

        public MemoryAccessInfo Attribute => _Slicer;

        public BYTES Data => _Data;

        #endregion

        #region API

        public void Update(BYTES data, MemoryAccessInfo encoding)
        {
            this._Slicer = encoding;
            this._Data = data;
        }

        public IntegerArray AsIntegerArray()
        {
            Guard.IsTrue(_Slicer.IsValidIndexer, nameof(_Slicer));
            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.SCALAR, nameof(_Slicer));
            return new IntegerArray(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.Encoding.ToIndex());
        }

        public ScalarArray AsScalarArray()
        {
            Guard.IsTrue(_Slicer.IsValidVertexAttribute, nameof(_Slicer));
            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.SCALAR, nameof(_Slicer));
            return new ScalarArray(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.ByteStride, _Slicer.Encoding, _Slicer.Normalized);
        }

        public Vector2Array AsVector2Array()
        {
            Guard.IsTrue(_Slicer.IsValidVertexAttribute, nameof(_Slicer));
            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.VEC2, nameof(_Slicer));
            return new Vector2Array(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.ByteStride, _Slicer.Encoding, _Slicer.Normalized);
        }

        public Vector3Array AsVector3Array()
        {
            Guard.IsTrue(_Slicer.IsValidVertexAttribute, nameof(_Slicer));
            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.VEC3, nameof(_Slicer));
            return new Vector3Array(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.ByteStride, _Slicer.Encoding, _Slicer.Normalized);
        }

        public Vector4Array AsVector4Array()
        {
            Guard.IsTrue(_Slicer.IsValidVertexAttribute, nameof(_Slicer));
            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.VEC4, nameof(_Slicer));
            return new Vector4Array(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.ByteStride, _Slicer.Encoding, _Slicer.Normalized);
        }

        public ColorArray AsColorArray(Single defaultW = 1)
        {
            Guard.IsTrue(_Slicer.IsValidVertexAttribute, nameof(_Slicer));
            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.VEC3 || _Slicer.Dimensions == DIMENSIONS.VEC4, nameof(_Slicer));
            return new ColorArray(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.ByteStride, _Slicer.Dimensions.DimCount(), _Slicer.Encoding, _Slicer.Normalized, defaultW);
        }

        public QuaternionArray AsQuaternionArray()
        {
            Guard.IsTrue(_Slicer.IsValidVertexAttribute, nameof(_Slicer));
            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.VEC4, nameof(_Slicer));
            return new QuaternionArray(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.ByteStride, _Slicer.Encoding, _Slicer.Normalized);
        }

        public Matrix2x2Array AsMatrix2x2Array()
        {
            Guard.IsTrue(_Slicer.IsValidVertexAttribute, nameof(_Slicer));
            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.MAT2, nameof(_Slicer));
            return new Matrix2x2Array(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.ByteStride, _Slicer.Encoding, _Slicer.Normalized);
        }

        public Matrix3x3Array AsMatrix3x3Array()
        {
            Guard.IsTrue(_Slicer.IsValidVertexAttribute, nameof(_Slicer));
            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.MAT3, nameof(_Slicer));
            return new Matrix3x3Array(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.ByteStride, _Slicer.Encoding, _Slicer.Normalized);
        }

        public Matrix4x4Array AsMatrix4x4Array()
        {
            Guard.IsTrue(_Slicer.IsValidVertexAttribute, nameof(_Slicer));
            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.MAT4, nameof(_Slicer));
            return new Matrix4x4Array(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.ByteStride, _Slicer.Encoding, _Slicer.Normalized);
        }

        public MultiArray AsMultiArray(int dimensions)
        {
            Guard.IsTrue(_Slicer.IsValidVertexAttribute, nameof(_Slicer));
            Guard.IsTrue(_Slicer.Dimensions == DIMENSIONS.SCALAR, nameof(_Slicer));
            Guard.IsTrue(_Slicer.ByteStride == 0, nameof(_Slicer));
            return new MultiArray(_Data, _Slicer.ByteOffset, _Slicer.ItemsCount, _Slicer.ByteStride, dimensions, _Slicer.Encoding, _Slicer.Normalized);
        }

        public IEnumerable<BYTES> GetItemsAsRawBytes()
        {
            var rowOffset = this._Slicer.ByteOffset;
            var rowStride = this._Slicer.StepByteLength;
            var itemSize = this._Slicer.Dimensions.DimCount() * _Slicer.Encoding.ByteLength();

            for (int i = 0; i < this._Slicer.ItemsCount; ++i)
            {
                yield return this._Data.Slice((i * rowStride) + rowOffset, itemSize);
            }
        }

        internal BYTES _GetBytes()
        {
            var o = this._Slicer.ByteOffset;
            var l = this._Slicer.StepByteLength * this._Slicer.ItemsCount;

            var data = _Data.Slice(o);

            data = data.Slice(0, Math.Min(data.Count, l));

            return data;
        }

        #endregion
    }
}
