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

#include "DatFile.h"

#include "BinaryData.h"
#include "Common/DatException.h"
#include "SubDirectory.h"
#include "Subfile.h"
#include "yaml-cpp/yaml.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() {
        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);
        }
        BinaryData file_data = GetFileData(dictionary_[file_id], 8);
        long long export_size = 0;
        std::vector<BinaryData> binary_data;
        std::vector<std::u16string> text_data;
        std::vector<YAML::Node> options;
        dictionary_[file_id]->PrepareForExport(file_data, export_size, binary_data, text_data, options);

        for (int i = 0; i < export_size; ++i) {
            binary_data[i].WriteToFile(path + "_" + std::to_string(i) + options[i]["extension"].as<std::string>());
        }
    }

    /// 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);
        }

        BinaryData file_data = GetFileData(dictionary_[file_id], 8);
        long long export_size = 0;
        std::vector<BinaryData> binary_data;
        std::vector<std::u16string> text_data;
        std::vector<YAML::Node> options;
        dictionary_[file_id]->PrepareForExport(file_data, export_size, binary_data, text_data, options);
        // TODO: Complete this function
    }

    /// 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->FileType();
            if (ext == type) {
                success += ExtractFile(i.second->file_id(), (path + std::to_string(i.second->file_id())));
            }
        }
        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->FileType();
            if (ext == type) {
                success += ExtractFile(i.second->file_id(), db);
            }
        }
        return success;
    }


    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);
        }

        fseek(file_handler_, 0, SEEK_END);
        file_size_ = ftell(file_handler_);
        fseek(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_) {
            fprintf(f, "%lld %lld %lld %lld %s\n", i.second->file_id(), i.second->file_offset(), i.second->file_size(),
                    i.second->block_size(), i.second->Extension());
        }
        fclose(f);
    }

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

    BinaryData DatFile::GetFileData(const Subfile *file, long long int offset = 0) {
        BinaryData mfile_id(4);
        ReadData(mfile_id, 4, file->file_offset() + 8);
        if (file->file_id() != mfile_id.ToNumber<4>(0))
            throw DatException("Bad DatFile::GetFileData() - file_id in Subfile doesn't match to file_id in DatFile.", READ_EXCEPTION);

        BinaryData data((unsigned)(file->file_size()));
        if (file->block_size() >= file->file_size() + 8) {
            ReadData(data, file->file_size(), file->file_offset() + offset);
            return data;
        }

        BinaryData fragments_count(4);
        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;

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

        BinaryData FragmentsDictionary(8 * unsigned(fragments_number));
        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);
            ReadData(data, std::min(fragment_size, file->file_size() - current_block_size), fragment_offset, current_block_size );
            current_block_size += fragment_size;
        }

        return data;
    }
}
}