/*
 * Copyright (C) 2012-2017 CypherCore <http://github.com/CypherCore>
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

using Framework.Constants;
using Framework.Cryptography;
using Framework.Dynamic;
using Framework.IO;
using Game.DataStorage;
using System;
using System.Collections.Generic;
using System.Net;

namespace Game.Network.Packets
{
    class Ping : ClientPacket
    {
        public Ping(WorldPacket packet) : base(packet) { }

        public override void Read()
        {
            Serial = _worldPacket.ReadUInt32();
            Latency = _worldPacket.ReadUInt32();
        }
        
        public uint Serial;
        public uint Latency;
    }

    class Pong : ServerPacket
    {
        public Pong(uint serial) : base(ServerOpcodes.Pong)
        {
            Serial = serial;
        }

        public override void Write()
        {
            _worldPacket.WriteUInt32(Serial);
        }

        uint Serial;
    }

    class AuthChallenge : ServerPacket
    {
        public AuthChallenge() : base(ServerOpcodes.AuthChallenge) { }

        public override void Write()
        {
            _worldPacket.WriteBytes(DosChallenge);
            _worldPacket.WriteBytes(Challenge);
            _worldPacket.WriteUInt8(DosZeroBits);
        }

        public byte[] Challenge = new byte[16];
        public byte[] DosChallenge = new byte[32]; // Encryption seeds
        public byte DosZeroBits;
    }

    class AuthSession : ClientPacket
    {
        public AuthSession(WorldPacket packet) : base(packet) { }

        public override void Read()
        {
            uint realmJoinTicketSize;

            DosResponse = _worldPacket.ReadUInt64();
            Build = _worldPacket.ReadUInt16();
            BuildType = _worldPacket.ReadInt8();
            RegionID = _worldPacket.ReadUInt32();
            BattlegroupID = _worldPacket.ReadUInt32();
            RealmID = _worldPacket.ReadUInt32();

            for (var i = 0; i < LocalChallenge.GetLimit(); ++i)
                LocalChallenge.Add(_worldPacket.ReadUInt8());

            Digest = _worldPacket.ReadBytes(24);

            UseIPv6 = _worldPacket.HasBit();
            realmJoinTicketSize = _worldPacket.ReadUInt32();
            if (realmJoinTicketSize != 0)
                RealmJoinTicket = _worldPacket.ReadString(realmJoinTicketSize);
        }

        public ushort Build;
        public sbyte BuildType;
        public uint RegionID;
        public uint BattlegroupID;
        public uint RealmID;
        public Array<byte> LocalChallenge = new Array<byte>(16);
        public byte[] Digest = new byte[24];
        public ulong DosResponse;
        public string RealmJoinTicket;
        public bool UseIPv6;
    }

    class AuthResponse : ServerPacket
    {
        public AuthResponse() : base(ServerOpcodes.AuthResponse)
        {
            WaitInfo = new Optional<AuthWaitInfo>();
            SuccessInfo = new Optional<AuthSuccessInfo>();
        }

        public override void Write()
        {
            _worldPacket.WriteUInt32(Result);
            _worldPacket.WriteBit(SuccessInfo.HasValue);
            _worldPacket.WriteBit(WaitInfo.HasValue);
            _worldPacket.FlushBits();

            if (SuccessInfo.HasValue)
            {
                _worldPacket.WriteUInt32(SuccessInfo.Value.VirtualRealmAddress);
                _worldPacket.WriteUInt32(SuccessInfo.Value.VirtualRealms.Count);
                _worldPacket.WriteUInt32(SuccessInfo.Value.TimeRested);
                _worldPacket.WriteUInt8(SuccessInfo.Value.ActiveExpansionLevel);
                _worldPacket.WriteUInt8(SuccessInfo.Value.AccountExpansionLevel);
                _worldPacket.WriteUInt32(SuccessInfo.Value.TimeSecondsUntilPCKick);
                _worldPacket.WriteUInt32(SuccessInfo.Value.AvailableRaces.Count);
                _worldPacket.WriteUInt32(SuccessInfo.Value.AvailableClasses.Count);
                _worldPacket.WriteUInt32(SuccessInfo.Value.Templates.Count);
                _worldPacket.WriteUInt32(SuccessInfo.Value.CurrencyID);
                _worldPacket.WriteUInt32(SuccessInfo.Value.Time);

                foreach (var race in SuccessInfo.Value.AvailableRaces)
                {
                    _worldPacket.WriteUInt8(race.Key); /// the current race
                    _worldPacket.WriteUInt8(race.Value); /// the required Expansion
                }

                foreach (var klass in SuccessInfo.Value.AvailableClasses)
                {
                    _worldPacket.WriteUInt8(klass.Key); /// the current class
                    _worldPacket.WriteUInt8(klass.Value); /// the required Expansion
                }

                _worldPacket.WriteBit(SuccessInfo.Value.IsExpansionTrial);
                _worldPacket.WriteBit(SuccessInfo.Value.ForceCharacterTemplate);
                _worldPacket.WriteBit(SuccessInfo.Value.NumPlayersHorde.HasValue);
                _worldPacket.WriteBit(SuccessInfo.Value.NumPlayersAlliance.HasValue);
                _worldPacket.FlushBits();

                {
                    _worldPacket.WriteUInt32(SuccessInfo.Value.Billing.BillingPlan);
                    _worldPacket.WriteUInt32(SuccessInfo.Value.Billing.TimeRemain);
                    // 3x same bit is not a mistake - preserves legacy client behavior of BillingPlanFlags::SESSION_IGR
                    _worldPacket.WriteBit(SuccessInfo.Value.Billing.InGameRoom); // inGameRoom check in function checking which lua event to fire when remaining time is near end - BILLING_NAG_DIALOG vs IGR_BILLING_NAG_DIALOG
                    _worldPacket.WriteBit(SuccessInfo.Value.Billing.InGameRoom); // inGameRoom lua return from Script_GetBillingPlan
                    _worldPacket.WriteBit(SuccessInfo.Value.Billing.InGameRoom); // not used anywhere in the client
                    _worldPacket.FlushBits();
                }

                if (SuccessInfo.Value.NumPlayersHorde.HasValue)
                    _worldPacket.WriteUInt16(SuccessInfo.Value.NumPlayersHorde.Value);

                if (SuccessInfo.Value.NumPlayersAlliance.HasValue)
                    _worldPacket.WriteUInt16(SuccessInfo.Value.NumPlayersAlliance.Value);

                foreach (VirtualRealmInfo virtualRealm in SuccessInfo.Value.VirtualRealms)
                    virtualRealm.Write(_worldPacket);

                foreach (var templat in SuccessInfo.Value.Templates)
                {
                    _worldPacket.WriteUInt32(templat.TemplateSetId);
                    _worldPacket.WriteUInt32(templat.Classes.Count);
                    foreach (var templateClass in templat.Classes)
                    {
                        _worldPacket.WriteUInt8(templateClass.ClassID);
                        _worldPacket.WriteUInt8(templateClass.FactionGroup);
                    }

                    _worldPacket.WriteBits(templat.Name.Length, 7);
                    _worldPacket.WriteBits(templat.Description.Length, 10);
                    _worldPacket.FlushBits();

                    _worldPacket.WriteString(templat.Name);
                    _worldPacket.WriteString(templat.Description);
                }
            }

            if (WaitInfo.HasValue)
                WaitInfo.Value.Write(_worldPacket);            
        }

        public Optional<AuthSuccessInfo> SuccessInfo; // contains the packet data in case that it has account information (It is never set when WaitInfo is set), otherwise its contents are undefined.
        public Optional<AuthWaitInfo> WaitInfo; // contains the queue wait information in case the account is in the login queue.
        public BattlenetRpcErrorCode Result; // the result of the authentication process, possible values are @ref BattlenetRpcErrorCode

        public class AuthSuccessInfo
        {
            public byte AccountExpansionLevel; // the current expansion of this account, the possible values are in @ref Expansions
            public byte ActiveExpansionLevel; // the current server expansion, the possible values are in @ref Expansions
            public uint TimeRested; // affects the return value of the GetBillingTimeRested() client API call, it is the number of seconds you have left until the experience points and loot you receive from creatures and quests is reduced. It is only used in the Asia region in retail, it's not implemented in TC and will probably never be.

            public uint VirtualRealmAddress; // a special identifier made from the Index, BattleGroup and Region. @todo implement
            public uint TimeSecondsUntilPCKick; // @todo research
            public uint CurrencyID; // this is probably used for the ingame shop. @todo implement
            public uint Time;

            public BillingInfo Billing;

            public List<VirtualRealmInfo> VirtualRealms = new List<VirtualRealmInfo>();     // list of realms connected to this one (inclusive) @todo implement
            public List<CharacterTemplate> Templates = new List<CharacterTemplate>(); // list of pre-made character templates. @todo implement

            public Dictionary<byte, byte> AvailableClasses; // the minimum AccountExpansion required to select the classes
            public Dictionary<byte, byte> AvailableRaces; // the minimum AccountExpansion required to select the races

            public bool IsExpansionTrial;
            public bool ForceCharacterTemplate; // forces the client to always use a character template when creating a new character. @see Templates. @todo implement
            public Optional<ushort> NumPlayersHorde; // number of horde players in this realm. @todo implement
            public Optional<ushort> NumPlayersAlliance; // number of alliance players in this realm. @todo implement

            public struct BillingInfo
            {
                public uint BillingPlan;
                public uint TimeRemain;
                public bool InGameRoom;
            }
        }
    }

    class WaitQueueUpdate : ServerPacket
    {
        public WaitQueueUpdate() : base(ServerOpcodes.WaitQueueUpdate) { }

        public override void Write()
        {
            WaitInfo.Write(_worldPacket);
        }

        public AuthWaitInfo WaitInfo;
    }

    class WaitQueueFinish : ServerPacket
    {
        public WaitQueueFinish() : base(ServerOpcodes.WaitQueueFinish) { }

        public override void Write() { }
    }

    class ConnectTo : ServerPacket
    {
        public ConnectTo() : base(ServerOpcodes.ConnectTo)
        {
            Payload = new ConnectPayload();
            Payload.PanamaKey = "F41DCB2D728CF3337A4FF338FA89DB01BBBE9C3B65E9DA96268687353E48B94C".ToByteArray();
            Payload.Adler32 = 0xA0A66C10;

            Crypt = new RsaCrypt();
            Crypt.InitializeEncryption(RsaStore.P, RsaStore.Q, RsaStore.DP, RsaStore.DQ, RsaStore.InverseQ);
        }

        public override void Write()
        {
            byte[] port = BitConverter.GetBytes((ushort)Payload.Where.Port);
            byte[] address = new byte[16];
            byte addressType = 3;
            if (Payload.Where.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
            {
                Buffer.BlockCopy(Payload.Where.Address.GetAddressBytes(), 0, address, 0, 4);
                addressType = 1;
            }
            else
            {
                Buffer.BlockCopy(Payload.Where.Address.GetAddressBytes(), 0, address, 0, 16);
                addressType = 2;
            }

            HmacHash hmacHash = new HmacHash(RsaStore.WherePacketHmac);
            hmacHash.Process(address, 16);
            hmacHash.Process(BitConverter.GetBytes(addressType), 1);
            hmacHash.Process(port, 2);
            hmacHash.Process(Haiku);
            hmacHash.Process(Payload.PanamaKey, 32);
            hmacHash.Process(PiDigits, 108);
            hmacHash.Finish(BitConverter.GetBytes(Payload.XorMagic), 1);

            ByteBuffer payload = new ByteBuffer();
            payload.WriteUInt32(Payload.Adler32);
            payload.WriteUInt8(addressType);
            payload.WriteBytes(address, 16);
            payload.WriteUInt16(Payload.Where.Port);
            payload.WriteString(Haiku);
            payload.WriteBytes(Payload.PanamaKey, 32);
            payload.WriteBytes(PiDigits, 108);
            payload.WriteUInt8(Payload.XorMagic);
            payload.WriteBytes(hmacHash.Digest);

            _worldPacket.WriteUInt64(Key);
            _worldPacket.WriteUInt32(Serial);
            _worldPacket.WriteBytes(Crypt.Encrypt(payload.GetData()), 256);
            _worldPacket.WriteUInt8(Con);
        }

        public ulong Key;
        public ConnectToSerial Serial;
        public ConnectPayload Payload;
        public byte Con;

        public string Haiku = "An island of peace\nCorruption is brought ashore\nPandarens will rise\n\0\0\0";
        public byte[] PiDigits = { 0x31, 0x41, 0x59, 0x26, 0x53, 0x58, 0x97, 0x93, 0x23, 0x84, 0x62, 0x64, 0x33, 0x83, 0x27, 0x95, 0x02, 0x88, 0x41, 0x97,
            0x16, 0x93, 0x99, 0x37, 0x51, 0x05, 0x82, 0x09, 0x74, 0x94, 0x45, 0x92, 0x30, 0x78, 0x16, 0x40, 0x62, 0x86, 0x20, 0x89,
            0x98, 0x62, 0x80, 0x34, 0x82, 0x53, 0x42, 0x11, 0x70, 0x67, 0x98, 0x21, 0x48, 0x08, 0x65, 0x13, 0x28, 0x23, 0x06, 0x64,
            0x70, 0x93, 0x84, 0x46, 0x09, 0x55, 0x05, 0x82, 0x23, 0x17, 0x25, 0x35, 0x94, 0x08, 0x12, 0x84, 0x81, 0x11, 0x74, 0x50,
            0x28, 0x41, 0x02, 0x70, 0x19, 0x38, 0x52, 0x11, 0x05, 0x55, 0x96, 0x44, 0x62, 0x29, 0x48, 0x95, 0x49, 0x30, 0x38, 0x19,
            0x64, 0x42, 0x88, 0x10, 0x97, 0x56, 0x65, 0x93, 0x34, 0x46, 0x12, 0x84, 0x75, 0x64, 0x82, 0x33, 0x78, 0x67, 0x83, 0x16,
            0x52, 0x71, 0x20, 0x19, 0x09, 0x14, 0x56, 0x48, 0x56, 0x69 };

        public class ConnectPayload
        {
            public IPEndPoint Where;
            public uint Adler32;
            public byte XorMagic = 0x2A;
            public byte[] PanamaKey = new byte[32];
        }

        RsaCrypt Crypt;
    }

    class AuthContinuedSession : ClientPacket
    {
        public AuthContinuedSession(WorldPacket packet) : base(packet) { }

        public override void Read()
        {
            DosResponse = _worldPacket.ReadUInt64();
            Key = _worldPacket.ReadUInt64();
            LocalChallenge = _worldPacket.ReadBytes(16);
            Digest = _worldPacket.ReadBytes(24);
        }

        public ulong DosResponse;
        public ulong Key;
        public byte[] LocalChallenge = new byte[16];
        public byte[] Digest = new byte[24];
    }

    class ResumeComms : ServerPacket
    {
        public ResumeComms(ConnectionType connection) : base(ServerOpcodes.ResumeComms, connection) { }

        public override void Write() { }
    }

    class ConnectToFailed : ClientPacket
    {
        public ConnectToFailed(WorldPacket packet) : base(packet) { }

        public override void Read()
        {
            Serial = (ConnectToSerial)_worldPacket.ReadUInt32();
            Con = _worldPacket.ReadUInt8();
        }

        public ConnectToSerial Serial;
        byte Con;
    }

    class EnableEncryption : ServerPacket
    {
        public EnableEncryption() : base(ServerOpcodes.EnableEncryption) { }

        public override void Write() { }
    }

    //Structs
    public struct AuthWaitInfo
    {
        public void Write(WorldPacket data)
        {
            data.WriteUInt32(WaitCount);
            data.WriteUInt32(WaitTime);
            data.WriteBit(HasFCM);
            data.FlushBits();
        }

        public uint WaitCount; // position of the account in the login queue
        public uint WaitTime; // Wait time in login queue in minutes, if sent queued and this value is 0 client displays "unknown time"
        public bool HasFCM; // true if the account has a forced character migration pending. @todo implement
    }

    struct VirtualRealmNameInfo
    {
        public VirtualRealmNameInfo(bool isHomeRealm, bool isInternalRealm, string realmNameActual, string realmNameNormalized)
        {
            IsLocal = isHomeRealm;
            IsInternalRealm = isInternalRealm;
            RealmNameActual = realmNameActual;
            RealmNameNormalized = realmNameNormalized;
        }

        public void Write(WorldPacket data)
        {
            data.WriteBit(IsLocal);
            data.WriteBit(IsInternalRealm);
            data.WriteBits(RealmNameActual.Length, 8);
            data.WriteBits(RealmNameNormalized.Length, 8);
            data.FlushBits();

            data.WriteString(RealmNameActual);
            data.WriteString(RealmNameNormalized);
        }

        public bool IsLocal;                    // true if the realm is the same as the account's home realm
        public bool IsInternalRealm;            // @todo research
        public string RealmNameActual;     // the name of the realm
        public string RealmNameNormalized; // the name of the realm without spaces
    }

    struct VirtualRealmInfo
    {
        public VirtualRealmInfo(uint realmAddress, bool isHomeRealm, bool isInternalRealm, string realmNameActual, string realmNameNormalized)
        {

            RealmAddress = realmAddress;
            RealmNameInfo = new VirtualRealmNameInfo(isHomeRealm, isInternalRealm, realmNameActual, realmNameNormalized);
        }

        public void Write(WorldPacket data)
        {
            data.WriteUInt32(RealmAddress);
            RealmNameInfo.Write(data);
        }

        public uint RealmAddress;             // the virtual address of this realm, constructed as RealmHandle::Region << 24 | RealmHandle::Battlegroup << 16 | RealmHandle::Index
        public VirtualRealmNameInfo RealmNameInfo;
    }
}