///////////////////////////////////////////////////////////////////
//*-------------------------------------------------------------*//
//| Part of the Game Jolt API C++ Library (http://gamejolt.com) |//
//*-------------------------------------------------------------*//
//| Released under the zlib License                             |//
//| More information available in the readme file               |//
//*-------------------------------------------------------------*//
///////////////////////////////////////////////////////////////////
#include "gjAPI.h"

#include <sstream>
#include <iostream>
#include <algorithm>

std::vector<std::string> gjAPI::s_asLog;


// ****************************************************************
/* constructor */
gjAPI::gjInterUser::gjInterUser(gjAPI* pAPI, gjNetwork* pNetwork)noexcept
: m_pAPI     (pAPI)
, m_pNetwork (pNetwork)
{
    // create NULL user for secure object handling
    gjData pNullData;
    pNullData["id"]         = "0";
    pNullData["username"]   = "NOT FOUND";
    pNullData["type"]       = "Guest";
    pNullData["avatar_url"] = GJ_API_AVATAR_DEFAULT;
    m_apUser[0] = new gjUser(pNullData, m_pAPI);

    // create guest user for secure object handling
    gjData pGuestData;
    pGuestData["id"]         = "-1";
    pGuestData["username"]   = "Guest";
    pGuestData["type"]       = "Guest";
    pGuestData["avatar_url"] = GJ_API_AVATAR_DEFAULT;
    m_apUser[-1] = new gjUser(pGuestData, m_pAPI);
}


// ****************************************************************
/* destructor */
gjAPI::gjInterUser::~gjInterUser()
{
    // delete all users
    FOR_EACH(it, m_apUser)
        SAFE_DELETE(it->second)

    // clear container
    m_apUser.clear();
}


// ****************************************************************
/* access user objects directly (may block) */
gjUser* gjAPI::gjInterUser::GetUser(const int& iID)
{
    gjUserPtr pOutput;

    if(this->__CheckCache(iID, &pOutput) == GJ_OK) return pOutput;
    if(this->FetchUserNow(iID, &pOutput) == GJ_OK) return pOutput;

    return m_apUser[0];
}

gjUser* gjAPI::gjInterUser::GetUser(const std::string& sName)
{
    gjUserPtr pOutput;

    if(this->__CheckCache(sName, &pOutput) == GJ_OK) return pOutput;
    if(this->FetchUserNow(sName, &pOutput) == GJ_OK) return pOutput;

    return m_apUser[0];
}


// ****************************************************************
/* access main user object directly (may block) */
gjUser* gjAPI::gjInterUser::GetMainUser()
{
    if(!m_pAPI->IsUserConnected()) return m_apUser[0];
    return this->GetUser(m_pAPI->GetUserName());
}


// ****************************************************************
/* delete all cached user objects */
void gjAPI::gjInterUser::ClearCache()
{
    // save NULL user and guest user
    gjUser* pNull  = m_apUser[0];  m_apUser.erase(0);
    gjUser* pGuest = m_apUser[-1]; m_apUser.erase(-1);

    // delete users
    FOR_EACH(it, m_apUser)
        SAFE_DELETE(it->second)

    // clear container
    m_apUser.clear();
    m_apUser[0]  = pNull;
    m_apUser[-1] = pGuest;
}


// ****************************************************************
/* check for cached user objects */
int gjAPI::gjInterUser::__CheckCache(const int& iID, gjUserPtr* ppOutput)
{
    // retrieve cached user
    if(m_apUser.count(iID))
    {
        if(ppOutput) (*ppOutput) = m_apUser[iID];
        return GJ_OK;
    }
    return GJ_NO_DATA_FOUND;
}

int gjAPI::gjInterUser::__CheckCache(const std::string& sName, gjUserPtr* ppOutput)
{
    // retrieve cached user
    FOR_EACH(it, m_apUser)
    {
        if(it->second->GetName() == sName)
        {
            if(ppOutput) (*ppOutput) = it->second;
            return GJ_OK;
        }
    }
    return GJ_NO_DATA_FOUND;
}


