
/*---------------------------------------------------------------------------*/
/* config.cpp                                                                */
/* copyright (c) innovaphone 2017 - 2019                                     */
/*                                                                           */
/*---------------------------------------------------------------------------*/

#include <platform.h>

#include "sdk/common/interface/database.h"
#include "sdk/common/interface/file.h"
#include "sdk/common/interface/task.h"
#include "sdk/common/interface/json_api.h"
#include "sdk/common/interface/webserver_plugin.h"
#include "sdk/common/interface/random.h"

#include "sdk/common/os/iomux.h"
#include "common/ilib/cipher.h"
#include "common/ilib/json.h"
#include "common/ilib/str.h"

#include "sdk/common/lib/tasks_postgresql.h"
#include "sdk/common/lib/appwebsocket.h"
#include "sdk/common/lib/config.h"

#define INSERT_UPDATE_SQL   "INSERT INTO config (name, value, unmanaged) VALUES (%s, %s, %b) ON CONFLICT (name) DO UPDATE SET value = %s, unmanaged = %b"

#define API_NAME            "Config"

size_t StringJsonBufferSize(const char * str)
{
    size_t result = 0;
    if (str != nullptr) {
        
        while (*str) {
            switch (*str) {
            case '"':
            case '\\':
            case '\b':
            case '\f':
            case '\n':
            case '\r':
            case '\t':
                result += 2;
                break;
            default:
                if (*str <= 0x1f) result += 6;
                else ++result;
            }
            ++str;
        }
    }    
    return result;
}

/*---------------------------------------------------------------------------*/
/* class TaskWriteConfig                                                     */
/*---------------------------------------------------------------------------*/

class TaskWriteConfig : public UDatabase, public ITask, public UIoExec {
private:
    IJsonApiConnection* const connection;
    IInstanceLog* const log;
    IDatabase* const database;
    ConfigItem* const itemListStart;
    json_io* const json;
    ConfigContext* const configContext;
    dword base;

public:
    TaskWriteConfig(IJsonApiConnection* connection, IDatabase* database, json_io* json, dword base, const char* src, ConfigItem* startItem, IInstanceLog* const log, ConfigContext* const configContext)
        : connection(connection),
        log(log),
        database(database),
        itemListStart(startItem),
        json(json),
        configContext(configContext)
    {
        this->base = base;
        this->src = _strdup(src);
    }

    ~TaskWriteConfig()
    {
        if (src) free((void*)src);
    }

    virtual void Start(UTask* user) override
    {
        this->log->Log(LOG_CONFIG, "TaskWriteConfig(%08x)::Start Starting task to write config", this);
        this->user = user;

        this->base = json->get_object(base, "ConfigItems");
        if (this->base == JSON_ID_NONE) {
            if (this->configContext->iomux == nullptr) this->user->TaskFailed(this);
            else this->configContext->iomux->SetExec(this, (void*)1);
            return;
        }

        if (!this->configContext->fileMode) { // database mode
            this->database->BeginTransaction(this);
            this->database->ExecSQL(this, 0, "DELETE FROM config WHERE unmanaged=false");
        }

        ConfigItem* cur = this->itemListStart;
        while (cur) {
            if (!cur->unmanaged) {
                cur->ReadValueFromJSON(this->json, base, this->connection);
                if (!this->configContext->fileMode && !cur->HasDefaultValue()) {  // just talk to database in database mode
                    int len = cur->WriteValueToString(nullptr) + 1; // determine the length first
                    char* tmpStr = (char*)malloc(len);
                    cur->WriteValueToString(tmpStr);
                    this->database->ExecSQL(this, 0, INSERT_UPDATE_SQL, cur->GetName(), tmpStr, false, tmpStr);
                    free(tmpStr);
                }
            }

            cur = cur->goNext();
        }

        if (!this->configContext->fileMode) this->database->EndTransaction(this);
        else this->configContext->iomux->SetExec(this, nullptr);
    }

    void IoExec(void* execContext) override {
        if (execContext == (void*)1) this->user->TaskFailed(this); // failure in Start
        else {
            if (this->configContext->WriteConfigJsonFile()) this->user->TaskComplete(this);
            else this->user->TaskFailed(this);
        }
    }

    IJsonApiConnection* GetConnection()
    {
        return this->connection;
    }

    virtual void DatabaseError(IDatabase* const database, db_error_t error) override
    {
        this->log->Log(LOG_CONFIG, "TaskWriteConfig(%08x)::DatabaseError DB Write command reported an error:%u", this, error);
        this->user->TaskFailed(this);
    }

    virtual void DatabaseEndTransactionResult(IDatabase* const database) override
    {
        this->log->Log(LOG_CONFIG, "TaskWriteConfig(%08x)::DatabaseEndTransactionResult writing of config items done", this);
        this->user->TaskComplete(this);
    }

    virtual void DatabaseExecSQLResult(IDatabase* const database, class IDataSet* dataset) override
    {
        delete dataset;
    }

    char* src;
};

/*---------------------------------------------------------------------------*/
/* class TaskWriteValue                                                      */
/*---------------------------------------------------------------------------*/

class TaskWriteValue : public UDatabase, public ITask, public UIoExec {
private:
    IInstanceLog* const log;
    IDatabase* const database;
    const char* name;
    const char* value;
    bool unmanaged;
    bool notify;
    ConfigContext* configContext;

public:
    TaskWriteValue(IDatabase* database, const char* name, const char* value, bool unmanaged, IInstanceLog* const log, ConfigContext* configContext, bool notify)
        : log(log),
        database(database),
        configContext(configContext)
    {
        this->name = name;
        this->value = value;
        this->unmanaged = unmanaged;
        this->notify = notify;
        this->configContext = configContext;
    }

    ~TaskWriteValue()
    {
    }

    virtual void Start(UTask* user) override
    {
        this->log->Log(LOG_CONFIG, "TaskWriteValue(%08x)::Start Starting task to write config item", this);
        this->user = user;
        if (!this->configContext->fileMode) this->database->ExecSQL(this, 0, INSERT_UPDATE_SQL, name, value, unmanaged, value);
        else this->configContext->iomux->SetExec(this, nullptr);
    }

    void IoExec(void* execContext) override
    {
        if (this->configContext->WriteConfigJsonFile()) {
            this->Notify();
            this->user->TaskComplete(this);
        }
        else {
            this->user->TaskFailed(this);
        }
    }

    void Notify() {
        if (notify)
        {
            configContext->ConfigChanged();
            for (ConfigApiContext* c = configContext->apiContextList.front(); c; c = c->goNext()) {
                configContext->SendConfig(c->GetConnection(), "ConfigUpdate", c->readSrc);
            }
        }
    }

    virtual void DatabaseError(IDatabase* const database, db_error_t error) override
    {
        this->log->Log(LOG_CONFIG, "TaskWriteValue(%08x)::DatabaseError DB Write command reported an error:%u", this, error);
        this->user->TaskFailed(this);
    }

    virtual void DatabaseExecSQLResult(IDatabase* const database, class IDataSet* dataset) override
    {
        delete dataset;
        this->Notify();
        this->user->TaskComplete(this);
    }
};


/*---------------------------------------------------------------------------*/
/* class TaskInitConfig                                                      */
/*---------------------------------------------------------------------------*/

class TaskInitConfig : public UTask, public ITask, public UDatabase {
private:
    IDatabase* const database;
    IInstanceLog* const log;
    ConfigItem* const itemListStart;
    bool readOnly;

public:
    TaskInitConfig(IDatabase* database, ConfigItem* itemListStart, IInstanceLog* const log, bool readOnly = false)
        : database(database),
        log(log),
        itemListStart(itemListStart),
        readOnly(readOnly)
    {
    }

    virtual ~TaskInitConfig() {}

    void TaskComplete(class ITask* const task) override
    {
        this->log->Log(LOG_CONFIG, "TaskInitCofnig(%08x)::TaskComplete database init done, reading saved items", this);
        delete task;
        this->database->ExecSQL(this, 0, "SELECT name, value FROM config");
    }

    void TaskFailed(class ITask* const task) override
    {
        this->log->Log(LOG_CONFIG, "TaskInitCofnig(%08x)::TaskFailed database init failed", this);
        delete task;
        this->user->TaskFailed(this);
    }

