#include "mdxf.h"
#include "mdxf_poll.h"

#include <mutex>

#include "avs/game.h"
#include "games/ddr/ddr.h"
#include "games/ddr/io.h"
#include "launcher/launcher.h"
#include "rawinput/rawinput.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"

#define MDFX_DEBUG_VERBOSE 0

#if MDFX_DEBUG_VERBOSE
#define log_debug(module, format_str, ...) logger::push( \
    LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY)
#else
#define log_debug(module, format_str, ...)
#endif

// constants
const size_t STATUS_BUFFER_SIZE = 32;
const size_t STATUS_BUFFER_NUM_ENTRIES = 16;

// static stuff
static uint8_t HEAD_P1 = 0;
static uint8_t HEAD_P2 = 0;

static uint16_t PREV_STATE_P1 = 0;
static uint16_t PREV_STATE_P2 = 0;
static uint64_t PREV_TIME_P1 = 0;
static uint64_t PREV_TIME_P2 = 0;

static std::mutex MUTEX_P1;
static std::mutex MUTEX_P2;

static bool IS_MDXF_ACTIVE = false;

static const uint8_t BACKFILL_INTERVAL_MS = 4;
static const uint8_t BACKFILL_PADDING_MS = 2;

// These are used to determine if a thread needs to be spun up to keep pad state ring buffers populated with enough recent polls
static uint64_t START_TIME = 0;
static int CALL_COUNT = 0;
static const int THRESHOLD_REFRESH_RATE = 120;

static std::atomic<bool> IS_REFRESH_RATE_MEASUREMENT_STARTED{false};
static std::atomic<bool> IS_REFRESH_RATE_DETERMINED{false};
static std::atomic<bool> IS_THREAD_NEEDED{false};
static std::atomic<bool> MDXF_THREAD_RUNNING{false};

static std::thread MDXF_THREAD;

static constexpr int THREAD_REFRESH_RATE_HZ = 125;
static constexpr auto THREAD_PERIOD = std::chrono::milliseconds(1000 / THREAD_REFRESH_RATE_HZ);

// buffers
#pragma pack(push, 1)
static struct {
    uint8_t STATUS_BUFFER_P1[STATUS_BUFFER_NUM_ENTRIES][STATUS_BUFFER_SIZE] {};
    uint8_t STATUS_BUFFER_P2[STATUS_BUFFER_NUM_ENTRIES][STATUS_BUFFER_SIZE] {};
} BUFFERS {};
#pragma pack(pop)

static bool STATUS_BUFFER_FREEZE = false;

// Decides which method to use for populating ring buffer entries for "padding".
// Overwritten in spicecfg using P4IO Buffer Algorithm option.
// THREAD_MODE: Spins a thread running at THREAD_REFRESH_RATE_HZ which periodically fills the ring
//              buffer with auxiliary entries. Falls back on BACKFILL_MODE
// BACKFILL_MODE: On every update cycle, fill the ring buffer with entries for the last known state
//                BACKFILL_INTERVAL_MS apart from each other from the time of the last entry to the
//                current time before adding the entry for the current state.
// AUTO_MODE: thread mode if <120Hz, backfill if >=120Hz
acio::MDXFBufferFillMode acio::MDXF_BUFFER_FILL_MODE = acio::MDXFBufferFillMode::AUTO_MODE;

typedef enum {
    ARKMDXP4_POLL = 0,
    INTERNAL_POLL = 1,
    EXTERNAL_POLL = 2
} MDXFPollSource;

typedef uint64_t (__cdecl *ARK_GET_TICK_TIME64_T)();

static uint64_t arkGetTickTime64() {
    static ARK_GET_TICK_TIME64_T getTickTime64 = nullptr;

    if (!getTickTime64) {
        HMODULE h = avs::game::DLL_INSTANCE;
        if (h) {
            getTickTime64 = (ARK_GET_TICK_TIME64_T)GetProcAddress(h, "arkGetTickTime64");
        }
    }
    // this works on 32-bit versions of avs, but not on 64.
    // it's better than nothing though.
    return getTickTime64 ? getTickTime64() : timeGetTime();
}

// Used to keep the ring buffer populated with steady updates. 60Hz interval is too slow
static void mdxf_thread_start() {
    bool expected = false;
    if (!MDXF_THREAD_RUNNING.compare_exchange_strong(expected, true)) {
        return;
    }

    log_info("mdxf", "starting poll thread");

    MDXF_THREAD = std::thread([] {
        SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);

        timeutils::PreciseSleepTimer timer;
        while (MDXF_THREAD_RUNNING.load(std::memory_order_acquire)) {
            mdxf_poll(false);
            timer.sleep(THREAD_PERIOD);
        }
    });
}