// ****************************************************************
/* process user data and cache user objects */
int gjAPI::gjInterUser::__Process(const std::string& sData, void* pAdd, gjUserPtr* ppOutput)
{
    // parse output
    gjDataList aaReturn;
    if(m_pAPI->ParseRequestKeypair(sData, &aaReturn) != GJ_OK)
    {
        gjAPI::ErrorLogAdd("API Error: could not parse user");
        if(ppOutput) (*ppOutput) = m_apUser[0];
        return GJ_REQUEST_FAILED;
    }

    // create and cache user object
    gjUser* pNewUser = new gjUser(aaReturn[0], m_pAPI);
    const int iID = pNewUser->GetID();

    if(m_apUser.count(iID))
    {
        SAFE_DELETE(pNewUser)
        pNewUser = m_apUser[iID];
    }
    else m_apUser[iID] = pNewUser;

    if(ppOutput) (*ppOutput) = pNewUser;
    return pNewUser ? GJ_OK : GJ_NO_DATA_FOUND;
}


// ****************************************************************
/* constructor */
gjAPI::gjInterTrophy::gjInterTrophy(gjAPI* pAPI, gjNetwork* pNetwork)noexcept
: m_iCache   (0)
, m_pAPI     (pAPI)
, m_pNetwork (pNetwork)
{
    // create NULL trophy for secure object handling
    gjData pNullData;
    pNullData["id"]         = "0";
    pNullData["title"]      = "NOT FOUND";
    pNullData["difficulty"] = "Bronze";
    pNullData["image_url"]  = GJ_API_TROPHY_DEFAULT_1;
    m_apTrophy[0] = new gjTrophy(pNullData, m_pAPI);

    // reserve some memory
    m_aiSort.reserve(GJ_API_RESERVE_TROPHY);
    m_aiSecret.reserve(GJ_API_RESERVE_TROPHY);
    m_aiHidden.reserve(GJ_API_RESERVE_TROPHY);

    // retrieve offline-cached trophy data
    this->__LoadOffCache();
}


// ****************************************************************
/* destructor */
gjAPI::gjInterTrophy::~gjInterTrophy()
{
    // delete all trophies
    FOR_EACH(it, m_apTrophy)
        SAFE_DELETE(it->second)

    // clear containers
    m_apTrophy.clear();
    m_aiSort.clear();
    m_aiSecret.clear();
    m_aiHidden.clear();
}


// ****************************************************************
/* access trophy objects directly (may block) */
gjTrophy* gjAPI::gjInterTrophy::GetTrophy(const int& iID)
{
    if(!m_pAPI->IsUserConnected() && m_iCache == 0) return m_apTrophy[0];
    if(m_apTrophy.size() <= 1)
    {
        // wait for prefetching
        if(GJ_API_PREFETCH) m_pNetwork->Wait(2);
        if(m_apTrophy.size() <= 1)
        {
            gjTrophyList apOutput;
            this->FetchTrophiesNow(0, &apOutput);
        }
    }
    return m_apTrophy.count(iID) ? m_apTrophy[iID] : m_apTrophy[0];
}


// ****************************************************************
/* delete all cached trophy objects */
void gjAPI::gjInterTrophy::ClearCache(const bool& bFull)
{
    const bool bRemoveAll = bFull || !GJ_API_OFFCACHE_TROPHY;

    if(bRemoveAll)
    {
        // save NULL trophy
        gjTrophy* pNull = m_apTrophy[0];
        m_apTrophy.erase(0);

        // delete trophies
        FOR_EACH(it, m_apTrophy)
            SAFE_DELETE(it->second)

        // clear container
        m_apTrophy.clear();
        m_apTrophy[0] = pNull;
    }

    // set cache status
    m_iCache = bRemoveAll ? 0 : 1;
}


// ****************************************************************
/* define layout of the returned trophy list */
void gjAPI::gjInterTrophy::SetSort(const int* piIDList, const size_t& iNum)
{
    if(iNum)
    {
        // clear sort list
        m_aiSort.clear();

        // add IDs to sort list
        for(size_t i = 0; i < iNum; ++i)
            m_aiSort.push_back(piIDList[i]);
    }

    // apply sort attribute
    FOR_EACH(it, m_apTrophy)
        it->second->__SetSort(0);
    for(size_t i = 0; i < m_aiSort.size(); ++i)
        if(m_apTrophy.count(m_aiSort[i])) m_apTrophy[m_aiSort[i]]->__SetSort(int(i+1));
}


// ****************************************************************
/* define secret trophy objects */
void gjAPI::gjInterTrophy::SetSecret(const int* piIDList, const size_t& iNum)
{
    if(iNum)
    {
        // clear secret list
        m_aiSecret.clear();

        // add IDs to secret list
        for(size_t i = 0; i < iNum; ++i)
            m_aiSecret.push_back(piIDList[i]);
    }

    // apply secret attribute
    FOR_EACH(it, m_apTrophy)
        it->second->__SetSecret(false);
    FOR_EACH(it, m_aiSecret)
        if(m_apTrophy.count(*it)) m_apTrophy[*it]->__SetSecret(true);
}