    virtual void Start(UTask* user) override
    {
        this->log->Log(LOG_CONFIG, "TaskInitCofnig(%08x)::Start start config table init task", this);
        this->user = user;
        if (this->readOnly) {   // no write access, so no need to setup tables/columns
            this->TaskComplete(nullptr);
        }
        else {
            TaskPostgreSQLInitTable* initTask = new TaskPostgreSQLInitTable(database, "config");
            initTask->AddColumn("name", "VARCHAR(256) UNIQUE NOT NULL PRIMARY KEY");
            initTask->AddColumn("value", "TEXT");
            initTask->AddColumn("unmanaged", "BOOLEAN");
            initTask->Start(this);
        }
    }

    virtual void DatabaseError(IDatabase* const database, db_error_t error) override
    {
        this->log->Log(LOG_CONFIG, "TaskInitConfig(%08x)::DatabaseError called with error:%u", this, error);
        this->user->TaskFailed(this);
    }

    virtual void DatabaseExecSQLResult(IDatabase* const database, class IDataSet* dataset) override
    {
        if (dataset->Eot()) {
            this->log->Log(LOG_CONFIG, "TaskInitCofnig(%08x)::TaskComplete reading saved items done", this);
            delete dataset;
            complete = true;
            this->user->TaskComplete(this);
        }
        else {
            ConfigItem* cur = this->itemListStart;
            while (cur) {
                if (strcmp(cur->GetName(), dataset->GetStringValue(0)) == 0) {
                    cur->ReadValueFromDataset(dataset);
                    break;
                }

                cur = cur->goNext();
            }

            dataset->FetchNextRow();
        }
    }
};

/*---------------------------------------------------------------------------*/
/* class TaskInitFileConfig                                                  */
/*---------------------------------------------------------------------------*/

class TaskInitFileConfig : public ITask, public UIoExec {
private:
    class ConfigContext* const configContext;

public:
    TaskInitFileConfig(class ConfigContext* configContext)
        : configContext(configContext)
    {
    }

    virtual ~TaskInitFileConfig() {
    }

    virtual void Start(UTask* user) override
    {
        this->user = user;
        // file reading is synchronous, but the task answer must be async
        this->configContext->iomux->SetExec(this, nullptr);
    }

    void IoExec(void* execContext) override
    {
        if (this->configContext->ReadConfigJsonFile()) {
            this->user->TaskComplete(this);
        }
        else {
            this->user->TaskFailed(this);
        }
    }
};

/*---------------------------------------------------------------------------*/
/* class TaskWriteConfigItems                                                */
/*---------------------------------------------------------------------------*/

class TaskWriteConfigItems : public UDatabase, public ITask, public UIoExec {
private:
    IInstanceLog* const log;
    IDatabase* const database;
    ConfigItem* const itemListStart;
    ConfigContext* const configContext;
    bool unmanaged;

public:
    TaskWriteConfigItems(IDatabase* database, ConfigContext* configContext, ConfigItem* startItem, IInstanceLog* const log, bool unmanaged)
        : log(log),
        database(database),
        itemListStart(startItem),
        configContext(configContext),
        unmanaged(unmanaged)
    {
    }

    ~TaskWriteConfigItems()
    {
    }

    virtual void Start(UTask* user) override
    {
        this->log->Log(LOG_CONFIG, "TaskWriteConfigItems(%08x)::Start starting task to write unmanaged config", this);
        this->user = user;

        if (!configContext->fileMode) {
            this->database->BeginTransaction(this);

            ConfigItem* cur = this->itemListStart;
            while (cur) {
                if (cur->unmanaged == this->unmanaged && cur->Changed()) {
                    if (cur->HasDefaultValue()) this->database->ExecSQL(this, 0, "DELETE FROM config WHERE name=%s", cur->name);
                    else {
                        int len = cur->WriteValueToString(nullptr) + 1; // determine the length first
                        char* tmpStr = (char*)malloc(len);
                        cur->WriteValueToString(tmpStr);
                        this->database->ExecSQL(this, 0, INSERT_UPDATE_SQL, cur->GetName(), tmpStr, cur->unmanaged, tmpStr, cur->unmanaged);
                        free(tmpStr);
                    }
                }
                cur = cur->goNext();
            }
            this->database->EndTransaction(this);
        }
        else {
            this->configContext->iomux->SetExec(this, nullptr); // file mode, enforce async mode first
        }
    }

    void IoExec(void* execContext) override
    {
        if (this->configContext->WriteConfigJsonFile()) this->user->TaskComplete(this);
        else this->user->TaskFailed(this);
    }

    virtual void DatabaseError(IDatabase* const database, db_error_t error) override
    {
        this->log->Log(LOG_CONFIG, "TaskWriteConfigItems(%08x)::DatabaseError DB Write command reported an error:%u", this, error);
        this->user->TaskFailed(this);
    }

    virtual void DatabaseEndTransactionResult(IDatabase* const database) override
    {
        this->log->Log(LOG_CONFIG, "TaskWriteConfigItems(%08x)::DatabaseEndTransactionResult writing of config items done", this);
        class ConfigContext* context = this->configContext;
        bool isUnmanaged = this->unmanaged;

        if (isUnmanaged) context->ConfigChangedUnmanaged();
        this->user->TaskComplete(this); // this is deleted inside TaskComplete, so use local variable context instead of this->configContext
        context->ResetChangedFlag(isUnmanaged);
    }

    virtual void DatabaseExecSQLResult(IDatabase* const database, class IDataSet* dataset) override
    {
        delete dataset;
    }
};


/*---------------------------------------------------------------------------*/
/* class ConfigApiContext                                                    */
/*---------------------------------------------------------------------------*/

ConfigApiContext::ConfigApiContext(IJsonApiConnection* connection, UConfigContextApi* user, IInstanceLog* const log)
    : connection(connection),
    user(user),
    log(log)
{
    this->connection->RegisterJsonApi(this);
    readSrc = 0;
}


ConfigApiContext::~ConfigApiContext()
{
    // Remove the item from ConfigContext's list of ConfigApiContext instances
    this->remove();
    if (readSrc) free((void*)readSrc);
}


const char* ConfigApiContext::Name()
{
    return API_NAME;
}


IJsonApiConnection* ConfigApiContext::GetConnection()
{
    return this->connection;
}


void ConfigApiContext::Message(class json_io& msg, word base, const char* mt, const char* src)
{
    if (strcmp(mt, "ReadConfig") == 0) {
        if (readSrc) free((char*)readSrc);
        readSrc = _strdup(src);
        this->log->Log(LOG_CONFIG, "ConfigApiContext(%08x)::Message app requested read of configuration", this);
        this->user->SendConfig(this->connection, "ReadConfigResult", src);
    }
    else if (strcmp(mt, "WriteConfig") == 0) {
        this->log->Log(LOG_CONFIG, "ConfigApiContext(%08x)::Message app requested writing of configuration", this);
        this->user->UpdateConfig(this->connection, &msg, base, src);
    }
    else if (strcmp(mt, "GetConfigItems") == 0) {
        this->log->Log(LOG_CONFIG, "ConfigApiContext(%08x)::Message app requested read of configuration items", this);
        this->user->SendConfigItems(this->connection, src);
    }
    else this->log->Log(LOG_CONFIG, "ConfigApiContext(%08x)::Message unkown message type:%s - ignoring", this, mt);

    this->connection->JsonApiMessageComplete();
}


void ConfigApiContext::JsonApiConnectionClosed()
{
    delete this;
}


/*---------------------------------------------------------------------------*/
/* class ConfigContext                                                       */
/*---------------------------------------------------------------------------*/

ConfigContext::ConfigContext(class IDatabase* database, class IInstanceLog* log)
    : database(database),
    log(log),
    iomux(nullptr)
{
    this->log->Log(LOG_CONFIG, "ConfigContext(%08x)::ConfigContext", this);
    this->fileName = nullptr;
    this->fileMode = false;
}


ConfigContext::~ConfigContext()
{
    this->log->Log(LOG_CONFIG, "ConfigContext(%08x)::~ConfigContext", this);
    free(this->fileName);
}


/*
when switching to file mode, further changes are written to the file instead of the database
also CreateInitTask then reads from the file instead of the database
if initFromDatabase is set, the config was previously stored inside the database
and is now written to the file at once
*/
bool ConfigContext::SwitchToFileMode(class IIoMux* iomux, const char* absoluteFileName, bool initFromDatabase)
{
    this->iomux = iomux;
    free(this->fileName);
    this->fileName = _strdup(absoluteFileName);
    this->fileMode = true;

    // check if file can be created/opened
    IFileWrite* fWrite = FileSystem::OpenWriteFile(absoluteFileName, true, this->log);
    if (fWrite == nullptr) {
        return false;
    }
    fWrite->Close();
    delete fWrite;

    if (initFromDatabase) {
        return this->WriteConfigJsonFile();
    }
    return true;
}


