/*
Copyright (c) 2012-2024 R. Danbrook
Copyright (c) 2020-2024 Rupert Carmichael
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
   contributors may be used to endorse or promote products derived from
   this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#include <cstdlib>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <regex>

#include "uiadapter.h"

#include "inputmanager.h"
#include "logdriver.h"

#include "jg/jg_nes.h"

namespace {

constexpr int DEADZONE = 5120;
constexpr size_t MAXPORTS = 12;

jg_inputstate_t coreinput[MAXPORTS]{};
jg_inputinfo_t *inputinfo[MAXPORTS]{nullptr};

jg_inputstate_t uistate;
jg_inputinfo_t uiinfo;

constexpr size_t NDEFS_UI = 14;
const char *defs_ui[NDEFS_UI] = {
    "ResetSoft", "ResetHard", "FDSNextSide", "FDSInsertEject",
    "QuickSave1", "QuickSave2", "QuickLoad1", "QuickLoad2",
    "Fullscreen", "Pause", "Mute", "FastForward", "Screenshot", "Quit"
};

const int ui_defaults[NDEFS_UI - 1] = {
    0xffbd + 1, 0xffbd + 2, 0xffbd + 3, 0xffbd + 4, 0xffbd + 5,
    0xffbd + 6, 0xffbd + 7, 0xffbd + 8, 'f', 'p', 'm', '`', 0xffbd + 9
};

bool uiprev[NDEFS_UI]{};

uint8_t undef8{};
uint16_t undef16{};

SDL_Joystick *joystick[MAXPORTS]{nullptr};
int jsports[MAXPORTS]{};
int jsiid[MAXPORTS]{};
int jstrig[MAXPORTS]{};

bool conflict{false};

}

InputManager::InputManager(JGManager& jgm, SettingManager& setmgr)
        : jgm(jgm), setmgr(setmgr) {
    for (size_t i = 0; i < jgm.get_coreinfo()->numinputs; ++i) {
        jg_set_inputstate(&coreinput[i], i);
    }

    // Set up UI inputs
    uiinfo.type = JG_INPUT_EXTERNAL;
    uiinfo.index = 21;
    uiinfo.name = "ui";
    uiinfo.fname = "User Interface";
    uiinfo.defs = defs_ui;
    uiinfo.numaxes = 0;
    uiinfo.numbuttons = NDEFS_UI;
    uistate.button = (uint8_t*)calloc(uiinfo.numbuttons, sizeof(uint8_t));
}

InputManager::~InputManager() {
    unassign();
    free(uistate.button);
}

void InputManager::reassign() {
    unassign();
    assign();
}

void InputManager::assign() {
    // Allocate memory for input states
    for (size_t i = 0; i < jgm.get_coreinfo()->numinputs; ++i) {
        inputinfo[i] = jgm.get_inputinfo(i);

        coreinput[i].axis = (int16_t*)calloc(inputinfo[i]->numaxes, sizeof(int16_t));
        coreinput[i].button = (uint8_t*)calloc(inputinfo[i]->numbuttons, sizeof(uint8_t));

        // There are always X, Y, and Z coords
        coreinput[i].coord = (int32_t*)calloc(3, sizeof(int32_t)); // Magic Number

        // There is always X and Y relative motion
        coreinput[i].rel = (int32_t*)calloc(2, sizeof(int32_t)); // Magic Number
    }

    remap_kb();
    remap_js();
}

void InputManager::unassign() {
    jxmap.clear();
    jamap.clear();
    jbmap.clear();
    jhmap.clear();
    kbmap.clear();
    msmap.clear();

    lightgun = false;

    for (size_t i = 0; i < jgm.get_coreinfo()->numinputs; ++i) {
        if (coreinput[i].axis) {
            free(coreinput[i].axis);
            coreinput[i].axis = nullptr;
        }
        if (coreinput[i].button) {
            free(coreinput[i].button);
            coreinput[i].button = nullptr;
        }
        if (coreinput[i].coord) {
            free(coreinput[i].coord);
            coreinput[i].coord = nullptr;
        }
        if (coreinput[i].rel) {
            free(coreinput[i].rel);
            coreinput[i].rel = nullptr;
        }
    }
}

void InputManager::remap_kb() {
    kbmap.clear();
    msmap.clear();

    // -1 to prevent "Quit" from being defined by default
    for (size_t i = 0; i < NDEFS_UI - 1; ++i) {
        std::string val = setmgr.get_input("ui", uiinfo.defs[i]);
        if (val.empty()) {
            setmgr.set_input("ui", uiinfo.defs[i], std::to_string(ui_defaults[i]));
            kbmap[ui_defaults[i]] = &uistate.button[i];
        }
        else {
            if (kbmap[std::stoi(val)] == nullptr) {
                kbmap[std::stoi(val)] = &uistate.button[i];
            }
            else {
                LogDriver::log(LogLevel::Warn,
                               std::string{"Input configuration conflict: "} +
                               "ui, " + uiinfo.defs[i]);
            }
        }
    }

    // If "Quit" was defined, apply the definition
    std::string val = setmgr.get_input("ui", uiinfo.defs[NDEFS_UI - 1]);
    if (!val.empty()) {
        kbmap[std::stoi(val)] = &uistate.button[NDEFS_UI - 1];
    }

    for (size_t i = 0; i < jgm.get_coreinfo()->numinputs; ++i) {
        inputinfo[i] = jgm.get_inputinfo(i);

        if (inputinfo[i]->type == JG_INPUT_POINTER || inputinfo[i]->type == JG_INPUT_GUN) {
            msmap[0] = &coreinput[i].coord[0];
            msmap[1] = &coreinput[i].coord[1];
        }

        if (inputinfo[i]->type == JG_INPUT_GUN) {
            lightgun = true;
        }

        for (size_t j = 0; j < inputinfo[i]->numbuttons; ++j) {
            // Keyboard/Mouse
            const char *idef = inputinfo[i]->defs[j + inputinfo[i]->numaxes];
            std::string val = setmgr.get_input(inputinfo[i]->name, idef);
            if (!val.empty()) {
                if (kbmap[std::stoi(val)] == nullptr) {
                    kbmap[std::stoi(val)] = &coreinput[i].button[j];
                }
                else {
                    LogDriver::log(LogLevel::Warn,
                                   std::string{"Input configuration conflict: "} +
                                   inputinfo[i]->name + ", " + idef);
                }
            }
        }
    }
}

void InputManager::remap_js() {
    jxmap.clear();
    jamap.clear();
    jbmap.clear();
    jhmap.clear();

    for (size_t i = 0; i < jgm.get_coreinfo()->numinputs; ++i) {
        inputinfo[i] = jgm.get_inputinfo(i);

        // Pattern for joystick axis definitions mapped to emulated axes
        std::regex pattern{"j[0-9][a]\\d+"};

        for (size_t j = 0; j < inputinfo[i]->numaxes; ++j) {
            std::string val = setmgr.get_input(std::string(inputinfo[i]->name) + "j",
                                               inputinfo[i]->defs[j]);
            if (val.empty()) {
                continue;
            }

            if (std::regex_match(val, pattern)) {
                int port = val[1] - '0';
                int inum = std::stoi(std::string(&val[3]));
                jxmap[(port * 100) + (inum / 2)] = &coreinput[i].axis[j];
            }
            else {
                LogDriver::log(LogLevel::Warn, std::string("Malformed input code: ") + val.c_str());
            }
        }

        // Reset pattern for axes acting as buttons, buttons, and hats
        pattern = "j[0-9][abh]\\d+";

        for (size_t j = 0; j < inputinfo[i]->numbuttons; ++j) {
            // Joystick
            std::string val = setmgr.get_input(std::string(inputinfo[i]->name) + "j",
                                   inputinfo[i]->defs[j + inputinfo[i]->numaxes]);
            if (val.empty()) {
                continue;
            }

            if (std::regex_match(val, pattern)) {
                int port = val[1] - '0';
                int inum = std::stoi(std::string(&val[3]));

                if (val[2] == 'b') {
                    jbmap[(port * 100) + inum] = &coreinput[i].button[j];
                }
                else if (val[2] == 'h') {
                    jhmap[(port * 100) + inum] = &coreinput[i].button[j];
                }
                else if (val[2] == 'a') {
                    jamap[(port * 100) + inum] = &coreinput[i].button[j];
                }
            }
            else {
                LogDriver::log(LogLevel::Warn, std::string("Malformed input code: ") + val.c_str());
            }
        }
    }

    for (size_t i = 0; i < NDEFS_UI; ++i) {
        std::string val = setmgr.get_input("uij", uiinfo.defs[i]);
        if (!val.empty()) {
            if (val[0] == 'j') {
                int port = val[1] - '0';
                int inum = std::stoi(std::string(&val[3]));

                if (val[2] == 'b') {
                    jbmap[(port * 100) + inum] = &uistate.button[i];
                }
                else if (val[2] == 'h') {
                    jhmap[(port * 100) + inum] = &uistate.button[i];
                }
                else if (val[2] == 'a') {
                    jamap[(port * 100) + inum] = &uistate.button[i];
                }
            }
        }
    }
}

void InputManager::set_inputdef(SDL_Event& evt) {
    // Check if an axis is being assigned
    bool axis = false;
    if ((cfg_name == "arkanoid" || cfg_name == "pachinko") && cfg_defnum < 1) {
        axis = true;
    }

    switch (evt.type) {
        case SDL_JOYBUTTONDOWN: {
            if (axis) {
                LogDriver::log(LogLevel::Warn, "Tried to configure an axis as a button");
                break;
            }

            SDL_Joystick *js = SDL_JoystickFromInstanceID(evt.jbutton.which);
            int port = SDL_JoystickGetPlayerIndex(js);
            int btn = evt.jbutton.button;
            if (jbmap[(port * 100) + btn] == nullptr) {
                std::string bstr{"j" + std::to_string(port) + "b" + std::to_string(btn)};
                setmgr.set_input(cfg_name + "j", cfg_def, bstr);
            }
            else {
                conflict = true;
            }
            set_cfg_running(false);
            break;
        }
        case SDL_JOYHATMOTION: {
            if (axis) {
                LogDriver::log(LogLevel::Warn, "Tried to configure an axis as a hat");
                break;
            }

            SDL_Joystick *js = SDL_JoystickFromInstanceID(evt.jhat.which);
            int port = SDL_JoystickGetPlayerIndex(js);
            int hat = 0;
            if (evt.jhat.value & SDL_HAT_UP) {
                hat = 0;
            }
            else if (evt.jhat.value & SDL_HAT_DOWN) {
                hat = 1;
            }
            else if (evt.jhat.value & SDL_HAT_LEFT) {
                hat = 2;
            }
            else if (evt.jhat.value & SDL_HAT_RIGHT) {
                hat = 3;
            }
            if (jhmap[(port * 100) + hat] == nullptr) {
                std::string hstr{"j" + std::to_string(port) + "h" + std::to_string(hat)};
                setmgr.set_input(cfg_name + "j", cfg_def, hstr);
            }
            else {
                conflict = true;
            }
            set_cfg_running(false);
            break;
        }
        case SDL_JOYAXISMOTION: {
            SDL_Joystick *js = SDL_JoystickFromInstanceID(evt.jhat.which);
            int port = SDL_JoystickGetPlayerIndex(js);
            int jaxis = evt.jaxis.axis * 2 + (evt.jaxis.value > 0 ? 1 : 0);

            if (jstrig[port] & (1 << evt.jaxis.axis) && evt.jaxis.value < 0) {
                break;
            }

            if (abs(evt.jaxis.value) >= DEADZONE) {
                if (jamap[(port * 100) + jaxis] == nullptr) {
                    std::string astr{"j" + std::to_string(port) + "a" + std::to_string(jaxis)};
                    setmgr.set_input(cfg_name + "j", cfg_def, astr);
                }
                else {
                    conflict = true;
                }
                set_cfg_running(false);
            }
            break;
        }
    }

    // Early return if nothing new was defined
    if (cfg_running) {
        return;
    }

    // Remap the joysticks
    remap_js();
}

void InputManager::event(SDL_Event& evt) {
    if (cfg_running) {
        set_inputdef(evt);
        return;
    }

    switch (evt.type) {
        case SDL_JOYDEVICEADDED: {
            int port = 0;

            // Choose next unplugged port
            for (int i = 0; i < MAXPORTS; ++i) {
                if (!jsports[i]) {
                    joystick[i] = SDL_JoystickOpen(evt.jdevice.which);
                    SDL_JoystickSetPlayerIndex(joystick[i], i);
                    jsports[i] = 1;
                    jsiid[i] = SDL_JoystickInstanceID(joystick[i]);
                    port = i;
                    jstrig[i] = 0;
                    for (int j = 0; j < SDL_JoystickNumAxes(joystick[i]); ++j) {
                        if (SDL_JoystickGetAxis(joystick[i], j) <= -(DEADZONE)) {
                            jstrig[i] |= 1 << j; // it's a trigger
                        }
                    }
                    break;
                }
            }

            LogDriver::log(LogLevel::Info, std::string("Joystick ") +
                           std::to_string(SDL_JoystickGetPlayerIndex(joystick[port]) + 1) +
                           " Connected: " + SDL_JoystickName(joystick[port]) +
                           " (Instance ID: " + std::to_string(jsiid[port]) + ")");
            break;
        }
        case SDL_JOYDEVICEREMOVED: {
            int id = evt.jdevice.which;
            for (int i = 0; i < MAXPORTS; ++i) {
                if (jsiid[i] == id) {
                    jsports[i] = 0;
                    LogDriver::log(LogLevel::Info, std::string("Joystick ") +
                                   std::to_string(i + 1) + " Disconnected (Instance ID: " +
                                   std::to_string(id) + ")");
                    SDL_JoystickClose(joystick[i]);
                    break;
                }
            }
            break;
        }

        case SDL_JOYBUTTONUP: {
            SDL_Joystick *js = SDL_JoystickFromInstanceID(evt.jbutton.which);
            int port = SDL_JoystickGetPlayerIndex(js);
            int btn = (port * 100) + evt.jbutton.button;
            if (jbmap[btn] != nullptr) {
                *jbmap[btn] = 0;
            }
            break;
        }
        case SDL_JOYBUTTONDOWN: {
            SDL_Joystick *js = SDL_JoystickFromInstanceID(evt.jbutton.which);
            int port = SDL_JoystickGetPlayerIndex(js);
            int btn = (port * 100) + evt.jbutton.button;
            if (jbmap[btn] != nullptr) {
                *jbmap[btn] = 1;
            }
            break;
        }

        case SDL_JOYHATMOTION: {
            SDL_Joystick *js = SDL_JoystickFromInstanceID(evt.jhat.which);
            int port = SDL_JoystickGetPlayerIndex(js);
            int hat = (port * 100);
            if (jhmap[hat + 0] != nullptr) {
                *jhmap[hat + 0] = evt.jhat.value & SDL_HAT_UP;
            }
            if (jhmap[hat + 1] != nullptr) {
                *jhmap[hat + 1] = (evt.jhat.value & SDL_HAT_DOWN) >> 2;
            }
            if (jhmap[hat + 2] != nullptr) {
                *jhmap[hat + 2] = (evt.jhat.value & SDL_HAT_LEFT) >> 3;
            }
            if (jhmap[hat + 3] != nullptr) {
                *jhmap[hat + 3] = (evt.jhat.value & SDL_HAT_RIGHT) >> 1;
            }
            break;
        }

        case SDL_JOYAXISMOTION: {
            SDL_Joystick *js = SDL_JoystickFromInstanceID(evt.jhat.which);
            int port = SDL_JoystickGetPlayerIndex(js);
            int axis = (port * 100) + (evt.jaxis.axis * 2);

            // Buttons
            if (jamap[axis] != nullptr) {
                *jamap[axis] = evt.jaxis.value < -16384;
            }
            if (jamap[axis + 1] != nullptr) {
                *jamap[axis + 1] = evt.jaxis.value > 16384;
            }

            // Analogue
            axis = (port * 100) + evt.jaxis.axis;
            if (jxmap[axis] != nullptr) {
                *jxmap[axis] = abs(evt.jaxis.value) > DEADZONE ? evt.jaxis.value : 0;
            }

            break;
        }
    }

    ui_events();
}

void InputManager::event(int key, bool pressed) {
    if (kbmap[key] != nullptr) {
        if (*kbmap[key] && pressed) {
            return;
        }
        *kbmap[key] = pressed;
    }
}

void InputManager::event(int x, int y) {
    if (msmap[0] != nullptr) {
        *msmap[0] = x;
        *msmap[1] = y;
    }
}

void InputManager::ui_events() {
    // Process any UI events - need to make sure it went from true to false to
    // emulate a "keyup" event
    for (size_t i = 0; i < NDEFS_UI; ++i) {
        if (uiprev[i] && !uistate.button[i]) {
            switch (i) {
                case 0: // ResetSoft
                    jgm.reset(0);
                    break;
                case 1: // ResetHard
                    jgm.reset(1);
                    break;
                case 2: // FDSInsertEject
                    jgm.media_insert();
                    break;
                case 3: // FDSNextSide
                    jgm.media_select();
                    break;
                case 4: // QuickSave1
                    jgm.state_qsave(0);
                    break;
                case 5: // QuickSave2
                    jgm.state_qsave(1);
                    break;
                case 6: // QuickLoad1
                    jgm.state_qload(0);
                    break;
                case 7: // QuickLoad2
                    jgm.state_qload(1);
                    break;
                case 8: // Fullscreen
                    UiAdapter::fullscreen();
                    break;
                case 9: // Pause
                    UiAdapter::pause();
                    break;
                case 10: // Mute
                    UiAdapter::mute();
                    break;
                case 11: // FastForward
                    UiAdapter::fastforward(false);
                    break;
                case 12: // Screenshot
                    UiAdapter::screenshot();
                    break;
                case 13: // Quit
                    UiAdapter::quit();
                    break;
            }
        }

        uiprev[i] = uistate.button[i];
    }

    if (uistate.button[11]) {
        UiAdapter::fastforward(true);
    }
}

std::vector<jg_inputinfo_t> InputManager::get_inputinfo() {
    std::vector<jg_inputinfo_t> input_info{};

    input_info.push_back(jg_nes_inputinfo(0, JG_NES_PAD1));
    input_info.push_back(jg_nes_inputinfo(1, JG_NES_PAD2));
    input_info.push_back(jg_nes_inputinfo(2, JG_NES_PAD3));
    input_info.push_back(jg_nes_inputinfo(3, JG_NES_PAD4));
    input_info.push_back(jg_nes_inputinfo(4, JG_NES_ZAPPER));
    input_info.push_back(jg_nes_inputinfo(5, JG_NES_ARKANOID));
    input_info.push_back(jg_nes_inputinfo(6, JG_NES_POWERPAD));
    input_info.push_back(jg_nes_inputinfo(7, JG_NES_POWERGLOVE));
    input_info.push_back(jg_nes_inputinfo(8, JG_NES_FAMILYTRAINER));
    input_info.push_back(jg_nes_inputinfo(9, JG_NES_PACHINKO));
    input_info.push_back(jg_nes_inputinfo(10, JG_NES_OEKAKIDSTABLET));
    input_info.push_back(jg_nes_inputinfo(11, JG_NES_KONAMIHYPERSHOT));
    input_info.push_back(jg_nes_inputinfo(12, JG_NES_BANDAIHYPERSHOT));
    input_info.push_back(jg_nes_inputinfo(13, JG_NES_CRAZYCLIMBER));
    input_info.push_back(jg_nes_inputinfo(14, JG_NES_MAHJONG));
    input_info.push_back(jg_nes_inputinfo(15, JG_NES_EXCITINGBOXING));
    input_info.push_back(jg_nes_inputinfo(16, JG_NES_TOPRIDER));
    input_info.push_back(jg_nes_inputinfo(17, JG_NES_POKKUNMOGURAA));
    input_info.push_back(jg_nes_inputinfo(18, JG_NES_PARTYTAP));
    input_info.push_back(jg_nes_inputinfo(19, JG_NES_VSSYS));
    input_info.push_back(jg_nes_inputinfo(20, JG_NES_KARAOKESTUDIO));

    input_info.push_back(uiinfo); // Special case

    return input_info;
}

std::string InputManager::get_inputdef(std::string device, std::string def) {
    std::string ret = setmgr.get_input(device, def);
    return ret;
}

void InputManager::clear_inputdef() {
    setmgr.set_input(cfg_name, cfg_def, std::string{});
    setmgr.set_input(cfg_name + "j", cfg_def, std::string{});
    remap_kb();
    remap_js();
}

void InputManager::set_inputcfg(std::string name, std::string def, int defnum) {
    set_cfg_running(true);
    cfg_name = name;
    cfg_def = def;
    cfg_defnum = defnum;
}

void InputManager::set_inputdef(int val) {
    // Check for mapping conflicts to avoid overwriting an active definition
    if (kbmap[val] != nullptr) {
        conflict = true;
        return;
    }

    setmgr.set_input(cfg_name, cfg_def, std::to_string(val));

    // Remap all keyboard definitions
    remap_kb();
}

void InputManager::set_cfg_running(bool running) {
    cfg_running = running;
    if (!running) {
        UiAdapter::show_inputmsg(conflict ? 2 : 0);
        conflict = false;
    }
}
