#include "InputMapper.h"

#include <memory>
#include <optional>

#include <QKeyEvent>

#include "BlueprintSaveRequestedEvent.h"
#include "BlueprintSelectionRequestedEvent.h"
#include "BuildHotkeyPressedEvent.h"
#include "BuildingType.h"
#include "ControlAction.h"
#include "DebugDrawToggleRequestedEvent.h"
#include "EscapeMenuRequestedEvent.h"
#include "EventManager.h"
#include "GhostRotationRequestedEvent.h"
#include "ModeCancelRequestedEvent.h"
#include "PanDirectionChangedEvent.h"
#include "PauseToggleRequestedEvent.h"
#include "SelectionClearRequestedEvent.h"
#include "SpeedStepRequestedEvent.h"
#include "TemporaryBlueprintCaptureRequestedEvent.h"
#include "TemporaryBlueprintPlaceRequestedEvent.h"
#include "TracePrintRequestedEvent.h"

namespace
{

// The building type a number-key build hotkey selects, or nullopt for the digits
// that are unbound (REQ-UI-HOTKEYS). Plain digits pick the transport buildings,
// Shift+digit the production ones.
std::optional<BuildingType> buildHotkeyType(int digit, bool shiftHeld)
{
    if (!shiftHeld)
    {
        switch (digit)
        {
        case 1: return BuildingType::Belt;
        case 2: return BuildingType::Splitter;
        case 3: return BuildingType::TunnelEntry;  // unified tunnel mode (REQ-BLD-TUNNEL-MODE); 4 unused
        }
        return std::nullopt;
    }

    switch (digit)
    {
    case 1: return BuildingType::Miner;
    case 2: return BuildingType::Smelter;
    case 3: return BuildingType::Assembler;
    case 4: return BuildingType::Shipyard;
    case 5: return BuildingType::SalvageBay;
    case 6: return BuildingType::ReprocessingPlant;
    }
    return std::nullopt;
}

}  // namespace

QString InputMapper::getBuildHotkeyLabel(BuildingType type)
{
    // Searched out of the binding table above rather than spelled out a second time,
    // so a badge can never claim a key the handler does not act on. The shift glyph
    // is written as a code point because the sources are not guaranteed to be read
    // as UTF-8 by every compiler this builds with. A plain stroke arrow rather than
    // U+21E7 UPWARDS WHITE ARROW: the standard UI fonts do not all carry the outlined
    // shift glyph, and the substitute they fall back to is unreadable at badge size.
    const QChar shiftGlyph(0x2191);  // U+2191 UPWARDS ARROW
    for (int digit = 1; digit <= 9; ++digit)
    {
        if (buildHotkeyType(digit, false) == type)
        {
            return QString::number(digit);
        }
        if (buildHotkeyType(digit, true) == type)
        {
            return shiftGlyph + QString::number(digit);
        }
    }
    return QString();
}

