﻿using System;
using System.Collections.Generic;
using System.IO;

namespace Oni.Metadata
{
    internal class CopyVisitor : MetaTypeVisitor
    {
        #region Private data
        private BinaryReader input;
        private BinaryWriter output;
        private Action<CopyVisitor> callback;
        private Stack<ActiveField> activeFields;
        private MetaType topLevelType;
        private MetaType currentType;
        private byte[] buffer;
        private int fieldSize;
        private int position;
        #endregion

        #region private class ActiveField

        private class ActiveField
        {
            public Field Field;
            public int Index;

            public ActiveField(Field field)
            {
                Field = field;
                Index = -1;
            }
        }

        #endregion

        public CopyVisitor(BinaryReader reader, BinaryWriter writer, Action<CopyVisitor> callback)
        {
            this.input = reader;
            this.output = writer;
            this.callback = callback;
            this.activeFields = new Stack<ActiveField>();
        }

        public MetaType TopLevelType => topLevelType;

        public MetaType Type => currentType;

        public Field Field
        {
            get
            {
                if (activeFields.Count == 0)
                    return null;

                return activeFields.Peek().Field;
            }
        }

        public int Position => position;

        public byte GetByte() => buffer[0];

        public short GetInt16() => (short)(buffer[0] | (buffer[1] << 8));

        public ushort GetUInt16() => (ushort)(buffer[0] | (buffer[1] << 8));

        public int GetInt32() => buffer[0] | (buffer[1] << 8) | (buffer[2] << 16) | (buffer[3] << 24);

        public uint GetUInt32() => (uint)buffer[0] | ((uint)buffer[1] << 8) | ((uint)buffer[2] << 16) | ((uint)buffer[3] << 24);

        public void SetInt32(int value)
        {
            buffer[0] = (byte)value;
            buffer[1] = (byte)(value >> 8);
            buffer[2] = (byte)(value >> 16);
            buffer[3] = (byte)(value >> 24);
        }

        public string GetCurrentFieldName()
        {
            if (activeFields.Count == 0)
                return null;

            List<string> names = new List<string>();

            foreach (ActiveField state in activeFields)
            {
                if (string.IsNullOrEmpty(state.Field.Name))
                    return null;

                if (state.Index >= 0)
                    names.Add(string.Format("{0}[{1}]", state.Field.Name, state.Index));
                else
                    names.Add(state.Field.Name);
            }

            names.Add(topLevelType.Name);
            names.Reverse();

            return string.Join(".", names.ToArray());
        }

        public string GetParentFieldName()
        {
            if (activeFields.Count == 0)
                return null;

            List<string> names = new List<string>();

            foreach (ActiveField state in activeFields)
            {
                if (string.IsNullOrEmpty(state.Field.Name))
                    return null;

                if (state.Index >= 0)
                    names.Add(string.Format("{0}[{1}]", state.Field.Name, state.Index));
                else
                    names.Add(state.Field.Name);
            }

            names.Add(topLevelType.Name);
            names.Reverse();
            names.RemoveAt(names.Count - 1);

            return string.Join(".", names.ToArray());
        }

        public override void VisitByte(MetaByte type) => CopyBytes(type);
        public override void VisitInt16(MetaInt16 type) => CopyBytes(type);
        public override void VisitUInt16(MetaUInt16 type) => CopyBytes(type);
        public override void VisitInt32(MetaInt32 type) => CopyBytes(type);
        public override void VisitUInt32(MetaUInt32 type) => CopyBytes(type);
        public override void VisitInt64(MetaInt64 type) => CopyBytes(type);
        public override void VisitUInt64(MetaUInt64 type) => CopyBytes(type);
        public override void VisitFloat(MetaFloat type) => CopyBytes(type);
        public override void VisitColor(MetaColor type) => CopyBytes(type);
        public override void VisitRawOffset(MetaRawOffset type) => CopyBytes(type);
        public override void VisitSepOffset(MetaSepOffset type) => CopyBytes(type);
        public override void VisitPointer(MetaPointer type) => CopyBytes(type);
        public override void VisitBoundingBox(MetaBoundingBox type) => CopyBytes(type);
        public override void VisitBoundingSphere(MetaBoundingSphere type) => CopyBytes(type);
        public override void VisitMatrix4x3(MetaMatrix4x3 type) => CopyBytes(type);
        public override void VisitPlane(MetaPlane type) => CopyBytes(type);
        public override void VisitQuaternion(MetaQuaternion type) => CopyBytes(type);
        public override void VisitVector2(MetaVector2 type) => CopyBytes(type);
        public override void VisitVector3(MetaVector3 type) => CopyBytes(type);

