#include <DatSubsystems/DatFileSystem.h>
#include <EasyLogging++/easylogging++.h>
#include <BinaryData.h>
#include <DatFile.h>
#include <SubFile.h>
#include <SubDirectory.h>

#include <functional>

namespace LOTRO_DAT {


    /*!
     * \author Gi1dor
     * \date 30.06.2018
     * Конструктор объекта модуля. Вызывается только из класса DatFile. Создаёт указатель но объект DatFile, но
     * не инициализирует модуль. Инизиализация происходит в функции Init
     * \param[in] datFilePtr Указатель на объект управляющего класса DatFile
     */

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

    /*!
     * \author Gi1dor
     * \date 29.06.2018
     * Возвращает бинарные данные файла. Помимо самого файла, в бинарном содержимом находится также метаинформация о нём.
     * Изменение параметра offset позволяет игнорировать метаинформацию и извлечь только непосредственно содержимое файла
     * \param[in] file_id Идентификатор получаемого файла
     * \param[in] offset Отступ от начала файла. Данные до отступа не извлекаются.
     * \return Возвращает result = ERROR и value = BinaryData, если файл не найден или информаци в словаре некорректная
     */

    DatOperationResult<BinaryData> DatFileSystem::GetFileData(const SubFile& file, long long int offset) {
        if (!dat)
            return DatOperationResult<BinaryData>(BinaryData(), ERROR,
                                                  "DatFileSystem error: no connection with Dat (dat is nullptr)");

        BinaryData mfile_id(20);
        auto operation = CheckCorrectSubfile(file);
        if (operation.result == ERROR || !operation.value)
            return DatOperationResult<BinaryData>(BinaryData(), ERROR,
                                                  "DATFSGETFILEDATA: Incorrect file" + std::to_string(file.file_id()));


        BinaryData data((unsigned) (file.file_size() + (8 - offset)));
        if (file.block_size() >= file.file_size() + 8) {
            dat->GetIO().ReadData(data, file.file_size() + (8 - offset), file.file_offset() + offset);
            return DatOperationResult<BinaryData>(data, SUCCESS);
        }

        BinaryData fragments_count(4);
        dat->GetIO().ReadData(fragments_count, 4, file.file_offset());

        long long fragments_number = fragments_count.ToNumber<4>(0);

        long long current_block_size = file.block_size() - offset - 8 * fragments_number;

        dat->GetIO().ReadData(data, current_block_size, file.file_offset() + offset);

        BinaryData FragmentsDictionary(8 * unsigned(fragments_number));
        dat->GetIO().ReadData(FragmentsDictionary, 8 * unsigned(fragments_number),
                              file.file_offset() + file.block_size() - 8 * fragments_number);


        for (long long i = 0; i < fragments_number; i++) {
            long long fragment_size = FragmentsDictionary.ToNumber<4>(8 * i);
            long long fragment_offset = FragmentsDictionary.ToNumber<4>(8 * i + 4);
            dat->GetIO().ReadData(data, std::min(fragment_size, file.file_size() - current_block_size),
                                  fragment_offset,
                                  current_block_size);
            current_block_size += fragment_size;
        }

        return DatOperationResult<BinaryData>(data, SUCCESS);
    }


    /*!
     * \author Gi1dor
     * \date 29.06.2018
     * Возвращает указатель на объект файла в словаре. Через этот указатель можно патчить/извлекать файл
     * \param[in] file_id идентификатор получаемого файла
     * \return Возвращает result = ERROR и value = nullptr, если файл не найден
     */

    DatOperationResult<std::shared_ptr<SubFile>> DatFileSystem::GetFile(long long file_id) {
        if (!dat)
            return DatOperationResult<std::shared_ptr<SubFile>>(nullptr, ERROR,
                                                                "DATFSGETFILE: no connection with Dat (dat is nullptr)");

        if (dictionary_.count(file_id) == 0) {
            auto operation = InitSubFile(file_id);
            if (operation.result == ERROR)
                return DatOperationResult<std::shared_ptr<SubFile>>(nullptr, ERROR);
        }

        return DatOperationResult<std::shared_ptr<SubFile>>(dictionary_[file_id], SUCCESS);
    }


