﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Oni.Imaging;

namespace Oni
{
    internal sealed class BinaryReader : IDisposable
    {
        #region Private data
        private static readonly byte[] seekBuffer = new byte[512];
        private static readonly Encoding encoding = Encoding.UTF8;
        private const float rotationAngleScale = MathHelper.Pi / 32767.5f;

        private FileStream stream;
        private byte[] buffer;
        private bool bigEndian;
        private InstanceFile instanceFile;
        #endregion

        public BinaryReader(string filePath)
        {
            this.buffer = new byte[8];
            this.stream = File.OpenRead(filePath);
        }

        public BinaryReader(string filePath, bool bigEndian)
            : this(filePath)
        {
            this.bigEndian = bigEndian;
        }

        public BinaryReader(string filePath, InstanceFile instanceFile)
            : this(filePath)
        {
            this.instanceFile = instanceFile;
        }

        public void Dispose()
        {
            if (stream != null)
                stream.Dispose();

            stream = null;
            buffer = null;
        }

        public string Name => stream.Name;

        public int Length => (int)stream.Length;

        public int Position
        {
            get
            {
                return (int)stream.Position;
            }
            set
            {
                int currentPosition = (int)stream.Position;
                int delta = value - currentPosition;

                if (delta == 0)
                    return;

                if (delta > 0 && delta <= seekBuffer.Length)
                    stream.Read(seekBuffer, 0, delta);
                else
                    stream.Position = value;
            }
        }

        public void Skip(int length)
        {
            Position += length;
        }

        public void SkipCString()
        {
            for (int b = 1; b != 0 && b != -1; b = stream.ReadByte())
                ;
        }

        public int Read(byte[] buffer, int offset, int length)
        {
            return stream.Read(buffer, offset, length);
        }

        public byte[] ReadBytes(int length)
        {
            var buffer = new byte[length];
            int offset = 0;

            while (length > 0)
            {
                int read = stream.Read(buffer, offset, length);

                if (read == 0)
                    break;

                offset += read;
                length -= read;
            }

            if (offset != buffer.Length)
            {
                var result = new byte[offset];
                Buffer.BlockCopy(buffer, 0, result, 0, offset);
                buffer = result;
            }

            return buffer;
        }

        public byte ReadByte()
        {
            int value = stream.ReadByte();

            if (value == -1)
                throw new EndOfStreamException();

            return (byte)value;
        }

        public bool ReadBoolean()
        {
            return (ReadByte() != 0);
        }

        public ushort ReadUInt16()
        {
            FillBuffer(2);

            if (bigEndian)
            {
                return (ushort)(buffer[1] | buffer[0] << 8);
            }

            return (ushort)(buffer[0] | buffer[1] << 8);
        }

        public ushort[] ReadUInt16Array(int length)
        {
            var array = new ushort[length];

            for (int i = 0; i < array.Length; i++)
                array[i] = ReadUInt16();

            return array;
        }

        public uint ReadUInt32()
        {
            FillBuffer(4);

            if (bigEndian)
            {
                return (uint)(buffer[3] | buffer[2] << 8 | buffer[1] << 16 | buffer[0] << 24);
            }

            return (uint)(buffer[0] | buffer[1] << 8 | buffer[2] << 16 | buffer[3] << 24);
        }

        public ulong ReadUInt64()
        {
            FillBuffer(8);

            ulong lo, hi;

            if (bigEndian)
            {
                hi = (uint)(buffer[3] | buffer[2] << 8 | buffer[1] << 16 | buffer[0] << 24);
                lo = (uint)(buffer[7] | buffer[6] << 8 | buffer[5] << 16 | buffer[4] << 24);
            }
            else
            {
                lo = (uint)(buffer[0] | buffer[1] << 8 | buffer[2] << 16 | buffer[3] << 24);
                hi = (uint)(buffer[4] | buffer[5] << 8 | buffer[6] << 16 | buffer[7] << 24);
            }

            return (hi << 32) | lo;
        }

        public short ReadInt16()
        {
            return (short)ReadUInt16();
        }

        public short[] ReadInt16Array(int length)
        {
            var array = new short[length];

            for (int i = 0; i < array.Length; i++)
                array[i] = ReadInt16();

            return array;
        }

        public int ReadInt32()
        {
            return (int)ReadUInt32();
        }

        public int[] ReadInt32VarArray()
        {
            return ReadInt32Array(ReadInt32());
        }

        public int[] ReadInt32Array(int length)
        {
            var array = new int[length];

            for (int i = 0; i < array.Length; i++)
                array[i] = ReadInt32();

            return array;
        }

        public long ReadInt64()
        {
            return (long)ReadUInt64();
        }

        public unsafe float ReadSingle()
        {
            uint value = ReadUInt32();
            return *((float*)&value);
        }

        public float[] ReadSingleArray(int length)
        {
            var data = new float[length];

            for (int i = 0; i < data.Length; i++)
                data[i] = ReadSingle();

            return data;
        }

