#include "ControlAction.h"

namespace
{

// A binding that a belt drag can take over. Availability is a property of the action,
// but a binding can be claimed by a different action while a gesture is in progress:
// right-click cancels the drag instead of leaving builder mode (REQ-BLD-BELT-DRAG), so
// ExitMode's right-click drops out and it is left with Q alone.
enum class BindingCondition
{
    Always,
    NotDraggingBelt
};

struct KeyBindingEntry
{
    ControlAction         action;
    int                   key;
    // Shown on the badge, and Ctrl additionally participates in matching. Every other
    // modifier is display only -- see resolveKeyAction.
    Qt::KeyboardModifiers modifiers;
};

struct MouseBindingEntry
{
    ControlAction    action;
    MouseBinding     binding;
    BindingCondition condition;
};

// Resolution is first-match-wins over these tables, so an entry that must beat another
// on the same input is listed above it -- CancelBeltLine before ExitMode on the right
// mouse button. Everything else is disjoint by availability: Q carries three actions
// whose availability rules partition the situations between them, so their order here is
// the order the reader meets them and nothing more (REQ-UI-HOTKEYS).
const KeyBindingEntry KEY_BINDINGS[] = {
    {ControlAction::Move,             Qt::Key_A,      Qt::NoModifier},
    {ControlAction::Move,             Qt::Key_D,      Qt::NoModifier},
    {ControlAction::GameSpeed,        Qt::Key_W,      Qt::NoModifier},
    {ControlAction::GameSpeed,        Qt::Key_S,      Qt::NoModifier},
    {ControlAction::TogglePause,      Qt::Key_Space,  Qt::NoModifier},
    {ControlAction::CopyTemporary,    Qt::Key_C,      Qt::NoModifier},
    {ControlAction::CreateBlueprint,  Qt::Key_C,      Qt::ControlModifier},
    {ControlAction::PasteTemporary,   Qt::Key_V,      Qt::NoModifier},
    {ControlAction::OpenBlueprints,   Qt::Key_V,      Qt::ControlModifier},
    // One binding, two badges: Shift picks the rotation direction and is read by the
    // handler, the way a build hotkey's digit is (REQ-BLD-ROTATE). Both entries match
    // the same press, and both resolve to the same action, so listing them twice costs
    // nothing and is what puts "R" and "Shift+R" on the row.
    {ControlAction::Rotate,           Qt::Key_R,      Qt::NoModifier},
    {ControlAction::Rotate,           Qt::Key_R,      Qt::ShiftModifier},
    {ControlAction::ExitMode,         Qt::Key_Q,      Qt::NoModifier},
    {ControlAction::ClearSelection,   Qt::Key_Q,      Qt::NoModifier},
    {ControlAction::EnterDeconstruct, Qt::Key_Q,      Qt::NoModifier},
    {ControlAction::OpenMenu,         Qt::Key_Escape, Qt::NoModifier},
};

const MouseBindingEntry MOUSE_BINDINGS[] = {
    {ControlAction::Select,             MouseBinding::LeftClick,     BindingCondition::Always},
    {ControlAction::Place,              MouseBinding::LeftClick,     BindingCondition::Always},
    {ControlAction::ApplySettings,      MouseBinding::LeftClick,     BindingCondition::Always},
    {ControlAction::ToggleDeconstruct,  MouseBinding::LeftClick,     BindingCondition::Always},
    {ControlAction::SelectArea,         MouseBinding::LeftDrag,      BindingCondition::Always},
    {ControlAction::PlaceBeltLine,      MouseBinding::LeftDrag,      BindingCondition::Always},
    {ControlAction::DeconstructArea,    MouseBinding::LeftDrag,      BindingCondition::Always},
    {ControlAction::AddToSelection,     MouseBinding::CtrlLeftClick, BindingCondition::Always},
    {ControlAction::AddAreaToSelection, MouseBinding::CtrlLeftDrag,  BindingCondition::Always},
    {ControlAction::CancelBeltLine,     MouseBinding::RightClick,    BindingCondition::Always},
    {ControlAction::ExitMode,           MouseBinding::RightClick,    BindingCondition::NotDraggingBelt},
};

bool isConditionMet(BindingCondition condition, const ControlContext& context)
{
    if (condition == BindingCondition::NotDraggingBelt) { return !context.draggingBelt; }
    return true;
}

bool isPlacementMode(const ControlContext& context)
{
    return context.mode == BuildMode::Builder || context.mode == BuildMode::Blueprint;
}

std::vector<ControlAction> filterAvailable(const std::vector<ControlAction>& actions,
                                           const ControlContext& context)
{
    std::vector<ControlAction> available;
    for (ControlAction action : actions)
    {
        if (isControlActionAvailable(action, context)) { available.push_back(action); }
    }
    return available;
}

}  // namespace

