/*---------------------------------------------------------------------------*/
/* tasks_postgresql.cpp                                                      */
/* copyright (c) innovaphone 2016                                            */
/*                                                                           */
/*---------------------------------------------------------------------------*/

#include <platform.h>
#include "sdk/common/interface/database.h"
#include "sdk/common/interface/task.h"
#include "../lib/tasks_postgresql.h"

/*---------------------------------------------------------------------------*/
/* PostgreSQLTableColumn                                                     */
/*---------------------------------------------------------------------------*/

PostgreSQLTableColumn::PostgreSQLTableColumn(const char * name, const char * type)
{
    this->name = _strdup(name);
    this->type = _strdup(type);
    this->exists = false;
}

PostgreSQLTableColumn::~PostgreSQLTableColumn()
{
    if (name) free(name);
    if (type) free(type);
}

/*---------------------------------------------------------------------------*/
/* PostgreSQLTableConstraint                                                 */
/*---------------------------------------------------------------------------*/

PostgreSQLTableConstraint::PostgreSQLTableConstraint(const char * name, const char * value)
{
    this->name = _strdup(name);
    this->value = _strdup(value);
    this->exists = false;
}

PostgreSQLTableConstraint::~PostgreSQLTableConstraint()
{
    if (name) free(name);
    if (value) free(value);
}

/*---------------------------------------------------------------------------*/
/* PostgreSQLTableConstraint                                                 */
/*---------------------------------------------------------------------------*/

PostgreSQLTableIndex::PostgreSQLTableIndex(const char * name, const char * value, bool unique, const char * usng)
{
    this->name = _strdup(name);
    this->value = _strdup(value);
    this->unique = unique;
    this->exists = false;
    this->usng = _strdup(usng);
}

PostgreSQLTableIndex::~PostgreSQLTableIndex()
{
    if (name) free(name);
    if (value) free(value);
    if (usng) free(usng);
}

/*---------------------------------------------------------------------------*/
/* TaskPostgreSQLInitTable                                                   */
/*---------------------------------------------------------------------------*/

TaskPostgreSQLInitTable::TaskPostgreSQLInitTable(class IDatabase * database, const char * tableName)
{
    this->user = NULL;
    this->constraintCount = 0;
    this->indexCount = 0;
    this->database = database;
    this->tableName = _strdup(tableName);
    this->state = Idle;
}

TaskPostgreSQLInitTable::~TaskPostgreSQLInitTable()
{
    if (tableName) free(tableName);
    while (!columns.empty()) {
        delete columns.front();
        columns.pop_front();
    }
    while (!constraints.empty()) {
        delete constraints.front();
        constraints.pop_front();
    }
    while (!indexes.empty()) {
        delete indexes.front();
        indexes.pop_front();
    }
}

void TaskPostgreSQLInitTable::AddColumn(const char * name, const char * type)
{
    columns.push_back(new class PostgreSQLTableColumn(name, type));
}

void TaskPostgreSQLInitTable::AddConstraint(const char * name, const char * value)
{
    constraints.push_back(new class PostgreSQLTableConstraint(name, value));
}

void TaskPostgreSQLInitTable::AddIndex(const char * name, const char * value, bool unique, const char * usng)
{
    indexes.push_back(new class PostgreSQLTableIndex(name, value, unique, usng));
}

void TaskPostgreSQLInitTable::Start(class UTask * user)
{
    this->user = user;

    if (tableName && database && database->Connected()) {
        state = CreateTable;
        database->ExecSQL(this, DB_EXEC_FETCH_ALL, "CREATE TABLE IF NOT EXISTS %S ();", tableName);
    }
    else {
        state = Finished;
        Failed();
    }
}

void TaskPostgreSQLInitTable::GetConstraintsQuery()
{
    state = GetConstraints;
    database->ExecSQL(this, DB_EXEC_FETCH_ALL, "SELECT constraint_name FROM information_schema.table_constraints WHERE table_name=%s", tableName);
}

void TaskPostgreSQLInitTable::GetIndexesQuery()
{
    state = GetIndexes;
    database->ExecSQL(this, DB_EXEC_FETCH_ALL, "SELECT indexrelname FROM pg_stat_all_indexes WHERE schemaname = 'public'");
}