    /*!
     * \author Gi1dor
     * \date 29.06.2018
     * Обновляет данные файла
     * \param[in] preinit_file неинициализированный объект файла, данные которого обновляются в словаре
     * \warning Новые данные не будут записаны в dat файле, пока не будет вызвана функция CommitDirectories()
     */

    DatOperationResult<> DatFileSystem::UpdateFileInfo(const SubFile &preinit_file) {
        auto operation = GetFile(preinit_file.file_id());

        if (operation.result == ERROR)
            return DatOperationResult<>(ERROR, "DATFS: Unable to update file info - no file in dict with id = " +
                                               std::to_string(preinit_file.file_id()));
        auto& file = operation.value;
        file->unknown1_ = preinit_file.unknown1();
        file->file_id_ = preinit_file.file_id();
        file->file_offset_ = preinit_file.file_offset();
        file->file_size_ = preinit_file.file_size();
        file->timestamp_ = preinit_file.timestamp();
        file->version_ = preinit_file.version();
        file->block_size_ = preinit_file.block_size();
        file->unknown2_ = preinit_file.unknown2();
        subfile_pending_update.insert(preinit_file.file_id());

        return DatOperationResult<>();
    }


    /*!
     * \author Gi1dor
     * \date 29.06.2018
     * Проверяет корректность файла, полученного из словаря
     *
     * \return Объект DatOperationResult<bool> для которого value = true, если файл корректный, и false - иначе.
     */

    DatOperationResult<bool> DatFileSystem::CheckCorrectSubfile(const SubFile& file) {
        if (!dat)
            return DatOperationResult<bool>(false, ERROR,
                                            "DATFSCORRECTSUBFILE: no connection with Dat (dat is nullptr)");
        if (file.file_size() < 16)
            return DatOperationResult<bool>(false, SUCCESS);

        BinaryData mfile_id(20);
        auto operation = dat->GetIO().ReadData(mfile_id, 20, file.file_offset() + 8);
        if (operation.result == ERROR)
            return DatOperationResult<bool>(false, ERROR, "DATFSCORRECTSUBFILE: cannot read file header data");

        return DatOperationResult<bool>((mfile_id.CheckCompression() || file.file_id() == mfile_id.ToNumber<4>(0)) &&
                                        file.file_size() < 50ll * 1024ll * 1024ll, SUCCESS);
    }


    /*!
     * \author Gi1dor
     * \date 29.06.2018
     * Записывает все изменения структуры файловой системы в dat-файл. 
     */

    DatOperationResult<> DatFileSystem::CommitDirectories() {
        if (!dat)
            return DatOperationResult<>(ERROR, "DATFSCOMMITDIRS: no connection with Dat (dat is nullptr)");

        for (auto file_id : subfile_pending_update) {
            if (dictionary_.count(file_id) == 0)
                continue;

//            auto operation = CheckCorrectSubfile(dictionary_[file_id]);
//            if (operation.result == ERROR) {
//                LOG(ERROR) << "Check subfile correctness failed";
//                continue;
//            }

//            if (!operation.value) {
//                LOG(ERROR) << "Incorrect SubFile " << file_id << " data: doesn't match";
//                continue;
//            }

            auto operation1 = dat->GetIO().WriteData(dictionary_[file_id]->MakeHeaderData(), 32,
                                                     dictionary_[file_id]->dictionary_offset());
            if (operation1.result == ERROR)
                LOG(ERROR) << "Unable to write data to dictionary for file " << file_id;
        }
        subfile_pending_update.clear();
        return DatOperationResult<>(SUCCESS);
    }


    /*!
     * \author Gi1dor
     * \date 29.06.2018
     * Инициализация модуля. Подготавливает файловую систему для работы
     */