/*
when switching to file mode items must be then written to the database by calling
e.g. UpdateManagedItems/UpdateUnmanagedItems
*/
void ConfigContext::SwitchToDatabaseMode()
{
    free(this->fileName);
    this->fileName = nullptr;
    this->fileMode = false;
}


bool ConfigContext::ReadConfigJsonFile()
{
    // shall we limit malloc here? hmmm ...
    size_t fileSize = FileSystem::GetFileSize(fileName, log);
    char* buffer = (char*)malloc(fileSize + 1);
    if (!buffer) {  // malloc failed
        log->Log(LOG_CONFIG, "TaskInitFileConfig::ReadConfigJsonFile malloc(%u) failed", (dword)fileSize);
        return false;
    }
    if (!FileSystem::ReadBinaryFile(fileName, (byte*)buffer, fileSize, log)) {
        log->Log(LOG_CONFIG, "TaskInitFileConfig::ReadConfigJsonFile ReadBinaryFile(%s) failed", fileName);
        free(buffer);
        return false;
    }
    buffer[fileSize] = '\0';
    class json_io json(buffer);
    if (!json.decode()) {
        log->Log(LOG_CONFIG, "TaskInitFileConfig::ReadConfigJsonFile json decode() failed");
        free(buffer);
        return false;
    }
    word base = json.get_object(JSON_ID_ROOT, nullptr);
    if (base == JSON_ID_NONE) {
        log->Log(LOG_CONFIG, "TaskInitFileConfig::ReadConfigJsonFile no root object found");
        free(buffer);
        return false;
    }

    ConfigItem* cur = this->items.front();

    while (cur) {
        cur->ReadValueFromJSON(&json, base, nullptr);
        cur = cur->goNext();
    }
    free(buffer);
    return true;
}


bool ConfigContext::WriteConfigJsonFile()
{
    // determine needed file size
    size_t fSize = 10;
    size_t fSizeNumbers = 0;
    ConfigItem* cur = this->items.front();

    while (cur) {
        fSize += strlen(cur->GetName()) + 7;    // 7 -> decoration "":"", here I oversize a bit
        switch (cur->GetType())
        {
        case config_item_t::config_item_bool:
            fSize += 5; // false
            break;
        case config_item_t::config_item_choice:
            fSize += (((ConfigChoice*)cur)->Value() != nullptr ? (strlen(((ConfigChoice*)cur)->Value()) * 2) : 0);
            break;
        case config_item_t::config_item_dword:
            fSize += 10;
            fSizeNumbers += 10;
            break;
        case config_item_t::config_item_int:
            fSize += 11;
            fSizeNumbers += 11;
            break;
        case config_item_t::config_item_long64:
            fSize += 21;
            fSizeNumbers += 21;
            break;
        case config_item_t::config_item_string:
            if (((ConfigString*)cur)->Value() != nullptr) {
                fSize += strlen(((ConfigString*)cur)->Value()) * 2;                // encrypted password has double length and standard string maybe too if quotes must be escaped                
                if (((ConfigString*)cur)->isPassword) {
                    fSizeNumbers += strlen(((ConfigString*)cur)->Value()) * 2 + 1;     // the encrypted password must be temporarily stored inside another buffer
                }
            }
            break;
        case config_item_t::config_item_ulong64:
            fSize += 20;
            fSizeNumbers += 20;
            break;
        }
        cur = cur->goNext();
    }

    class json_io json(nullptr);
    word base = json.add_object(JSON_ID_ROOT, nullptr);

    char* buffer = (char*)malloc(fSize);
    char* bufferNumbers = (char*)malloc(fSizeNumbers);
    char* tmp = bufferNumbers;
    if (buffer == nullptr || bufferNumbers == nullptr) {
        this->log->Log(LOG_CONFIG, "ConfigContext(%08x)::WriteConfigJsonFile couldn't malloc(%u) or malloc(%u)", this, (dword)fSize, (dword)fSizeNumbers);
        free(buffer);
        free(bufferNumbers);
        return false;
    }

    cur = this->items.front();

    while (cur) {
        if (!cur->HasDefaultValue()) {
            cur->WriteValueToJSON(&json, base, tmp, nullptr);
        }
        cur = cur->goNext();
    }

    dword encodedSize = json.encode(base, buffer);
    if (encodedSize > (dword)fSize) {
        this->log->Log(LOG_CONFIG, "ConfigContext(%08x)::WriteConfigJsonFile buffer for encoded JSON is too small (encoded %u < calculated %u)", this, encodedSize, (dword)fSize);
        ASSERT(false, "ConfigContext::WriteConfigJsonFile buffer for encoded JSON is too small");
    }

    bool ret = FileSystem::WriteBinaryFile(this->fileName, (const byte*)buffer, (size_t)encodedSize, this->log);
    free(buffer);
    free(bufferNumbers);
    return ret;
}


void ConfigContext::RegisterConfigItem(class ConfigItem* item)
{
    this->items.push_back(item);
}


ITask* ConfigContext::CreateInitTask(class IDatabase* database, bool readOnly)
{
    if (database) this->database = database;
    this->log->Log(LOG_CONFIG, "ConfigContext(%08x)::CreateInitTask", this);
    ConfigItem* cur = this->items.front();
    while (cur) {
        cur->Reset();
        cur = cur->goNext();
    }
    if (this->fileMode) return new TaskInitFileConfig(this);
    else return new TaskInitConfig(this->database, this->items.front(), this->log, readOnly);
}


class ITask* ConfigContext::UpdateUnmanagedItems()
{
    this->log->Log(LOG_CONFIG, "ConfigContext(%08x)::UpdateUnmanagedItems", this);
    return new TaskWriteConfigItems(database, this, this->items.front(), this->log, true);
}

class ITask* ConfigContext::UpdateManagedItems()
{
    this->log->Log(LOG_CONFIG, "ConfigContext(%08x)::UpdateUnmanagedItems", this);
    return new TaskWriteConfigItems(database, this, this->items.front(), this->log, false);
}


class ITask* ConfigContext::WriteItemValue(ConfigItem* item, const char* value, bool notify)
{
    this->log->Log(LOG_CONFIG, "ConfigContext(%08x)::WriteItemValue", this);
    class ITask* task = new TaskWriteValue(this->database, item->name, value, item->unmanaged, this->log, this, notify);
    ConfigItem* cur = this->items.front();
    while (cur) {
        if (!strcmp(cur->GetName(), item->name)) {
            item->changed = true;
            item->SetWriteValue(value);
        }
        cur = cur->goNext();
    }
    return task;
}

class ConfigItem* ConfigContext::FindConfigItem(const char* name)
{
    if (name) {
        ConfigItem* cur = this->items.front();
        while (cur) {
            if (!strcmp(cur->GetName(), name)) {
                return cur;
            }
            cur = cur->goNext();
        }
    }
    return 0;
}

void ConfigContext::SendConfig(class IJsonApiConnection * connection, const char * mt, const char * src)
{
    char buffer[WS_MAX_DATA_SIZE];
    char convBuffer[WS_MAX_DATA_SIZE];
    char * convBufPtr = convBuffer;
    json_io msg(buffer);

    word base = msg.add_object(JSON_ID_ROOT, 0);
    msg.add_string(base, "api", API_NAME);
    msg.add_string(base, "mt", mt);
    msg.add_string(base, "src", src);
    if (!this->CanReadConfig(connection)) {
        msg.add_string(base, "result", "AccessDenied");
    }
    else {
        word objBase = msg.add_object(base, "ConfigItems");
        ConfigItem * cur = this->items.front();
        while (cur) {
            if (cur->unmanaged == false) {
                cur->WriteValueToJSON(&msg, objBase, convBufPtr, connection);
            }
            cur = cur->goNext();
        }
    }

    this->log->Log(LOG_CONFIG, "ConfigContext(%08x)::SendConfig sending configuration to app", this);
    connection->JsonApiMessage(msg, buffer);
}


