#ifndef _H_SOLDATINFO_
#define _H_SOLDATINFO_

#include <vector>
#include <string>
#include <sstream>
#include <stdexcept>

class SoldatInfo
{
    public:
        // Class to hold player and spectator data
        class Client
        {
            private:
                int id;
                int team;
                int kills;
                int deaths;
                int ping;
                std::string name;
                std::string ip;

            public:
                Client(int _id, int _team, int _kills, int _deaths, int _ping, std::string _name, std::string _ip)
                    :   id(_id),
                        team(_team),
                        kills(_kills),
                        deaths(_deaths),
                        ping(_ping),
                        name(_name),
                        ip(_ip)
                {
                }

                // Returns player id
                int Id() const
                {
                    return id;
                }

                // Returns player team
                int Team() const
                {
                    return team;
                }

                // Returns player kills
                int Kills() const
                {
                    return kills;
                }

                // Returns player deaths
                int Deaths() const
                {
                    return deaths;
                }

                // Returns player ping
                int Ping() const
                {
                    return ping;
                }

                // Returns player name
                std::string Name() const
                {
                    return name;
                }

                // Returns player id
                std::string Ip() const
                {
                    return ip;
                }
        };

        // typedefs for client iterators
        typedef std::vector<Client>::iterator iterator;
        typedef std::vector<Client>::const_iterator const_iterator;
        typedef std::vector<Client>::reverse_iterator reverse_iterator;
        typedef std::vector<Client>::const_reverse_iterator const_reverse_iterator;

    private:
        // Internal typedefs
        typedef unsigned char byte;
        typedef unsigned short ushort;
        typedef unsigned long ulong;

        // Internal compile-time constants to save some typing (these will be computed by the compiler)
        enum
        {
            MAX_PLAYERS         = 32,
            NAME_LENGTH         = 24,
            MAP_LENGTH          = 16,
            PLAYERS_OFFSET      = 0,
            TEAMS_OFFSET        = PLAYERS_OFFSET + MAX_PLAYERS * (NAME_LENGTH + 1),
            KILLS_OFFSET        = TEAMS_OFFSET + MAX_PLAYERS,
            DEATHS_OFFSET       = KILLS_OFFSET + MAX_PLAYERS * 2,
            PINGS_OFFSET        = DEATHS_OFFSET + MAX_PLAYERS * 2,
            IDS_OFFSET          = PINGS_OFFSET + MAX_PLAYERS,
            IPS_OFFSET          = IDS_OFFSET + MAX_PLAYERS,
            INFO_OFFSET         = IPS_OFFSET + MAX_PLAYERS * 4
        };

        // Some private members for internal usage
        bool empty;

        int team_scores[4];
        int time_limit;
        int cur_time;
        int kill_limit;
        int game_mode;

        std::string map;

        std::vector<Client> players;
        std::vector<Client> specs;

        // Convert ushort from little endian to host byte order
        ushort ltohs(const byte *leshort) const
        {
            // In little endian byte order the least significant byte is first
            // To convert to host byte order we OR the low order byte with the
            // shifted high order byte, this works for both host byte orders
            return (leshort[0] | (leshort[1] << 8));
        }

        // Convert ulong from little endian to host byte order
        ulong ltohl(const byte *lelong) const
        {
            // The same logic applies here, we just need to do it with 4 bytes instead of 2
            return (lelong[0] | (lelong[1] << 8) | (lelong[2] << 16) | (lelong[3] << 24));
        }

        void ThrowOnEmpty() const
        {
            if (empty)
                throw std::logic_error("soldat info object initialized but empty");
        }

    public:
        // This constructor initializes an empty SoldatInfo object
        // NOTE. It is illegal to call any methods on an empty info object
        // Doing so will throw an exception of std::logic_error
        SoldatInfo() : empty(true)
        {
            // We need to tell our indexers that our object is empty as well!
            Players.base = this;
            Specs.base = this;
        }

