//
// Created by Иван_Архипов on 17.11.2017.
//
#define UNICODE
#define _UNICODE 

#include "Database.h"
#include "Common/DatException.h"
#include "BinaryData.h"
#include "Common/CommonFunctions.h"

#include <cstring>

namespace LOTRO_DAT {
    Database::Database() {
        db_ = nullptr;
    }

    Database::Database(const std::string &filename) {
        InitDatabase(filename.c_str());
    }

    Database::~Database() {
        if (db_ != nullptr) {
            ExecSql("COMMIT TRANSACTION");
            sqlite3_finalize(insert_request_);
            sqlite3_close(db_);
        }
    }

    void Database::InitDatabase(const std::string &filename) {
        if (sqlite3_open(filename.c_str(), &db_) != SQLITE_OK) {
            sqlite3_close(db_);
            throw DatException("Bad Database::InitDatabase() - sqlite3_open returned an error..."
                    , DATABASE_EXCEPTION);
        }

		ExecSql("PRAGMA synchronous = OFF");
		ExecSql("PRAGMA count_changes = OFF");
		ExecSql("PRAGMA journal_mode = MEMORY");
		ExecSql("PRAGMA temp_store = MEMORY");
		ExecSql("PRAGMA encoding = \"UTF-8\";");

        ExecSql(CreateTableCommand_);

		sqlite3_prepare_v2(db_, InsertFileCommand_.c_str(), InsertFileCommand_.length(), &insert_request_, nullptr);
		sqlite3_prepare_v2(db_, FetchOneCommand.c_str(), FetchOneCommand.length(), &fetch_one_request_, nullptr);


        ExecSql("BEGIN TRANSACTION");
    }

	void Database::ExecSql(const std::string &sql) {
        if (db_ == nullptr)
            throw DatException("Bad Database::ExecSql() - database hasn't been opened!", DATABASE_EXCEPTION);

		char *error;
		if (sqlite3_exec(db_, sql.c_str(), nullptr, nullptr, &error) != SQLITE_OK) {
			fprintf(stderr, "SQLite3 error: %s\n", sqlite3_errmsg(db_));
			throw DatException((std::string("Bad Database::ExecSql() - unable to perform request")
				+ std::string(sql)).c_str(), DATABASE_EXCEPTION);
		}
	}

    void Database::PushFile(const BinaryData &binary_data, const std::u16string &text_data, const std::string &options) {
        if (db_ == nullptr)
            throw DatException("Bad Database::PushFile() - database hasn't been opened!", DATABASE_EXCEPTION);

        sqlite3_bind_blob(insert_request_, 1, binary_data.data(), binary_data.size(), SQLITE_TRANSIENT);
        sqlite3_bind_text16(insert_request_, 2, text_data.c_str(), -1, SQLITE_TRANSIENT);
        sqlite3_bind_text(insert_request_, 3, options.c_str(), -1, SQLITE_TRANSIENT);

        if (sqlite3_step(insert_request_) != SQLITE_DONE) {
            fprintf(stderr, "SQLite3 error: %s\n", sqlite3_errmsg(db_));
            throw DatException((std::string("Bad Database::PushTextFile() - unable to perform push operation")).c_str(),
                               DATABASE_EXCEPTION);
        }

        sqlite3_reset(insert_request_);
    }

    bool Database::GetNextFile(BinaryData &binary_data, std::u16string &text_data, YAML::Node &options) {
        if (db_ == nullptr)
            throw DatException("Bad Database::GetNexFile() - database hasn't been opened!", DATABASE_EXCEPTION);

        int result = sqlite3_step(fetch_one_request_);

        if (result == SQLITE_ROW) {
            binary_data = BinaryData((char *) sqlite3_column_blob(fetch_one_request_, 0),
                                     unsigned(sqlite3_column_bytes(fetch_one_request_, 0)));

            text_data = std::u16string((char16_t *)sqlite3_column_text16(fetch_one_request_, 1));

            std::string _options = std::string((char *)sqlite3_column_text(fetch_one_request_, 2),
                                               unsigned(sqlite3_column_bytes(fetch_one_request_, 2)));
            options = YAML::Load(_options);
            return true;
        }

        if (result == SQLITE_DONE) {
            return false;
        }

        fprintf(stderr, "SQLite3 fetch_one request returned %d code. SQLite message is: %s", result, sqlite3_errmsg(db_));
        throw DatException("Bad Database::GetNextFile() - sqlite3 - error", DATABASE_EXCEPTION);
    }

    void Database::RemoveDatabase() {
        if (db_ == nullptr)
            throw DatException("Bad Database::RemoveDatabase() - database hasn't been opened!", DATABASE_EXCEPTION);
        throw DatException("Database::RemoveDatabase() haven't been implemented yet...", DATABASE_EXCEPTION);
        // TODO: Implement function
    }

    void Database::ClearDatabase() {
        if (db_ == nullptr)
            throw DatException("Bad Database::ClearDatabase() - database hasn't been opened!", DATABASE_EXCEPTION);
        ExecSql(ClearTableCommand_);
    }
}