void ConfigContext::SendConfigItems(IJsonApiConnection* connection, const char* src)
{
    char buffer[60000];
    char numBuf[4096];
    char* convNumBuf = numBuf;
    json_io msg(buffer);

    word base = msg.add_object(JSON_ID_ROOT, 0);
    msg.add_string(base, "api", API_NAME);
    msg.add_string(base, "mt", "GetConfigItemsResult");
    msg.add_string(base, "src", src);
    if (!this->CanReadConfig(connection)) msg.add_string(base, "result", "AccessDenied");
    else {
        word items = msg.add_array(base, "ConfigItems");
        ConfigItem* cur = this->items.front();
        while (cur != nullptr) {
            // Only send unmannaged items!
            if (cur->unmanaged == false) {
                cur->WriteItemTypeToJSON(&msg, items, convNumBuf);
            }
            cur = cur->goNext();
        }
    }

    this->log->Log(LOG_CONFIG, "ConfigContext(%08x)::SendConfigItems sending configuration items to app", this);
    connection->JsonApiMessage(msg, buffer);
}


void ConfigContext::SendWriteConfigResult(IJsonApiConnection* connection, const char* src, const char* result = "ok")
{
    this->log->Log(LOG_CONFIG, "ConfigApiContext(%08x)::SendWriteConfigResult sending WriteConfigResult with result:%s", this, result);
    char buffer[512];
    json_io msg(buffer);
    word base = msg.add_object(JSON_ID_ROOT, 0);
    msg.add_string(base, "api", API_NAME);
    msg.add_string(base, "mt", "WriteConfigResult");
    msg.add_string(base, "src", src);
    msg.add_string(base, "result", result);
    connection->JsonApiMessage(msg, buffer);
}


void ConfigContext::UpdateConfig(class IJsonApiConnection* connection, class json_io* json, dword base, const char* src)
{
    if (!this->CanWriteConfig(connection)) {
        this->log->Log(LOG_CONFIG, "ConfigContext(%08x)::UpdateConfig app denied writing of configuration", this);
        this->SendWriteConfigResult(connection, src, "AccessDenied");
    }
    else {
        this->log->Log(LOG_CONFIG, "ConfigApiContext(%08x)::UpdateConfig start writing of config to database", this);
        ITask* tmp = new TaskWriteConfig(connection, this->database, json, base, src, this->items.front(), this->log, this);
        tmp->Start(this);
    }
}


void ConfigContext::TaskComplete(class ITask* const task)
{
    this->log->Log(LOG_CONFIG, "ConfigContext(%08x)::ConfigChanged()", this);
    this->ConfigChanged();

    TaskWriteConfig * tmp = static_cast<TaskWriteConfig *>(task);
    for (ConfigApiContext* c = this->apiContextList.front(); c; c = c->goNext()) {
        if (c->GetConnection() != tmp->GetConnection()) {
            this->SendConfig(c->GetConnection(), "ConfigUpdate", c->readSrc);
        }
        else {
            this->SendWriteConfigResult(tmp->GetConnection(), tmp->src, "ok");
        }
    }

    delete task;
}


void ConfigContext::TaskFailed(class ITask* const task)
{
    TaskWriteConfig * tmp = static_cast<TaskWriteConfig*>(task);
    for (ConfigApiContext * c = this->apiContextList.front(); c; c = c->goNext()) {
        if (c->GetConnection() == tmp->GetConnection()) {
            this->SendWriteConfigResult(tmp->GetConnection(), tmp->src, "failed");
        }
    }
    
    delete task;
    this->log->Log(LOG_CONFIG, "ConfigContext(%08x)::TaskFailed writing config changes to DB failed", this);
}


JsonApi* ConfigContext::JsonApiRequested(IJsonApiConnection* connection)
{
    if (connection->JsonApiPermission(Name())) {
        ConfigApiContext* ctx = new ConfigApiContext(connection, this, this->log);
        this->apiContextList.push_back(ctx);
        return ctx;
    }
    return 0;
}


void ConfigContext::ResetChangedFlag(bool isUnmanaged)
{
    this->log->Log(LOG_CONFIG, "ConfigContext(%08x)::ResetChangedFlag", this);
    for (ConfigItem* s = this->items.front(); s; s = s->goNext()) {
        if (s->unmanaged == isUnmanaged) s->changed = false;
    }
}

/*---------------------------------------------------------------------------*/
/* class ConfigItem                                                          */
/*---------------------------------------------------------------------------*/

ConfigItem::ConfigItem(ConfigContext* context, config_item_t type, const char* name, bool unmanaged)
    : log(context->log)
{
    this->log->Log(LOG_CONFIG, "ConfigItem(%08x)::ConfigItem name:%s unmanaged:%u", this, name, unmanaged);
    this->type = type;
    this->name = _strdup(name);
    this->unmanaged = unmanaged;
    this->changed = false;
    context->RegisterConfigItem(this);
}


ConfigItem::~ConfigItem()
{
    this->log->Log(LOG_CONFIG, "ConfigItem(%08x)::~ConfigItem", this);
    free(this->name);
}

/*---------------------------------------------------------------------------*/
/* class ConfigChoice                                                        */
/*---------------------------------------------------------------------------*/

ConfigChoice::ConfigChoice(ConfigContext* context, const char* name, size_t defVal, const char** options, bool unmanaged)
    : ConfigItem(context, config_item_choice, name, unmanaged)
{
    this->optionsCount = 0;
    this->log->Log(LOG_CONFIG, "ConfigChoice(%08x)::ConfigChoice created ConfigChoice object (name:%s defVal:%u unmanaged:%u) - options: ", this, name, defVal, unmanaged);
    if (this->log->LogFlagSet(LOG_CONFIG)) {
        if (options == nullptr || *options == nullptr) this->log->Log(LOG_CONFIG, "    nullptr");
        else {
            const char** tmp = options;
            while (*tmp) {
                this->log->Log(LOG_CONFIG, "    %s", *tmp);
                ++tmp;
                ++this->optionsCount;
            }
        }
    }
    else {
        const char** tmp = options;
        while (*tmp++)++this->optionsCount;
    }

    ASSERT(defVal < this->optionsCount, "ConfigChoice::ConfigChoice given defVal is out of options array range");
    this->valueIdx = defVal;
    this->defValueIdx = defVal;
    this->choiceValues = options;
}


ConfigChoice::~ConfigChoice()
{
    this->log->Log(LOG_CONFIG, "ConfigChoice(%08x)::~ConfigChoice", this);
}


void ConfigChoice::ReadValueFromDataset(IDataSet* dataset)
{
    const char* valStr = dataset->GetStringValueWithNull("value");
    bool found = false;
    if (valStr == nullptr) {
        this->log->Log(LOG_CONFIG, "ConfigChoice(%08x)::ReadValueFromDataset(): No choice value for config item '%s' found in database - using default (%u, '%s')", this, this->GetName(), this->defValueIdx, this->choiceValues[this->defValueIdx]);
        return;
    }

    // Because the values can change, the value itself and not the index will be safed to the database. So we need to determine the index.
    for (size_t i = 0; i < this->optionsCount; ++i) {
        if (strcmp(valStr, this->choiceValues[i]) == 0) {
            found = true;
            this->valueIdx = i;
            break;
        }
    }

    if (!found) {
        this->valueIdx = this->defValueIdx;
        this->log->Log(LOG_CONFIG, "ConfigChoice(%08x)::ReadValueFromDataset(): value read (%s) is not inside choice value list - using default index (%u, '%s')", this, valStr, this->defValueIdx, this->choiceValues[this->defValueIdx]);
    }
    else this->log->Log(LOG_CONFIG, "ConfigChoice(%08x)::ReadValueFromDataset(): updated value to %u ('%s')", this, this->valueIdx, valStr);
}


void ConfigChoice::ReadValueFromJSON(json_io* json, dword base, IJsonApiConnection* connection)
{
    this->changed = false;
    bool found = false;
    size_t idx = 0;
    if (connection != nullptr) {
        idx = json->get_unsigned(base, this->GetName(), &found);
    }
    else {
        const char* valStr = json->get_string(base, this->GetName(), &found);
        if (found && valStr) {
            // Because the values can change, the value itself and not the index will be safed to the database. So we need to determine the index.
            for (size_t i = 0; i < this->optionsCount; ++i) {
                if (strcmp(valStr, this->choiceValues[i]) == 0) {
                    found = true;
                    idx = i;
                    break;
                }
            }
        }
    }
    if (found && idx != this->valueIdx) {
        this->log->Log(LOG_CONFIG, "ConfigChoice(%08x)::ReadValueFromJSON updated value to %u (string value:%s)", this, idx, (idx < this->optionsCount ? this->choiceValues[idx] : "<index out of range>"));
        if (idx >= this->optionsCount) return;

        this->valueIdx = idx;
        if (connection != nullptr) {    // if connection is nullptr, ReadValueFromJSON is called during initialization by file, not database, so the changed flag shouldn't be set
            this->changed = true;
        }
    }
}