void TaskPostgreSQLInitTable::DatabaseExecSQLResult(IDatabase * const database, class IDataSet * dataset)
{
    switch (state)
    {
    case TaskPostgreSQLInitTable::CreateTable:
        delete dataset;
        state = GetColumns;
        database->ExecSQL(this, DB_EXEC_FETCH_ALL, "SELECT column_name FROM information_schema.columns WHERE table_name=%s;", tableName);
        break;
    case TaskPostgreSQLInitTable::GetColumns:
    {
        while (!dataset->Eot()) {
            const char * value = dataset->GetStringValueWithNull(0);
            for (unsigned i = 0; i < columns.size(); i++) {
                if (value && !strcmp(value, columns[i]->name)) columns[i]->exists = true;
            }
            dataset->Next();
        }
        delete dataset;
        bool updateNeeded = false;
        char statement[8196];
        int l = _sprintf(statement, "ALTER TABLE \"%s\"", tableName);
        for (unsigned i = 0; i < columns.size(); i++) {
            if (!columns[i]->exists) {
                l += _sprintf(statement + l, "%s ADD COLUMN \"%s\" %s", updateNeeded ? "," : "", columns[i]->name, columns[i]->type);
                updateNeeded = true;
            }
        }
        if (updateNeeded) {
            state = AddMissingColumns;
            database->ExecSQL(this, DB_EXEC_FETCH_ALL, statement);
        }
        else if (constraints.size() > 0) {
            GetConstraintsQuery();
        }
        else if (indexes.size() > 0) {
            GetIndexesQuery();
        }
        else {
            state = Finished;
            Complete();
        }
    }   break;
    case TaskPostgreSQLInitTable::AddMissingColumns:
        delete dataset;
        if (constraints.size() > 0) {
            GetConstraintsQuery();
        }
        else if (indexes.size() > 0) {
            GetIndexesQuery();
        }
        else {
            state = Finished;
            Complete();
        }
        break;
    case TaskPostgreSQLInitTable::GetConstraints:
    {
        while (!dataset->Eot()) {
            const char * value = dataset->GetStringValueWithNull(0);
            for (unsigned i = 0; i < constraints.size(); i++) {
                if (value && !strcmp(value, constraints[i]->name)) constraints[i]->exists = true;
            }
            dataset->Next();
        }
        delete dataset;

        char statement[8196];
        for (unsigned i = 0; i < constraints.size(); i++) {
            if (!constraints[i]->exists) {
                _sprintf(statement, "ALTER TABLE \"%s\" ADD CONSTRAINT \"%s\" %s", tableName, constraints[i]->name, constraints[i]->value);
                constraintCount++;
                database->ExecSQL(this, DB_EXEC_FETCH_ALL, statement);
            }
        }
        // No constraints to be added
        if (constraintCount > 0) {
            state = AddMissingConstraints;
        }
        // There may be indexes
        else if (indexes.size() > 0) {
            GetIndexesQuery();
        }
        else {
            state = Finished;
            Complete();
        }
    }   break;
    case TaskPostgreSQLInitTable::AddMissingConstraints:
        delete dataset;
        if (--constraintCount == 0) {
            if (indexes.size() > 0) {
                GetIndexesQuery();
            }
            else {
                state = Finished;
                Complete();
            }
        }
        break;
    case TaskPostgreSQLInitTable::GetIndexes:
    {
        while (!dataset->Eot()) {
            const char * value = dataset->GetStringValueWithNull(0);
            for (unsigned i = 0; i < indexes.size(); i++) {
                if (value && !strcmp(value, indexes[i]->name)) indexes[i]->exists = true;
            }
            dataset->Next();
        }
        delete dataset;

        char statement[8196];
        for (unsigned i = 0; i < indexes.size(); i++) {
            if (!indexes[i]->exists) {
                if (indexes[i]->unique) {
                    _sprintf(statement, "CREATE UNIQUE INDEX \"%s\" ON \"%s\" USING %s (%s)", indexes[i]->name, tableName, indexes[i]->usng ? indexes[i]->usng : "btree", indexes[i]->value);
                }
                else {
                    _sprintf(statement, "CREATE INDEX \"%s\" ON \"%s\" USING %s (%s)", indexes[i]->name, tableName, indexes[i]->usng ? indexes[i]->usng : "btree", indexes[i]->value);
                }
                indexCount++;
                database->ExecSQL(this, DB_EXEC_FETCH_ALL, statement);
            }
        }
        if (indexCount > 0) {
            state = AddMissingIndexes;
        }
        else {
            state = Finished;
            Complete();
        }
    }   break;
    case TaskPostgreSQLInitTable::AddMissingIndexes:
        delete dataset;
        if (--indexCount == 0) {
            state = Finished;
            Complete();
        }
        break;
    default:
        ASSERT(false, "TaskDatabaseInitTable::DatabaseExecSQLResult");
        break;
    }
}

void TaskPostgreSQLInitTable::DatabaseError(IDatabase * const database, db_error_t error)
{
    state = Finished;
    Failed();
}


/*---------------------------------------------------------------------------*/
/* TaskPostgreSQLInitEnum                                                    */
/*---------------------------------------------------------------------------*/