        // This constructor takes a pointer to the buffer which contains the REFRESH packet
        // it assumes that you send it a valid REFRESH packet, eg. a packet of 1188 bytes
        SoldatInfo(const byte *packet) : empty(false)
        {
            // This will be used to keep track of our get pointer to packet
            const byte *packet_ptr;

            // Iterate through players
            for (int i = 0; i < MAX_PLAYERS; i++)
            {
                // First let's make sure this is a valid player
                packet_ptr = packet + TEAMS_OFFSET + i;
                int team = *packet_ptr;

                // The team variable will be:
                //     0 if the player is a valid player in non team based game mode
                //   1-4 if the player is a valid player in a team based game mode
                //     5 if the player is a spectator
                // This section of code is rather self explationary so I won't comment it much
                if (team > 5)
                    continue;

                // This might require a quick explanation, REFRESH packet stores names as length-prefixed string
                // So we first retrieve the length, and then build a std::string object according to the length
                packet_ptr = packet + PLAYERS_OFFSET + i * (NAME_LENGTH + 1);
                int len = *packet_ptr++;
                const char *name_ptr = reinterpret_cast<const char*>(packet_ptr);
                std::string name(name_ptr, len);

                packet_ptr = packet + KILLS_OFFSET + i * 2;
                int kills = ltohs(packet_ptr);

                packet_ptr = packet + DEATHS_OFFSET + i * 2;
                int deaths = ltohs(packet_ptr);

                packet_ptr = packet + PINGS_OFFSET + i;
                int ping = *packet_ptr;

                packet_ptr = packet + IDS_OFFSET + i;
                int id = *packet_ptr;

                // The following code might also require some briefing
                // Soldat stores IPs in network byte order (big endian), unlike its other data
                // Because most likely many people aren't interested in having an ip in long integral form
                // we'll generate a dotted version of it. If you really need the long version, you can use
                // eg. inet_addr(ip) to convert it (see MSDN for example)
                packet_ptr = packet + IPS_OFFSET + i * 4;
                std::ostringstream ip;
                ip << (int)packet_ptr[0] << "." << (int)packet_ptr[1] << "." << (int)packet_ptr[2] << "." << (int)packet_ptr[3];

                // Now we have all the necessary data, let's build our player object, and then add it to either players or specs
                Client p(id, team, kills, deaths, ping, name, ip.str());

                if (team < 5)
                    players.push_back(p);
                else
                    specs.push_back(p);
            }

            // The rest of the data is easy to retrieve, let's point our get pointer to the section after the players
            packet_ptr = packet + INFO_OFFSET;

            // Again, this code is pretty self explationary, so no comments are needed
            for (int i = 0; i < 4; i++)
            {
                team_scores[i] = ltohs(packet_ptr);
                packet_ptr += 2;
            }

            // We use the same technique to read the map name as we used to read players' names
            int len = *packet_ptr++;
            const char *name_ptr = reinterpret_cast<const char*>(packet_ptr);
            map = std::string(name_ptr, len);
            packet_ptr += MAP_LENGTH;

            // The time data in the packet are stored as ticks (1 sec = 60 ticks)
            // Since time limit can only be specified in minutes, let's convert it to mins as well
            time_limit = TicksToMins(ltohl(packet_ptr));
            packet_ptr += 4;

            // It is unlikely that anyone needs the current time as precise as ticks, but you never know
            // which is why we won't convert it; Instead we'll provide an auxiliary method for converting (just a simple div by 60)
            cur_time = ltohl(packet_ptr);
            packet_ptr += 4;

            kill_limit = ltohs(packet_ptr);
            packet_ptr += 2;

            game_mode = *packet_ptr;

            // Lastly let's initialize our client indexers
            Players.base = this;
            Players.clients = &players;

            Specs.base = this;
            Specs.clients = &specs;
        }

        // NOTE. Because our class uses standard container std::vector and standard string std::string
        // We do not need to provide our own copy constructor, assignment operator or destructor, because
        // these types provide them, so we can rely on default shallow copy -mechanism our compiler provides
        // If you want to make this type extensible, you might want to provide an empty virtual destructor though

        // The following methods are just getters to encapsulate the private data