void ConfigChoice::WriteValueToJSON(json_io* json, dword base, char*& convBuf, IJsonApiConnection* connection)
{
    if (connection != nullptr) json->add_int(base, this->GetName(), (int)this->valueIdx, convBuf);
    else json->add_string(base, this->GetName(), this->choiceValues[this->valueIdx]);
}


void ConfigChoice::WriteItemTypeToJSON(json_io* json, dword base, char*& convBuf)
{
    dword obj = json->add_object(base, "item");
    json->add_string(obj, "name", this->GetName());
    json->add_string(obj, "type", CFG_TYPE_CHOICE);

    if (this->choiceValues) {
        dword grpArr = json->add_array(obj, "choices");
        const char** cur = this->choiceValues;
        while (*cur) json->add_string(grpArr, nullptr, *cur++);
    }
}


ConfigItem::json_size_t ConfigChoice::GetJsonBufferSize(class IJsonApiConnection * connection)
{
    size_t len = StringJsonBufferSize(this->GetName()) + 3; // + 3 = 2x " and 1 comma
    
    if (connection != nullptr) len += 10; // should be enough for the integer number.
    else len += StringJsonBufferSize(this->choiceValues[this->valueIdx]) + 2;

    return ConfigItem::json_size_t(len, 0);
}


int ConfigChoice::WriteValueToString(char* dest)
{
    const char* tmp = this->choiceValues[this->valueIdx];
    return dest == nullptr ? (int)strlen(tmp) : _sprintf(dest, "%s", tmp);
}


bool ConfigChoice::HasDefaultValue() const
{
    return this->valueIdx == this->defValueIdx;
}

void ConfigChoice::Reset()
{
    this->valueIdx = this->defValueIdx;
}

void ConfigChoice::SetValue(const char* option)
{
    const char** cur = this->choiceValues;
    size_t newValueIdx = 0;
    while (*cur) {
        if (!strcmp(*cur++, option)) {
            this->SetValueIdx(newValueIdx);
            return;
        }
        newValueIdx++;
    }
    this->SetValueIdx(0);
}


void ConfigChoice::SetValueIdx(size_t valueIdx)
{
    if (this->valueIdx != valueIdx) this->changed = true;
    if (valueIdx < this->optionsCount) {
        this->valueIdx = valueIdx;
    }
    else {
        this->log->Log(LOG_CONFIG, "ConfigChoice::SetValueIdx valueIdx:%u is out of range (itemCount:%u)", valueIdx, this->optionsCount);
        this->valueIdx = 0;
    }
}


void ConfigChoice::SetWriteValue(const char* value)
{
    const char** cur = this->choiceValues;
    size_t newValueIdx = 0;
    while (*cur) {
        if (!strcmp(*cur++, value)) {
            this->SetValueIdx(newValueIdx);
            return;
        }
        newValueIdx++;
    }
    this->SetValueIdx(0);
}

/*---------------------------------------------------------------------------*/
/* class ConfigBool                                                          */
/*---------------------------------------------------------------------------*/

ConfigBool::ConfigBool(ConfigContext* context, const char* name, bool defVal, bool unmanaged)
    : ConfigItem(context, config_item_bool, name, unmanaged)
{
    this->log->Log(LOG_CONFIG, "ConfigBool(%08x)::ConfigBool created ConfigBool object (name:%s defVal:%s unmanaged:%u)", this, name, (defVal ? "TRUE" : "FALSE"), unmanaged);
    this->value = defVal;
    this->defValue = defVal;
}


ConfigBool::~ConfigBool()
{
    this->log->Log(LOG_CONFIG, "ConfigBool(%08x)::~ConfigBool", this);
}


void ConfigBool::ReadValueFromDataset(class IDataSet* dataset)
{
    this->value = dataset->GetBoolValue("value");
    this->log->Log(LOG_CONFIG, "ConfigBool(%08x)::ReadValueFromDataset updated value to %s", this, (this->value ? "TRUE" : "FALSE"));
}


void ConfigBool::ReadValueFromJSON(json_io* json, dword base, IJsonApiConnection* connection)
{
    this->changed = false;
    bool found = false;
    bool res = json->get_bool(base, this->GetName(), &found);
    if (found && res != this->value) {
        this->log->Log(LOG_CONFIG, "ConfigBool(%08x)::ReadValueFromJSON updated value to %s", this, (res ? "TRUE" : "FALSE"));
        this->value = res;
        if (connection != nullptr) {    // if connection is nullptr, ReadValueFromJSON is called during initialization by file, not database, so the changed flag shouldn't be set
            this->changed = true;
        }
    }
}


void ConfigBool::WriteValueToJSON(json_io* json, dword base, char*& convBuf, IJsonApiConnection* connection)
{
    json->add_bool(base, this->GetName(), this->value);
}


int ConfigBool::WriteValueToString(char* dest)
{
    char buf[32];
    return _sprintf(dest ? dest : buf, "%s", (this->value ? "TRUE" : "FALSE"));
}

ConfigItem::json_size_t ConfigBool::GetJsonBufferSize(class IJsonApiConnection * connection)
{
    // Normally + 2 for two ", but since sizeof counts the closing zero, too, there is already one byte to much 
    size_t len = StringJsonBufferSize(this->GetName()) + 3 + (this->value ? sizeof("TRUE") : sizeof("FALSE"));
    return ConfigItem::json_size_t(len, 0);
}

void ConfigBool::WriteItemTypeToJSON(json_io* json, dword base, char*& convBuf)
{
    dword obj = json->add_object(base, "item");
    json->add_string(obj, "name", this->GetName());
    json->add_string(obj, "type", CFG_TYPE_BOOL);
}


bool ConfigBool::HasDefaultValue() const
{
    return this->value == this->defValue;
}


void ConfigBool::Reset()
{
    this->value = this->defValue;
}


void ConfigBool::SetValue(bool value)
{
    if (this->value != value) this->changed = true;
    this->value = value;
}


void ConfigBool::SetWriteValue(const char* value)
{
    if (strcmp(value, "true") == 0) this->value = true;
    else this->value = false;
}

/*---------------------------------------------------------------------------*/
/* class ConfigInt                                                           */
/*---------------------------------------------------------------------------*/

ConfigInt::ConfigInt(ConfigContext* context, const char* name, int defVal, bool unmanaged, int minVal, int maxVal)
    : ConfigItem(context, config_item_int, name, unmanaged)
{
    this->log->Log(LOG_CONFIG, "ConfigInt(%08x)::ConfigInt created ConfigInt object (name:%s defVal:%u unmanaged:%u)", this, name, defVal, unmanaged);
    this->value = defVal;
    this->defValue = defVal;
    this->minVal = minVal;
    this->maxVal = maxVal;
}


ConfigInt::~ConfigInt()
{
    this->log->Log(LOG_CONFIG, "ConfigInt(%08x)::~ConfigInt()", this);
}


void ConfigInt::ReadValueFromDataset(IDataSet* dataset)
{
    this->value = dataset->GetIntValue("value");
    this->log->Log(LOG_CONFIG, "ConfigInt(%08x)::ReadValueFromDataset updated value to %i", this, this->value);
}


void ConfigInt::ReadValueFromJSON(json_io* json, dword base, IJsonApiConnection* connection)
{
    this->changed = false;
    bool found = true;
    int res = json->get_int(base, this->GetName(), &found);
    if (found && res != this->value) {
        if (res < this->minVal) {
            this->log->Log(LOG_CONFIG, "ConfigInt(%08x)::ReadValueFromJSON(): Value found (%i) is less than minVal (%i) - setting to minVal", this, res, this->minVal);
            res = this->minVal;
        }
        else if (res > this->maxVal) {
            this->log->Log(LOG_CONFIG, "ConfigInt(%08x)::ReadValueFromJSON(): Value found (%i) is greater than maxVal (%i) - setting to maxVal", this, res, this->maxVal);
            res = this->maxVal;
        }
        this->value = res;
        if (connection != nullptr) {    // if connection is nullptr, ReadValueFromJSON is called during initialization by file, not database, so the changed flag shouldn't be set
            this->changed = true;
        }
        this->log->Log(LOG_CONFIG, "ConfigInt(%08x)::ReadValueFromJSON updated value to %i", this, this->value);
    }
}


