//
// Created by Иван_Архипов on 31.10.2017.
//

#include "DatFile.h"

#include "../BinaryData/BinaryData.h"
#include "../Common/DatException.h"
#include "../SubDirectory/SubDirectory.h"
#include "../Subfiles/Subfile.h"

#include <locale>

extern "C++"
{
namespace LOTRO_DAT {
    DatFile::DatFile() {
        dat_state_ = CLOSED;
    }

    DatFile::DatFile(const char *filename, int dat_id) {
        dat_id_ = dat_id;
        dat_state_ = CLOSED;
        OpenDatFile(filename);
        ReadSuperBlock();
        MakeDirectories();
        try {
            MakeDictionary();
        } catch (...) {
            fprintf(stderr, "Unable to make dictionary!! Unable to init DatFile!!!");
            return;
        }
        if (dat_state_ == SUCCESS_DICTIONARY)
            dat_state_ = READY;
        else
            throw DatException("Bad DatFile initialization! Not all init states were successfully passed!",
                               INIT_EXCEPTION);
    }

    DatFile::DatFile(const std::string &filename, int dat_id) {
        dat_id_ = dat_id;
        dat_state_ = CLOSED;
        OpenDatFile(filename.c_str());
        ReadSuperBlock();
        MakeDirectories();
        MakeDictionary();
        if (dat_state_ == SUCCESS_DICTIONARY)
            dat_state_ = READY;
        else
            throw DatException("Bad DatFile initialization! Not all init states were successfully passed!",
                               INIT_EXCEPTION);
    }

    DatFile::~DatFile() {
        if (file_handler_ != nullptr)
            fclose(file_handler_);
        delete file_handler_;
        delete root_directory_;
    }

    /// Extracts file with file_id.
    /// If path is undefined then it will be recognised as current working directory
    /// Output file path consists of "path + file_id + file_extension";
    /// NOTICE: The directory, mentioned in "std::string path" variable SHOULD BE ALREADY CREATED;
    /// Otherwise DatException() will be thrown.
    /// Returns true, if file was successfully extracted;
    /// Throws DatException() if undefined behaviour happened

    bool DatFile::ExtractFile(long long file_id, const std::string path) {
        if (dat_state_ != READY) {
            throw DatException("Bad DatFile::ExtractFile() - invalid DatFile state!", EXPORT_EXCEPTION);
        }
        return dictionary_[file_id]->ExportFile(path.c_str());
    }

    /// Extracts file with file_id to database "db".
    /// DATABASE SHOULD BE ALREADY CREATED; Otherwise DatException will be called.
    /// NOTICE: The directory, mentioned in "std::string path" variable SHOULD BE ALREADY CREATED;
    /// Otherwise DatException() will be thrown.
    /// Returns true, if file was successfully extracted;
    /// Throws DatException() if undefined behaviour happened

    bool DatFile::ExtractFile(long long file_id, Database *db) {
        if (dat_state_ != READY) {
            throw DatException("Bad DatFile::ExtractFile() - invalid DatFile state!", EXPORT_EXCEPTION);
        }
        return dictionary_[file_id]->ExportFile(db);
    }

    /// Extracts all files with specific type to "path + type + file_id + extension" files;
    /// If path is undefined then it will be recognised as current working directory
    /// NOTICE: The directory, mentioned in "std::string path" variable SHOULD BE ALREADY CREATED;
    /// Otherwise DatException() will be thrown.
    /// Returns number of successfully extracted files
    /// Throws DatException() if undefined behaviour happened

    int DatFile::ExtractAllFilesByType(FILE_TYPE type, std::string path) {
        if (dat_state_ != READY) {
            throw DatException("Bad DatFile::ExtractAllFilesByType() - invalid DatFile state!", EXPORT_EXCEPTION);
        }

        int success = 0;
        for (auto i : dictionary_) {
            FILE_TYPE ext = i.second->ext();
            if (ext == type) {
                success += i.second->ExportFile((path + std::to_string(i.second->file_id())).c_str());
            }
        }
        return success;
    }

    /// Extracts all files with specific type to database "db";
    /// DATABASE SHOULD BE ALREADY CREATED; Otherwise DatException will be called.
    /// Returns number of successfully extracted files
    /// Throws DatException() if undefined behaviour happened

    int DatFile::ExtractAllFilesByType(FILE_TYPE type, Database *db) {
        if (dat_state_ != READY) {
            throw DatException("Bad DatFile::ExtractAllFilesByType() - invalid DatFile state!", EXPORT_EXCEPTION);
        }

        int success = 0;
        for (auto i : dictionary_) {
            FILE_TYPE ext = i.second->ext();
            if (ext == type) {
                success += i.second->ExportFile(db);
            }
        }
        return success;
    }

    /// Patches .dat with text file with specific file_id, gossip_id and values, equal to values in text database.
    /// std::string text contains of text data, surrounded by '[' and ']', where <--DO_NOT_TOUCH!--> represents
    ///     position of variables in order, described in args_order.
    /// std::string args_order contains of numbers, divided by '-'. Ex. "1-2-3-4". There should be not less numbers, than
    ///     <--DO_NOT_TOUCH!--> pieces in text variable/
    /// std::string args contains of numbers, divided by ' '. These numbers are references to variables. There should
    ///     be not less references, than the biggest number in args_order
    /// Returns true if text was succesfully patched.
    /// Throws DatException() if undefined behaviour happened;

    bool DatFile::PatchTextFile(long long file_id, long long gossip_id, std::string text, std::string args_order,
                                std::string args) {

    }

    /// Patches .dat with text file with specific file_id and gossip_id.
    /// All text file data is got from database.
    /// Returns true if text file was successfully patched.
    /// Throws DatException() if undefined behaviour happened;

    bool DatFile::PatchTextFile(long long file_id, long long gossip_id, Database *db) {

    }

    /// Patches .dat with binary file with specific file_id.
    /// All file data is got from file in "file_path".
    /// Returns true if file was successfully patched.
    /// Throws DatException() if undefined behaviour happened;

    bool DatFile::PatchBinaryFile(long long file_id, std::string file_path) {

    }

    /// Patches .dat with binary file with specific file_id.
    /// All file data is got from Database "db".
    /// Returns true if file was successfully patched.
    /// Throws DatException() if undefined behaviour happened;

    bool DatFile::PatchBinaryFile(long long file_id, Database *db) {

    }

    void DatFile::OpenDatFile(const char *dat_name) {
        if (dat_state_ != CLOSED)
            throw DatException("Bad initialisation of DatFile - current DatFile isn't in correct state!",
                               INIT_EXCEPTION);

        fopen_s(&file_handler_, dat_name, "r+b");

        if (file_handler_ == nullptr) {
            std::string err = "Bad DatFile::OpenDatFile. Unable to open file ";
            err += dat_name;
            throw DatException(err.c_str(), INIT_EXCEPTION);
        }

        _fseeki64(file_handler_, 0, SEEK_END);
        file_size_ = _ftelli64(file_handler_);
        _fseeki64(file_handler_, 0, SEEK_SET);

        dat_state_ = SUCCESS_OPENED;
    }

    void DatFile::ReadSuperBlock() {
        if (dat_state_ != SUCCESS_OPENED)
            throw DatException("Bad DatFile::ReadSuperBlock() - DatFile isn't in valid state!", INIT_EXCEPTION);

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

        constant1_ = data.ToNumber<4>(0x100);
        constant2_ = data.ToNumber<4>(0x140);
        version1_ = data.ToNumber<4>(0x14C);
        version2_ = data.ToNumber<4>(0x150);
        root_directory_offset_ = data.ToNumber<4>(0x160);

        auto size1 = data.ToNumber<4>(0x148);

        if (constant1_ != 0x4C5000)
            throw DatException(
                    "Bad DatFile::ReadSuperBlock - variable at position 0x100 is not equal to .dat file constant!",
                    INIT_EXCEPTION);

        if (constant2_ != 0x5442)
            throw DatException(
                    "Bad DatFile::ReadSuperBlock - variable at position 0x140 is not equal to .dat file constant!",
                    INIT_EXCEPTION);

        if (file_size_ != size1)
            throw DatException(
                    "Bad DatFile::ReadSuperBlock - variable at 0x148 position is not equal to .dat file size!",
                    INIT_EXCEPTION);

        dat_state_ = SUCCESS_SUPERBLOCK;
    }

    void DatFile::MakeDirectories() {
        if (dat_state_ != SUCCESS_SUPERBLOCK)
            throw DatException("Bad DatFile::MakeDirectories() - DatFile isn't in valid state!", INIT_EXCEPTION);
        root_directory_ = new SubDirectory((unsigned) root_directory_offset_, this);
        dat_state_ = SUCCESS_DIRECTORIES;
    }

    void DatFile::MakeDictionary() {
        if (dat_state_ != SUCCESS_DIRECTORIES)
            throw DatException("Bad DatFile::MakeDictionary() - DatFile isn't in valid state!", INIT_EXCEPTION);
        try {
            root_directory_->MakeDictionary(dictionary_);
        } catch (...) {
            fprintf(stderr, "Bad DatFile::MakeDictionary() - File is corrupted?\n");
            return;
        }
        dat_state_ = SUCCESS_DICTIONARY;
    }

    void DatFile::ReadData(BinaryData &data, long long size, long long offset, long long data_offset) {
        if (dat_state_ == CLOSED)
            throw DatException("Bad DatFile::ReadData() - DatFile isn't in valid state!", READ_EXCEPTION);

        if (data_offset + size > data.size()) {
            std::string err = "Bad DatFile::ReadData - trying to read more than BinaryData size\n";
            err += std::string("Reading ") + std::to_string(size) + std::string(" bytes from ")
                   + std::to_string(offset) + std::string(" position in dat file.");
            throw DatException(err.c_str(), READ_EXCEPTION);
        }

        if (offset + size > file_size()) {
            std::string err = "Bad DatFile::ReadData - trying to read more than DatFile size elapsed\n";
            err += std::string("Reading ") + std::to_string(size) + std::string(" bytes from ")
                   + std::to_string(offset) + std::string(" position in dat file.");
            throw DatException(err.c_str(), READ_EXCEPTION);
        }

        _fseeki64(file_handler_, offset, SEEK_SET);
        fread(data.data() + data_offset, size, 1, file_handler_);
        data.CheckCompression();
    }

    void DatFile::WriteData(const BinaryData &data, long long size, long long offset, long long data_offset) {
        if (dat_state_ != READY)
            throw DatException("Bad DatFile::WriteData() - DatFile isn't in valid state!", WRITE_EXCEPTION);

        _fseeki64(file_handler_, offset, SEEK_SET);
        if (data_offset + size > data.size())
            throw DatException("Bad DatFile::WriteData - trying to write more than BinaryData size", WRITE_EXCEPTION);

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

    long long DatFile::constant1() const {
        return constant1_;
    }

    long long DatFile::constant2() const {
        return constant2_;
    }

    long long DatFile::file_size() const {
        return file_size_;
    }

    long long DatFile::version1() const {
        return version1_;
    }

    long long DatFile::version2() const {
        return version2_;
    }

    void DatFile::WriteUnorderedDictionary(std::string path) const {
        FILE *f;
        fopen_s(&f, (path + "dict.txt").c_str(), "w");
        fprintf(f, "file_id offset size size2 extension\n");
        for (auto i : dictionary_) {
            std::string extension = "unk";
            FILE_TYPE ext = i.second->ext();
            if (ext == TEXT) extension = "txt";
            if (ext == JPG) extension = "jpg";
            if (ext == DDS) extension = "dds";
            if (ext == WAV) extension = "wav";
            if (ext == OGG) extension = "ogg";
            if (ext == FONT) extension = "font";
            fprintf(f, "%lld %lld %lld %lld %s\n", i.second->file_id(), i.second->file_offset(), i.second->file_size(),
                    i.second->block_size(), extension.c_str());
        }
        fclose(f);
    }

    long long DatFile::files_number() const {
        return dictionary_.size();
    }
}
}