TaskPostgreSQLInitEnum::TaskPostgreSQLInitEnum(IDatabase * const database, const char * enumName)
{
    this->enumName = _strdup(enumName);
    this->sqlCmdCnt = 0;
    this->database = database;
    this->state = Idle;
}


TaskPostgreSQLInitEnum::~TaskPostgreSQLInitEnum()
{
    free(this->enumName);
    for (auto enumValue : enumValues) {
        free(enumValue);
    }
}


void TaskPostgreSQLInitEnum::CreateEnum()
{
    char buffer[8192];
    char * p = buffer;
    int restLen = sizeof(buffer);
    int tmp;
    bool start = true;
    buffer[0] = '\0';

    for (auto enumValue : enumValues) {
        if (start) {
            char query[] = "CREATE TYPE %s AS ENUM ('%s'";
            tmp = (int)strlen(this->enumName) + (int)strlen(enumValue) + sizeof(query) - 5;
            ASSERT(restLen > tmp, "TaskPostgreSQLInitEnum::CreateEnum(): Length of SQL statement is more than 8k");
            _sprintf(p, query, this->enumName, enumValue);
            start = false;
        }
        else {
            char query[] = ",'%s'";
            tmp = (int)strlen(enumValue) + sizeof(query) - 3;
            ASSERT(restLen > tmp, "TaskPostgreSQLInitEnum::CreateEnum(): Length of SQL statement is more than 8k");
            _sprintf(p, query, enumValue);
        }
        restLen -= tmp;
        p += tmp;
    }

    if (buffer[0] == '\0') {
        this->state = Finished;
        Complete();
    }
    else {
        ASSERT(restLen > 1, "TaskPostgreSQLInitEnum::CreateEnum(): Length of SQL statement is more than 8k");
        *p++ = ')';
        *p = '\0';

        this->state = CreateEnumType;
        database->ExecSQL(this, 0, buffer);
        this->sqlCmdCnt++;
    }
}

void TaskPostgreSQLInitEnum::UpdateEnum(IDataSet * dataset)
{
    std::deque<char *> curValues;
    while (!dataset->Eot()) {
        curValues.push_back(_strdup(dataset->GetStringValue(0)));
        dataset->Next();
    }

    for (auto enumValue : enumValues) {
        bool found = false;
        for (auto curValue : curValues) {
            if (strcmp(enumValue, curValue) == 0) {
                found = true;
                break;
            }
        }

        if (!found) {
            database->ExecSQL(this, 0, "ALTER TYPE %n ADD VALUE %s", this->enumName, enumValue);
            this->sqlCmdCnt++;
        }
    }

    for (auto curValue : curValues) {
        free(curValue);
    }

    if (this->state == GetEnumValues) {
        this->state = Finished;
        Complete();
    }
    else
        this->state = AddEnumItem;
}


void TaskPostgreSQLInitEnum::AddValue(const char * name)
{
    enumValues.push_back(_strdup(name));
}


void TaskPostgreSQLInitEnum::Start(class UTask * user)
{
    this->user = user;
    if (enumValues.empty()) {
        this->state = Finished;
        Complete();
    }
    else {
        this->state = GetEnum;
        this->database->ExecSQL(this, DB_EXEC_FETCH_ALL, "SELECT typname FROM pg_type WHERE typname=%s", this->enumName);
    }
}


void TaskPostgreSQLInitEnum::DatabaseError(IDatabase * const database, db_error_t error)
{
    this->state = Finished;
    Failed();
}


void TaskPostgreSQLInitEnum::DatabaseExecSQLResult(IDatabase * const database, class IDataSet * dataset)
{
    switch (this->state)
    {
    case GetEnum:
        if (dataset->Eot())
            CreateEnum();
        else {
            this->state = GetEnumValues;
            this->database->ExecSQL(this, DB_EXEC_FETCH_ALL, "SELECT unnest(enum_range(NULL::%n))", this->enumName);
        }
        break;

    case GetEnumValues:
        UpdateEnum(dataset);
        break;

    case CreateEnumType:
    case AddEnumItem:
        this->sqlCmdCnt--;
        if (this->sqlCmdCnt == 0) {
            this->state = Finished;
            Complete();
        }
        break;

    default:
        break;
    }

    delete dataset;
}


/*---------------------------------------------------------------------------*/
/* TaskPostgreSQLInitSequence                                                */
/*---------------------------------------------------------------------------*/

TaskPostgreSQLInitSequence::TaskPostgreSQLInitSequence(IDatabase * const database, const char * sequenceName)
{
    this->seqName = _strdup(sequenceName);
    this->database = database;
}


TaskPostgreSQLInitSequence::~TaskPostgreSQLInitSequence()
{
    free(this->seqName);
}


void TaskPostgreSQLInitSequence::Start(class UTask * user)
{
    this->user = user;
    this->database->ExecSQL(this, DB_EXEC_FETCH_ALL, "CREATE SEQUENCE IF NOT EXISTS %n", this->seqName);
}


