#include "UserDB.h"
#include "FilesystemAbstraction.h"

const String userDir = "/u/";

template <class ForwardIterator, class T>
ForwardIterator lower_bound(ForwardIterator first, ForwardIterator last, const T &val)
{
    ForwardIterator it;
    int count, step;
    count = last - first;
    while (count > 0)
    {
        it = first;
        step = count / 2;
        it += step;
        if (*it < val)
        {
            first = ++it;
            count -= step + 1;
        }
        else
            count = step;
    }
    return first;
}

static String userFileName(const String &userName)
{
    return userDir + userName;
}

bool User::load(const String &stringId)
{
    stringId_ = stringId;
    assert(stringId_.length() > 0);
    const auto fileName = userFileName(stringId_);
    if (!portablefs::exists(fileName.c_str()))
        return false;

    auto file = portablefs::open(fileName.c_str(), "r");

    size_t sessionsInFile;
    file.read((uint8_t *)&sessionsInFile, sizeof(numSessions_));

    init(stringId, sessionsInFile * 2);

    size_t expectedSize = sizeof(SessionIdType) * numSessions_;
    auto bytesRead = file.read((uint8_t *)sessionIds_, expectedSize);
    numSessions_ = sessionsInFile;

    assert(expectedSize == bytesRead);
    return true;
}

void User::init(const String &stringId, size_t sessionAllocateSize)
{
    if (sessionIds_ != nullptr)
    {
        free(sessionIds_);
        sessionIds_ = nullptr;
    }
    stringId_ = stringId;
    numSessionsAllocated_ = sessionAllocateSize;
    sessionIds_ = (SessionIdType *)ps_malloc(sizeof(SessionIdType) * numSessionsAllocated_);
    numSessions_ = 0;
}

void User::save()
{
    if (!portablefs::exists(userDir.c_str()))
        portablefs::mkdir(userDir.c_str());

    auto file = portablefs::open(userFileName(stringId_).c_str(), "w");
    file.write((uint8_t *)&numSessions_, sizeof(numSessions_));
    file.write((uint8_t *)sessionIds_, sizeof(SessionIdType) * numSessions_);
}

void User::freeResources()
{
    if (sessionIds_ != nullptr)
    {
        free(sessionIds_);
        sessionIds_ = nullptr;
    }
}

void User::remove()
{
    portablefs::remove(userFileName(stringId_).c_str());
    freeResources();
    stringId_ = "";
    numSessions_ = 0;
    numSessionsAllocated_ = 0;
}

void User::growSessionArrayIfNecessary()
{
    assert(numSessions_ <= numSessionsAllocated_);

    if (numSessions_ < numSessionsAllocated_)
        return;

    numSessionsAllocated_ *= 2;
    sessionIds_ = (SessionIdType *)ps_realloc(sessionIds_, numSessionsAllocated_);

    assert(numSessions_ < numSessionsAllocated_);
}

void User::insertSession(SessionIdType newSessionId)
{
    growSessionArrayIfNecessary();
    assert(numSessionsAllocated_ > numSessions_);
    SessionIdType *insertPos = lower_bound(sessionIds_, sessionIds_ + numSessions_, newSessionId);
    const size_t moveStartIdx = sessionIds_ + numSessions_ - insertPos;
    for (size_t i = numSessions_ - 1; i >= moveStartIdx; --i)
        sessionIds_[i + 1] = sessionIds_[i];
    sessionIds_[moveStartIdx] = newSessionId;
    numSessions_ += 1;
}

bool User::removeSession(SessionIdType sessionIdToRemove)
{
    SessionIdType *removePos = lower_bound(sessionIds_, sessionIds_ + numSessions_, sessionIdToRemove);
    const size_t removeIdx = sessionIds_ + numSessions_ - removePos;
    if (sessionIds_[removeIdx] != sessionIdToRemove)
        return false;

    for (size_t i = removeIdx; i < numSessions_ - 1; ++i)
        sessionIds_[i] = sessionIds_[i + 1];

    numSessions_ -= 1;
    return true;
}

// --------------------------------------------------------------------------------------------------------------------

User *UserStorage::getUserInfo(const String &stringId)
{
    // index 0 is the unassigned user
    for (size_t i = 1; i < numUsers_; ++i)
        if (users_[i].stringId() == stringId)
            return &users_[i];
    return nullptr;
}

User *UserStorage::getUnassignedUser()
{
    return &users_[0];
}

User *UserStorage::addNewUser(const String &stringId)
{
    if (numUsers_ >= MAX_USERS)
        return nullptr;
    
    if(stringId.length() >= USER_STRING_ID_MAX_LEN)
        return nullptr;

    auto userIdx = numUsers_;
    numUsers_++;
    assert(numUsers_ < MAX_USERS);
    users_[userIdx].init(stringId);
    return &users_[userIdx];
}

bool UserStorage::deleteUser(const String &stringId)
{
    User *userPtr = getUserInfo(stringId);

    if (userPtr == nullptr)
        return false;
    size_t userIdx = userPtr - users_;
    userPtr->remove();
    assert(numUsers_ > 0);
    if (userIdx != numUsers_ - 1)
        users_[userIdx] = users_[numUsers_ - 1];
    return true;
}

void UserStorage::fillFromFileSystem()
{
    for (size_t i = 0; i < numUsers_; ++i)
        users_[i].freeResources();

    numUsers_ = 1;

    users_[0].load("_unassigned");
    portablefs::Dir d(userDir);
    while (d.next())
    {
        if (d.isFile() && d.fileName()[0] != '_')
        {
            users_[numUsers_].load(d.fileName());
            ++numUsers_;
        }
    }
}