bool isControlActionAvailable(ControlAction action, const ControlContext& context)
{
    switch (action)
    {
    case ControlAction::None:
        return false;

    case ControlAction::Move:
    case ControlAction::GameSpeed:
    case ControlAction::TogglePause:
    case ControlAction::OpenBlueprints:
    case ControlAction::OpenMenu:
        return true;

    // Does nothing until something has been captured with C, so it is not offered
    // before then (REQ-UI-BLUEPRINT-TEMP, REQ-UI-CONTROLS-ACCURACY).
    case ControlAction::PasteTemporary:
        return context.temporaryBlueprintExists;

    case ControlAction::Select:
    case ControlAction::SelectArea:
    case ControlAction::AddToSelection:
    case ControlAction::AddAreaToSelection:
        return context.mode == BuildMode::None;

    // The three cases of Q, in the order REQ-UI-HOTKEYS evaluates them: it leaves the
    // active mode, else clears the selection, else enters deconstruct mode. A selection
    // and a build mode never coexist (REQ-UI-SELECTION-EXCLUSIVE), so the first two are
    // already disjoint; what the empty-selection condition below adds is the third step,
    // which is why entering deconstruct mode by key takes two presses while something is
    // selected -- the Deconstruct button still gets there in one click.
    case ControlAction::ClearSelection:
        return context.mode == BuildMode::None
               && context.selection != ControlSelection::None;
    case ControlAction::EnterDeconstruct:
        return context.mode == BuildMode::None
               && context.selection == ControlSelection::None;

    // Both need something a blueprint can be made of; a selection of ships or debris
    // leaves them inert (REQ-UI-HOTKEYS).
    case ControlAction::CopyTemporary:
    case ControlAction::CreateBlueprint:
        return context.mode == BuildMode::None && context.placeableBuildingSelected;

    // Place and ApplySettings are the same click; which one it is depends on whether
    // the ghost under the cursor is a transfer target (REQ-UI-BLUEPRINT-TRANSFER).
    case ControlAction::Place:
        return isPlacementMode(context) && !context.hoveredGhostIsTransfer;
    case ControlAction::ApplySettings:
        return context.mode == BuildMode::Blueprint && context.hoveredGhostIsTransfer;

    // The only building type placed by dragging (REQ-BLD-BELT-DRAG).
    case ControlAction::PlaceBeltLine:
        return context.mode == BuildMode::Builder
               && context.builderType == BuildingType::Belt;

    case ControlAction::Rotate:
        return isPlacementMode(context);
    case ControlAction::CancelBeltLine:
        return context.draggingBelt;
    case ControlAction::ExitMode:
        return context.mode != BuildMode::None;

    case ControlAction::ToggleDeconstruct:
    case ControlAction::DeconstructArea:
        return context.mode == BuildMode::Deconstruct;
    }
    return false;
}

std::vector<ControlBinding> getControlActionBindings(ControlAction action,
                                                     const ControlContext& context)
{
    std::vector<ControlBinding> bindings;
    for (const MouseBindingEntry& entry : MOUSE_BINDINGS)
    {
        if (entry.action != action)                    { continue; }
        if (!isConditionMet(entry.condition, context)) { continue; }
        ControlBinding binding;
        binding.isMouse = true;
        binding.mouse   = entry.binding;
        bindings.push_back(binding);
    }
    for (const KeyBindingEntry& entry : KEY_BINDINGS)
    {
        if (entry.action != action) { continue; }
        ControlBinding binding;
        binding.key       = entry.key;
        binding.modifiers = entry.modifiers;
        bindings.push_back(binding);
    }
    return bindings;
}