void ConfigInt::WriteValueToJSON(json_io* json, dword base, char*& convBuf, IJsonApiConnection* connection)
{
    json->add_int(base, this->GetName(), this->value, convBuf);
}


int ConfigInt::WriteValueToString(char* dest)
{
    char buf[32];
    return _sprintf(dest ? dest : buf, "%i", this->value);
}


void ConfigInt::WriteItemTypeToJSON(json_io* json, dword base, char*& convBuf)
{
    dword obj = json->add_object(base, "item");
    json->add_string(obj, "name", this->GetName());
    json->add_string(obj, "type", CFG_TYPE_INT);
    if (this->minVal > INT32_MIN) json->add_int(obj, "minVal", this->minVal, convBuf);
    if (this->maxVal < INT32_MAX) json->add_int(obj, "maxVal", this->maxVal, convBuf);
}

ConfigItem::json_size_t ConfigInt::GetJsonBufferSize(class IJsonApiConnection * connection)
{
    size_t numLen = 11;
    size_t len = StringJsonBufferSize(this->GetName()) + 3 + numLen;
    
    if (this->minVal > INT32_MIN) {
        len += 21; //  Max number size + strlen(",\"minVal\":")
        numLen += 11;
    }
    if (this->maxVal < INT32_MAX) {
        len += 20; //  Max number size + strlen(",\"maxVal\":")
        numLen += 10;
    }
   
    return ConfigItem::json_size_t(len, numLen);
}

bool ConfigInt::HasDefaultValue() const
{
    return this->value == this->defValue;
}


void ConfigInt::Reset()
{
    this->value = this->defValue;
}


void ConfigInt::SetValue(int value)
{
    if (value < this->minVal) {
        this->log->Log(LOG_CONFIG, "ConfigInt(%08x)::SetValue(): Value found (%i) is less than minVal (%i) - setting to minVal", this, value, this->minVal);
        value = this->minVal;
    }
    else if (value > this->maxVal) {
        this->log->Log(LOG_CONFIG, "ConfigInt(%08x)::SetValue(): Value found (%i) is greater than maxVal (%i) - setting to maxVal", this, value, this->maxVal);
        value = this->maxVal;
    }

    if (this->value != value) this->changed = true;
    this->value = value;
}

void ConfigInt::SetWriteValue(const char* value)
{
    this->value = atoi(value);
}

/*---------------------------------------------------------------------------*/
/* class ConfigDWord                                                         */
/*---------------------------------------------------------------------------*/

ConfigDword::ConfigDword(ConfigContext* context, const char* name, dword defVal, bool unmanaged, dword minVal, dword maxVal)
    : ConfigItem(context, config_item_dword, name, unmanaged)
{
    this->log->Log(LOG_CONFIG, "ConfigDword(%08x)::ConfigDword created ConfigDword object (name:%s defVal:%d unmanaged:%u)", this, name, defVal, unmanaged);
    this->value = defVal;
    this->defValue = defVal;
    this->minVal = minVal;
    this->maxVal = maxVal;
}


ConfigDword::~ConfigDword()
{
    this->log->Log(LOG_CONFIG, "ConfigDword(%08x)::~ConfigDword", this);
}


void ConfigDword::ReadValueFromDataset(IDataSet* dataset)
{
    this->value = dataset->GetIntValue("value");
    this->log->Log(LOG_CONFIG, "ConfigDword(%08x)::ReadValueFromDataset updated value to %d", this, this->value);
}


void ConfigDword::ReadValueFromJSON(json_io* json, dword base, IJsonApiConnection* connection)
{
    this->changed = false;
    bool found = false;
    dword res = json->get_unsigned(base, this->GetName(), &found);
    if (found && res != this->value) {
        if (res < this->minVal) {
            this->log->Log(LOG_CONFIG, "ConfigDword(%08x)::ReadValueFromJSON(): Value found (%d) is less than minVal (%d) - setting to minVal", this, res, this->minVal);
            res = this->minVal;
        }
        else if (res > this->maxVal) {
            this->log->Log(LOG_CONFIG, "ConfigDword(%08x)::ReadValueFromJSON(): Value found (%d) is greater than maxVal (%d) - setting to maxVal", this, res, this->maxVal);
            res = this->maxVal;
        }

        this->value = res;
        if (connection != nullptr) {    // if connection is nullptr, ReadValueFromJSON is called during initialization by file, not database, so the changed flag shouldn't be set
            this->changed = true;
        }
        this->log->Log(LOG_CONFIG, "ConfigDword(%08x)::ReadValueFromJSON updated value to %d", this, this->value);
    }
}


void ConfigDword::WriteValueToJSON(json_io* json, dword base, char*& convBuf, IJsonApiConnection* connection)
{
    json->add_unsigned(base, this->GetName(), this->value, convBuf);
}


int ConfigDword::WriteValueToString(char* dest)
{
    char buf[32];
    return _sprintf(dest ? dest : buf, "%u", this->value);
}


void ConfigDword::WriteItemTypeToJSON(json_io* json, dword base, char*& convBuf)
{
    dword obj = json->add_object(base, "item");
    json->add_string(obj, "name", this->GetName());
    json->add_string(obj, "type", CFG_TYPE_DWORD);
    if (this->minVal > 0) json->add_unsigned(obj, "minVal", this->minVal, convBuf);
    if (this->maxVal < UINT32_MAX) json->add_unsigned(obj, "maxVal", this->maxVal, convBuf);
}


ConfigItem::json_size_t ConfigDword::GetJsonBufferSize(class IJsonApiConnection * connection)
{
    size_t numLen = 10;
    size_t len = StringJsonBufferSize(this->GetName()) + 3 + numLen;
        
    if (this->minVal > 0) {
        len += 20; //  Max number size + strlen(",\"minVal\":")
        numLen += 10;
    }
    if (this->maxVal < UINT32_MAX) {
        len += 20; //  Max number size + strlen(",\"maxVal\":")
        numLen += 10;
    }

    return ConfigItem::json_size_t(len, numLen);
}


bool ConfigDword::HasDefaultValue() const
{
    return this->value == this->defValue;
}


void ConfigDword::Reset()
{
    this->value = this->defValue;
}


void ConfigDword::SetValue(dword value)
{
    if (value < this->minVal) {
        this->log->Log(LOG_CONFIG, "ConfigDword(%08x)::SetValue(): Value to set (%d) is less than minVal (%d) - setting to minVal", this, value, this->minVal);
        value = this->minVal;
    }
    else if (value > this->maxVal) {
        this->log->Log(LOG_CONFIG, "ConfigDword(%08x)::SetValue(): Value to set (%d) is greater than maxVal (%d) - setting to maxVal", this, value, this->maxVal);
        value = this->maxVal;
    }


    if (this->value != value) this->changed = true;
    this->value = value;
}

void ConfigDword::SetWriteValue(const char* value)
{
    this->value = (unsigned int)atoi(value);
}

/*---------------------------------------------------------------------------*/
/* class ConfigLong64                                                        */
/*---------------------------------------------------------------------------*/

ConfigLong64::ConfigLong64(ConfigContext* context, const char* name, long64 defVal, bool unmanaged, long64 minVal, long64 maxVal)
    : ConfigItem(context, config_item_long64, name, unmanaged)
{
    this->log->Log(LOG_CONFIG, "ConfigLong64(%08x)::ConfigLong64 Created ConfigDword object (name:%s defVal:%lli unmanaged:%u)", this, name, (long long)defVal, unmanaged);
    this->value = defVal;
    this->defValue = defVal;
    this->minVal = minVal;
    this->maxVal = maxVal;
}


ConfigLong64::~ConfigLong64()
{
    this->log->Log(LOG_CONFIG, "ConfigLong64(%08x)::~ConfigLong64()", this);
}


void ConfigLong64::ReadValueFromDataset(IDataSet* dataset)
{
    this->value = dataset->GetLong64Value("value");
    this->log->Log(LOG_CONFIG, "ConfigLong64(%08x)::ReadValueFromDataset updated value to %lli", this, (long long)this->value);
}


