// Copyright © 2017-2018 Atomic Software, LLC. All Rights Reserved.
// See LICENSE.md for full license information.

using Atom.Core.Diagnostics;
using Atom.Core.Extensions;
using Atom.Core.Game.GameObjects.Characters;
using Atom.Core.Game.GameObjects.Items;
using Atom.Core.Serialization;
using System;
using System.IO;
using System.Text;

namespace Atom.Core.Networking.Messages
{
    public class NetworkMessage : IDisposable
    {
        public const int MaxMessageSize = ushort.MaxValue;
        public MessageProtocol Protocol { get; set; }
        public ushort Department { get; set; }
        public ushort Command { get; set; }
        public Client Client { get; set; }

        private readonly MemoryStream Stream;
        private readonly BinaryReader Reader;
        private readonly BinaryWriter Writer;

        public NetworkMessage(byte[] bytes)
        {
            Stream = new MemoryStream(bytes);
            Reader = new BinaryReader(Stream);

            Protocol = (MessageProtocol) ReadUInt16();
            Department = (ushort) ((ushort) Protocol >> 10);
            Command = (ushort) ((ushort) Protocol & 1023);
        }

        public NetworkMessage(MessageProtocol protocol) : this((ushort) protocol)
        {
        }

        public NetworkMessage(ushort protocol)
        {
            Stream = new MemoryStream();
            Writer = new BinaryWriter(Stream);

            Protocol = (MessageProtocol) protocol;
            Department = (ushort) (protocol >> 10);
            Command = (ushort) (protocol & 1023);

            Write(protocol);
        }

        public void Send(Client client)
        {
            client.SendBytes(ToArray());
            Log.Info($"Sent: {this}");

            Dispose();
        }

        public void SendLocal(Client client)
        {
            client.SendBytes(ToArray(client.EncryptionTable));
            Log.Info($"Sent: {this}");

            Dispose();
        }

        public bool ReadBoolean() => Reader.ReadBoolean();

        public byte ReadByte() => Reader.ReadByte();

        public byte[] ReadBytes(int count) => Reader.ReadBytes(count);

        public byte[] ReadBytesAndSize()
        {
            var BufferLength = Reader.ReadByte();

            return Reader.ReadBytes(BufferLength);
        }

        public char ReadChar() => Reader.ReadChar();

        public decimal ReadDecimal() => Reader.ReadDecimal();

        public double ReadDouble() => Reader.ReadDouble();

        public short ReadInt16() => Reader.ReadInt16();

        public int ReadInt32() => Reader.ReadInt32();

        public long ReadInt64() => Reader.ReadInt64();

        public sbyte ReadSByte() => Reader.ReadSByte();

        public float ReadSingle() => Reader.ReadSingle();

        public string ReadString()
        {
            var length = ReadByte();

            return ReadString(length);
        }

        public string ReadString(int length)
        {
            var ReturnValue = string.Empty;
            var Buffer = new byte[length];
            var Count = 0;

            Stream.Read(Buffer, 0, Buffer.Length);

            if (Buffer[length - 1] != 0)
            {
                Count = length;
            }
            else
            {
                while (Buffer[Count] != 0 && Count < length)
                {
                    Count++;
                }
            }

            if (Count > 0)
            {
                ReturnValue = Encoding.ASCII.GetString(Buffer, 0, Count);
            }

            return ReturnValue;
        }

        public ushort ReadUInt16() => Reader.ReadUInt16();

        public uint ReadUInt32() => Reader.ReadUInt32();

        public ulong ReadUInt64() => Reader.ReadUInt64();

        public void Write(char value) => Writer?.Write(value);

        public void Write(byte value) => Writer?.Write(value);

        public void Write(sbyte value) => Writer?.Write(value);

        public void Write(ushort value) => Writer?.Write(value);

        public void Write(short value) => Writer?.Write(value);

        public void Write(uint value) => Writer?.Write(value);

        public void Write(int value) => Writer?.Write(value);

        public void Write(ulong value) => Writer?.Write(value);

        public void Write(long value) => Writer?.Write(value);

        public void Write(float value) => Writer?.Write(value);

        public void Write(double value) => Writer?.Write(value);

        public void Write(decimal value) => Writer?.Write(value);

        public void Write(bool value) => Writer?.Write(value);

        public void Write(byte[] value) => Writer?.Write(value);

        public void Write(string value) => Writer?.Write(Encoding.ASCII.GetBytes(value));

        public void Write(string value, int length)
        {
            var StringBytes = Encoding.ASCII.GetBytes(value);

            Write(StringBytes);

            for (var i = 0; i < length - StringBytes.Length; i++)
            {
                Write((byte)0);
            }
        }

        public void Write(string value, bool writeLength)
        {
            if (!writeLength)
            {
                Write(value);
                return;
            }

            Write((byte)value.Length);
            Write(Encoding.ASCII.GetBytes(value));
        }

        public void WriteTerminate(string value)
        {
            value += char.MinValue;

            Write((byte)value.Length);
            Write(Encoding.ASCII.GetBytes(value));
        }

