﻿using System;
using System.IO;

namespace Oni.Metadata
{
    internal abstract class MetaType
    {
        private string name;
        private int size;
        private bool isFixedSize;
        private bool? isLeaf;

        protected MetaType()
        {
        }

        protected MetaType(string name, int size)
        {
            this.size = size;
            this.name = name;
        }

        #region Primitive Types

        public static readonly MetaChar Char = new MetaChar();
        public static readonly MetaByte Byte = new MetaByte();
        public static readonly MetaInt16 Int16 = new MetaInt16();
        public static readonly MetaUInt16 UInt16 = new MetaUInt16();
        public static readonly MetaInt32 Int32 = new MetaInt32();
        public static readonly MetaUInt32 UInt32 = new MetaUInt32();
        public static readonly MetaInt64 Int64 = new MetaInt64();
        public static readonly MetaUInt64 UInt64 = new MetaUInt64();
        public static readonly MetaColor Color = new MetaColor();
        public static readonly MetaFloat Float = new MetaFloat();
        public static readonly MetaVector2 Vector2 = new MetaVector2();
        public static readonly MetaVector3 Vector3 = new MetaVector3();
        public static readonly MetaQuaternion Quaternion = new MetaQuaternion();
        public static readonly MetaPlane Plane = new MetaPlane();
        public static readonly MetaBoundingBox BoundingBox = new MetaBoundingBox();
        public static readonly MetaBoundingSphere BoundingSphere = new MetaBoundingSphere();
        public static readonly MetaMatrix4x3 Matrix4x3 = new MetaMatrix4x3();
        public static readonly MetaRawOffset RawOffset = new MetaRawOffset();
        public static readonly MetaSepOffset SepOffset = new MetaSepOffset();
        public static readonly MetaString String16 = new MetaString(16);
        public static readonly MetaString String32 = new MetaString(32);
        public static readonly MetaString String48 = new MetaString(48);
        public static readonly MetaString String63 = new MetaString(63);
        public static readonly MetaString String64 = new MetaString(64);
        public static readonly MetaString String128 = new MetaString(128);
        public static readonly MetaString String256 = new MetaString(256);

        public static MetaPadding Padding(int length) => new MetaPadding(length);

        public static MetaPadding Padding(int length, byte fillByte) => new MetaPadding(length, fillByte);

        public static MetaArray Array(int length, MetaType elementType) => new MetaArray(elementType, length);

        public static MetaVarArray ShortVarArray(MetaType elementType) => new MetaVarArray(Int16, elementType);

        public static MetaVarArray VarArray(MetaType elementType) => new MetaVarArray(Int32, elementType);

        public static MetaString String(int length)
        {
            switch (length)
            {
                case 16:
                    return String16;
                case 32:
                    return String32;
                case 64:
                    return String64;
                case 128:
                    return String128;
                case 256:
                    return String256;
                default:
                    return new MetaString(length);
            }
        }

        public static MetaPointer Pointer(TemplateTag tag) => new MetaPointer(tag);

        public static MetaEnum Enum<T>()
        {
            Type underlyingType = System.Enum.GetUnderlyingType(typeof(T));

            if (underlyingType == typeof(Byte))
                return new MetaEnum(MetaType.Byte, typeof(T));
            if (underlyingType == typeof(Int16))
                return new MetaEnum(MetaType.Int16, typeof(T));
            if (underlyingType == typeof(UInt16))
                return new MetaEnum(MetaType.UInt16, typeof(T));
            if (underlyingType == typeof(Int32))
                return new MetaEnum(MetaType.Int32, typeof(T));
            if (underlyingType == typeof(UInt32))
                return new MetaEnum(MetaType.UInt32, typeof(T));
            if (underlyingType == typeof(Int64))
                return new MetaEnum(MetaType.Int64, typeof(T));
            if (underlyingType == typeof(UInt64))
                return new MetaEnum(MetaType.UInt64, typeof(T));

            throw new InvalidOperationException(System.String.Format("Unsupported enum type {0}", underlyingType));
        }

        #endregion

        public string Name
        {
            get { return name; }
            protected set { name = value; }
        }

        public int Size
        {
            get { return size; }
            protected set { size = value; }
        }

        public bool IsFixedSize
        {
            get { return isFixedSize; }
            protected set { isFixedSize = value; }
        }

        public bool IsLeaf
        {
            get
            {
                if (!isLeaf.HasValue)
                    isLeaf = IsLeafImpl();

                return isLeaf.Value;
            }
        }

        protected abstract bool IsLeafImpl();

        public bool IsBlittable
        {
            get
            {
                return (this == MetaType.Byte
                    || this == MetaType.Int16
                    || this == MetaType.UInt16
                    || this == MetaType.Int32
                    || this == MetaType.UInt32
                    || this == MetaType.Int64
                    || this == MetaType.UInt64
                    || this == MetaType.Float
                    || this == MetaType.Color
                    || this == MetaType.Matrix4x3
                    || this == MetaType.Plane
                    || this == MetaType.Quaternion
                    || this == MetaType.Vector2
                    || this == MetaType.Vector3);
            }
        }

        public abstract void Accept(IMetaTypeVisitor visitor);

        internal int Copy(BinaryReader input, BinaryWriter output, Action<CopyVisitor> callback)
        {
            var copyVisitor = new CopyVisitor(input, output, callback);
            Accept(copyVisitor);
            return copyVisitor.Position;
        }
    }
}