static void mdxf_thread_stop() {
    if (!MDXF_THREAD_RUNNING.exchange(false)) {
        return;
    }

    if (MDXF_THREAD.joinable()) {
        MDXF_THREAD.join();
    }

}

// Snaps measured refresh rate to best fit
static int snap_refresh_rate(int measured_hz) {
    static constexpr std::array<int, 6> rates = {
        60, 120, 144, 165, 180, 240
    };

    int best = rates[0];
    int best_err = std::fabs(measured_hz - best);

    for (int r : rates) {
        int err = std::fabs(measured_hz - r);
        if (err < best_err) {
            best = r;
            best_err = err;
        }
    }
    return best;
}

// Increments the number of times the update function was called,
// then calculates the current refresh rate of the game
// (20 seconds after the game starts, until 25 seconds)
static void count_calls_from_game() {
    if (IS_REFRESH_RATE_DETERMINED) {
        return;
    }

    const uint64_t current_time = arkGetTickTime64();

    if (!IS_REFRESH_RATE_MEASUREMENT_STARTED) {
        if (START_TIME == 0) {
            START_TIME = current_time;
        }

        // boot screen takes about 10 seconds, so let's wait for double that
        if ((current_time - START_TIME) < 20000) {
            // too early, do nothing
            return;
        } else {
            // 20s has passed for the first time, start measuring on next call
            IS_REFRESH_RATE_MEASUREMENT_STARTED = true;
            START_TIME = current_time;
            log_debug("mdxf", "measurement begin");
            return;
        }
    }

    const uint64_t elapsed_time = current_time - START_TIME;
    CALL_COUNT++;
    if (elapsed_time >= 5000) {
        double measured_hz = static_cast<double>(CALL_COUNT) * 1000.0 / static_cast<double>(elapsed_time);
        
        // Account for the main loop calling this twice per iteration
        measured_hz *= 0.5;
        const int snapped_hz = snap_refresh_rate(static_cast<int>(measured_hz));

        IS_REFRESH_RATE_DETERMINED = true;
        IS_THREAD_NEEDED = (snapped_hz < THRESHOLD_REFRESH_RATE);
        log_info(
            "mdxf",
            "detected: {} Hz, best fit: {} Hz",
            static_cast<int>(measured_hz),
            snapped_hz);

        if (IS_THREAD_NEEDED) {
            mdxf_thread_start();
        }
    }
}

/*
 * Implementations
 */

static uint64_t __cdecl ac_io_mdxf_get_control_status_buffer(int node, void *out, uint8_t index, uint8_t head_in) {

    // Default error value (matches original mask behavior)
    auto error_ret = static_cast<uint64_t>(node - 0x11) & 0xFFFFFFFFFFFFFF00;
    
    // Dance Dance Revolution
    if (avs::game::is_model("MDX")) {
        // Select player-specific state
        std::mutex* mutex = nullptr;
        uint8_t* head = nullptr;
        uint8_t (*buffer)[STATUS_BUFFER_SIZE];
        size_t size = STATUS_BUFFER_NUM_ENTRIES;
        
        if (node == 17 || node == 25) {
            mutex = &MUTEX_P1;
            head = &HEAD_P1;
            buffer = BUFFERS.STATUS_BUFFER_P1;
        } else if (node == 18 || node == 26) {
            mutex = &MUTEX_P2;
            head = &HEAD_P2;
            buffer = BUFFERS.STATUS_BUFFER_P2;
        } else {
            memset(out, 0, STATUS_BUFFER_SIZE);
            return error_ret;
        }
        
        std::lock_guard<std::mutex> lock(*mutex);
        
        const uint8_t start_index = (head_in == 0xFF) ? *head : head_in;

        // Compute ring index: walk backwards from start_index as index increases
        // Assumes ring buffer size is a power of two
        const size_t mask = size - 1;
        const size_t offset = static_cast<size_t>(index) & mask;
        const size_t i = (static_cast<size_t>(start_index) - offset + size) & mask;

        // Copy the chosen entry
        memcpy(out, buffer[i], STATUS_BUFFER_SIZE);

        // Return the start value actually used
        return static_cast<uint64_t>(start_index);
    }

    return error_ret;
}