void TaskPostgreSQLInitSequence::DatabaseError(IDatabase * const database, db_error_t error)
{
    this->error = true;
    this->Failed();
}


void TaskPostgreSQLInitSequence::DatabaseExecSQLResult(IDatabase * const database, class IDataSet * dataset)
{
    delete dataset;
    this->Complete();
}


/*---------------------------------------------------------------------------*/
/* TaskPostgreSQLInitSequence                                                */
/*---------------------------------------------------------------------------*/

TaskPostgreSQLInitTrigger::TaskPostgreSQLInitTrigger(IDatabase * const database, const char * triggerName)
{
    this->database = database;
    this->triggerName = _strdup(triggerName);
    this->tableName = nullptr;
    this->statement = nullptr;
    this->update = false;
    this->triggerExist = false;
    this->state = CheckingTrigger;
}


TaskPostgreSQLInitTrigger::~TaskPostgreSQLInitTrigger()
{
    free(this->triggerName);
    free(this->tableName);
    free(this->statement);
}


void TaskPostgreSQLInitTrigger::DefineTrigger(const char * condition, const char * table, const char * fncName, bool updateIfExists)
{
    this->update = updateIfExists;
    this->tableName = _strdup(table);
    size_t s = 59 + strlen(condition) + strlen(fncName);
    this->statement = static_cast<char *>(malloc(s));

    sprintf(this->statement, "CREATE TRIGGER %%n %s ON %%n FOR EACH ROW EXECUTE PROCEDURE %s()", condition, fncName);
}


void TaskPostgreSQLInitTrigger::Start(class UTask * user)
{
    this->user = user;
    this->CheckTrigger();
}


void TaskPostgreSQLInitTrigger::DatabaseError(IDatabase * const database, db_error_t error)
{
    this->error = true;
    this->Failed();
}


void TaskPostgreSQLInitTrigger::DatabaseExecSQLResult(IDatabase * const database, class IDataSet * dataset)
{
    switch (this->state)
    {
    case CheckingTrigger:
        this->triggerExist = !dataset->Eot();
        if (this->triggerExist) {
            if (!this->update) {
                this->Complete();
            }
            else this->DeleteTrigger();
        }
        else this->CreateTrigger();
        break;

    case DeletingTrigger:
        this->CreateTrigger();
        break;

    case CreatingTrigger:
        this->Complete();
        break;
    }

    delete dataset;
}


void TaskPostgreSQLInitTrigger::CheckTrigger()
{
    this->state = CheckingTrigger;
    this->database->ExecSQL(this, DB_EXEC_FETCH_ALL, "SELECT tgname FROM pg_trigger WHERE tgname=%s", this->triggerName);
}


void TaskPostgreSQLInitTrigger::DeleteTrigger()
{
    this->state = DeletingTrigger;
    this->database->ExecSQL(this, DB_EXEC_FETCH_ALL, "DROP TRIGGER %n ON %n", this->triggerName, this->tableName);
}


void TaskPostgreSQLInitTrigger::CreateTrigger()
{
    this->state = CreatingTrigger;
    this->database->ExecSQL(this, DB_EXEC_FETCH_ALL, this->statement, this->triggerName, this->tableName);
}


/*---------------------------------------------------------------------------*/
/* TaskPostgreSQLInitSequence                                                */
/*---------------------------------------------------------------------------*/

TaskPostgreSQLInitFunction::TaskPostgreSQLInitFunction(IDatabase * const database, const char * fncName)
{
    this->database = database;
    this->fncName = _strdup(fncName);
    this->fncBody = nullptr;
}


TaskPostgreSQLInitFunction::~TaskPostgreSQLInitFunction()
{
    free(this->fncName);
    free(this->fncBody);
}


void TaskPostgreSQLInitFunction::DatabaseError(IDatabase * const database, db_error_t error)
{
    this->error = true;
    this->Failed();
}


void TaskPostgreSQLInitFunction::DatabaseExecSQLResult(IDatabase * const database, class IDataSet * dataset)
{
    delete dataset;
    this->Complete();
}


void TaskPostgreSQLInitFunction::TriggerFunction(const char * fncBody)
{
    this->fncBody = static_cast<char *>(malloc(79 + strlen(fncName) + strlen(fncBody)));
    sprintf(this->fncBody, "CREATE OR REPLACE FUNCTION %s()\nRETURNS TRIGGER AS $$\n%s;\n$$ language 'plpgsql'", this->fncName, fncBody);
}


void TaskPostgreSQLInitFunction::Start(class UTask * user)
{
    this->user = user;
    this->database->ExecSQL(this, DB_EXEC_FETCH_ALL, this->fncBody);
}
