// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

#nullable disable

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

// ReSharper disable ConditionIsAlwaysTrueOrFalse (we're allowing nulls to be passed to the writer where the underlying class doesn't).
// ReSharper disable HeuristicUnreachableCode

namespace osu.Game.IO.Legacy
{
    /// <summary> SerializationWriter.  Extends BinaryWriter to add additional data types,
    /// handle null strings and simplify use with ISerializable. </summary>
    public class SerializationWriter : BinaryWriter
    {
        public SerializationWriter(Stream s, bool leaveOpen = false)
            : base(s, Encoding.UTF8, leaveOpen)
        {
        }

        /// <summary> Static method to initialise the writer with a suitable MemoryStream. </summary>
        public static SerializationWriter GetWriter()
        {
            MemoryStream ms = new MemoryStream(1024);
            return new SerializationWriter(ms);
        }

        /// <summary> Writes a string to the buffer.  Overrides the base implementation so it can cope with nulls </summary>
        public override void Write(string str)
        {
            if (str == null)
            {
                Write((byte)ObjType.nullType);
            }
            else
            {
                Write((byte)ObjType.stringType);
                base.Write(str);
            }
        }

        /// <summary> Writes a byte array to the buffer.  Overrides the base implementation to
        /// send the length of the array which is needed when it is retrieved </summary>
        public override void Write(byte[] b)
        {
            if (b == null)
            {
                Write(-1);
            }
            else
            {
                int len = b.Length;
                Write(len);
                if (len > 0) base.Write(b);
            }
        }

        /// <summary> Writes a char array to the buffer.  Overrides the base implementation to
        /// sends the length of the array which is needed when it is read. </summary>
        public override void Write(char[] c)
        {
            if (c == null)
            {
                Write(-1);
            }
            else
            {
                int len = c.Length;
                Write(len);
                if (len > 0) base.Write(c);
            }
        }

        /// <summary>
        /// Writes DateTime to the buffer.
        /// </summary>
        /// <param name="dt"></param>
        public void Write(DateTime dt)
        {
            Write(dt.ToUniversalTime().Ticks);
        }

        /// <summary> Writes a generic ICollection (such as an IList(T)) to the buffer.</summary>
        public void Write<T>(List<T> c) where T : ILegacySerializable
        {
            if (c == null)
            {
                Write(-1);
            }
            else
            {
                int count = c.Count;
                Write(count);
                for (int i = 0; i < count; i++)
                    c[i].WriteToStream(this);
            }
        }

        /// <summary> Writes a generic IDictionary to the buffer. </summary>
        public void Write<TKey, TValue>(IDictionary<TKey, TValue> d)
        {
            if (d == null)
            {
                Write(-1);
            }
            else
            {
                Write(d.Count);

                foreach (KeyValuePair<TKey, TValue> kvp in d)
                {
                    WriteObject(kvp.Key);
                    WriteObject(kvp.Value);
                }
            }
        }

        /// <summary> Writes an arbitrary object to the buffer.  Useful where we have something of type "object"
        /// and don't know how to treat it.  This works out the best method to use to write to the buffer. </summary>
        public void WriteObject(object obj)
        {
            if (obj == null)
            {
                Write((byte)ObjType.nullType);
            }
            else
            {
                switch (obj)
                {
                    case bool boolObj:
                        Write((byte)ObjType.boolType);
                        Write(boolObj);
                        break;

                    case byte byteObj:
                        Write((byte)ObjType.byteType);
                        Write(byteObj);
                        break;

                    case ushort ushortObj:
                        Write((byte)ObjType.uint16Type);
                        Write(ushortObj);
                        break;

                    case uint uintObj:
                        Write((byte)ObjType.uint32Type);
                        Write(uintObj);
                        break;

                    case ulong ulongObj:
                        Write((byte)ObjType.uint64Type);
                        Write(ulongObj);
                        break;

                    case sbyte sbyteObj:
                        Write((byte)ObjType.sbyteType);
                        Write(sbyteObj);
                        break;

                    case short shortObj:
                        Write((byte)ObjType.int16Type);
                        Write(shortObj);
                        break;

                    case int intObj:
                        Write((byte)ObjType.int32Type);
                        Write(intObj);
                        break;

                    case long longObj:
                        Write((byte)ObjType.int64Type);
                        Write(longObj);
                        break;

                    case char charObj:
                        Write((byte)ObjType.charType);
                        base.Write(charObj);
                        break;

                    case string stringObj:
                        Write((byte)ObjType.stringType);
                        base.Write(stringObj);
                        break;

                    case float floatObj:
                        Write((byte)ObjType.singleType);
                        Write(floatObj);
                        break;

                    case double doubleObj:
                        Write((byte)ObjType.doubleType);
                        Write(doubleObj);
                        break;

                    case decimal decimalObj:
                        Write((byte)ObjType.decimalType);
                        Write(decimalObj);
                        break;

                    case DateTime dateTimeObj:
                        Write((byte)ObjType.dateTimeType);
                        Write(dateTimeObj);
                        break;

                    case byte[] byteArray:
                        Write((byte)ObjType.byteArrayType);
                        base.Write(byteArray);
                        break;

                    case char[] charArray:
                        Write((byte)ObjType.charArrayType);
                        base.Write(charArray);
                        break;

                    default:
                        throw new IOException("Serialization of arbitrary type is not supported.");
                } // switch
            } // if obj==null
        } // WriteObject

        public void WriteRawBytes(byte[] b)
        {
            base.Write(b);
        }

        public void WriteByteArray(byte[] b)
        {
            if (b == null)
            {
                Write(-1);
            }
            else
            {
                int len = b.Length;
                Write(len);
                if (len > 0) base.Write(b);
            }
        }

        public void WriteUtf8(string str)
        {
            WriteRawBytes(Encoding.UTF8.GetBytes(str));
        }
    }
}
