#include "avshook.h"

#include <map>
#include <optional>

#include "avs/core.h"
#include "avs/ea3.h"
#include "avs/game.h"
#include "util/detour.h"
#include "util/fileutils.h"
#include "util/logging.h"
#include "util/utils.h"

#ifdef min
#undef min
#endif

static bool FAKE_FILE_OPEN = false;

static std::map<std::string, std::string> ROM_FILE_MAP;
static std::string *ROM_FILE_CONTENTS = nullptr;

namespace hooks::avs::config {
    bool DISABLE_VFS_DRIVE_REDIRECTION = false;
    bool LOG = false;
};

using namespace hooks::avs;

#define WRAP_DEBUG_FMT(format, ...) \
    if (config::LOG) { \
        log_misc("avshook", "{}: " format " = 0x{:x}",  __FUNCTION__, __VA_ARGS__, static_cast<unsigned>(value)); \
    }
#define AVS_HOOK(f) hook_function(::avs::core::IMPORT_NAMES.f, #f, &::avs::core::f, f)

template<typename T>
static void hook_function(const char *source_name, const char *target_name, T **source, T *target) {
    if (!detour::trampoline_try(avs::core::DLL_NAME.c_str(), source_name, target, source)) {
        log_warning("avshook", "could not hook {} ({})", target_name, source_name);
    }
}

static inline bool is_fake_fd(avs::core::avs_file_t fd) {
    return FAKE_FILE_OPEN && fd == 1337;
}

static bool is_dest_file(const char *name) {
    static std::string path_dest = fmt::format("/dev/raw/{}.dest", avs::game::DEST[0]);
    static std::string path_bin = fmt::format("/dev/raw/{}.bin", avs::game::DEST[0]);

    return !_stricmp(name, path_dest.c_str()) || !_stricmp(name, path_bin.c_str());
}

static bool is_dest_file(const char *name, uint16_t mode) {
    return mode == 1 && is_dest_file(name);
}

static bool is_dest_spec_ea3_config(const char *name) {
    static std::string path = fmt::format("/prop/ea3-config-{}{}.xml", avs::game::DEST[0], avs::game::SPEC[0]);

    return !_stricmp(name, path.c_str());
}

static bool is_spam_file(const char *file) {
    static const char *spam_prefixes[] = {
        "/mnt/bm2d/ngp",
        "/afp",
        "/dev/nvram/pm_eco.xml",
        "/dev/nvram/pm_gamesys.xml",
        "/dev/nvram/pm_clock.xml",
    };

    for (auto &spam : spam_prefixes) {
        if (string_begins_with(file, spam)) {
            return true;
        }
    }

    return false;
}

static int avs_fs_fstat(avs::core::avs_file_t fd, struct avs::core::avs_stat *st) {
    if (is_fake_fd(fd) && ROM_FILE_CONTENTS) {
        if (st) {
            st->filesize = static_cast<uint32_t>(ROM_FILE_CONTENTS->length());
            st->padding.st_dev = 0;
        }
        return 1;
    }

    return avs::core::avs_fs_fstat(fd, st);
}

static int avs_fs_lstat(const char *name, struct avs::core::avs_stat *st) {
    if (name == nullptr) {
        return avs::core::avs_fs_lstat(name, st);
    }

    if (is_dest_file(name) || is_dest_spec_ea3_config(name)) {
        if (st) {
            st->filesize = 0;
            st->padding.st_dev = 0;
        }
        return 1;
    }

    auto value = avs::core::avs_fs_lstat(name, st);
    if (!is_spam_file(name)) {
        WRAP_DEBUG_FMT("name: {}", name);
    }

    return value;
}

static avs::core::avs_file_t avs_fs_open(const char *name, uint16_t mode, int flags) {
    if (name == nullptr) {
        return avs::core::avs_fs_open(name, mode, flags);
    }

    if (!FAKE_FILE_OPEN && (is_dest_file(name, mode) || ROM_FILE_MAP.contains(name))) {
        FAKE_FILE_OPEN = true;

        if (ROM_FILE_MAP.contains(name)) {
            ROM_FILE_CONTENTS = &ROM_FILE_MAP.at(name);
        }

        log_info("avshook", "opening fake file '{}'", name);

        return 1337;
    }

    auto value = avs::core::avs_fs_open(name, mode, flags);

    if (!is_spam_file(name)) {
        WRAP_DEBUG_FMT("name: {} mode: {} flags: {}", name, mode, flags);
    }

    return value;
}

static void avs_fs_close(avs::core::avs_file_t fd) {
    if (is_fake_fd(fd)) {
        FAKE_FILE_OPEN = false;
        ROM_FILE_CONTENTS = nullptr;

        log_info("hooks::avs", "closing fake fd");
    } else {
        avs::core::avs_fs_close(fd);
    }
}