static bool __cdecl ac_io_mdxf_set_output_level(unsigned int a1, unsigned int a2, uint8_t value) {
    if (avs::game::is_model("MDX")) {
        static const struct {
            int a2[4];
        } mapping[] = {
            {
                // a1 == 17
                {
                    games::ddr::Lights::GOLD_P1_STAGE_UP_RIGHT,
                    games::ddr::Lights::GOLD_P1_STAGE_DOWN_LEFT,
                    games::ddr::Lights::GOLD_P1_STAGE_UP_LEFT,
                    games::ddr::Lights::GOLD_P1_STAGE_DOWN_RIGHT
                }
            },
            {
                // a1 == 18
                {
                    games::ddr::Lights::GOLD_P2_STAGE_UP_RIGHT,
                    games::ddr::Lights::GOLD_P2_STAGE_DOWN_LEFT,
                    games::ddr::Lights::GOLD_P2_STAGE_UP_LEFT,
                    games::ddr::Lights::GOLD_P2_STAGE_DOWN_RIGHT
                }
            }
        };
        if ((a1 == 17 || a1 == 18) && (a2 < 4)) {
            // get light from mapping
            const auto light = mapping[a1 - 17].a2[a2];

            // get lights
            auto &lights = games::ddr::get_lights();

            // write lights
            GameAPI::Lights::writeLight(RI_MGR, lights[light], value / 128.f);
        }
    }
    
    return true;
}

static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFPollSource source, uint64_t current_time) {

    // check freeze
    if (STATUS_BUFFER_FREEZE) {
        return true;
    }

    // Dance Dance Revolution
    if (avs::game::is_model("MDX")) {
        // Marks this module as actively being used, allowing this function to be called from other sources
        if (source == ARKMDXP4_POLL) {
            if (!IS_MDXF_ACTIVE) {
                log_debug("mdxf", "initializing mdxf I/O support");
                IS_MDXF_ACTIVE = true;
                if (acio::MDXF_BUFFER_FILL_MODE == acio::MDXFBufferFillMode::THREAD_MODE) {
                    IS_THREAD_NEEDED = true;
                    mdxf_thread_start();
                }
            }
            if (acio::MDXF_BUFFER_FILL_MODE == acio::MDXFBufferFillMode::AUTO_MODE) {
                count_calls_from_game();
            }
        }
        
        uint8_t (*buffer)[STATUS_BUFFER_SIZE];
        uint8_t *head = nullptr;
        uint16_t *prev_state = nullptr;
        uint64_t *prev_time = nullptr;
        std::mutex* mutex = nullptr;
        
        switch (node) {
            case 17:
            case 25:
                mutex = &MUTEX_P1;
                head = &HEAD_P1;
                prev_state = &PREV_STATE_P1;
                prev_time = &PREV_TIME_P1;
                buffer = BUFFERS.STATUS_BUFFER_P1;
                break;
            case 18:
            case 26:
                mutex = &MUTEX_P2;
                head = &HEAD_P2;
                prev_state = &PREV_STATE_P2;
                prev_time = &PREV_TIME_P2;
                buffer = BUFFERS.STATUS_BUFFER_P2;
                break;
            default:
                // return failure on unknown node
                return false;
        }
    
        // Sensor Map (LDUR):
        // FOOT DOWN = bit 32-35 = byte 4, bit 0-3
        // FOOT UP = bit 36-39 = byte 4, bit 4-7
        // FOOT RIGHT = bit 40-43 = byte 5, bit 0-3
        // FOOT LEFT = bit 44-47 = byte 5, bit 4-7
        static const size_t buttons_p1[] = {
                games::ddr::Buttons::P1_PANEL_UP,
                games::ddr::Buttons::P1_PANEL_DOWN,
                games::ddr::Buttons::P1_PANEL_LEFT,
                games::ddr::Buttons::P1_PANEL_RIGHT,
        };
        static const size_t buttons_p2[] = {
                games::ddr::Buttons::P2_PANEL_UP,
                games::ddr::Buttons::P2_PANEL_DOWN,
                games::ddr::Buttons::P2_PANEL_LEFT,
                games::ddr::Buttons::P2_PANEL_RIGHT,
        };

        // decide on button map
        const size_t *button_map = nullptr;
        int player = 0;
        switch (node) {
            case 17:
            case 25:
                button_map = &buttons_p1[0];
                player = 1;
                break;
            case 18:
            case 26:
                button_map = &buttons_p2[0];
                player = 2;
                break;
        }
        
        uint16_t current_state;
        
        // Only call getState() if called externally when actual input events happen, otherwise use previous known state
        if (source == EXTERNAL_POLL) {
            // get buttons
            auto &buttons = games::ddr::get_buttons();
            
            // get analogs
            bool analog_left = false;
            bool analog_right = false;
            games::ddr::get_analog_x_axis(player, analog_left, analog_right);
            bool analog_up = false;
            bool analog_down = false;
            games::ddr::get_analog_y_axis(player, analog_up, analog_down);
            
            uint8_t up_down = 0;
            uint8_t left_right = 0;
            if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[0])) || analog_up) {
                up_down |= 0xF0;
            }
            if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[1])) || analog_down) {
                up_down |= 0x0F;
            }
            if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[2])) || analog_left) {
                left_right |= 0xF0;
            }
            if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[3])) || analog_right) {
                left_right |= 0x0F;
            }
            current_state = (uint16_t(up_down) << 8) | left_right;
        } else {
            current_state = *prev_state;
        }
        
        std::lock_guard<std::mutex> lock(*mutex);
        
        const bool has_state_changed = *prev_state != current_state;
        const bool has_time_changed = *prev_time < current_time;
        
        // If state hasn't changed and either the update was triggered externally or the time hasn't changed, then don't advance head pointer or write a new entry
        if (!has_state_changed && (source == EXTERNAL_POLL || !has_time_changed)) {
            return true;
        }
        
        // The start and stop time cutoffs for backfilling entries. Min(..) ensures times aren't negative.
        // The stop time is just before the current_time, set by BACKFILL_PADDING_MS, which avoids the last backfilled entry being too close to current_time.
        uint64_t start_time = *prev_time;
        const uint64_t stop_time = current_time - std::min<uint64_t>(current_time, BACKFILL_PADDING_MS);
        
        // Ensures the first iteration will write the first entry at current_time and not backfill to time 0ms.
        if (start_time == 0) {
            start_time = current_time - std::min<uint64_t>(current_time, BACKFILL_INTERVAL_MS);
        }
        
        // Ensures only STATUS_BUFFER_NUM_ENTRIES entries at most are backfilled
        const uint64_t max_backfill = BACKFILL_INTERVAL_MS * STATUS_BUFFER_NUM_ENTRIES;
        const uint64_t min_time = current_time - std::min<uint64_t>(current_time, max_backfill);
        if (start_time < min_time) {
            start_time = min_time;
        }
        
        // Don't backfill entries if called externally or if a separate thread is being used to fill auxiliary entries
        if (source == EXTERNAL_POLL || IS_THREAD_NEEDED) {
            start_time = stop_time - 1;
        }
        
        uint64_t time = start_time;
        uint16_t state = *prev_state;
        
        // Backfill entries a fixed interval apart from each other between prev_time and current_time
        while (time < stop_time) {
            // Advance head pointer
            *head = (*head + 1) % STATUS_BUFFER_NUM_ENTRIES;
            uint8_t* buffer_entry = buffer[*head];

            // Clear buffer
            memset(buffer_entry, 0, STATUS_BUFFER_SIZE);
            
            time += BACKFILL_INTERVAL_MS;
            
            // If the stop time is reached, then write current_time and current_state instead for this final iteration
            const bool isEdge = (time >= stop_time);
            if (isEdge) {
                state = current_state;
                time = current_time;
            }
            
            // Write button state
            buffer_entry[4] = (state >> 8) & 0xFF;
            buffer_entry[5] = state & 0xFF;
            
            // Write game time
            *(uint64_t*)&buffer_entry[0x18] = time;
        }
        
        *prev_state = current_state;
        *prev_time = current_time;
    }

    // return success
    return true;
}

