#include "eamuse.h"

#include <fstream>
#include <thread>

#include "avs/game.h"
#include "cfg/config.h"
#include "games/io.h"
#include "rawinput/rawinput.h"
#include "util/logging.h"
#include "util/time.h"
#include "util/utils.h"

#include "bt5api.h"

// state
static bool CARD_INSERT[2] = {false, false};
static double CARD_INSERT_TIME[2] = {0, 0};
static double CARD_INSERT_TIMEOUT = 2.0;
static char CARD_INSERT_UID[2][8];
static char CARD_INSERT_UID_ENABLE[2] = {false, false};
static int COIN_STOCK = 0;
static bool COIN_BLOCK = false;
static std::thread *COIN_INPUT_THREAD;
static bool COIN_INPUT_THREAD_ACTIVE = false;
static uint16_t KEYPAD_STATE[] = {0, 0};
static uint16_t KEYPAD_STATE_OVERRIDES[] = {0, 0};
static uint16_t KEYPAD_STATE_OVERRIDES_BT5[] = {0, 0};
static uint16_t KEYPAD_STATE_OVERRIDES_READER[] = {0, 0};
static uint16_t KEYPAD_STATE_OVERRIDES_OVERLAY[] = {0, 0};
static std::string EAMUSE_GAME_NAME;
static ConfigKeypadBindings KEYPAD_BINDINGS {};

bool eamuse_get_card(int active_count, int unit_id, uint8_t *card) {

    // get unit index
    int index = unit_id > 0 && active_count > 1 ? 1 : 0;

    // reader card input
    if (CARD_INSERT_UID_ENABLE[index]) {
        CARD_INSERT_UID_ENABLE[index] = false;
        memcpy(card, CARD_INSERT_UID[index], 8);
        log_info("eamuse", "Inserted card from reader {}: {}", index, bin2hex(card, 8));
        return true;
    }

    // get file path
    std::filesystem::path path;
    if (!KEYPAD_BINDINGS.card_paths[index].empty()) {
        path = KEYPAD_BINDINGS.card_paths[index];
    } else {
        path = index > 0 ? "card1.txt" : "card0.txt";
    }

    // call the next function
    return eamuse_get_card(path, card, index);
}

bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index) {

    // Check if card overrides are present
    if (!CARD_OVERRIDES[index].empty()) {

        // Override is present
        for (int n = 0; n < 16; n++) {
            char c = CARD_OVERRIDES[index].c_str()[n];
            bool digit = c >= '0' && c <= '9';
            bool character_big = c >= 'A' && c <= 'F';
            bool character_small = c >= 'a' && c <= 'f';
            if (!digit && !character_big && !character_small) {
                log_warning("eamuse",
                            "{} card override contains an invalid character sequence at byte {} (16 characters, 0-9/A-F only)",
                            CARD_OVERRIDES[index], n);

                return false;
            }
        }
        // Log info
        log_info("eamuse", "Inserted card override: {}", CARD_OVERRIDES[index]);

        // Card is valid, convert and set it.
        hex2bin(CARD_OVERRIDES[index].c_str(), card);

        // success
        return true;
    }
    // Overrides are not present. Use the standard file reading method.
    return eamuse_get_card(path, card);
}

bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card) {

    // open file
    std::ifstream f(path);
    if (!f) {
        log_warning("eamuse", "{} can not be opened!", path.string());
        return false;
    }

    // get size
    f.seekg(0, f.end);
    auto length = (size_t) f.tellg();
    f.seekg(0, f.beg);

    // check size
    if (length < 16) {
        log_warning("eamuse", "{} is too small (must be at least 16 characters)", path.string());
        return false;
    }

    // read file
    char buffer[17];
    f.read(buffer, 16);
    buffer[16] = 0;

    // verify card
    for (int n = 0; n < 16; n++) {
        char c = buffer[n];
        bool digit = c >= '0' && c <= '9';
        bool character_big = c >= 'A' && c <= 'F';
        bool character_small = c >= 'a' && c <= 'f';
        if (!digit && !character_big && !character_small) {
            log_warning("eamuse",
                "{} contains an invalid character sequence at byte {} (16 characters, 0-9/A-F only)",
                path.string(), n);

            return false;
        }
    }

    // info
    log_info("eamuse", "Inserted {}: {}", path.string(), buffer);

    // convert hex to bytes
    hex2bin(buffer, card);

    // success
    return true;
}

void eamuse_card_insert(int unit) {
    CARD_INSERT[unit] = true;
    CARD_INSERT_TIME[unit] = get_performance_seconds();
}