bool InputMapper::handleKeyPress(QKeyEvent* event, const ControlContext& context)
{
    // Auto-repeat says nothing new about which keys are down, and a held action is
    // already held.
    if (event->isAutoRepeat()) { return false; }

    // Number-key build-mode hotkeys (REQ-UI-HOTKEYS). nativeVirtualKey gives the
    // physical digit independent of keyboard layout and Shift (with Shift held, key()
    // for the number row can arrive as Key_Exclam etc.). VK_1..VK_9 = 0x31..0x39.
    // Not part of the ControlAction table: these are advertised on the build buttons
    // rather than in the controls panel, and getBuildHotkeyLabel already reads the
    // same binding table this does (REQ-UI-CONTROLS-ACCURACY).
    const quint32 virtualKey = event->nativeVirtualKey();
    if (virtualKey >= 0x31 && virtualKey <= 0x39)
    {
        const std::optional<BuildingType> type = buildHotkeyType(
            static_cast<int>(virtualKey - 0x30),
            (event->modifiers() & Qt::ShiftModifier) != 0);
        if (type.has_value())
        {
            EventManager::getInstance()->sendEventImmediately(
                std::make_shared<BuildHotkeyPressedEvent>(*type));
            return true;
        }
    }

    // Development controls, deliberately outside the table so they are never offered
    // to the player (REQ-UI-CONTROLS-ACCURACY).
    switch (event->key())
    {
    case Qt::Key_F3:
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<DebugDrawToggleRequestedEvent>());
        return true;
    case Qt::Key_F4:
        EventManager::getInstance()->addEvent(
            std::make_shared<TracePrintRequestedEvent>());
        return true;
    default:
        break;
    }

    // Everything else: the key names an action, the action names an event. Which key
    // is bound to what, and whether it does anything in this situation, are both the
    // table's business -- this switch only knows what each action means
    // (REQ-UI-HOTKEYS, REQ-UI-CONTROLS-ACCURACY).
    switch (resolveKeyAction(event->key(), event->modifiers(), context))
    {
    case ControlAction::Move:
        // A parameter of the action rather than an action of its own: the table binds
        // both keys to Move and the direction is read off the key here, as the rotation
        // direction and the build hotkey's digit are.
        if (event->key() == Qt::Key_A) { m_panLeftHeld = true; }
        else                           { m_panRightHeld = true; }
        updatePanDirection();
        return true;
    case ControlAction::GameSpeed:
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<SpeedStepRequestedEvent>(event->key() == Qt::Key_W ? +1 : -1));
        return true;
    case ControlAction::TogglePause:
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<PauseToggleRequestedEvent>());
        return true;
    case ControlAction::Rotate:
        // Shift reverses the rotation direction (REQ-BLD-ROTATE).
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<GhostRotationRequestedEvent>(
                (event->modifiers() & Qt::ShiftModifier) != 0));
        return true;
    case ControlAction::EnterDeconstruct:
    case ControlAction::ExitMode:
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<ModeCancelRequestedEvent>());
        return true;
    case ControlAction::ClearSelection:
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<SelectionClearRequestedEvent>());
        return true;
    case ControlAction::CopyTemporary:
        // The BlueprintLibrary owns the selection and blueprint-capture logic; it drives
        // placement mode from there (REQ-UI-BLUEPRINT-TEMP).
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<TemporaryBlueprintCaptureRequestedEvent>());
        return true;
    case ControlAction::PasteTemporary:
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<TemporaryBlueprintPlaceRequestedEvent>());
        return true;
    case ControlAction::CreateBlueprint:
        // Decided by MainWindow, the only widget that can pause the game and dim the
        // window for a modal.
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<BlueprintSaveRequestedEvent>());
        return true;
    case ControlAction::OpenBlueprints:
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<BlueprintSelectionRequestedEvent>());
        return true;
    case ControlAction::OpenMenu:
        EventManager::getInstance()->sendEventImmediately(
            std::make_shared<EscapeMenuRequestedEvent>());
        return true;
    default:
        // Either nothing is bound to the key here, or what is bound is a mouse gesture
        // the view handles. Unconsumed, so ordinary Qt shortcuts keep working.
        return false;
    }
}

bool InputMapper::handleKeyRelease(QKeyEvent* event, const ControlContext& context)
{
    if (event->isAutoRepeat()) { return false; }

    // Only held actions have a release worth acting on. Resolved through the table
    // rather than matched against A and D directly, so the keys stay rebindable in one
    // place rather than two.
    if (resolveKeyAction(event->key(), event->modifiers(), context) != ControlAction::Move)
    {
        return false;
    }

    if (event->key() == Qt::Key_A) { m_panLeftHeld = false; }
    else                           { m_panRightHeld = false; }
    updatePanDirection();
    return true;
}

void InputMapper::releaseAll()
{
    m_panLeftHeld  = false;
    m_panRightHeld = false;
    updatePanDirection();
}

void InputMapper::updatePanDirection()
{
    // Holding both keys cancels out rather than favouring one.
    PanDirection direction = PanDirection::None;
    if (m_panLeftHeld != m_panRightHeld)
    {
        direction = m_panLeftHeld ? PanDirection::Left : PanDirection::Right;
    }

    if (direction == m_panDirection) { return; }
    m_panDirection = direction;
    EventManager::getInstance()->sendEventImmediately(
        std::make_shared<PanDirectionChangedEvent>(direction));
}