    DatOperationResult<> DatFileSystem::Init() {
        if (!dat)
            return DatOperationResult<>(ERROR, "DATFSINIT: no connection with Dat (dat is nullptr)");

        DeInit();
        patched_file_end = dat->GetIO().file_size;
        SubDirectory root_directory(0, (unsigned) dat->GetIO().root_directory_offset);
        subdir_init_queue_.insert(root_directory);
        return DatOperationResult<>(SUCCESS);
    }


    /*!
     * \author Gi1dor
     * \date 29.06.2018
     * Завершает работу файловой системы и записывает изменения в dat файл
     * 
     * \return Вернёт DatOperationResult<>(ERROR), если случилась ошибка записи структуры файлов в dat файл
     */

    DatOperationResult<> DatFileSystem::DeInit() {
        if (!dat)
            return DatOperationResult<>(ERROR, "DATFSDEINIT: no connection with Dat (dat is nullptr)");

        auto operation = CommitDirectories();

        dictionary_.clear();

        visited_subdirectories_offsets_.clear();
        visited_subfiles_ids_.clear();

        subdir_init_queue_.clear();
        subfile_init_map_.clear();
        subfile_init_queue_.clear();

        subfile_pending_update.clear();

        if (operation.result == ERROR)
            return DatOperationResult<>(ERROR, "DEINITFS: unable to commit directories.");

        return DatOperationResult<>(SUCCESS);
    }


    /*!
     * \author Gi1dor
     * \date 29.06.2018
     * Записывает общую информацию о файловой системе. Требует полной инициализации всех папок и файлов.
     *
     * \param[in] file Объект файла, в конец которого будут записываться данные.
     */

    void DatFileSystem::PrintInformaion(FILE *file) {
        if (!dat) {
            LOG(ERROR) << "DATFSPRINTINFO: no connection with Dat (dat is nullptr)";
            return;
        }

        InitAllFiles();

        fprintf(file, "============= FS INFO SECTION =============\n");
        fprintf(file, "Files in dictionary number: %d\n", dictionary_.size());
        fprintf(file, "Files visited: %d\n", visited_subfiles_ids_.size());
        fprintf(file, "Folders visited: %d\n", visited_subdirectories_offsets_.size());
        fprintf(file, "File patched size: %lld\n", patched_file_end);

        std::map<FILE_TYPE, size_t> filetypes_count{{TEXT, 0},
                                                    {JPG, 0},
                                                    {DDS, 0},
                                                    {WAV, 0},
                                                    {OGG, 0},
                                                    {FONT, 0},
                                                    {UNKNOWN, 0}};

        for (const auto &datfile : dictionary_) {
            filetypes_count[datfile.second->FileType()]++;
        }

        fprintf(file, "TEXT files number = %d\n", filetypes_count[TEXT]);
        fprintf(file, "JPG files number = %d\n", filetypes_count[JPG]);
        fprintf(file, "DDS files number = %d\n", filetypes_count[DDS]);
        fprintf(file, "WAV files number = %d\n", filetypes_count[WAV]);
        fprintf(file, "OGG files number = %d\n", filetypes_count[OGG]);
        fprintf(file, "FONT files number = %d\n", filetypes_count[FONT]);
        fprintf(file, "UNKNOWN files number = %d\n", filetypes_count[UNKNOWN]);
    }

    /*!
     * \author Gi1dor
     * \date 29.06.2018
     * Инициализация файла с заданным id. Ищет информацию о файле, инициализирует его в соответствии с типом файла и
     * помещает в словарь. Работает быстрее, если все папки уже были проинициализированы. В противном случае - пытается
     * инициализировать каждую из папок, пока не найдёт в ней файла с требуем id.
     *
     * \param[in] file_id Id файла, который нужно проинициализировать.
     * \return Объект DatOperationResult. Если файл не удалось проинициализировать - поле result будет равно ERROR
     */