void ConfigLong64::ReadValueFromJSON(json_io* json, dword base, IJsonApiConnection* connection)
{
    this->changed = false;
    bool found = false;
    long64 res = json->get_long64(base, this->GetName(), &found);
    if (found && res != this->value) {
        if (res < this->minVal) {
            this->log->Log(LOG_CONFIG, "ConfigLong64(%08x)::ReadValueFromJSON(): Value found (%d) is less than minVal (%d) - setting to minVal", this, res, this->minVal);
            res = this->minVal;
        }
        else if (res > this->maxVal) {
            this->log->Log(LOG_CONFIG, "ConfigLong64(%08x)::ReadValueFromJSON(): Value found (%d) is greater than maxVal (%d) - setting to maxVal", this, res, this->maxVal);
            res = this->maxVal;
        }

        this->value = res;
        if (connection != nullptr) {    // if connection is nullptr, ReadValueFromJSON is called during initialization by file, not database, so the changed flag shouldn't be set
            this->changed = true;
        }
        this->log->Log(LOG_CONFIG, "ConfigLong64(%08x)::ReadValueFromJSON updated value to %lli", this, (long long)this->value);
    }
}


void ConfigLong64::WriteValueToJSON(json_io* json, dword base, char*& convBuf, IJsonApiConnection* connection)
{
    json->add_long64(base, this->GetName(), this->value, convBuf);
}


int ConfigLong64::WriteValueToString(char* dest)
{
    char buf[32];
    return _sprintf(dest ? dest : buf, "%lli", (long long)this->value);
}


void ConfigLong64::WriteItemTypeToJSON(json_io* json, dword base, char*& convBuf)
{
    dword obj = json->add_object(base, "item");
    json->add_string(obj, "name", this->GetName());
    json->add_string(obj, "type", CFG_TYPE_LONG64);
    if (this->minVal > INT64_MIN) json->add_long64(obj, "minVal", this->minVal, convBuf);
    if (this->maxVal < INT64_MAX) json->add_long64(obj, "maxVal", this->maxVal, convBuf);
}


ConfigItem::json_size_t ConfigLong64::GetJsonBufferSize(class IJsonApiConnection * connection)
{
    size_t numLen = 20;
    size_t len = StringJsonBufferSize(this->GetName()) + 3 + numLen;
    
    if (this->minVal > INT64_MIN) {
        len += 30; //  Max number size + strlen(",\"minVal\":")
        numLen += 20;
    }
    if (this->maxVal < INT32_MAX) {
        len += 29; //  Max number size + strlen("\"maxVal\":")
        numLen += 19;
    }
    
    return ConfigItem::json_size_t(len, numLen);
}


bool ConfigLong64::HasDefaultValue() const
{
    return this->value == this->defValue;
}


void ConfigLong64::Reset()
{
    this->value = this->defValue;
}


void ConfigLong64::SetValue(long64 value)
{
    if (value < this->minVal) {
        this->log->Log(LOG_CONFIG, "ConfigLong64(%08x)::SetValue(): Value found (%d) is less than minVal (%d) - setting to minVal", this, value, this->minVal);
        value = this->minVal;
    }
    else if (value > this->maxVal) {
        this->log->Log(LOG_CONFIG, "ConfigLong64(%08x)::SetValue(): Value found (%d) is greater than maxVal (%d) - setting to maxVal", this, value, this->maxVal);
        value = this->maxVal;
    }

    if (this->value != value) this->changed = true;
    this->value = value;
}


void ConfigLong64::SetWriteValue(const char* value)
{
    this->value = strtoll(value, 0, 0);
}

/*---------------------------------------------------------------------------*/
/* class ConfigUlong64                                                       */
/*---------------------------------------------------------------------------*/

ConfigUlong64::ConfigUlong64(ConfigContext* context, const char* name, ulong64 defVal, bool unmanaged, ulong64 minVal, ulong64 maxVal)
    : ConfigItem(context, config_item_ulong64, name, unmanaged)
{
    this->log->Log(LOG_CONFIG, "ConfigUlong64(%08x)::ConfigUlong64 created ConfigUlong64 object (name:%s defVal:%llu unmanaged:%u)", this, name, (unsigned long long)defVal, unmanaged);
    this->value = defVal;
    this->defValue = defVal;
    this->minVal = minVal;
    this->maxVal = maxVal;
}


ConfigUlong64::~ConfigUlong64()
{
    this->log->Log(LOG_CONFIG, "ConfigUlong64(%08x)::~ConfigUlong64", this);
}


void ConfigUlong64::ReadValueFromDataset(IDataSet* dataset)
{
    this->value = dataset->GetULong64Value("value");
    this->log->Log(LOG_CONFIG, "ConfigUlong64(%08x)::ReadValueFromDataset updated value to %llu", this, (unsigned long long)this->value);
}


void ConfigUlong64::ReadValueFromJSON(json_io* json, dword base, IJsonApiConnection* connection)
{
    this->changed = false;
    bool found = false;
    ulong64 res = json->get_ulong64(base, this->GetName(), &found);
    if (found && res != this->value) {
        if (res < this->minVal) {
            this->log->Log(LOG_CONFIG, "ConfigUlong64(%08x)::ReadValueFromJSON(): Value found (%d) is less than minVal (%d) - setting to minVal", this, res, this->minVal);
            res = this->minVal;
        }
        else if (res > this->maxVal) {
            this->log->Log(LOG_CONFIG, "ConfigUlong64(%08x)::ReadValueFromJSON(): Value found (%d) is greater than maxVal (%d) - setting to maxVal", this, res, this->maxVal);
            res = this->maxVal;
        }

        this->value = res;
        if (connection != nullptr) {    // if connection is nullptr, ReadValueFromJSON is called during initialization by file, not database, so the changed flag shouldn't be set
            this->changed = true;
        }
        this->log->Log(LOG_CONFIG, "ConfigUlong64(%08x)::ReadValueFromJSON updated value to %llu", this, (unsigned long long)this->value);
    }
}


void ConfigUlong64::WriteValueToJSON(json_io* json, dword base, char*& convBuf, IJsonApiConnection* connection)
{
    json->add_ulong64(base, this->GetName(), this->value, convBuf);
}


int ConfigUlong64::WriteValueToString(char* dest)
{
    char buf[32];
    return _sprintf(dest ? dest : buf, "%llu", (unsigned long long)this->value);
}


void ConfigUlong64::WriteItemTypeToJSON(json_io* json, dword base, char*& convBuf)
{
    dword obj = json->add_object(base, "item");
    json->add_string(obj, "name", this->GetName());
    json->add_string(obj, "type", CFG_TYPE_ULONG64);
    if (this->minVal > 0) json->add_ulong64(obj, "minVal", this->minVal, convBuf);
    if (this->maxVal < UINT64_MAX) json->add_ulong64(obj, "maxVal", this->maxVal, convBuf);
}


ConfigItem::json_size_t ConfigUlong64::GetJsonBufferSize(class IJsonApiConnection * connection)
{
    size_t numLen = 19;
    size_t len = StringJsonBufferSize(this->GetName()) + 3;
    
    if (this->minVal > 0) {
        len += 29; //  Max number size + strlen(",\"minVal\":")
        numLen += 19;
    }
    if (this->maxVal < INT32_MAX) {
        len += 29; //  Max number size + strlen(",\"maxVal\":")
        numLen += 19;
    }
    
    return ConfigItem::json_size_t(len, numLen);
}


bool ConfigUlong64::HasDefaultValue() const
{
    return this->value == this->defValue;
}


void ConfigUlong64::Reset()
{
    this->value = this->defValue;
}


void ConfigUlong64::SetValue(ulong64 value)
{
    if (value < this->minVal) {
        this->log->Log(LOG_CONFIG, "ConfigUlong64(%08x)::SetValue(): Value found (%d) is less than minVal (%d) - setting to minVal", this, value, this->minVal);
        value = this->minVal;
    }
    else if (value > this->maxVal) {
        this->log->Log(LOG_CONFIG, "ConfigUlong64(%08x)::SetValue(): Value found (%d) is greater than maxVal (%d) - setting to maxVal", this, value, this->maxVal);
        value = this->maxVal;
    }

    if (this->value != value) this->changed = true;
    this->value = value;
}


void ConfigUlong64::SetWriteValue(const char* value)
{
    this->value = strtoull(value, 0, 0);
}

