#include <DatSubsystems/DatPatcher.h>
#include <SubfileData.h>
#include <DatFile.h>
#include <yaml-cpp/yaml.h>

namespace LOTRO_DAT {

    DatPatcher::DatPatcher(DatFile *datFilePtr) : dat(datFilePtr) {
    }

    /*!
     * \Author Gi1dor
     * \date 07.07.2018
     * Обновление файла. Записывает данные файла в dat контейнер, используя информацию в SubfileData. Если ранее файл не существовал - он не будет создан (вернёт ошибку).
     * \warning В процессе применения локаль будет автоматически переключена в положение PATCHED
     * \param[in] data Новые данные файла
     * \param[in] single_file Флаг, который означает, что применяемый патч состоит из одного файла.
     * Если true, то функция будет управлять состоянием DatStatus и обновлять его.
     * Иначе эта обязанность делегируется вызывающей функции
     */

    DatOperationResult<> DatPatcher::PatchFile(const SubfileData &data, bool single_file) {
        if (!data.options["did"]) {
            // Subfile data does not contain any Dat ID number, so skipping it
            return DatOperationResult<>();
        }

        auto dat_id = data.options["did"].as<long long>();
        if (dat_id != dat->GetDatID()) {
            // Subfile Dat ID does not match host DAT ID, so we shouldn't patch it
            return DatOperationResult<>();
        }

        auto file_id = data.options["fid"].as<long long>();

        if (single_file) {
            dat->GetStatusModule().SetStatus(DatStatus::E_PATCHING);
            dat->GetStatusModule().SetFinishedParts(0);
            dat->GetStatusModule().SetTotalParts(1);
        }

        dat->GetStatusModule().SetDebugMessage("Patching file with id " + std::to_string(file_id));

        auto getfile_operation = dat->GetFileSystem().GetFile(file_id);
        if (getfile_operation.result == ERROR) {
            if (single_file) {
                dat->GetStatusModule().SetDefaultStatus();
            }

            return DatOperationResult<>(ERROR,
                                        "PATCHSUBFILEDATA: Unable to find file with id " + std::to_string(file_id));
        }

        auto &file = getfile_operation.value;

        // If file has inactive category, then we should set it to patched state in order to commit patch and
        // then in ApplyFilePatch() function, if new category is still inactive, return dictionary to its original state;

        if (dat->GetLocaleManager().CategoryIsInactive(file->category) != 0) {
            auto operation = dat->GetLocaleManager().GetLocaleFile(file->file_id(), DatLocaleManager::PATCHED);
            if (operation.result == SUCCESS)
                dat->GetFileSystem().UpdateFileInfo(operation.value);
        }

        if (data.options["cat"].IsDefined())
            file->category = data.options["cat"].as<long long>();
        else
            file->category = 1;

        auto getdata_operation = dat->GetFileSystem().GetFileData(*file, 0);

        if (getdata_operation.result == ERROR) {
            if (single_file) {
                dat->GetStatusModule().SetDefaultStatus();
            }

            return DatOperationResult<>(ERROR,
                                        "PATCHSUBFILEDATA: can't get file data for id = " + std::to_string(file_id));
        }

        auto &file_data = getdata_operation.value;

        BinaryData patch_data = file->MakeForImport(file_data, data);

        auto result = ApplyFilePatch(file, patch_data);

        if (single_file) {
            dat->GetStatusModule().SetDefaultStatus();
        }

        if (result.result == ERROR) {
            return DatOperationResult<>(ERROR,
                                        "PATCHSUBFILEDATA: applyfilepatch failed for id = " + std::to_string(file_id));
        }

        return DatOperationResult<>(SUCCESS);
    }


    /*!
     * \Author Gi1dor
     * \date 07.07.2018
     * Обновление всех файлов в Database.
     * \warning В процессе применения локаль будет автоматически переключена в положение PATCHED
     * \param[in] db Указатель на базу данных. База должна быть проинициализирована
     */

