#include "dusk/menu_pointer.h"

#include "d/d_pane_class.h"
#include "dusk/settings.h"
#include "m_Do/m_Do_graphic.h"

#include <aurora/rmlui.hpp>
#include <dolphin/pad.h>

#include <algorithm>
#include <chrono>

namespace dusk::menu_pointer {
namespace {
using Clock = std::chrono::steady_clock;

constexpr auto kTapMaxDuration = std::chrono::milliseconds(300);
constexpr f32 kTapMoveThresholdDp = 12.0f;

struct Gesture {
    bool active = false;
    bool movedTooFar = false;
    bool crossedTarget = false;
    bool pressTargetValid = false;
    Context pressContext = Context::None;
    TargetId pressTarget = InvalidTarget;
    f32 startX = 0.0f;
    f32 startY = 0.0f;
    Clock::time_point startedAt{};
};

State s_state;
bool s_clickConsumed = false;
Context s_lastContext = Context::None;
Context s_currentContext = Context::None;
u8 s_lastDialogChoice = 0xFF;
u8 s_currentDialogChoice = 0xFF;
bool s_lastDialogChoiceValid = false;
bool s_currentDialogChoiceValid = false;
bool s_lastDialogClicked = false;
bool s_currentDialogClicked = false;
bool s_mouseActive = false;
bool s_mouseButtonCaptured = false;
s32 s_mouseButton = -1;
u32 s_suppressedPadHoldMask = 0;
u32 s_suppressedPadNextReadMask = 0;
Context s_deferredActivationContext = Context::None;
TargetId s_deferredActivationTarget = InvalidTarget;
Gesture s_gesture;
bool s_hoverTargetValid = false;
TargetId s_hoverTarget = InvalidTarget;
bool s_clickPending = false;
Context s_clickContext = Context::None;
TargetId s_clickTarget = InvalidTarget;
bool s_clickTargetValid = false;

s32 scancode_from_rml_button(s32 button) noexcept {
    switch (button) {
    case 0:
        return PAD_KEY_MOUSE_LEFT;
    case 1:
        return PAD_KEY_MOUSE_RIGHT;
    case 2:
        return PAD_KEY_MOUSE_MIDDLE;
    default:
        return PAD_KEY_INVALID;
    }
}

bool is_mouse_scancode(s32 scancode) noexcept {
    return scancode >= PAD_KEY_MOUSE_X2 && scancode <= PAD_KEY_MOUSE_LEFT;
}

PADButton pad_button_for_scancode(u32 port, s32 scancode) noexcept {
    u32 count = 0;
    PADKeyButtonBinding* bindings = PADGetKeyButtonBindings(port, &count);
    if (bindings == nullptr) {
        return 0;
    }

    for (u32 i = 0; i < count; ++i) {
        if (bindings[i].scancode == scancode) {
            return bindings[i].padButton;
        }
    }

    return 0;
}

s32 menu_confirm_mouse_scancode() noexcept {
    constexpr u32 port = PAD_CHAN0;
    u32 count = 0;
    PADKeyButtonBinding* bindings = PADGetKeyButtonBindings(port, &count);
    if (bindings == nullptr) {
        return PAD_KEY_MOUSE_LEFT;
    }

    for (u32 i = 0; i < count; ++i) {
        if (bindings[i].padButton == PAD_BUTTON_A && is_mouse_scancode(bindings[i].scancode)) {
            return bindings[i].scancode;
        }
    }

    return pad_button_for_scancode(port, PAD_KEY_MOUSE_LEFT) != 0 ? PAD_KEY_INVALID :
                                                                    PAD_KEY_MOUSE_LEFT;
}

bool mouse_button_is_menu_confirm(s32 button) noexcept {
    const s32 scancode = scancode_from_rml_button(button);
    return scancode != PAD_KEY_INVALID && scancode == menu_confirm_mouse_scancode();
}

void suppress_pad_for_mouse_button(s32 button, bool held) noexcept {
    const s32 scancode = scancode_from_rml_button(button);
    if (scancode == PAD_KEY_INVALID) {
        return;
    }

    const PADButton padButton = pad_button_for_scancode(PAD_CHAN0, scancode);
    if (padButton == 0) {
        return;
    }

    s_suppressedPadNextReadMask |= padButton;
    if (held) {
        s_suppressedPadHoldMask |= padButton;
    } else {
        s_suppressedPadHoldMask &= ~padButton;
    }
}

f32 tap_move_threshold() noexcept {
    auto* context = aurora::rmlui::get_context();
    if (context == nullptr) {
        return kTapMoveThresholdDp;
    }

    return kTapMoveThresholdDp * std::max(context->GetDensityIndependentPixelRatio(), 1.0f);
}

void update_gesture_movement(f32 x, f32 y) noexcept {
    if (!s_gesture.active || s_gesture.movedTooFar) {
        return;
    }

    const f32 dx = x - s_gesture.startX;
    const f32 dy = y - s_gesture.startY;
    const f32 threshold = tap_move_threshold();
    if (dx * dx + dy * dy > threshold * threshold) {
        s_gesture.movedTooFar = true;
    }
}

void clear_click_state() noexcept {
    s_clickConsumed = false;
    s_clickPending = false;
    s_clickContext = Context::None;
    s_clickTarget = InvalidTarget;
    s_clickTargetValid = false;
    s_state.clicked = false;
}

void set_position_from_rml(f32 x, f32 y) noexcept {
    auto* context = aurora::rmlui::get_context();
    if (context == nullptr) {
        return;
    }

    const auto dimensions = context->GetDimensions();
    const f32 width = std::max(static_cast<f32>(dimensions.x), 1.0f);
    const f32 height = std::max(static_cast<f32>(dimensions.y), 1.0f);

    s_state.x = mDoGph_gInf_c::getMinXF() + x / width * mDoGph_gInf_c::getWidthF();
    s_state.y = mDoGph_gInf_c::getMinYF() + y / height * mDoGph_gInf_c::getHeightF();
    s_state.valid = true;
}

void clear_input_state() noexcept {
    s_state = {};
    clear_click_state();
    s_lastDialogChoice = 0xFF;
    s_currentDialogChoice = 0xFF;
    s_lastDialogChoiceValid = false;
    s_currentDialogChoiceValid = false;
    s_lastDialogClicked = false;
    s_currentDialogClicked = false;
    s_mouseActive = false;
    s_mouseButtonCaptured = false;
    s_mouseButton = -1;
    s_suppressedPadHoldMask = 0;
    s_suppressedPadNextReadMask = 0;
    s_deferredActivationContext = Context::None;
    s_deferredActivationTarget = InvalidTarget;
    s_gesture = {};
    s_hoverTargetValid = false;
    s_hoverTarget = InvalidTarget;
}

}  // namespace

bool handle_fallthrough_pointer(f32 x, f32 y, Phase phase, bool touch, s32 mouseButton) noexcept {
    if (!enabled()) {
        return false;
    }

    if (!touch) {
        if (phase == Phase::Press) {
            if (!mouse_button_is_menu_confirm(mouseButton)) {
                return false;
            }
            s_mouseButtonCaptured = true;
            s_mouseButton = mouseButton;
            suppress_pad_for_mouse_button(mouseButton, true);
        } else if (phase == Phase::Release) {
            if (!s_mouseButtonCaptured || s_mouseButton != mouseButton) {
                return false;
            }
            suppress_pad_for_mouse_button(mouseButton, false);
            s_mouseButtonCaptured = false;
            s_mouseButton = -1;
        } else if (phase == Phase::Cancel) {
            if (s_mouseButtonCaptured) {
                suppress_pad_for_mouse_button(s_mouseButton, false);
                s_mouseButtonCaptured = false;
                s_mouseButton = -1;
            } else if (!s_mouseActive) {
                return false;
            }
        }
        s_mouseActive = true;
    }

    if (phase != Phase::Cancel) {
        update_gesture_movement(x, y);
        set_position_from_rml(x, y);
    }
    s_state.touch = touch;

    switch (phase) {
    case Phase::Press:
        clear_click_state();
        s_gesture = {
            .active = true,
            .startX = x,
            .startY = y,
            .startedAt = Clock::now(),
        };
        s_state.down = true;
        s_state.pressed = true;
        break;
    case Phase::Release: {
        const bool shortEnough =
            s_gesture.active && Clock::now() - s_gesture.startedAt <= kTapMaxDuration;
        const bool stillEnough = s_gesture.active && !s_gesture.movedTooFar;
        const bool targetClean = s_gesture.active && !s_gesture.crossedTarget;
        s_clickContext = s_gesture.pressContext;
        s_clickTarget = s_gesture.pressTarget;
        s_clickTargetValid = s_gesture.pressTargetValid;
        s_clickPending = shortEnough && stillEnough && targetClean;
        s_state.down = false;
        s_state.released = true;
        s_state.clicked = s_clickPending;
        s_gesture = {};
        break;
    }
    case Phase::Cancel:
        clear_click_state();
        s_gesture = {};
        s_state.down = false;
        break;
    case Phase::Move:
    default:
        break;
    }

    return true;
}

void begin_game_frame() noexcept {
    s_currentContext = Context::None;
    s_currentDialogChoice = 0xFF;
    s_currentDialogChoiceValid = false;
    s_currentDialogClicked = false;
    s_clickConsumed = false;
    if (!enabled()) {
        clear_input_state();
    }
}

void end_game_frame() noexcept {
    if (s_gesture.active && s_gesture.pressTargetValid &&
        s_currentContext == s_gesture.pressContext && !s_hoverTargetValid)
    {
        s_gesture.crossedTarget = true;
    }

    s_lastContext = s_currentContext;
    s_lastDialogChoice = s_currentDialogChoice;
    s_lastDialogChoiceValid = s_currentDialogChoiceValid;
    s_lastDialogClicked = s_currentDialogClicked;
    s_state.pressed = false;
    s_state.released = false;
    s_state.clicked = false;
    if (!s_state.down) {
        s_state.valid = false;
    }
    s_clickConsumed = false;
    s_clickPending = false;
    s_clickContext = Context::None;
    s_clickTarget = InvalidTarget;
    s_clickTargetValid = false;
    s_hoverTargetValid = false;
    s_hoverTarget = InvalidTarget;
}

void begin_context(Context context) noexcept {
    if (context == Context::None || !enabled()) {
        return;
    }

    if (s_lastContext == Context::None && s_currentContext == Context::None) {
        s_state = {};
        s_mouseActive = false;
        s_mouseButtonCaptured = false;
        s_mouseButton = -1;
        s_suppressedPadHoldMask = 0;
        s_suppressedPadNextReadMask = 0;
        s_deferredActivationContext = Context::None;
        s_deferredActivationTarget = InvalidTarget;
        s_gesture = {};
        s_hoverTargetValid = false;
        s_hoverTarget = InvalidTarget;
        clear_click_state();
    }

    s_currentContext = context;
}

bool active() noexcept {
    return s_currentContext != Context::None || s_lastContext != Context::None;
}

bool enabled() noexcept {
    return getSettings().game.enableMenuPointer.getValue();
}

bool mouse_capture_active() noexcept {
    return enabled() && s_mouseButtonCaptured;
}

const State& state() noexcept {
    return s_state;
}

void set_hover_target(TargetId target) noexcept {
    s_hoverTargetValid = true;
    s_hoverTarget = target;

    if (s_gesture.active && !s_gesture.pressTargetValid && s_state.down) {
        s_gesture.pressContext = s_currentContext;
        s_gesture.pressTarget = target;
        s_gesture.pressTargetValid = true;
    }

    if (s_gesture.active && s_gesture.pressTargetValid &&
        (s_currentContext != s_gesture.pressContext || target != s_gesture.pressTarget))
    {
        s_gesture.crossedTarget = true;
    }
}

bool click_matches_hover_target() noexcept {
    if (!s_clickPending || !s_hoverTargetValid) {
        return false;
    }

    if (!s_clickTargetValid) {
        return true;
    }

    return s_currentContext == s_clickContext && s_hoverTarget == s_clickTarget;
}

bool consume_click() noexcept {
    if (s_clickConsumed || !click_matches_hover_target()) {
        return false;
    }

    s_clickConsumed = true;
    s_clickPending = false;
    s_state.clicked = false;
    return true;
}

bool peek_click() noexcept {
    return !s_clickConsumed && click_matches_hover_target();
}

void set_dialog_choice(u8 choice, bool clicked) noexcept {
    s_currentDialogChoice = choice;
    s_currentDialogChoiceValid = true;
    s_currentDialogClicked = clicked;
}

bool get_dialog_choice(u8& choice) noexcept {
    if (s_currentDialogChoiceValid) {
        choice = s_currentDialogChoice;
        return true;
    }
    if (s_lastDialogChoiceValid) {
        choice = s_lastDialogChoice;
        return true;
    }
    return false;
}

bool consume_dialog_click(u8& choice) noexcept {
    if (s_currentDialogChoiceValid && s_currentDialogClicked) {
        choice = s_currentDialogChoice;
        s_currentDialogClicked = false;
        return true;
    }
    if (s_lastDialogChoiceValid && s_lastDialogClicked) {
        choice = s_lastDialogChoice;
        s_lastDialogClicked = false;
        return true;
    }
    return false;
}

void defer_activation(Context context, TargetId target) noexcept {
    s_deferredActivationContext = context;
    s_deferredActivationTarget = target;
}

bool consume_deferred_activation(Context context, TargetId target) noexcept {
    if (s_deferredActivationContext != context || s_deferredActivationTarget != target) {
        return false;
    }

    s_deferredActivationContext = Context::None;
    s_deferredActivationTarget = InvalidTarget;
    return true;
}

void clear_deferred_activation(Context context) noexcept {
    if (s_deferredActivationContext != context) {
        return;
    }

    s_deferredActivationContext = Context::None;
    s_deferredActivationTarget = InvalidTarget;
}

u32 suppressed_pad_buttons(u32 port) noexcept {
    if (port != PAD_CHAN0) {
        return 0;
    }

    return s_suppressedPadHoldMask | s_suppressedPadNextReadMask;
}

void finish_pad_suppression_read(u32 port) noexcept {
    if (port != PAD_CHAN0) {
        return;
    }

    s_suppressedPadNextReadMask = 0;
}

bool hit_rect(f32 left, f32 top, f32 right, f32 bottom, f32 padding) noexcept {
    const auto& state = menu_pointer::state();
    if (!state.valid) {
        return false;
    }

    if (left > right) {
        std::swap(left, right);
    }
    if (top > bottom) {
        std::swap(top, bottom);
    }

    return state.x >= left - padding && state.x <= right + padding && state.y >= top - padding &&
           state.y <= bottom + padding;
}

bool hit_pane(CPaneMgr* pane, f32 padding) noexcept {
    if (pane == nullptr || pane->getPanePtr() == nullptr) {
        return false;
    }

    Mtx mtx;
    Vec v0 = pane->getGlobalVtx(&mtx, 0, false, 0);
    Vec v1 = pane->getGlobalVtx(&mtx, 1, false, 0);
    Vec v2 = pane->getGlobalVtx(&mtx, 2, false, 0);
    Vec v3 = pane->getGlobalVtx(&mtx, 3, false, 0);
    const f32 left = std::min({v0.x, v1.x, v2.x, v3.x});
    const f32 right = std::max({v0.x, v1.x, v2.x, v3.x});
    const f32 top = std::min({v0.y, v1.y, v2.y, v3.y});
    const f32 bottom = std::max({v0.y, v1.y, v2.y, v3.y});
    return hit_rect(left, top, right, bottom, padding);
}

bool hit_pane(J2DPane* pane, f32 padding) noexcept {
    if (pane == nullptr || !pane->isVisible()) {
        return false;
    }

    const JGeometry::TBox2<f32>& bounds = pane->getBounds();
    return hit_rect(bounds.i.x, bounds.i.y, bounds.f.x, bounds.f.y, padding);
}

}  // namespace dusk::menu_pointer