        private void CopyBytes(MetaType type)
        {
            BeginCopy(type, 1);
            EndCopy();
        }

        public override void VisitString(MetaString type)
        {
            BeginCopy(type, 1);

            bool zeroFound = false;

            for (int i = 0; i < type.Size; i++)
            {
                if (zeroFound)
                    buffer[i] = 0;
                else if (buffer[i] == 0)
                    zeroFound = true;
            }

            EndCopy();
        }

        public override void VisitPadding(MetaPadding type)
        {
            BeginCopy(type, 1);

            if (type.FillByte == 0)
            {
                Array.Clear(buffer, 0, type.Size);
            }
            else
            {
                for (int i = 0; i < type.Size; i++)
                    buffer[i] = type.FillByte;
            }

            EndCopy();
        }

        public override void VisitStruct(MetaStruct type)
        {
            if (topLevelType == null)
                topLevelType = type;

            foreach (Field field in type.Fields)
            {
                BeginCopyField(field);
                field.Type.Accept(this);
                EndCopyField(field);
            }
        }

        internal void BeginCopyField(Field field)
        {
            activeFields.Push(new ActiveField(field));
        }

        internal void EndCopyField(Field field)
        {
            if (activeFields.Peek().Field != field)
                throw new InvalidOperationException();

            activeFields.Pop();
        }

        public override void VisitArray(MetaArray type)
        {
            CopyArray(type.ElementType, type.Count);
        }

        public override void VisitVarArray(MetaVarArray type)
        {
            BeginCopyField(type.CountField);
            type.CountField.Type.Accept(this);
            EndCopyField(type.CountField);

            int length;

            if (type.CountField.Type == MetaType.Int16)
                length = GetInt16();
            else
                length = GetInt32();

            if (length < 0)
                throw new InvalidDataException(string.Format("Invalid array length: 0x{0:x} at offset 0x{1:x}", length, position));

            CopyArray(type.ElementType, length);
        }

        private void CopyArray(MetaType elementType, int count)
        {
            if (elementType.IsBlittable)
            {
                BeginCopy(elementType, count);
                EndCopy();
                return;
            }

            for (int i = 0; i < count; i++)
            {
                BeginCopyArrayElement(i);
                elementType.Accept(this);
                EndCopyArrayElement(i);
            }
        }

        private void BeginCopyArrayElement(int index)
        {
            if (activeFields.Count == 0)
                return;

            activeFields.Peek().Index = index;
        }

        private void EndCopyArrayElement(int index)
        {
            if (activeFields.Count == 0)
                return;

            ActiveField field = activeFields.Peek();

            if (field.Index != index)
                throw new InvalidOperationException();

            field.Index = -1;
        }

        private void BeginCopy(MetaType type, int count)
        {
            currentType = type;

            fieldSize = type.Size * count;

            if (buffer == null || buffer.Length < fieldSize)
                buffer = new byte[fieldSize * 2];

            input.Read(buffer, 0, fieldSize);
        }

        private void EndCopy()
        {
            if (callback != null)
                callback(this);

            if (output != null)
                output.Write(buffer, 0, fieldSize);

            position += fieldSize;
        }
    }
}