        // Returns whether object is empty or not
        bool Empty() const
        {
            return empty;
        }

        // Returns the game mode
        int Gamemode() const
        {
            ThrowOnEmpty();
            return game_mode;
        }

        // Returns map name
        std::string Map() const
        {
            ThrowOnEmpty();
            return map;
        }

        // Returns current time in ticks
        int Timeleft() const
        {
            ThrowOnEmpty();
            return cur_time;
        }

        // Returns time limit in mins
        int Timelimit() const
        {
            ThrowOnEmpty();
            return time_limit;
        }

        // Returns the kill/score/point limit
        int Limit() const
        {
            ThrowOnEmpty();
            return kill_limit;
        }

        // Returns team score
        // Throws std::out_of_range on invalid subscript
        int Teamscore(int team) const
        {
            ThrowOnEmpty();

            if (team < 1 || team > 4)
                throw std::out_of_range("invalid team subscript");

            return team_scores[team-1];
        }

        // Returns number of players
        int NumPlayers() const
        {
            ThrowOnEmpty();
            return static_cast<int>(players.size());
        }

        // Returns number of specs
        int NumSpecs() const
        {
            ThrowOnEmpty();
            return static_cast<int>(specs.size());
        }

        // A simple hack to support array-like syntax for clients :)
        // Also provides an interface that supports STL iterators
        struct
        {
            friend class SoldatInfo;

            private:
                SoldatInfo* base;
                std::vector<Client>* clients;

            public:
                // Indexers, throws std::ouf_of_range if client is out of range
                Client& operator[](int client)
                {
                    base->ThrowOnEmpty();
                    return clients->at(client);
                }

                const Client& operator[](int client) const
                {
                    base->ThrowOnEmpty();
                    return clients->at(client);
                }

                // Iterator interface, supports iterators, const_iterators, reverse_iterators and const_reverse_iterators
                iterator begin()
                {
                    base->ThrowOnEmpty();
                    return clients->begin();
                }

                const_iterator begin() const
                {
                    base->ThrowOnEmpty();
                    return clients->begin();
                }

                iterator end()
                {
                    base->ThrowOnEmpty();
                    return clients->end();
                }

                const_iterator end() const
                {
                    base->ThrowOnEmpty();
                    return clients->end();
                }

                reverse_iterator rbegin()
                {
                    base->ThrowOnEmpty();
                    return clients->rbegin();
                }

                const_reverse_iterator rbegin() const
                {
                    base->ThrowOnEmpty();
                    return clients->rbegin();
                }

                reverse_iterator rend()
                {
                    base->ThrowOnEmpty();
                    return clients->rend();
                }

                const_reverse_iterator rend() const
                {
                    base->ThrowOnEmpty();
                    return clients->rend();
                }
        } Players, Specs;


        // The following methods are static auxiliary methods

        // Returns a string representation of a given gamemode
        // Throws std::out_of_range on invalid subscript
        static std::string GamemodeToStr(int gamemode)
        {
            static const char *gamemodes[] = {
                "Deathmatch",
                "Pointmatch",
                "Teammatch",
                "Capture the Flag",
                "Rambomatch",
                "Infiltration",
                "Hold the Flag"
            };

            if (gamemode < 0 || gamemode > 6)
                throw std::out_of_range("invalid gamemode subscript");

            return gamemodes[gamemode];
        }

        // Returns a string representation of a given team
        // Throws std::out_of_range on invalid subscript
        static std::string TeamToStr(int team)
        {
            static const char *teams[] = {
                "None",
                "Alpha",
                "Bravo",
                "Charlie",
                "Delta",
                "Spectator"
            };

            if (team < 0 || team > 4)
                throw std::out_of_range("invalid team subscript");

            return teams[team];
        }

        // Converts ticks to seconds
        static int TicksToSecs(int ticks)
        {
            return (ticks / 60);
        }

        // Converts ticks to minutes
        static int TicksToMins(int ticks)
        {
            return (TicksToSecs(ticks) / 60);
        }
};

#endif