// ****************************************************************
/* define hidden trophy objects */
void gjAPI::gjInterTrophy::SetHidden(const int* piIDList, const size_t& iNum)
{
    if(iNum)
    {
        // clear hidden list
        m_aiHidden.clear();

        // add IDs to hidden list
        for(size_t i = 0; i < iNum; ++i)
            m_aiHidden.push_back(piIDList[i]);
    }

    // apply hidden attribute and remove all hidden trophy objects
    FOR_EACH(it, m_aiHidden)
        if(m_apTrophy.count(*it)) m_apTrophy.erase(m_apTrophy.find(*it));
}


// ****************************************************************
/* check for cached trophy objects */
int gjAPI::gjInterTrophy::__CheckCache(const int& iAchieved, gjTrophyList* papOutput)
{
    // retrieve cached trophies
    if(m_apTrophy.size() > 1)
    {
        if(papOutput)
        {
            gjTrophyList apConvert;
            apConvert.reserve(GJ_API_RESERVE_TROPHY);

            // add sorted trophies
            FOR_EACH(it, m_aiSort)
                if(m_apTrophy.count(*it)) apConvert.push_back(m_apTrophy[*it]);

            // add missing unsorted trophies
            FOR_EACH(it, m_apTrophy)
            {
                if(it->first)
                {
                    if(std::find(apConvert.begin(), apConvert.end(), it->second) == apConvert.end())
                        apConvert.push_back(it->second);
                }
            }

            // check for achieved status
            FOR_EACH(it, apConvert)
            {
                gjTrophy* pTrophy = (*it);

                if((iAchieved > 0 &&  pTrophy->IsAchieved()) ||
                   (iAchieved < 0 && !pTrophy->IsAchieved()) || !iAchieved)
                    (*papOutput).push_back(pTrophy);
            }
        }
        return GJ_OK;
    }
    return GJ_NO_DATA_FOUND;
}


// ****************************************************************
/* process trophy data and cache trophy objects */
int gjAPI::gjInterTrophy::__Process(const std::string& sData, void* pAdd, gjTrophyList* papOutput)
{
    // parse output
    gjDataList aaReturn;
    if(m_pAPI->ParseRequestKeypair(sData, &aaReturn) != GJ_OK)
    {
        gjAPI::ErrorLogAdd("API Error: could not parse trophies");
        return GJ_REQUEST_FAILED;
    }

    // offline-cache trophy data
    if(!aaReturn.empty()) this->__SaveOffCache(sData);
    if(m_iCache == 0) m_iCache = 2;

    // create and cache trophy objects
    FOR_EACH(it, aaReturn)
    {
        gjTrophy* pNewTrophy = new gjTrophy(*it, m_pAPI);
        const int iID = pNewTrophy->GetID();

        if(m_apTrophy.count(iID))
        {
            *m_apTrophy[iID] = *pNewTrophy;
            SAFE_DELETE(pNewTrophy)
        }
        else m_apTrophy[iID] = pNewTrophy;
    }

    // apply attributes
    this->SetSort  (NULL, 0);
    this->SetSecret(NULL, 0);
    this->SetHidden(NULL, 0);

    return (this->__CheckCache(P_TO_I(pAdd), papOutput) == GJ_OK) ? GJ_OK : GJ_NO_DATA_FOUND;
}


// ****************************************************************
/* save trophy data to a cache file */
void gjAPI::gjInterTrophy::__SaveOffCache(const std::string& sData)
{
    if(!GJ_API_OFFCACHE_TROPHY) return;
    if(m_iCache != 0) return;

    // open cache file
    std::FILE* pFile = std::fopen(GJ_API_OFFCACHE_NAME, "w");
    if(pFile)
    {
        // write data and close cache file
        std::fputs("[TROPHY]\n", pFile);
        std::fputs(sData.c_str(), pFile);
        std::fclose(pFile);
    }
}