void eamuse_card_insert(int unit, const uint8_t *card) {
    memcpy(CARD_INSERT_UID[unit], card, 8);
    CARD_INSERT[unit] = true;
    CARD_INSERT_TIME[unit] = get_performance_seconds();
    CARD_INSERT_UID_ENABLE[unit] = true;
}

bool eamuse_card_insert_consume(int active_count, int unit_id) {

    // get unit index
    int index = unit_id > 0 && active_count > 1 ? 1 : 0;

    // bt5api
    if (BT5API_ENABLED) {
        bt5api_poll_reader_card((uint8_t) index);
    }

    // check for card insert
    auto keypad_buttons = games::get_buttons_keypads(eamuse_get_game());
    auto offset = unit_id * games::KeypadButtons::Size;
    if ((CARD_INSERT[index] && fabs(get_performance_seconds() - CARD_INSERT_TIME[index]) < CARD_INSERT_TIMEOUT)
    || GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::InsertCard + offset))) {
        log_info("eamuse", "Card insert on {}/{}", unit_id + 1, active_count);
        CARD_INSERT[index] = false;
        return true;
    }

    return false;
}

bool eamuse_coin_get_block() {
    return COIN_BLOCK;
}

void eamuse_coin_set_block(bool block) {
    COIN_BLOCK = block;
}

int eamuse_coin_get_stock() {
    return COIN_STOCK;
}

void eamuse_coin_set_stock(int amount) {
    COIN_STOCK = amount;
}

bool eamuse_coin_consume(int amount) {
    if (COIN_STOCK < amount) {
        return false;
    } else {
        COIN_STOCK -= amount;
        return true;
    }
}

int eamuse_coin_consume_stock() {
    int stock = COIN_STOCK;
    COIN_STOCK = 0;
    return stock;
}

int eamuse_coin_add() {
    return ++COIN_STOCK;
}

void eamuse_coin_start_thread() {

    // set active
    COIN_INPUT_THREAD_ACTIVE = true;

    // create thread
    COIN_INPUT_THREAD = new std::thread([]() {
        auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game());
        static bool COIN_INPUT_KEY_STATE = false;
        while (COIN_INPUT_THREAD_ACTIVE) {

            // check input key
            if (overlay_buttons && GameAPI::Buttons::getState(RI_MGR, overlay_buttons->at(
                    games::OverlayButtons::InsertCoin))) {
                if (!COIN_INPUT_KEY_STATE) {
                    if (COIN_BLOCK)
                        log_info("eamuse", "coin inserted while blocked");
                    else {
                        log_info("eamuse", "coin insert");
                        COIN_STOCK++;
                    }
                }
                COIN_INPUT_KEY_STATE = true;
            } else {
                COIN_INPUT_KEY_STATE = false;
            }

            // once every two frames
            Sleep(1000 / 30);
        }
    });
}

void eamuse_coin_stop_thread() {
    COIN_INPUT_THREAD_ACTIVE = false;
    COIN_INPUT_THREAD->join();
    delete COIN_INPUT_THREAD;
    COIN_INPUT_THREAD = nullptr;
}

void eamuse_set_keypad_overrides(size_t unit, uint16_t keypad_state) {

    // check unit
    if (unit >= std::size(KEYPAD_STATE_OVERRIDES)) {
        return;
    }

    // set state
    KEYPAD_STATE_OVERRIDES[unit] = keypad_state;
}

void eamuse_set_keypad_overrides_bt5(size_t unit, uint16_t keypad_state) {

    // check unit
    if (unit >= std::size(KEYPAD_STATE_OVERRIDES_BT5)) {
        return;
    }

    // set state
    KEYPAD_STATE_OVERRIDES_BT5[unit] = keypad_state;
}

void eamuse_set_keypad_overrides_reader(size_t unit, uint16_t keypad_state) {

    // check unit
    if (unit >= std::size(KEYPAD_STATE_OVERRIDES_READER)) {
        return;
    }

    // set state
    KEYPAD_STATE_OVERRIDES_READER[unit] = keypad_state;
}

void eamuse_set_keypad_overrides_overlay(size_t unit, uint16_t keypad_state) {

    // check unit
    if (unit >= std::size(KEYPAD_STATE_OVERRIDES_OVERLAY)) {
        return;
    }

    // set state
    KEYPAD_STATE_OVERRIDES_OVERLAY[unit] = keypad_state;
}