    DatOperationResult<int> DatPatcher::PatchAllDatabase(Database *db) {
        if (!db)
            return DatOperationResult<int>(0, ERROR, "PATCHALLDATABASE: db is nullptr");

        dat->GetStatusModule().SetStatus(DatStatus::E_PATCHING);
        dat->GetStatusModule().SetFinishedParts(0);
        dat->GetStatusModule().SetDebugMessage("Patching database...");

        SubfileData data;
        data = db->GetNextFile();

        int successfully_patched = 0;
        unsigned db_rows = db->CountRows();
        dat->GetStatusModule().SetTotalParts(db_rows);

        for (unsigned i = 0; i < db_rows; ++i) {
            auto operation = PatchFile(data, false);
            if (operation.result == SUCCESS)
                successfully_patched++;
            
            dat->GetStatusModule().SetFinishedParts(i);

            data = db->GetNextFile();
        }

        LOG(INFO) << "Database import success: patched " + std::to_string(successfully_patched) + " out of " +
                                                       std::to_string(db_rows) + " files";

        dat->GetStatusModule().SetDefaultStatus();
        return DatOperationResult<int>(successfully_patched, SUCCESS);
    }


    /*!
     * \Author Gi1dor
     * \date 07.07.2018
     * Функция, вызываемая функцией PatchFile, отвечающая за корректную запись файла в dat контейнер и обновление
     * информации о нём в файловой системе
     * \param[in] file Указатель на объект файла в файловой системе (полученный через DatFileSystem::GetFileInfo)
     * \param[in] data Бинарные данные собранного нового файла, готовые к записи в dat
     */

    DatOperationResult<> DatPatcher::ApplyFilePatch(std::shared_ptr<SubFile> file, BinaryData &data) {
        long long file_id = file->file_id();

        if (dat->GetLocaleManager().GetCurrentLocale() != DatLocaleManager::PATCHED) {
            LOG(INFO) << "Changing locale to PATCHED(RU) in order to patch file";
            dat->GetLocaleManager().SetLocale(DatLocaleManager::PATCHED);
        }

        if (dat->GetLocaleManager().CategoryIsInactive(file->category) &&
            dat->GetLocaleManager().GetLocaleFile(file_id, DatLocaleManager::PATCHED).result == SUCCESS)
        {
            dat->GetFileSystem().UpdateFileInfo(
                    dat->GetLocaleManager().GetLocaleFile(file_id, DatLocaleManager::PATCHED).value);
        }

        //LOG(INFO) << "Patching file with id = " << file_id;

        if (dat->GetLocaleManager().GetLocaleFile(file_id, DatLocaleManager::ORIGINAL).result == ERROR)
            dat->GetLocaleManager().UpdateLocaleFile(DatLocaleManager::ORIGINAL, SubFile(*file));


        SubFile new_file = SubFile(*file);

        if (dat->GetLocaleManager().GetLocaleFile(file_id, DatLocaleManager::PATCHED).result == ERROR
            || data.size() > file->block_size()) {

            new_file.file_offset_ = dat->GetFileSystem().patched_file_end;
            new_file.block_size_ = std::max(data.size(), 256u);
            dat->GetFileSystem().patched_file_end += new_file.block_size_ + 8;
        }
        new_file.file_size_ = data.size() - 8;


        data.Append(BinaryData::FromNumber<4>(0), 0); // set additional fragments count to zero

        if (file_id != data.ToNumber<4>(8))
            LOG(WARNING) << "Created data's file_id " << file_id << "doesn't match to original: "
                         << data.ToNumber<4>(8);

        auto operation = dat->GetIO().WriteData(data, data.size(), new_file.file_offset());
        if (operation.result == ERROR)
            return DatOperationResult<>(ERROR, "APPLYPATCHFILE: Unable to write data for file with id " +
                                               std::to_string(file_id));

        dat->GetFileSystem().UpdateFileInfo(new_file);
        dat->GetLocaleManager().UpdateLocaleFile(DatLocaleManager::PATCHED, new_file);


        // If file category is inactive, then return file header data in dictionary to original state
        dat->GetLocaleManager().UpdateCategory(file_id, new_file.category);

        if (dat->GetLocaleManager().CategoryIsInactive(new_file.category))
            dat->GetFileSystem().UpdateFileInfo(
                    dat->GetLocaleManager().GetLocaleFile(file_id, DatLocaleManager::ORIGINAL).value);

        //LOG(INFO) << "Successfully patched file with id = " << file_id;

        return DatOperationResult<>(SUCCESS);
    }
};