﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using Oni.Imaging;

namespace Oni
{
    internal class BinaryWriter : System.IO.BinaryWriter
    {
        private static readonly byte[] padding = new byte[32];
        private static readonly Encoding encoding = Encoding.UTF8;
        private readonly Stack<int> positionStack = new Stack<int>();

        public BinaryWriter(Stream stream)
            : base(stream, encoding)
        {
        }

        public void WriteInstanceId(int index)
        {
            Write(InstanceFileWriter.MakeInstanceId(index));
        }

        public void Write(IEnumerable<ImporterDescriptor> descriptors)
        {
            foreach (var descriptor in descriptors)
                Write(descriptor);
        }

        public void Write(ImporterDescriptor descriptor)
        {
            if (descriptor == null)
                Write(0);
            else
                Write(InstanceFileWriter.MakeInstanceId(descriptor.Index));
        }

        public void Write(Color c)
        {
            Write(c.ToBgra32());
        }

        public void Write(Vector2 v)
        {
            Write(v.X);
            Write(v.Y);
        }

        public void Write(Vector3 v)
        {
            Write(v.X);
            Write(v.Y);
            Write(v.Z);
        }

        public void Write(Vector4 v)
        {
            Write(v.X);
            Write(v.Y);
            Write(v.Z);
            Write(v.W);
        }

        public void Write(Quaternion q)
        {
            Write(q.X);
            Write(q.Y);
            Write(q.Z);
            Write(-q.W);
        }

        public void Write(Plane p)
        {
            Write(p.Normal);
            Write(p.D);
        }

        public void Write(BoundingBox bbox)
        {
            Write(bbox.Min);
            Write(bbox.Max);
        }

        public void Write(BoundingSphere bsphere)
        {
            Write(bsphere.Center);
            Write(bsphere.Radius);
        }

        public void WriteMatrix4x3(Matrix m)
        {
            Write(m.M11);
            Write(m.M12);
            Write(m.M13);
            Write(m.M21);
            Write(m.M22);
            Write(m.M23);
            Write(m.M31);
            Write(m.M32);
            Write(m.M33);
            Write(m.M41);
            Write(m.M42);
            Write(m.M43);
        }

        public void Write(short[] a)
        {
            foreach (short v in a)
                Write(v);
        }

        public void Write(ushort[] a)
        {
            foreach (ushort v in a)
                Write(v);
        }

        public void Write(int[] a)
        {
            foreach (int v in a)
                Write(v);
        }

        public void Write(int[] v, int startIndex, int length)
        {
            for (int i = startIndex; i < startIndex + length; i++)
                Write(v[i]);
        }

        public void Write(IEnumerable<float> a)
        {
            foreach (float v in a)
                Write(v);
        }

        public void Write(IEnumerable<int> a)
        {
            foreach (int i in a)
                Write(i);
        }

        public void Write(Color[] a)
        {
            foreach (Color v in a)
                Write(v);
        }

        public void Write(IEnumerable<Vector2> a)
        {
            foreach (Vector2 v in a)
                Write(v);
        }

        public void Write(IEnumerable<Vector3> a)
        {
            foreach (Vector3 v in a)
                Write(v);
        }

        public void Write(IEnumerable<Plane> a)
        {
            foreach (Plane v in a)
                Write(v);
        }

        public void Write(string s, int maxLength)
        {
            if (s == null)
            {
                Skip(maxLength);
                return;
            }

            if (encoding.GetByteCount(s) > maxLength)
                throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "The string '{0}' is too long (max length is {1})", s, maxLength));

            byte[] data = new byte[maxLength];
            encoding.GetBytes(s, 0, s.Length, data, 0);
            Write(data);
        }

        public void WriteByte(int value)
        {
            if (value < byte.MinValue || byte.MaxValue < value)
                throw new ArgumentOutOfRangeException("Value too large for Byte", "value");

            Write((byte)value);
        }

        public void WriteInt16(int value)
        {
            if (value < short.MinValue || short.MaxValue < value)
                throw new ArgumentOutOfRangeException("Value too large for Int16", "value");

            Write((short)value);
        }

        public void WriteUInt16(int value)
        {
            if (value < 0 || value > UInt16.MaxValue)
                throw new ArgumentOutOfRangeException("Value too large for UInt16", "value");

            Write((ushort)value);
        }

        public void Write(byte value, int count)
        {
            if (value == 0 && Position == OutStream.Length)
            {
                Seek(count, SeekOrigin.Current);
            }
            else
            {
                for (int i = 0; i < count; i++)
                    Write(value);
            }
        }

        public void Skip(int length)
        {
            Position += length;
        }

        public override Stream BaseStream
        {
            get
            {
                //
                // Note: return base OutStream directly instead of BaseStream to avoid
                // flushing the stream.
                //

                return base.OutStream;
            }
        }

        public void PushPosition(int newPosition)
        {
            positionStack.Push(Position);
            Position = newPosition;
        }

        public void PopPosition()
        {
            Position = positionStack.Pop();
        }

        public int Position
        {
            get
            {
                return (int)BaseStream.Position;
            }
            set
            {
                int currentPosition = (int)OutStream.Position;
                int delta = value - currentPosition;

                if (delta == 0)
                    return;

                //
                // Prevent changing the output stream position for small changes of position.
                // This avoids flushing the write cache of the stream which results in poor perf.
                //

                if (0 < delta && delta <= 32 && Position == OutStream.Length)
                    OutStream.Write(padding, 0, delta);
                else
                    OutStream.Position = value;
            }
        }

        public void WriteAt(int position, int value)
        {
            PushPosition(position);
            Write(value);
            PopPosition();
        }

        public void WriteAt(int position, short value)
        {
            PushPosition(position);
            Write(value);
            PopPosition();
        }

        public int Align32()
        {
            int position = Utils.Align32(Position);
            Position = position;
            return position;
        }
    }
}