        public unsafe double ReadDouble()
        {
            ulong value = ReadUInt64();
            return *((double*)&value);
        }

        public Vector2 ReadVector2()
        {
            return new Vector2(ReadSingle(), ReadSingle());
        }

        public Vector2[] ReadVector2VarArray()
        {
            return ReadVector2Array(ReadInt32());
        }

        public Vector2[] ReadVector2Array(int length)
        {
            var data = new Vector2[length];

            for (int i = 0; i < data.Length; i++)
                data[i] = ReadVector2();

            return data;
        }

        public Vector3 ReadVector3()
        {
            return new Vector3(ReadSingle(), ReadSingle(), ReadSingle());
        }

        public Vector3[] ReadVector3VarArray()
        {
            return ReadVector3Array(ReadInt32());
        }

        public Vector3[] ReadVector3Array(int length)
        {
            var data = new Vector3[length];

            for (int i = 0; i < data.Length; i++)
                data[i] = ReadVector3();

            return data;
        }

        public Plane ReadPlane()
        {
            return new Plane(ReadVector3(), ReadSingle());
        }

        public Plane[] ReadPlaneVarArray()
        {
            return ReadPlaneArray(ReadInt32());
        }

        public Plane[] ReadPlaneArray(int length)
        {
            var data = new Plane[length];

            for (int i = 0; i < data.Length; i++)
                data[i] = ReadPlane();

            return data;
        }

        public Quaternion ReadQuaternion()
        {
            return new Quaternion(ReadSingle(), ReadSingle(), ReadSingle(), -ReadSingle());
        }

        public Quaternion ReadCompressedQuaternion()
        {
            return Quaternion.CreateFromAxisAngle(Vector3.UnitX, ReadInt16() * rotationAngleScale)
                * Quaternion.CreateFromAxisAngle(Vector3.UnitY, ReadInt16() * rotationAngleScale)
                * Quaternion.CreateFromAxisAngle(Vector3.UnitZ, ReadInt16() * rotationAngleScale);
        }

        public BoundingBox ReadBoundingBox()
        {
            return new BoundingBox(ReadVector3(), ReadVector3());
        }

        public Matrix ReadMatrix4x3()
        {
            Matrix m;
            m.M11 = ReadSingle();
            m.M12 = ReadSingle();
            m.M13 = ReadSingle();
            m.M14 = 0.0f;
            m.M21 = ReadSingle();
            m.M22 = ReadSingle();
            m.M23 = ReadSingle();
            m.M24 = 0.0f;
            m.M31 = ReadSingle();
            m.M32 = ReadSingle();
            m.M33 = ReadSingle();
            m.M34 = 0.0f;
            m.M41 = ReadSingle();
            m.M42 = ReadSingle();
            m.M43 = ReadSingle();
            m.M44 = 1.0f;
            return m;
        }

        public Color ReadColor()
        {
            uint color = ReadUInt32();

            var r = (byte)((color >> 16) & 0xff);
            var g = (byte)((color >> 08) & 0xff);
            var b = (byte)((color >> 00) & 0xff);
            var a = (byte)((color >> 24) & 0xff);

            return new Color(r, g, b, a);
        }

        public Color[] ReadColorArray(int length)
        {
            var data = new Color[length];

            for (int i = 0; i < data.Length; i++)
                data[i] = ReadColor();

            return data;
        }

        public string ReadString(int maxLength)
        {
            var bytes = ReadBytes(maxLength);

            for (int i = 0; i < bytes.Length; i++)
            {
                if (bytes[i] == 0)
                    return encoding.GetString(bytes, 0, i);
            }

            return encoding.GetString(bytes);
        }

        public string ReadCString()
        {
            var buffer = new List<byte>(64);
            byte b;

            while ((b = ReadByte()) != 0)
                buffer.Add(b);

            return encoding.GetString(buffer.ToArray());
        }

        public InstanceDescriptor ReadInstance()
        {
            return instanceFile.ResolveLink(ReadInt32());
        }

        public InstanceDescriptor[] ReadInstanceArray(int length)
        {
            var data = new InstanceDescriptor[length];

            for (int i = 0; i < data.Length; i++)
                data[i] = ReadInstance();

            return data;
        }

        public InstanceDescriptor ReadLink()
        {
            return instanceFile.GetDescriptor(ReadInt32());
        }

        public InstanceDescriptor[] ReadLinkArray(int length)
        {
            var data = new InstanceDescriptor[length];

            for (int i = 0; i < data.Length; i++)
                data[i] = ReadLink();

            return data;
        }

        private void FillBuffer(int count)
        {
            int offset = 0;

            while (count > 0)
            {
                int read = stream.Read(buffer, offset, count);

                if (read == 0)
                    throw new EndOfStreamException();

                offset += read;
                count -= read;
            }
        }
    }
}