/*---------------------------------------------------------------------------*/
/* class ConfigString                                                        */
/*---------------------------------------------------------------------------*/

ConfigString::ConfigString(ConfigContext* context, const char* name, const char* defVal, bool unmanaged, bool isPassword)
    : ConfigItem(context, config_item_string, name, unmanaged)
{
    this->log->Log(LOG_CONFIG, "ConfigString(%08x)::ConfigString created ConfigString object (name:%s defVal:%s unmanaged:%u)", this, name, (defVal ? defVal : "nullptr"), unmanaged);
    this->value = nullptr;
    this->isPassword = isPassword;
    this->defValue = _strdup(defVal ? defVal : "");
    this->value = _strdup(this->defValue);
}


ConfigString::~ConfigString()
{
    this->log->Log(LOG_CONFIG, "ConfigString(%08x)::~ConfigString", this);
    free(this->value);
    free(this->defValue);
}

const char* ConfigString::CryptKey()
{
    // do never change the following string anymore, otherwise existing passwords will be wrong!
    static const char* seed = "HAVBSD\xd6K231zh41\xfcr89hINDYz8132rbKF()q3rg)f8Z";
    static char tmp[46] = { '\0' };

    if (!*tmp) {
        for (size_t i = 0; i < strlen(seed); i++) {
            switch (i % 5) {
            case 0: tmp[i] = seed[i]; break;
            case 1: tmp[i] = seed[i] - 1; break;
            case 2: tmp[i] = seed[i] + 2; break;
            }
        }
        tmp[strlen(seed)] = '\0';
    }
    return tmp;
}

void ConfigString::Encrypt(const char* seed, const char* password, char* out, size_t len)
{
    size_t sz = strlen(password);
    const char* key = CryptKey();
    char* tmpKey = (char*)malloc(strlen(seed) + strlen(key) + 2);
    byte* tmpOut = (byte*)malloc(len);
    _sprintf(tmpKey, "%s:%s", seed, key);
    Cipher c;
    c.Init(CIPHER_RC4, (const byte*)tmpKey, (int)strlen(tmpKey), true);
    c.Crypt((const byte*)password, tmpOut, (int)sz);
    str::from_hexmem(tmpOut, (const dword)sz, out);
    free(tmpKey);
    free(tmpOut);
}

void ConfigString::Decrypt(const char* seed, const char* password, char* out, size_t len)
{
    size_t sz = strlen(password);
    const char* key = CryptKey();
    char* tmpKey = (char*)malloc(strlen(seed) + strlen(key) + 2);
    byte* tmpOut = (byte*)malloc(len);
    sz = str::to_hexmem(password, tmpOut, (dword)len);
    _sprintf(tmpKey, "%s:%s", seed, key);
    Cipher c;
    c.Init(CIPHER_RC4, (const byte*)tmpKey, (int)strlen(tmpKey), false);
    c.Crypt(tmpOut, (byte*)out, (int)sz);
    out[sz] = '\0';
    free(tmpKey);
    free(tmpOut);
}

void ConfigString::ReadValueFromDataset(IDataSet* dataset)
{
    free(this->value);
    if (this->isPassword) {
        const char* value = dataset->GetStringValue("value");
        size_t len = strlen(value) / 2 + 1;
        this->value = (char*)malloc(len);
        this->Decrypt(this->GetName(), value, this->value, len);
    }
    else {
        this->value = _strdup(dataset->GetStringValue("value"));
    }
    this->changed = false; // reading from db doesn't count as change
    if (this->isPassword) {
        this->log->Log(LOG_CONFIG, "ConfigString(%08x)::ReadValueFromDataset updated password value", this);
    }
    else {
        this->log->Log(LOG_CONFIG, "ConfigString(%08x)::ReadValueFromDataset updated value to %s", this, (this->value ? this->value : "nullptr"));
    }
}


void ConfigString::ReadValueFromJSON(json_io* json, dword base, IJsonApiConnection* connection)
{
    this->changed = false;
    char* val = nullptr;
    bool releaseValBuffer = false;

    if (this->isPassword) {
        if (connection != nullptr) {    // if connection is not nullptr, the decryption information are contained within the JSON
            dword obj = json->get_object(base, this->GetName());
            if (obj != JSON_ID_NONE) {
                const char* pwd = json->get_string(obj, "value");
                const char* seed = json->get_string(obj, "key");
                size_t dlen = strlen(pwd) / 2 + 1;
                if (dlen > 1024) {
                    releaseValBuffer = true;
                    val = static_cast<char*>(malloc(dlen));
                }
                else val = static_cast<char*>(alloca(dlen));

                static_cast<AppWebsocket*>(connection)->AppWebsocketDecrypt(seed, pwd, val, dlen);
            }
        }
        else {  // otherwise we decrypt with our own decryption mechanism
            char* valEncrypted = const_cast<char*>(json->get_string(base, this->GetName()));
            if (valEncrypted != nullptr) {
                size_t len = strlen(valEncrypted) / 2 + 1;
                val = (char*)alloca(len);
                this->Decrypt(this->GetName(), valEncrypted, val, len);
            }
        }
    }
    else val = const_cast<char*>(json->get_string(base, this->GetName()));

    if (val != nullptr) {
        if (this->value == nullptr || strcmp(val, this->value) != 0) {
            if (connection != nullptr) {    // if connection is nullptr, ReadValueFromJSON is called during initialization by file, not database, so the changed flag shouldn't be set
                this->changed = true;
            }
            free(this->value);
            this->value = _strdup(val);
            this->log->Log(LOG_CONFIG, "ConfigString(%08x)::ReadValueFromJSON updated value to %s", this, this->isPassword ? "<hidden>" : this->value);
        }
    }

    if (releaseValBuffer) free(val);
}


void ConfigString::WriteValueToJSON(json_io* json, dword base, char*& convBuf, IJsonApiConnection* connection)
{
    // Password items aren't sent to the JavaScript app.
    if (!this->isPassword) json->add_string(base, this->GetName(), this->Value());
    else if (connection == nullptr && this->value != nullptr) {
        // but saved encrypted inside the file
        size_t len = strlen(this->value) * 2 + 1;
        // we must use the convBuf here, as the encrypted value must be in a persistent buffer until decoding
        this->Encrypt(this->GetName(), this->value, convBuf, len);
        json->add_string(base, this->GetName(), convBuf, (dword)len);
        convBuf += len;
    }
}


int ConfigString::WriteValueToString(char* dest)
{
    if (!this->isPassword) {
        if (!dest) return (int)strlen(this->value);
        return _sprintf(dest, "%s", this->value);
    }
    else {
        if (!dest) return (int)strlen(this->value) * 2;
        else {
            this->Encrypt(this->GetName(), this->value, dest, strlen(this->value) * 2 + 1);
            return (int)strlen(dest);
        }
    }
}


ConfigItem::json_size_t ConfigString::GetJsonBufferSize(class IJsonApiConnection * connection)
{
    size_t len = StringJsonBufferSize(this->GetName()) + 5;
    size_t vlen = this->value == nullptr ? 0 : StringJsonBufferSize(this->value);
    if (this->isPassword) vlen *= 2; // Encoded password needs twice the size.
    len += vlen;
    
    return ConfigItem::json_size_t(len, 0);
}


void ConfigString::WriteItemTypeToJSON(json_io* json, dword base, char*& convBuf)
{
    dword obj = json->add_object(base, "item");
    json->add_string(obj, "name", this->GetName());
    json->add_string(obj, "type", CFG_TYPE_STRING);
    if (this->isPassword) {
        json->add_bool(obj, "password", true);
        json->add_bool(obj, "passwordSet", this->value != nullptr && *this->value != '\0');
    }
}


bool ConfigString::HasDefaultValue() const
{
    return strcmp(this->value, this->defValue) == 0;
}


void ConfigString::Reset()
{
    free(this->value);
    this->value = _strdup(this->defValue);
}

void ConfigString::SetValue(const char* value)
{
    const char* tmpVal = value == nullptr ? "" : value;
    if (this->value == nullptr || strcmp(tmpVal, this->value) != 0) {
        this->changed = true;
    }
    free(this->value);
    this->value = _strdup(value == nullptr ? "" : value);
}


void ConfigString::SetWriteValue(const char* value)
{
    free(this->value);
    this->value = _strdup(value == nullptr ? "" : value);
}