// ****************************************************************
/* load trophy data from a cache file */
void gjAPI::gjInterTrophy::__LoadOffCache()
{
    if(!GJ_API_OFFCACHE_TROPHY) return;
    if(m_iCache != 0) return;

    // open cache file
    std::FILE* pFile = std::fopen(GJ_API_OFFCACHE_NAME, "r");
    if(pFile)
    {
        // read trophy header
        char acHeader[32];
        std::fscanf(pFile, "%31[^\n]%*c", acHeader);

        // read trophy data
        std::string sData;
        while(true)
        {
            char acLine[1024];
            std::fscanf(pFile, "%1023[^\n]%*c", acLine);
            if(std::feof(pFile)) break;

            if(std::strlen(acLine) > 1)
            {
                sData += acLine;
                sData += '\n';
            }
        }

        // close cache file
        std::fclose(pFile);

        if(!sData.empty())
        {
            // flag offline caching and load offline-cached trophies
            m_iCache = 1;
            this->__Process(sData, NULL, NULL);
        }
    }
}


// ****************************************************************
/* constructor */
gjAPI::gjInterScore::gjInterScore(gjAPI* pAPI, gjNetwork* pNetwork)noexcept
: m_pAPI     (pAPI)
, m_pNetwork (pNetwork)
{
    // create NULL score table for secure object handling
    gjData pNullData;
    pNullData["id"]   = "0";
    pNullData["name"] = "NOT FOUND";
    m_apScoreTable[0] = new gjScoreTable(pNullData, m_pAPI);
}


// ****************************************************************
/* destructor */
gjAPI::gjInterScore::~gjInterScore()
{
    // delete all score tables and scores entries
    FOR_EACH(it, m_apScoreTable)
        SAFE_DELETE(it->second)

    // clear container
    m_apScoreTable.clear();
}


// ****************************************************************
/* access score table objects directly (may block) */
gjScoreTable* gjAPI::gjInterScore::GetScoreTable(const int &iID)
{
    if(m_apScoreTable.size() <= 1)
    {
        // wait for prefetching
        if(GJ_API_PREFETCH) m_pNetwork->Wait(2);
        if(m_apScoreTable.size() <= 1)
        {
            gjScoreTableMap apOutput;
            this->FetchScoreTablesNow(&apOutput);
        }
    }
    gjScoreTable* pPrimary = gjScoreTable::GetPrimary();
    return iID ? (m_apScoreTable.count(iID) ? m_apScoreTable[iID] : m_apScoreTable[0]) : (pPrimary ? pPrimary : m_apScoreTable[0]);
}


// ****************************************************************
/* delete all cached score table objects and score entries */
void gjAPI::gjInterScore::ClearCache()
{
    // save NULL score table
    gjScoreTable* pNull = m_apScoreTable[0]; m_apScoreTable.erase(0);

    // delete score tables and scores entries
    FOR_EACH(it, m_apScoreTable)
        SAFE_DELETE(it->second)

    // clear container
    m_apScoreTable.clear();
    m_apScoreTable[0] = pNull;
}


// ****************************************************************
/* check for cached score table objects */
int gjAPI::gjInterScore::__CheckCache(gjScoreTableMap* papOutput)
{
    // retrieve cached score tables
    if(m_apScoreTable.size() > 1)
    {
        if(papOutput)
        {
            FOR_EACH(it, m_apScoreTable)
                if(it->first) (*papOutput)[it->first] = it->second;
        }
        return GJ_OK;
    }
    return GJ_NO_DATA_FOUND;
}


// ****************************************************************
/* process score table data and cache score table objects */
int gjAPI::gjInterScore::__Process(const std::string& sData, void* pAdd, gjScoreTableMap* papOutput)
{
    // parse output
    gjDataList aaReturn;
    if(m_pAPI->ParseRequestKeypair(sData, &aaReturn) != GJ_OK)
    {
        gjAPI::ErrorLogAdd("API Error: could not parse score tables");
        return GJ_REQUEST_FAILED;
    }

    // create and cache score tables
    FOR_EACH(it, aaReturn)
    {
        gjScoreTable* pNewScoreTable = new gjScoreTable(*it, m_pAPI);
        const int iID = pNewScoreTable->GetID();

        if(m_apScoreTable.count(iID)) SAFE_DELETE(pNewScoreTable)
        else m_apScoreTable[iID] = pNewScoreTable;
    }

    return (this->__CheckCache(papOutput) == GJ_OK) ? GJ_OK : GJ_NO_DATA_FOUND;
}


// ****************************************************************
/* constructor */
gjAPI::gjInterDataStore::gjInterDataStore(const int& iType, gjAPI* pAPI, gjNetwork* pNetwork)noexcept
: m_iType    (iType)
, m_pAPI     (pAPI)
, m_pNetwork (pNetwork)
{
}