static int avs_fs_copy(const char *sname, const char *dname) {
    if (sname == nullptr || dname == nullptr) {
        return avs::core::avs_fs_copy(sname, dname);
    }

    auto value = avs::core::avs_fs_copy(sname, dname);
    WRAP_DEBUG_FMT("sname: {} dname {}", sname, dname);

    return value;
}

static avs::core::avs_file_t avs_fs_opendir(const char *path) {
    if (path == nullptr) {
        return avs::core::avs_fs_opendir(path);
    }

    auto value = avs::core::avs_fs_opendir(path);
    WRAP_DEBUG_FMT("path: {}", path);

    return value;
}

static int avs_fs_mount(const char *mountpoint, const char *fsroot, const char *fstype, void *data) {
    // avs redirection breaks ongaku paradise
    if (mountpoint == nullptr ||
        fsroot == nullptr ||
        fstype == nullptr ||
        avs::game::is_model("JC9")) {
        return avs::core::avs_fs_mount(mountpoint, fsroot, fstype, data);
    }

    std::optional<std::string> new_fs_root = std::nullopt;

    if (_stricmp(mountpoint, "/mnt/ea3-config.xml") == 0 && is_dest_spec_ea3_config(fsroot)) {
        new_fs_root = fmt::format("/{}", avs::ea3::CFG_PATH);
    }

    // remap drive mounts to `dev/vfs/drive_x` where x is the drive letter
    if (!config::DISABLE_VFS_DRIVE_REDIRECTION &&
        (_strnicmp(fsroot, "d:", 2) == 0 ||
        _strnicmp(fsroot, "e:", 2) == 0 ||
        _strnicmp(fsroot, "f:", 2) == 0) &&
        _stricmp(fstype, "fs") == 0)
    {
        // sub path is everything after the drive and colon characters
        const char drive_letter[2] {
            static_cast<char>(std::tolower(static_cast<unsigned char>(fsroot[0]))),
            '\0',
        };
        const auto separator = fsroot[2] == '/' ? "" : "/";
        const auto sub_path = &fsroot[2];
        const std::filesystem::path mapped_path = fmt::format(
                "dev/vfs/drive_{}{}{}",
                drive_letter,
                separator,
                sub_path);

        // create the mapped directory path
        std::error_code err;
        std::filesystem::create_directories(mapped_path, err);

        if (err) {
            log_warning("hooks::avs", "failed to create '{}': {}", mapped_path.string(), err.message());
        } else {

            // if this is the `e:\`, then create the special directories
            if (drive_letter[0] == 'e' &&
                (sub_path[0] == '/' || sub_path[0] == '\\') &&
                sub_path[1] == '\0')
            {
                fileutils::dir_create_log("hooks::avs", mapped_path / "tmp");
                fileutils::dir_create_log("hooks::avs", mapped_path / "up");
            }

            log_misc("hooks::avs", "source directory '{}' remapped to '{}'",
                    fsroot,
                    mapped_path.string());
        }

        new_fs_root = mapped_path.string();
    }

    auto fs_root_data = new_fs_root.has_value() ? new_fs_root->c_str() : fsroot;
    auto value = avs::core::avs_fs_mount(mountpoint, fs_root_data, fstype, data);

    WRAP_DEBUG_FMT("mountpoint: {}, fsroot: {}, fstype: {}", mountpoint, fs_root_data, fstype);

    return value;
}

static size_t avs_fs_read(avs::core::avs_file_t fd, uint8_t *data, uint32_t data_size) {
    if (is_fake_fd(fd) && ROM_FILE_CONTENTS) {
        const auto size = std::min(static_cast<size_t>(data_size), ROM_FILE_CONTENTS->length());

        memcpy(data, ROM_FILE_CONTENTS->c_str(), size);

        return size;
    }

    return avs::core::avs_fs_read(fd, data, data_size);
}

static int property_file_write(avs::core::property_ptr prop, const char* path) {
    if (prop == nullptr || path == nullptr) {
        return avs::core::property_file_write(prop, path);
    }

    // resort anthem dumps eacoin to /dev/nvram/eacoin AND /eacoin.xml
    // it never reads /eacoin.xml, so it's probably a development leftover
    if (avs::game::is_model("JDZ") && _stricmp(path, "/eacoin.xml") == 0) {
        return 0;
    }

    auto value = avs::core::property_file_write(prop, path);

    WRAP_DEBUG_FMT("path: {}", path);

    return value;
}

void hooks::avs::init() {
    log_info("hooks::avs", "initializing");

    AVS_HOOK(avs_fs_fstat);
    AVS_HOOK(avs_fs_lstat);
    AVS_HOOK(avs_fs_open);
    AVS_HOOK(avs_fs_close);
    AVS_HOOK(avs_fs_copy);
    AVS_HOOK(avs_fs_opendir);
    AVS_HOOK(avs_fs_mount);
    AVS_HOOK(avs_fs_read);
    AVS_HOOK(property_file_write);
}

void hooks::avs::set_rom(const char *path, const char *contents) {
    ROM_FILE_MAP.emplace(path, contents);
}