        // CUSTOM WRITE METHODS
        public void WriteLogin(SimpleCharacter character)
        {
            Write(character.CharNo);
            Write(character.Name, 20);
            Write((ushort) character.Level);
            Write(character.Slot);
            Write(character.MapIndx, 12);
            WriteDeleteInfo(character);
            WriteShape(character);
            WriteEquipment(character);
            Write(character.KQHandle);
            Write(character.KQMapIndx, 12);
            Write(character.KQPosition.X);
            Write(character.KQPosition.Y);
            Write(character.KQMapIndx != "" ? character.KQDate.Shift() : 0);
            WriteNameChangeData(character);
            Write((int) character.TutorialState);
            Write(character.TutorialStep);
            Write((byte) 0x00);
        }

        public void WriteDeleteInfo(SimpleCharacter character)
        {
            if (!character.IsDeleted)
            {
                Fill(5, 0);
                return;
            }
            
            Write((byte)character.DeletedDate.Year);
            Write((byte)character.DeletedDate.Month);
            Write((byte)character.DeletedDate.Day);
            Write((byte)character.DeletedDate.Hour);
            Write((byte)character.DeletedDate.Minute);
        }

        public void WriteShape(SimpleCharacter character)
        {
            Write(Convert.ToByte(0x01 | (byte)character.Class << 2 | (byte)character.Gender << 7));
            Write(character.Hair);
            Write(character.HairColor);
            Write(character.Face);
        }

        public void WriteEquipment(SimpleCharacter character)
        {
            Write(character.GetEquippedItem(ItemSlot.HEAD));
            Write(character.GetEquippedItem(ItemSlot.MOUTH));
            Write(character.GetEquippedItem(ItemSlot.RIGHTHAND));
            Write(character.GetEquippedItem(ItemSlot.BODY));
            Write(character.GetEquippedItem(ItemSlot.LEFTHAND));
            Write(character.GetEquippedItem(ItemSlot.PANT));
            Write(character.GetEquippedItem(ItemSlot.BOOT));
            Write(character.GetEquippedItem(ItemSlot.ACCBOOT));
            Write(character.GetEquippedItem(ItemSlot.ACCPANT));
            Write(character.GetEquippedItem(ItemSlot.ACCBODY));
            Write(character.GetEquippedItem(ItemSlot.ACCHEAD));
            Write(character.GetEquippedItem(ItemSlot.MINIMON_R));
            Write(character.GetEquippedItem(ItemSlot.EYE));
            Write(character.GetEquippedItem(ItemSlot.ACCLEFTHAND));
            Write(character.GetEquippedItem(ItemSlot.ACCRIGHTHAND));
            Write(character.GetEquippedItem(ItemSlot.ACCBACK));
            Write(character.GetEquippedItem(ItemSlot.COSEFF));
            Write(character.GetEquippedItem(ItemSlot.ACCHIP));
            Write(character.GetEquippedItem(ItemSlot.MINIMON));
            Write(character.GetEquippedItem(ItemSlot.ACCSHIELD));
            Write((byte) 0xA0);
            Write((byte) 0x00);
            Write((byte) 0xF0);
        }

        public void WriteNameChangeData(SimpleCharacter character)
        {
            Write(character.ShouldChangeName());

            if (!character.ShouldChangeName())
            {
                Fill(4, 0);
                return;
            }

            Write(true);
            Write(0);
        }

        public void Write(ISerializable value, SerializeType type = SerializeType.Default) => value.Serialize(this, type);

        public void Fill(int length, byte value)
        {
            for (var i = 0; i < length; i++)
            {
                Write(value);
            }
        }

        public override string ToString() => $"Protocol=0x{Protocol:X} ({Protocol}), Length={Stream.Length - 2}";

        public byte[] ToArray(EncryptionTable encryption = null)
        {
            byte[] ReturnValue;
            var MessageBytes = Stream.ToArray();

            encryption?.CryptBytes(MessageBytes , 0, MessageBytes .Length);

            if (MessageBytes .Length <= 0xff)
            {
                ReturnValue = new byte[MessageBytes .Length + 1];

                Buffer.BlockCopy(MessageBytes , 0, ReturnValue, 1, MessageBytes .Length);

                ReturnValue[0] = (byte)MessageBytes .Length;
            }
            else
            {
                ReturnValue = new byte[MessageBytes .Length + 3];

                Buffer.BlockCopy(MessageBytes , 0, ReturnValue, 3, MessageBytes .Length);
                Buffer.BlockCopy(BitConverter.GetBytes((ushort)MessageBytes .Length), 0, ReturnValue, 1, 2);
            }

            return ReturnValue;
        }

        public void Dispose()
        {
            if (Stream.CanRead || Stream.CanWrite)
            {
                Stream.Close();
            }

            Writer?.Close();
            Reader?.Close();
            Stream?.Dispose();
            Writer?.Dispose();
            Reader?.Dispose();
        }
    }
}