// ****************************************************************
/* destructor */
gjAPI::gjInterDataStore::~gjInterDataStore()
{
    this->ClearCache();
}


// ****************************************************************
/* create and access data store items directly */
gjDataItem* gjAPI::gjInterDataStore::GetDataItem(const std::string& sKey)
{
    // create new data store item
    if(!m_apDataItem.count(sKey))
    {
        gjData asDataItemData;
        asDataItemData["key"] = sKey;
        m_apDataItem[sKey] = new gjDataItem(asDataItemData, m_iType, m_pAPI);
    }

    return m_apDataItem.count(sKey) ? m_apDataItem[sKey] : NULL;
}


// ****************************************************************
/* delete all cached data store items */
void gjAPI::gjInterDataStore::ClearCache()
{
    // delete data store items
    FOR_EACH(it, m_apDataItem)
        SAFE_DELETE(it->second)

    // clear container
    m_apDataItem.clear();
}


// ****************************************************************
/* check for cached data store items */
int gjAPI::gjInterDataStore::__CheckCache(gjDataItemMap* papOutput)
{
    // retrieve cached data store items
    if(!m_apDataItem.empty())
    {
        if(papOutput)
        {
            FOR_EACH(it, m_apDataItem)
                (*papOutput)[it->first] = it->second;
        }
        return GJ_OK;
    }
    return GJ_NO_DATA_FOUND;
}


// ****************************************************************
/* process data store data and cache data store items */
int gjAPI::gjInterDataStore::__Process(const std::string& sData, void* pAdd, gjDataItemMap* papOutput)
{
    // parse output
    gjDataList aaReturn;
    if(m_pAPI->ParseRequestKeypair(sData, &aaReturn) != GJ_OK)
    {
        gjAPI::ErrorLogAdd("API Error: could not parse data store items");
        return GJ_REQUEST_FAILED;
    }

    // create and cache data store items
    FOR_EACH(it, aaReturn)
    {
        gjDataItem* pNewDataItem = new gjDataItem(*it, m_iType, m_pAPI);
        const std::string& sKey = pNewDataItem->GetKey();

        if(m_apDataItem.count(sKey))
        {
            SAFE_DELETE(pNewDataItem)
            pNewDataItem = m_apDataItem[sKey];
        }
        else m_apDataItem[sKey] = pNewDataItem;

        if(papOutput) (*papOutput)[sKey] = pNewDataItem;
    }

    return aaReturn.empty() ? GJ_NO_DATA_FOUND : GJ_OK;
}


// ****************************************************************
/* constructor */
gjAPI::gjInterFile::gjInterFile(gjAPI* pAPI, gjNetwork* pNetwork)noexcept
: m_pAPI     (pAPI)
, m_pNetwork (pNetwork)
{
    // reserve some memory
    m_asFile.reserve(GJ_API_RESERVE_FILE);
}


// ****************************************************************
/* destructor */
gjAPI::gjInterFile::~gjInterFile()
{
    this->ClearCache();
}


// ****************************************************************
/* delete all cached file paths */
void gjAPI::gjInterFile::ClearCache()
{
    // clear container
    m_asFile.clear();
}


// ****************************************************************
/* check for cached files */
int gjAPI::gjInterFile::__CheckCache(const std::string& sPath)
{
    // compare cached file paths
    FOR_EACH(it, m_asFile)
    {
        if(sPath == (*it))
            return GJ_OK;
    }
    return GJ_NO_DATA_FOUND;
}


// ****************************************************************
/* process downloaded file */
int gjAPI::gjInterFile::__Process(const std::string& sData, void* pAdd, std::string* psOutput)
{
    // save path of the file
    if(this->__CheckCache(sData) != GJ_OK) m_asFile.push_back(sData);
    if(psOutput) (*psOutput) = sData;

    return GJ_OK;
}