ControlContextKind getControlContextKind(const ControlContext& context)
{
    switch (context.mode)
    {
    case BuildMode::Builder:     return ControlContextKind::Build;
    case BuildMode::Blueprint:   return ControlContextKind::Blueprint;
    case BuildMode::Deconstruct: return ControlContextKind::Deconstruct;
    case BuildMode::None:        break;
    }
    return context.selection == ControlSelection::None ? ControlContextKind::General
                                                       : ControlContextKind::Selection;
}

std::vector<ControlAction> getContextActions(const ControlContext& context)
{
    // The candidates of each context, in the order REQ-UI-CONTROLS-CONTENT lists them,
    // then filtered by availability.
    //
    // The General and Selection lists differ rather than one filtered list serving
    // both, because the additive-selection rows are an omission and not an
    // unavailability: Ctrl+click does work with nothing selected, it just picks the
    // object like a plain click would. "Add / remove from selection" is a row that
    // means nothing until there is a selection to add to, so it waits for one
    // (REQ-UI-CONTROLS-ACCURACY permits omitting an available binding).
    switch (getControlContextKind(context))
    {
    case ControlContextKind::Build:
    case ControlContextKind::Blueprint:
        return filterAvailable({ControlAction::Place, ControlAction::ApplySettings,
                                ControlAction::PlaceBeltLine, ControlAction::Rotate,
                                ControlAction::CancelBeltLine, ControlAction::ExitMode},
                               context);
    case ControlContextKind::Deconstruct:
        return filterAvailable({ControlAction::ToggleDeconstruct,
                                ControlAction::DeconstructArea, ControlAction::ExitMode},
                               context);
    // ClearSelection is listed last for the same reason ExitMode is above: the row that
    // backs the player out of the context sits at the bottom of the context's rows
    // wherever there is one (REQ-UI-CONTROLS-CONTENT).
    case ControlContextKind::Selection:
        return filterAvailable({ControlAction::Select, ControlAction::SelectArea,
                                ControlAction::AddToSelection,
                                ControlAction::AddAreaToSelection,
                                ControlAction::CopyTemporary,
                                ControlAction::CreateBlueprint,
                                ControlAction::ClearSelection},
                               context);
    case ControlContextKind::General:
        break;
    }
    return filterAvailable({ControlAction::Select, ControlAction::SelectArea,
                            ControlAction::EnterDeconstruct},
                           context);
}

std::vector<ControlAction> getAlwaysAvailableActions(const ControlContext& context)
{
    return filterAvailable({ControlAction::Move, ControlAction::GameSpeed,
                            ControlAction::TogglePause, ControlAction::PasteTemporary,
                            ControlAction::OpenBlueprints, ControlAction::OpenMenu},
                           context);
}

ControlAction resolveKeyAction(int key, Qt::KeyboardModifiers modifiers,
                               const ControlContext& context)
{
    // Ctrl distinguishes a chord from the bare key (Ctrl+C is not C); every other
    // modifier is ignored, so Shift+A still pans and Shift+R still rotates. This is
    // what the key handler has always done, and matching modifiers exactly instead
    // would silently drop those presses.
    const bool controlHeld = (modifiers & Qt::ControlModifier) != 0;
    for (const KeyBindingEntry& entry : KEY_BINDINGS)
    {
        if (entry.key != key) { continue; }
        const bool entryNeedsControl = (entry.modifiers & Qt::ControlModifier) != 0;
        if (entryNeedsControl != controlHeld) { continue; }
        if (isControlActionAvailable(entry.action, context)) { return entry.action; }
    }
    return ControlAction::None;
}

ControlAction resolveMouseAction(MouseBinding binding, const ControlContext& context)
{
    for (const MouseBindingEntry& entry : MOUSE_BINDINGS)
    {
        if (entry.binding != binding)                        { continue; }
        if (!isConditionMet(entry.condition, context))       { continue; }
        if (isControlActionAvailable(entry.action, context)) { return entry.action; }
    }
    return ControlAction::None;
}