    DatOperationResult<> DatFileSystem::InitSubFile(long long file_id) {
        if (!dat)
            return DatOperationResult<>(ERROR, "DATFSINITSUBFILE: no connection with Dat (dat is nullptr)");

        while (subfile_init_map_.count(file_id) == 0) {
            if (subdir_init_queue_.empty())
                return DatOperationResult<>(ERROR,
                                            "DATFSINITSUBFILE: Cannot init file with id = "
                                            + std::to_string(file_id) + " (NOT FOUND)");

            SubDirectory dir = *subdir_init_queue_.begin();
            subdir_init_queue_.erase(subdir_init_queue_.begin());
            InitSubDirectory(dir);
        }

        SubFile file = subfile_init_map_[file_id];
        subfile_init_queue_.erase(file);
        subfile_init_map_.erase(file_id);

        auto initialised_file = SubFile::MakeSubfile(*dat, file);

        if (!initialised_file)
            return DatOperationResult<>(ERROR, "DATFSINITSUBFILE: initialised subfile pointer is empty");

        if (!CheckCorrectSubfile(*initialised_file).value)
            return DatOperationResult<>(ERROR, "DATFSINITSUBFILE: initialised file " + std::to_string(file.file_id()) + " is incorrect");

        if (dictionary_.count(file_id) > 0) {
            LOG(WARNING) << "Dublicate files id = " << file_id << "dictionary offsets = "
                         << dictionary_[file_id]->dictionary_offset() << " and "
                         << initialised_file->dictionary_offset();
            if (CheckCorrectSubfile(*dictionary_[file_id]).value)
                LOG(ERROR) << "    FILE IN DICTIONARY IS ALREADY CORRECT!";

            dictionary_[file_id] = initialised_file;
        }

        if (dictionary_.count(file_id) == 0)
            dictionary_[file_id] = initialised_file;

        return DatOperationResult<>();
    }


    /*!
     * \author Gi1dor
     * \date 29.06.2018
     * Инициализация всех папок. Собирает информацию обо всех файлах в .dat файле для быстрого доступа к ним.
     * \warning Сами файлы не инициализируются. Инициализация файла выполнится при первом вызове GetFile() или GetFileData()
     *
     * \return Объект DatOperationResult
     */

    DatOperationResult<> DatFileSystem::InitAllDirectories() {
        if (!dat)
            return DatOperationResult<>(ERROR, "DATFSINITALLDIRS: no connection with Dat (dat is nullptr)");

        while (!subdir_init_queue_.empty()) {
            SubDirectory dir = *subdir_init_queue_.begin();
            subdir_init_queue_.erase(subdir_init_queue_.begin());
            InitSubDirectory(dir);
        }
        return DatOperationResult<>();
    }


    /*!
     * \author Gi1dor
     * \date 29.06.2018
     * Инициализация всех файлов. Полностью инициализирует все внутренние папки и файлы и помещает последние в словарь.
     *
     * \return Объект DatOperationResult
     */

    DatOperationResult<> DatFileSystem::InitAllFiles() {
        if (!dat)
            return DatOperationResult<>(ERROR, "DATFSINITALLFILES: no connection with Dat (dat is nullptr)");

        InitAllDirectories();

        while (!subfile_init_queue_.empty()) {
            SubFile file = *subfile_init_queue_.begin();
            InitSubFile(file.file_id());
        }
        return DatOperationResult<>();
    }


    /*!
     * \author Gi1dor
     * \date 29.06.2018
     * Инициализация папки. Читает внутреннюю структуру папки и получает информацию о файлах и подпапках.
     *
     * Структура папки:
     * 63 * 8 байт под описание подпапок. По 8 байт на папку.
     * Из них по 4 байта на 2 поля:
     * unknown - неизвестное поле
     * offset - положение подпапки в .dat файле
     *
     * 4 байта - количество файлов в папке. В каждой папке не более 64 Файлов.
     *
     * 32 * 64 байта под описание файлов. По 32 байта на каждый файл.
     *
     * Из них по 4 байта на каждое из 8 полей:
     * unknown1 - неизвестное поле
     * file_id - уникальный идентификатор файла
     * file_offset - положение содержимого файла
     * file_size - размер файла в байтах
     * timestamp - timestamp последнего редактирования
     * version - версия файла
     * block_size - размер блока данных, выделенного под файл
	 * unknown2 - неизвестное поле
     *
     * \param[in] dir объект папки SubDirectory
     * \return Объект DatOperationResult
     */