// ****************************************************************
/* constructor */
gjAPI::gjAPI(const int iGameID, const std::string sGamePrivateKey)noexcept
: m_iGameID         (iGameID)
, m_sGamePrivateKey (sGamePrivateKey)
, m_sUserName       ("")
, m_sUserToken      ("")
, m_iNextPing       (0)
, m_bActive         (false)
, m_bConnected      (false)
, m_sProcUserName   ("")
, m_sProcUserToken  ("")
{
    // init error log
    gjAPI::ErrorLogReset();

    // pre-process the game ID
    m_sProcGameID = gjAPI::UtilIntToString(m_iGameID);

    // create network object
    m_pNetwork = new gjNetwork(this);

    // create sub-interface objects
    m_pInterUser            = new gjInterUser(this, m_pNetwork);
    m_pInterTrophy          = new gjInterTrophy(this, m_pNetwork);
    m_pInterScore           = new gjInterScore(this, m_pNetwork);
    m_pInterDataStoreGlobal = new gjInterDataStore(0, this, m_pNetwork);
    m_pInterDataStoreUser   = new gjInterDataStore(1, this, m_pNetwork);
    m_pInterFile            = new gjInterFile(this, m_pNetwork);

    // prefetch score tables
    if(GJ_API_PREFETCH && iGameID) m_pInterScore->FetchScoreTablesCall(GJ_NETWORK_NULL_THIS(gjScoreTableMap));
}


// ****************************************************************
/* destructor */
gjAPI::~gjAPI()
{
    // logout last user
    this->Logout();

    // delete network object
    SAFE_DELETE(m_pNetwork)

    // delete sub-interface objects
    SAFE_DELETE(m_pInterUser)
    SAFE_DELETE(m_pInterTrophy)
    SAFE_DELETE(m_pInterScore)
    SAFE_DELETE(m_pInterDataStoreGlobal)
    SAFE_DELETE(m_pInterDataStoreUser)
    SAFE_DELETE(m_pInterFile)
}


// ****************************************************************
/* explicitly initialize the object */
void gjAPI::Init(const int& iGameID, const std::string& sGamePrivateKey)
{
    // save game data
    m_iGameID         = iGameID;
    m_sGamePrivateKey = sGamePrivateKey;

    // pre-process the game ID
    m_sProcGameID = gjAPI::UtilIntToString(m_iGameID);

    // prefetch score tables
    if(GJ_API_PREFETCH && iGameID) m_pInterScore->FetchScoreTablesCall(GJ_NETWORK_NULL_THIS(gjScoreTableMap));
}


// ****************************************************************
/* main update function of the library */
void gjAPI::Update()
{
    // update network object
    m_pNetwork->Update();

    if(!this->IsUserConnected()) return;

    if(m_iNextPing)
    {
        // update ping for the user session
        const time_t iCurTime = time(NULL);
        if(iCurTime >= m_iNextPing)
        {
            m_iNextPing = iCurTime + GJ_API_PING_TIME;
            this->__PingSession(m_bActive);
        }
    }
}


// ****************************************************************
/* logout with specific user */
int gjAPI::Logout()
{
    if(!this->IsUserConnected()) return GJ_NOT_CONNECTED;

    // clear user specific data
    m_pInterTrophy->ClearCache(false);
    m_pInterDataStoreUser->ClearCache();

    // close the user session
    if(m_iNextPing) this->__CloseSession();

    // clear main user data
    m_sUserName      = "";
    m_sUserToken     = "";
    m_sProcUserName  = "";
    m_sProcUserToken = "";

    // clear connection
    m_bConnected = false;

    return GJ_OK;
}


// ****************************************************************
/* parse a valid response string in keypair format */
int gjAPI::ParseRequestKeypair(const std::string& sInput, gjDataList* paaOutput)
{
    if(!paaOutput) return GJ_INVALID_INPUT;

    gjData aData;
    std::istringstream sStream(sInput);
    std::string sToken;

    // loop through input string
    while(std::getline(sStream, sToken))
    {
        // remove redundant characters
        gjAPI::UtilTrimString(&sToken);
        if(sToken.empty()) continue;

        // separate key and value
        const size_t iPos        = sToken.find(':');
        const std::string sKey   = sToken.substr(0, iPos);
        const std::string sValue = sToken.substr(iPos + 2, sToken.length() - iPos - 3);

        // next data block on same key
        if(aData.count(sKey.c_str()))
        {
            paaOutput->push_back(aData);
            aData.clear();
        }

        // create key and save value
        aData[sKey.c_str()] = sValue;
    }

    // insert last data block and check size
    if(!aData.empty()) paaOutput->push_back(aData);
    if(paaOutput->empty())
    {
        paaOutput->push_back(aData);
        gjAPI::ErrorLogAdd("API Error: string parsing failed");
        return GJ_INVALID_INPUT;
    }

    // check for failed request
    if(paaOutput->front()["success"] != "true")
    {
        gjAPI::ErrorLogAdd("API Error: request was unsuccessful");
        gjAPI::ErrorLogAdd("API Error: " + paaOutput->front()["message"]);
        return GJ_REQUEST_FAILED;
    }

    return GJ_OK;
}