uint16_t eamuse_get_keypad_state(size_t unit) {

    // check unit
    if (unit >= std::size(KEYPAD_STATE)) {
        return 0;
    }

    // reset
    KEYPAD_STATE[unit] = KEYPAD_STATE_OVERRIDES[unit];
    KEYPAD_STATE[unit] |= KEYPAD_STATE_OVERRIDES_BT5[unit];
    KEYPAD_STATE[unit] |= KEYPAD_STATE_OVERRIDES_READER[unit];
    KEYPAD_STATE[unit] |= KEYPAD_STATE_OVERRIDES_OVERLAY[unit];

    // bt5api
    if (BT5API_ENABLED) {
        bt5api_poll_reader_keypad((uint8_t) unit);
    }

    // get keybinds and get offset for unit
    auto keypad_buttons = games::get_buttons_keypads(eamuse_get_game());
    auto offset = unit * games::KeypadButtons::Size;

    // parse
    if (GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::Keypad0 + offset))) {
        KEYPAD_STATE[unit] |= 1 << EAM_IO_KEYPAD_0;
    }
    if (GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::Keypad1 + offset))) {
        KEYPAD_STATE[unit] |= 1 << EAM_IO_KEYPAD_1;
    }
    if (GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::Keypad2 + offset))) {
        KEYPAD_STATE[unit] |= 1 << EAM_IO_KEYPAD_2;
    }
    if (GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::Keypad3 + offset))) {
        KEYPAD_STATE[unit] |= 1 << EAM_IO_KEYPAD_3;
    }
    if (GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::Keypad4 + offset))) {
        KEYPAD_STATE[unit] |= 1 << EAM_IO_KEYPAD_4;
    }
    if (GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::Keypad5 + offset))) {
        KEYPAD_STATE[unit] |= 1 << EAM_IO_KEYPAD_5;
    }
    if (GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::Keypad6 + offset))) {
        KEYPAD_STATE[unit] |= 1 << EAM_IO_KEYPAD_6;
    }
    if (GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::Keypad7 + offset))) {
        KEYPAD_STATE[unit] |= 1 << EAM_IO_KEYPAD_7;
    }
    if (GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::Keypad8 + offset))) {
        KEYPAD_STATE[unit] |= 1 << EAM_IO_KEYPAD_8;
    }
    if (GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::Keypad9 + offset))) {
        KEYPAD_STATE[unit] |= 1 << EAM_IO_KEYPAD_9;
    }
    if (GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::Keypad00 + offset))) {
        KEYPAD_STATE[unit] |= 1 << EAM_IO_KEYPAD_00;
    }
    if (GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::KeypadDecimal + offset))) {
        KEYPAD_STATE[unit] |= 1 << EAM_IO_KEYPAD_DECIMAL;
    }
    if (GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::InsertCard + offset))) {
        KEYPAD_STATE[unit] |= 1 << EAM_IO_INSERT;
    }

    // return state
    return KEYPAD_STATE[unit];
}

std::string eamuse_get_keypad_state_str(size_t unit) {
    auto state = eamuse_get_keypad_state(unit);
    std::ostringstream ss;
    if (state & 1 << EAM_IO_KEYPAD_0) ss << "0";
    if (state & 1 << EAM_IO_KEYPAD_1) ss << "1";
    if (state & 1 << EAM_IO_KEYPAD_2) ss << "2";
    if (state & 1 << EAM_IO_KEYPAD_3) ss << "3";
    if (state & 1 << EAM_IO_KEYPAD_4) ss << "4";
    if (state & 1 << EAM_IO_KEYPAD_5) ss << "5";
    if (state & 1 << EAM_IO_KEYPAD_6) ss << "6";
    if (state & 1 << EAM_IO_KEYPAD_7) ss << "7";
    if (state & 1 << EAM_IO_KEYPAD_8) ss << "8";
    if (state & 1 << EAM_IO_KEYPAD_9) ss << "9";
    if (state & 1 << EAM_IO_KEYPAD_00) ss << "00";
    if (state & 1 << EAM_IO_KEYPAD_DECIMAL) ss << "00";
    if (state & 1 << EAM_IO_INSERT) ss << "00";
    return ss.str();
}

bool eamuse_keypad_state_naive() {
    return KEYPAD_BINDINGS.keypads[0].empty() && KEYPAD_BINDINGS.keypads[1].empty();
}

void eamuse_set_game(std::string game) {
    if (EAMUSE_GAME_NAME != game) {
        EAMUSE_GAME_NAME = std::move(game);
        eamuse_update_keypad_bindings();
    }
}

void eamuse_update_keypad_bindings() {
    KEYPAD_BINDINGS = Config::getInstance().getKeypadBindings(EAMUSE_GAME_NAME);
}

std::string eamuse_get_game() {
    return EAMUSE_GAME_NAME;
}