    DatOperationResult<> DatFileSystem::InitSubDirectory(SubDirectory dir) {
        if (!dat)
            return DatOperationResult<>(ERROR, "DATFSINITSUBDIR: no connection with Dat (dat is nullptr)");

        BinaryData subdir_data(63 * 8 + 4 + 32 * 64);
        auto operation = dat->GetIO().ReadData(subdir_data, 63 * 8 + 4 + 32 * 64, dir.offset());
        if (operation.result == ERROR)
            return DatOperationResult<>(ERROR, "DATFSINITSUBDIR: cannot init dir (read error) with offset = " +
                                               std::to_string((dir.offset())));

        if (subdir_data.ToNumber<4>(0) != 0 || subdir_data.ToNumber<4>(4) != 0)
            return DatOperationResult<>(ERROR, "DATFSINITSUBDIR: incorrect dir. Nulls must be at first 8 bytes of dir");

        // Subfiles section
        long long subfiles_number = subdir_data.ToNumber<4>(63 * 8);
        if (subfiles_number > 64)
            return DatOperationResult<>(ERROR, "DATINITSUBDIR: incorrect subdir: files number > 64");

        // Subdirs section
        for (unsigned i = 8; i < 63 * 8; i += 8) {
            if (subdir_data.ToNumber<4>(i) == 0 || subdir_data.ToNumber<4>(i + 4) == 0)
                break;

            long long unknown = subdir_data.ToNumber<4>(i);
            long long offset = subdir_data.ToNumber<4>(i + 4);

            if (unknown == 0 || offset == 0)
                break;

            if (visited_subdirectories_offsets_.count(offset) != 0)
                continue;

            visited_subdirectories_offsets_.insert(offset);
            subdir_init_queue_.insert(SubDirectory(unknown, offset));
        }

        // Subfiles section

        for (int i = 0; i < subfiles_number; i++) {
            long long header_start = 63 * 8 + 4 + 32 * i;

            SubFile file = SubFile(
                    *dat,
                    dir.offset() + header_start,
                    subdir_data.ToNumber<4>(header_start + 0),  // unknown1
                    subdir_data.ToNumber<4>(header_start + 4),  // file_id
                    subdir_data.ToNumber<4>(header_start + 8),  // file_offset
                    subdir_data.ToNumber<4>(header_start + 12), // file_size
                    subdir_data.ToNumber<4>(header_start + 16), // timestamp
                    subdir_data.ToNumber<4>(header_start + 20), // version
                    subdir_data.ToNumber<4>(header_start + 24), // block_size
                    subdir_data.ToNumber<4>(header_start + 28)  // unknown2
            );

            if (file.version() == 0 || file.unknown2() != 0)
                continue;

            if (visited_subfiles_ids_.count(file.file_id()) || file.file_id() == -1)
                break;

            visited_subfiles_ids_.insert(file.file_id());
            subfile_init_map_[file.file_id_] = file;
            subfile_init_queue_.insert(file);
        }
        return DatOperationResult<>();
    }


    /*!
     * \author Gi1dor
     * \date 03.07.2018
     * Выполнение функции function для всех файлов в dat контейнере.
     *
     * \param[in] function Функтор, принимающий в качестве аргумента ссылку на объект-указатель std::shared_ptr<SubFile>
     */

    DatOperationResult<> DatFileSystem::PerformOperationOnAllFiles(const std::function<void (std::shared_ptr<SubFile>&)>& function) {
        InitAllFiles();
        for (auto& file_pair: dictionary_)
            function(file_pair.second);
        return DatOperationResult<>();
    }

    int DatFileSystem::GetInitialisedFilesNumber() const {
        return dictionary_.size();
    }
}