// ****************************************************************
/* parse a valid response string in Dump format */
int gjAPI::ParseRequestDump(const std::string& sInput, std::string* psOutput)
{
    if(!psOutput) return GJ_INVALID_INPUT;

    // read status
    const std::string sStatus = sInput.substr(0, sInput.find_first_of(13));

    // read data
    (*psOutput) = sInput.substr(sStatus.length()+2);

    // check for failed request
    if(sStatus != "SUCCESS")
    {
        gjAPI::ErrorLogAdd("API Error: request was unsuccessful");
        gjAPI::ErrorLogAdd("API Error: " + (*psOutput));
        return GJ_REQUEST_FAILED;
    }

    return GJ_OK;
}


// ****************************************************************
/* delete all cached objects */
void gjAPI::ClearCache()
{
    // clear cache of all sub-interfaces
    m_pInterUser->ClearCache();
    m_pInterTrophy->ClearCache(true);
    m_pInterScore->ClearCache();
    m_pInterDataStoreGlobal->ClearCache();
    m_pInterDataStoreUser->ClearCache();
    m_pInterFile->ClearCache();
}


// ****************************************************************
/* escape a string for proper url calling */
std::string gjAPI::UtilEscapeString(const std::string& sString)
{
    std::string sOutput = "";

    // loop through input string
    for(size_t i = 0; i < sString.length(); ++i)
    {
        // check the character type
        if
        (
            (48 <= sString[i] && sString[i] <=  57) || // 0-9
            (65 <= sString[i] && sString[i] <=  90) || // ABC...XYZ
            (97 <= sString[i] && sString[i] <= 122) || // abc...xyz
            (
                sString[i] == '~' || sString[i] == '.'  ||
                sString[i] == '-' || sString[i] == '_'
            )
        )
        {
            // add valid character
            sOutput += sString[i];
        }
        else
        {
            // convert character to hexadecimal value
            sOutput += "%" + gjAPI::UtilCharToHex(sString[i]);
        }
    }

    return sOutput;
}


// ****************************************************************
/* trim a standard string on both sides */
void gjAPI::UtilTrimString(std::string* psInput)
{
    const size_t iFirst = psInput->find_first_not_of(" \n\r\t");
    if(iFirst != std::string::npos) psInput->erase(0, iFirst);

    const size_t iLast = psInput->find_last_not_of(" \n\r\t");
    if(iLast != std::string::npos) psInput->erase(iLast+1);
}


// ****************************************************************
/* convert a character into his hexadecimal value */
std::string gjAPI::UtilCharToHex(const char& cChar)
{
    int iValue = (int)cChar;
    if(iValue < 0) iValue += 256;

    char acBuffer[8];
    std::sprintf(acBuffer, "%02X", iValue);

    return acBuffer;
}


// ****************************************************************
/* simply convert an integer into a string */
std::string gjAPI::UtilIntToString(const int& iInt)
{
    char acBuffer[32];
    std::sprintf(acBuffer, "%d", iInt);

    return acBuffer;
}


// ****************************************************************
/* create a folder hierarchy */
void gjAPI::UtilCreateFolder(const std::string& sFolder)
{
    size_t iPos = 0;

    // loop through path
    while((iPos = sFolder.find_first_of("/\\", iPos+2)) != std::string::npos)
    {
        const std::string sSubFolder = sFolder.substr(0, iPos);

        // create subfolder
#if defined(_GJ_WINDOWS_)
        CreateDirectoryA(sSubFolder.c_str(), NULL);
#else
        mkdir(sSubFolder.c_str(), S_IRWXU);
#endif
    }
}


// ****************************************************************
/* get timestamp as string */
std::string gjAPI::UtilTimestamp(const time_t iTime)
{
    // format the time value
    tm* pFormat = std::localtime(&iTime);

    // create output
    char acBuffer[16];
    std::sprintf(acBuffer, "%02d:%02d:%02d", pFormat->tm_hour, pFormat->tm_min, pFormat->tm_sec);

    return acBuffer;
}


// ****************************************************************
/* reset error log */
void gjAPI::ErrorLogReset()
{
    if(GJ_API_LOGFILE)
    {
        // remove error log file if empty
        if(s_asLog.empty())
            std::remove(GJ_API_LOGFILE_NAME);
    }
}