static bool __cdecl ac_io_mdxf_update_control_status_buffer(int node) {
    return ac_io_mdxf_update_control_status_buffer_impl(node, ARKMDXP4_POLL, arkGetTickTime64());
}

// Used for triggering updates of the controller states from outside arkmdxp4.dll main refresh loop (i.e. within rawinput.cpp on controller events)
void mdxf_poll(bool isExternal) {
    if (IS_MDXF_ACTIVE) {
        const MDXFPollSource source = isExternal ? EXTERNAL_POLL : INTERNAL_POLL;
        const uint64_t call_time_ms = arkGetTickTime64();
        ac_io_mdxf_update_control_status_buffer_impl(17, source, call_time_ms);
        ac_io_mdxf_update_control_status_buffer_impl(18, source, call_time_ms);
    }
}

/*
 * Module stuff
 */

acio::MDXFModule::MDXFModule(HMODULE module, acio::HookMode hookMode) : ACIOModule("MDXF", module, hookMode) {
    this->status_buffer = (uint8_t*) &BUFFERS;
    this->status_buffer_size = sizeof(BUFFERS);
    this->status_buffer_freeze = &STATUS_BUFFER_FREEZE;
}

void acio::MDXFModule::attach() {
    ACIOModule::attach();

    // hooks
    ACIO_MODULE_HOOK(ac_io_mdxf_get_control_status_buffer);
    ACIO_MODULE_HOOK(ac_io_mdxf_set_output_level);
    ACIO_MODULE_HOOK(ac_io_mdxf_update_control_status_buffer);
}

acio::MDXFModule::~MDXFModule() {
    if (IS_THREAD_NEEDED) {
        mdxf_thread_stop();
    }
}