int eamuse_get_game_keypads() {
    if (avs::game::is_model("JDZ") ||
        avs::game::is_model("KDZ") ||
        avs::game::is_model("LDJ") ||
        avs::game::is_model("JDX") ||
        avs::game::is_model("KDX") ||
        avs::game::is_model("MDX") ||
        avs::game::is_model("J33") ||
        avs::game::is_model("K33") ||
        avs::game::is_model("L33") ||
        avs::game::is_model("M32"))
    {
        return 2;
    }

    return 1;
}

int eamuse_get_game_keypads_name() {
    auto game_name = eamuse_get_game();

    if (game_name == "Beatmania IIDX" ||
        game_name == "Dance Dance Revolution" ||
        game_name == "GitaDora")
    {
        return 2;
    }

    return 1;
}

void eamuse_autodetect_game() {
    if (avs::game::is_model("KFC"))
        eamuse_set_game("Sound Voltex");
    else if (avs::game::is_model("JDZ") ||
             avs::game::is_model("KDZ") ||
             avs::game::is_model("LDJ"))
        eamuse_set_game("Beatmania IIDX");
    else if (avs::game::is_model("J44") ||
             avs::game::is_model("K44") ||
             avs::game::is_model("L44"))
        eamuse_set_game("Jubeat");
    else if (avs::game::is_model("KDM"))
        eamuse_set_game("Dance Evolution");
    else if (avs::game::is_model("NBT"))
        eamuse_set_game("Beatstream");
    else if (avs::game::is_model("I36"))
        eamuse_set_game("Metal Gear");
    else if (avs::game::is_model("KBR") ||
             avs::game::is_model("LBR") ||
             avs::game::is_model("MBR"))
        eamuse_set_game("Reflec Beat");
    else if (avs::game::is_model("KBI"))
        eamuse_set_game("Tenkaichi Shogikai");
    else if (avs::game::is_model("K39") ||
             avs::game::is_model("L39") ||
             avs::game::is_model("M39"))
        eamuse_set_game("Pop'n Music");
    else if (avs::game::is_model("KGG"))
        eamuse_set_game("Steel Chronicle");
    else if (avs::game::is_model("JGT"))
        eamuse_set_game("Road Fighters 3D");
    else if (avs::game::is_model("PIX"))
        eamuse_set_game("Museca");
    else if (avs::game::is_model("R66"))
        eamuse_set_game("Bishi Bashi Channel");
    else if (avs::game::is_model("J32") ||
             avs::game::is_model("J33") ||
             avs::game::is_model("K32") ||
             avs::game::is_model("K33") ||
             avs::game::is_model("L32") ||
             avs::game::is_model("L33") ||
             avs::game::is_model("M32"))
        eamuse_set_game("GitaDora");
    else if (avs::game::is_model("JDX") ||
             avs::game::is_model("KDX") ||
             avs::game::is_model("MDX"))
        eamuse_set_game("Dance Dance Revolution");
    else if (avs::game::is_model("PAN"))
        eamuse_set_game("Nostalgia");
    else if (avs::game::is_model("JMA") ||
             avs::game::is_model("KMA") ||
             avs::game::is_model("LMA"))
        eamuse_set_game("Quiz Magic Academy");
    else if (avs::game::is_model("MMD"))
        eamuse_set_game("FutureTomTom");
    else if (avs::game::is_model("KK9"))
        eamuse_set_game("Mahjong Fight Club");
    else if (avs::game::is_model("JMP"))
        eamuse_set_game("HELLO! Pop'n Music");
    else if (avs::game::is_model("KLP"))
        eamuse_set_game("LovePlus");
    else if (avs::game::is_model("NSC"))
        eamuse_set_game("Scotto");
    else if (avs::game::is_model("REC"))
        eamuse_set_game("DANCERUSH");
    else if (avs::game::is_model("KCK") ||
             avs::game::is_model("NCK"))
        eamuse_set_game("Winning Eleven");
    else if (avs::game::is_model("NCG"))
        eamuse_set_game("Otoca D'or");
    else if (avs::game::is_model("LA9"))
        eamuse_set_game("Charge Machine");
    else if (avs::game::is_model("JC9"))
        eamuse_set_game("Ongaku Paradise");
    else if (avs::game::is_model("TBS"))
        eamuse_set_game("Busou Shinki: Armored Princess Battle Conductor");
    else {
        log_warning("eamuse", "unknown game model: {}", avs::game::MODEL);
        eamuse_set_game("unknown");
    }
}

void eamuse_scard_callback(uint8_t slot_no, card_info_t *cardinfo) {
    if (cardinfo->card_type > 0) {
        eamuse_card_insert(slot_no, cardinfo->uid);
    }
}
