#include "BinaryData.h"
#include "DatFile.h"
#include "EasyLogging++/easylogging++.h"
#include <DatOperationResult.h>

#include <algorithm>
#include <locale>
#include <DatSubsystems/DatIO.h>
#include <zconf.h>


#ifdef WIN32
#define fseek _fseeki64
#define ftell _ftelli64
#endif

unsigned int RSHash(const std::string& str) {
    unsigned int b    = 378551;
    unsigned int a    = 63689;
    unsigned int hash = 0;

    for (char i : str) {
        hash = hash * a + i;
        a    = a * b;
    }

    return hash;
}

extern "C++"
{
namespace LOTRO_DAT {

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

    DatIO::DatIO(DatFile *datFile) : dat(datFile), file_handler_(nullptr), filename_("none"), actual_dat_size_(0) {
    }


    DatIO::~DatIO() {
        DeInit();
    }

    //------------------------------------------------//
    // INIT SECTION
    //------------------------------------------------//

    /*!
     * \author Gi1dor
     * \date 30.06.2018
     * Инициализация модуля. Открывает файл на чтение и блокирует его для доступа из других программ.
     * Читает заголовочную информацию, необходимую для инициализации остальных модулей. Не должна вызываться где-либо,
     * кроме как из управляющего объекта DatFile.
     * \param[in] filename имя файла для открытия (включая путь)
     * \return Возвращает result = ERROR в случае, если не удалось открыть файл или файл некорректный
     */


    DatOperationResult<> DatIO::Init(const std::string &filename) {
        LOG(INFO) << "Initializing IO: " << filename;

        filename_ = filename;

        auto result = OpenDatFile();
        if (result.result == ERROR) {
            LOG(ERROR) << "Error in OpenDatFile: " << result.msg;
            DeInit();
            return result;
        }

        result = ReadSuperBlock();
        if (result.result == ERROR) {
            LOG(ERROR) << "Error in ReadSuperBlock: " << result.msg;
            DeInit();
            return result;
        }

        LOG(INFO) << "Successfull initializing IO: " << filename;
        return DatOperationResult<>(SUCCESS, "DatIO initialized successfully");
    }

    /*!
     * \author Gi1dor
     * \date 30.06.2018
     * Открытие файла, имя которого записано в переменной file_handler_
     * \return Возвращает result = ERROR в случае, если не удалось открыть файл
     */

    DatOperationResult<> DatIO::OpenDatFile() {
        LOG(DEBUG) << "DatIO: Started opening DatFile";

        file_handler_ = fopen(filename_.c_str(), "r+b");

        if (file_handler_ == nullptr) {
            LOG(ERROR) << "DatIO: Unable to open file " << filename_ << ". Presumably - no file found...";
            return DatOperationResult<>(ERROR, std::string("Unable to locate and open file " + filename_));
        }

        fseek(file_handler_, 0, SEEK_END);
        actual_dat_size_ = ftell(file_handler_);
        fseek(file_handler_, 0, SEEK_SET);

        LOG(INFO) << "DatIO: file opened";
        return DatOperationResult<>(SUCCESS, std::string("Successfully opened file " + filename_));
    }

    /*!
     * \author Gi1dor
     * \date 30.06.2018
     * Инициализация основных переменных файла, располагающихся в начале.
     *
     * Известные переменные (каждая по 4 байта):
     *
     * Офсет - описание переменной
     * =========
     * 0x100 - константа, одинаковая для всех .dat файлов
     * 0x140 - ещё одна константа, одинаковая для всех .dat файлов
     * 0x14C - Первая переменная версии .dat файла
     * 0x148 - размер .dat файла. Указывает на начало свободного места. Необязательно должен являться физическим концом
     * 0x150 - Вторая переменная версии dat файла
     * 0x154 - Положение журнала фрагментации (который представляет собой список свободных блоков в dat-файле, куда можно писать новые данные
     * 0x158 - Позиция окончания журнала фрагментации (?)
     * 0x15C - Кол-во записей в журнале фрагментации (?)
     * 0x160 - Офсет корневой папки
     * 0x19C - Свободное место в dat файле (?)
     *
     * \return Возвращает result = ERROR в случае, если константы не совпадают с требуемыми значениями
     */


    DatOperationResult<> DatIO::ReadSuperBlock() {
        LOG(INFO) << "DatIO: Started reading superblock";

        BinaryData data(1024);
        ReadData(data, 1024);

        constant1 = data.ToNumber<4>(0x100);
        constant2 = data.ToNumber<4>(0x140);
        version1 = data.ToNumber<4>(0x14C);
        file_size = data.ToNumber<4>(0x148);
        version2 = data.ToNumber<4>(0x150);
        fragmentation_journal_offset = data.ToNumber<4>(0x154);
        fragmentation_journal_end = data.ToNumber<4>(0x158);
        fragmentation_journal_size = data.ToNumber<4>(0x15C);
        root_directory_offset = data.ToNumber<4>(0x160);
        free_dat_size = data.ToNumber<4>(0x19C);

        if (constant1 != 0x4C5000)
            return DatOperationResult<>(ERROR, "Variable at position 0x100 is not equal to .dat file constant!");

        if (constant2 != 0x5442)
            return DatOperationResult<>(ERROR, "Variable at position 0x140 is not equal to .dat file constant!");

        LOG(INFO) << "DatIO: Superblock read successfully";
        return DatOperationResult<>(SUCCESS, "Superblock read successfully.");
    }


    //------------------------------------------------//
    // PUBLIC READ/WRITE SECTION
    //------------------------------------------------//


    /*!
     * \author Gi1dor
     * \date 30.06.2018
     * Чтение бинарных данных из файла. Считает size байт dat файла, начиная с положения offset, и запишет их в
     * объект data, причём положение первого байта считанных данных будет равно значению data_offset
     *
     * \param[in, out] data Объект класса BinaryData, в который будут считаны данные
     * \param[in] size Количество байт, которые будут считаны
     * \param[in] offset Положение начала данных в dat файле, откуда считать
     * \param[in] data_offset Положение записи. Считанные данные будут записаны с отступом data_offset в data
     *
     * \return Возвращает result = ERROR в случае, если считать не удалось (неверные входные данные)
     */

    DatOperationResult<>
    DatIO::ReadData(BinaryData &data, long long size, long long offset, long long data_offset) const {
        if (file_handler_ == nullptr)
            return DatOperationResult<>(ERROR, "IOREADDATA: file handler is null pointer on reading data.");

        if (data_offset + size > data.size())
            return DatOperationResult<>(ERROR, "IOREADDATA: Parameters: offset, size are out of range.");

        if (offset + size > actual_dat_size_)
            return DatOperationResult<>(ERROR, "IOREADDATA: Reading more than DatFile size elapsed.");

        if (offset != ftell(file_handler_))
            fseek(file_handler_, offset, SEEK_SET);

        fread(data.data() + data_offset, unsigned(size), 1, file_handler_);
        return DatOperationResult<>(SUCCESS, "Read data successful.");
    }


    /*!
     * \author Gi1dor
     * \date 10.07.2018
     *
     * Проверка буфера перед записью новых size_to_write байт в конец файла. Если буфер мал - то дозаписывается
     * новый чистый блок размера MAX_EOF_BUFFER (константное поле класса DatIO);
     *
     * \param[in] size_to_write Размер данных в байтах, которые будут сейчас записаны в конец файла
     *
     */

    void DatIO::UpdateBufferIfNeeded(long long size_to_write) {
        BinaryData empty_buffer(std::max(MAX_EOF_BUFFER, unsigned(size_to_write)));
        fseek(file_handler_, 0, SEEK_END);
        fwrite(empty_buffer.data(), empty_buffer.size(), 1, file_handler_);
        actual_dat_size_ += empty_buffer.size();
    }


    /*!
     * \author Gi1dor
     * \date 30.06.2018
     * Запись бинарных данных в dat файл. Запишет size байт данных из data, начиная с позиции data_offset,
     * в позицию offset .dat файла.
     *
     * \param[in] data Объект класса BinaryData, откуда будут взяты данные для записи
     * \param[in] size Количество байт для записи
     * \param[in] offset Положение начала данных в dat файле, куда записывать
     * \param[in] data_offset Положение начала данных в data, от которого будет взято size байт и записано в dat файл
     *
     * \return Возвращает result = ERROR в случае, если записать не удалось (неверные входные данные)
     */

    DatOperationResult<>
    DatIO::WriteData(const BinaryData &data, long long size, long long offset, long long data_offset) {
        if (file_handler_ == nullptr)
            return DatOperationResult<>(ERROR, "IOWRITEDATA: file handler is null pointer on writing data.");

        if (data_offset + size > data.size())
            return DatOperationResult<>(ERROR, "IOWRITEDATA: writing more than BinaryData size.");

        if (offset + size > actual_dat_size_)
            UpdateBufferIfNeeded(offset + size - actual_dat_size_);

        if (offset != ftell(file_handler_))
            fseek(file_handler_, offset, SEEK_SET);

        fwrite(data.data() + data_offset, unsigned(size), 1, file_handler_);

        return DatOperationResult<>(SUCCESS, "Data writing successful.");
    }


    //------------------------------------------------//
    // DEINIT SECTION
    //------------------------------------------------//

    /*!
     * \author Gi1dor
     * \date 30.06.2018
     * Деинициализация модуля. Не должна вызываться где-либо, кроме как из управляющего объекта DatFile.
     */

    DatOperationResult<> DatIO::DeInit() {
        if (!dat->Initialized()) {
            ClearData();
            return DatOperationResult<>(SUCCESS);
        }

        if (file_handler_ != nullptr)
            fclose(file_handler_);

        truncate64(filename_.c_str(), actual_dat_size_);

        filename_ = "none";
        file_handler_ = nullptr;

        constant1 = 0;
        constant2 = 0;
        file_size = 0;
        version1 = 0;
        version2 = 0;
        fragmentation_journal_size = 0;
        fragmentation_journal_end = 0;
        root_directory_offset = 0;
        fragmentation_journal_offset = 0;
        return DatOperationResult<>(SUCCESS, "File deinitialisation successfull");
    }

    DatOperationResult<> DatIO::ModifyFragmentationJournal() {
        if (fragmentation_journal_size == 0)
            return DatOperationResult<>(SUCCESS,
                                      "DatIO: Fragmentation journal is empty. Nothing to do.");
        LOG(DEBUG) << "Modifying fragmentation journal";
        BinaryData data(4);
        ReadData(data, 4, fragmentation_journal_offset + 8 * fragmentation_journal_size);
        LOG(INFO) << "FREE_SIZE BLOCK = " << data.ToNumber<4>(0);

        long long free_size = data.ToNumber<4>(0);
        long long free_offset = file_size;

        BinaryData nulldata = BinaryData(unsigned(free_size));
        WriteData(nulldata, nulldata.size(), file_size);
        file_size += nulldata.size();

        WriteData(BinaryData::FromNumber<4>(free_size), 4,
                  fragmentation_journal_offset + 8 * fragmentation_journal_size);
        WriteData(BinaryData::FromNumber<4>(free_offset), 4,
                  fragmentation_journal_offset + 8 * fragmentation_journal_size + 4);

        //nulldata = BinaryData(8);
        //WriteData(nulldata, nulldata.size(), fragmentation_journal_offset + 16);
        LOG(DEBUG) << "Finished modifying fragmentation journal";
        return DatOperationResult<>(SUCCESS, "Fragmentation journal patched successfully!");
    }

    DatOperationResult<> DatIO::UpdateHeader() {
        LOG(DEBUG) << "Updating header";
        WriteData(BinaryData::FromNumber<4>(constant1), 4, 0x100);
        WriteData(BinaryData::FromNumber<4>(constant2), 4, 0x140);
        //WriteData(BinaryData::FromNumber<4>(    0     ), 4, 0x144);
        WriteData(BinaryData::FromNumber<4>(file_size), 4, 0x148);
        WriteData(BinaryData::FromNumber<4>(version1), 4, 0x14C);
        WriteData(BinaryData::FromNumber<4>(version2), 4, 0x150);
        WriteData(BinaryData::FromNumber<4>(fragmentation_journal_offset), 4, 0x154);
        WriteData(BinaryData::FromNumber<4>(fragmentation_journal_end), 4, 0x158);
        WriteData(BinaryData::FromNumber<4>(fragmentation_journal_size), 4, 0x15C);
        WriteData(BinaryData::FromNumber<4>(root_directory_offset), 4, 0x160);
        WriteData(BinaryData::FromNumber<4>(free_dat_size), 4, 0x19C);
        LOG(DEBUG) << "Finished updating header";
        return DatOperationResult<>(SUCCESS, "File header patched successfully");;
    }


    /*!
     * \author Gi1dor
     * \date 30.06.2018
     * Возвращает размер dat файла в байтах (может отличаться от значения внутренней переменной file_size dat файла
     */

    DatOperationResult<long long> DatIO::GetActualDatSize() {
        return DatOperationResult<long long>(actual_dat_size_, SUCCESS);
    }


    /*!
     * \author Gi1dor
     * \date 30.06.2018
     * Возвращает имя dat файла, которое было передано при инициализации
     */

    DatOperationResult<std::string> DatIO::GetFilename() {
        if (filename_ == "none")
            return DatOperationResult<std::string>("", ERROR, "DATIOGETFILENAME: not initialised");
        return DatOperationResult<std::string>(filename_, SUCCESS);
    }

    /*!
     * \author Gi1dor
     * \date 30.06.2018
     * Выводит информацию о состоянии модуля в файл
     * \param[in] file указатель на объект файла, в конец которого будет напечатана информация
     * \warning Файл file должен быть открыт для записи. После завершения работы функции файл остаётся открытым
     */

    void DatIO::PrintInformaion(FILE *file) {
        if (!file)
            return;

        fprintf(file, "========== IO info ==========\n");
        fprintf(file, "Filename = %s\n", filename_.c_str());
        fprintf(file, "File header data:\n");
        fprintf(file, "constant1 = %lld\n", constant1);
        fprintf(file, "constant2 = %lld\n", constant2);
        fprintf(file, "file_size = %lld\n", file_size);
        fprintf(file, "version1 = %lld\n", version1);
        fprintf(file, "version2 = %lld\n", version2);
        fprintf(file, "fragment_journal_offset = %lld\n", fragmentation_journal_offset);
        fprintf(file, "fragment_journal_end = %lld\n", fragmentation_journal_end);
        fprintf(file, "fragment_journal_size = %lld\n", fragmentation_journal_size);
        fprintf(file, "root_dir_offset = %lld\n", root_directory_offset);
        fprintf(file, "free_dat_size = %lld\n", free_dat_size);
        fprintf(file, "DatLibrary: EOF buffer size constant = %d\n", MAX_EOF_BUFFER);
    }

    unsigned int DatIO::GetHeaderHash() {
        std::string values =
                std::to_string(constant1) +
                std::to_string(constant2) +
                std::to_string(file_size) +
                std::to_string(version1) +
                std::to_string(version2) +
                std::to_string(fragmentation_journal_offset) +
                std::to_string(fragmentation_journal_end) +
                std::to_string(fragmentation_journal_size) +
                std::to_string(root_directory_offset) +
                std::to_string(free_dat_size);

        return RSHash(values);
    }

    void DatIO::ClearData() {
        constant1 = -1;
        constant2 = -1;
        file_size = -1;
        version1 = -1;
        version2 = -1;
        fragmentation_journal_size = -1;
        fragmentation_journal_end = -1;
        root_directory_offset = -1;
        fragmentation_journal_offset = -1;
        free_dat_size = -1;
        actual_dat_size_ = -1;
    }

}
}