// ****************************************************************
/* add error log entry */
void gjAPI::ErrorLogAdd(const std::string& sMsg)
{
    const std::string sTimeMsg = "[" + gjAPI::UtilTimestamp() + "] " + sMsg;

    // add message
    s_asLog.push_back(sTimeMsg);

    if(GJ_API_LOGFILE)
    {
        // add message to error log file
        std::FILE* pFile = std::fopen(GJ_API_LOGFILE_NAME, "a");
        if(pFile)
        {
            std::fprintf(pFile, "%s\n", sTimeMsg.c_str());
            std::fclose(pFile);
        }
    }

#if defined(_GJ_DEBUG_)

    // print message to terminal
    std::cerr << "(!GJ) " << sTimeMsg << std::endl;

#endif
}


// ****************************************************************
/* open the user session */
int gjAPI::__OpenSession()
{
    if(!this->IsUserConnected()) return GJ_NOT_CONNECTED;

    // send non-blocking open request
    if(m_pNetwork->SendRequest("/sessions/open/"
                               "?game_id="    + m_sProcGameID   +
                               "&username="   + m_sProcUserName +
                               "&user_token=" + m_sProcUserToken,
                               NULL, this, &gjAPI::Null, NULL, GJ_NETWORK_NULL_THIS(std::string))) return GJ_REQUEST_FAILED;

    // init session attributes
    m_iNextPing = std::time(NULL) + GJ_API_PING_TIME;
    m_bActive   = true;

    return GJ_OK;
}


// ****************************************************************
/* ping the user session */
int gjAPI::__PingSession(const bool& bActive)
{
    if(!this->IsUserConnected()) return GJ_NOT_CONNECTED;

    // use active status
    const std::string sActive = bActive ? "active" : "idle";

    // send non-blocking ping request
    if(m_pNetwork->SendRequest("/sessions/ping/"
                               "?game_id="    + m_sProcGameID    +
                               "&username="   + m_sProcUserName  +
                               "&user_token=" + m_sProcUserToken +
                               "&status="     + sActive,
                               NULL, this, &gjAPI::Null, NULL, GJ_NETWORK_NULL_THIS(std::string))) return GJ_REQUEST_FAILED;

    return GJ_OK;
}


// ****************************************************************
/* close the user session */
int gjAPI::__CloseSession()
{
    if(!this->IsUserConnected()) return GJ_NOT_CONNECTED;

    // send non-blocking close request
    if(m_pNetwork->SendRequest("/sessions/close/"
                               "?game_id="    + m_sProcGameID   +
                               "&username="   + m_sProcUserName +
                               "&user_token=" + m_sProcUserToken,
                               NULL, this, &gjAPI::Null, NULL, GJ_NETWORK_NULL_THIS(std::string))) return GJ_REQUEST_FAILED;

    // clear session attributes
    m_iNextPing = 0;

    return GJ_OK;
}


// ****************************************************************
/* callback for login with specific user */
int gjAPI::__LoginCallback(const std::string& sData, void* pAdd, int* pbOutput)
{
    // check for success
    gjDataList aaReturn;
    if(this->ParseRequestKeypair(sData, &aaReturn) != GJ_OK)
    {
        gjAPI::ErrorLogAdd("API Error: could not authenticate user <" + m_sUserName + ">");

        // clear main user data
        m_sUserName      = "";
        m_sUserToken     = "";
        m_sProcUserName  = "";
        m_sProcUserToken = "";

        // determine error type
        const int iError = std::strcmp(SAFE_MAP_GET(aaReturn[0], "success").c_str(), "false") ? GJ_NETWORK_ERROR : GJ_REQUEST_FAILED;

        if(pbOutput) (*pbOutput) = iError;
        return pbOutput ? GJ_OK : iError;
    }

    // set connection
    m_bConnected = true;

    // open the user session
    if(pAdd) this->__OpenSession();

    // prefetch user data
    if(GJ_API_PREFETCH)
    {
        m_pInterUser->FetchUserCall(0, GJ_NETWORK_NULL_THIS(gjUserPtr));
        m_pInterTrophy->FetchTrophiesCall(0, GJ_NETWORK_NULL_THIS(gjTrophyList));
        m_pInterDataStoreUser->FetchDataItemsCall(GJ_NETWORK_NULL_THIS(gjDataItemMap));
    }

    if(pbOutput) (*pbOutput) = GJ_OK;
    return GJ_OK;
}