#pragma once

// input map — four sources, three concepts. the framework owns the slots and the mechanism; which
// key or button means what is the game's, declared through crStickConfig / crButtonConfig.
//
//              stick                    button                   pointer (crUi)
//   keyboard   4 scancodes              scancode                 —
//   gamepad    LEFT / RIGHT axes        gamepad button           —
//   touch      region claim, floating   crUi::ActionButton       first finger
//   mouse      — (a delta, not a pose)  button number            cursor
//
// clocks, and this is the part that bites:
//   pointer  render frame — crUi is redrawn every frame and its edges die with it
//   stick    sampled once per frame ahead of the fixed loop; every sim step that frame reads it
//   button   edges latch on the event and a sim step drains them, exactly once each. a render
//            frame runs 0..N sim steps, so frame-scoped edges would be lost at high fps and
//            repeated at low
//
// a touch is claimed in this order: menu widget > action face > stick region. a button's touch
// binding lives where it is drawn, not in crButtonConfig — a face that stopped being drawn stops
// claiming
//
// ActiveSource() names the row the player last meant something on, for prompts. it is advisory:
// every source stays live regardless, so a prompt reading stale for one frame costs nothing

#include <SDL3/SDL_assert.h>
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_scancode.h>
#include <SDL3/SDL_init.h>
#include <SDL3/SDL_events.h>

#include "crStruct.h"
#include "crUserData.h"

class crApp;

enum class EStickId : uint8_t
{
    LEFT = 0,
    RIGHT,

    _SIZE
};

// which device the player last meant something with, for prompts. keyboard and mouse are one source:
// the same hand position, and a key prompt should not swap out because a cursor moved
enum class EInputSource : uint8_t
{
    KEYBOARD_MOUSE = 0,
    GAMEPAD,
    TOUCH,

    _SIZE
};

// one analog stick, in virtual UI units so the reach stays thumb-sized on any density.
// a touch claims it by going down inside the region and keeps it until the touch ends
struct crStickConfig
{
    float2 regionMin;             // canvas coords (bottom-left, Y-up) — same space crUi lays out in
    float2 regionMax;
    float  radius   = 90.0f;      // distance to full deflection
    float  deadzone = 0.05f;      // fraction of radius; touch needs little, the pad needs much more

    // keyboard fallback. unbound by default — the framework has no opinion on which keys a game uses
    SDL_Scancode keyUp    = SDL_SCANCODE_UNKNOWN;
    SDL_Scancode keyDown  = SDL_SCANCODE_UNKNOWN;
    SDL_Scancode keyLeft  = SDL_SCANCODE_UNKNOWN;
    SDL_Scancode keyRight = SDL_SCANCODE_UNKNOWN;
};

// a button slot. what index 3 means is the game's business — FIRE and JUMP are game vocabulary,
// so the framework holds the slots and the bindings, never the names
struct crButtonConfig
{
    SDL_Scancode      key   = SDL_SCANCODE_UNKNOWN;
    SDL_GamepadButton pad   = SDL_GAMEPAD_BUTTON_INVALID;
    uint8_t           mouse = 0;   // SDL_BUTTON_LEFT is 1, so 0 reads as unbound

    // the touch binding is not here: crUi::ActionButton declares its rect where it draws, and this
    // system pulls it. an on-screen button that stopped being drawn stops claiming touches
};

// unified primary pointer — mouse or the first touch finger, whichever is acting. the UI's single
// input source. pressed/released are frame edges: events set them, EndFrame() clears them once the
// frame has consumed them
struct crPointerInput
{
    float2 posPx;             // window pixels, top-left origin Y-down (raw SDL space)
    bool   down     = false;
    bool   pressed  = false;   // went down this frame
    bool   released = false;   // went up this frame
};

// one tracked touch, alongside the unified pointer rather than replacing it — the UI still reads
// the pointer. released fingers survive the frame they end on so the edge can be consumed
struct crFingerInput
{
    SDL_FingerID id;
    float2       posPx;        // same space as crPointerInput
    float2       startPosPx;   // where it went down — a floating-origin stick measures from here
    bool         down     = false;
    bool         pressed  = false;
    bool         released = false;   // a canceled touch never sets this (see UpdateInput)
};

class crInputSystem
{
public:
    static constexpr float CAMERA_ZOOM_STEP   = 1.1f;   // wheel dolly factor (distance clamp lives in crCamera)
    static constexpr float CAMERA_ORBIT_SPEED = 0.005f; // radians per dragged pixel (orbit / look)
    static constexpr float CAMERA_FLY_SPEED   = 10.0f;  // meters per second (RMB + WASDQE)
    static constexpr float CAMERA_FLY_BOOST   = 5.0f;   // LSHIFT multiplier

    // our own cap, not the device's — SDL exposes no per-device touch-point limit.
    // panels top out around 10 (iOS reports 5, iPad 11); touches past this are dropped
    static constexpr int32_t MAX_TRACKED_FINGERS = 10;

    // SDL applies no deadzone of its own and documents thumbsticks as "centered within ~8000 of
    // zero" — a quarter of the range — so the pad needs far more slack than a touch origin does
    static constexpr float GAMEPAD_DEADZONE = 0.20f;

    // travel before a moving mouse takes the active source. presses and taps switch it outright,
    // but motion arrives unasked — a bumped desk would otherwise flip the prompts mid-game
    static constexpr float MOUSE_ACTIVATE_PX = 16.0f;

    static constexpr int32_t MAX_BUTTONS = 8;

    // dev free-camera (mouse orbit / RMB fly / wheel dolly) — off by default so it never fights a
    // game that owns the camera and the pointer; a game or console can flip it on
    bool devCamera = false;

private:
    crPointerInput _pointer;
    SDL_FingerID   _activeFingerId = 0;
    bool           _fingerActive   = false;   // a finger currently drives the pointer (first finger wins; others are ignored)

    crFingerInput _fingers[ MAX_TRACKED_FINGERS ];
    int32_t       _fingerCount = 0;

    crStickConfig _stickConfig[ static_cast<int32_t>( EStickId::_SIZE ) ];
    float2        _stickValue[ static_cast<int32_t>( EStickId::_SIZE ) ];
    SDL_FingerID  _stickFinger[ static_cast<int32_t>( EStickId::_SIZE ) ] = {};
    bool          _stickClaimed[ static_cast<int32_t>( EStickId::_SIZE ) ] = {};   // finger id 0 is legal, so claims need their own flag

    SDL_Gamepad* _gamepad = nullptr;   // player one only — a second pad would need index mapping

    // starts on keyboard+mouse even on a phone: SDL reports a mouse in a mobile browser too, so
    // there is nothing to detect from. the first touch corrects it before a prompt is ever read
    EInputSource _activeSource   = EInputSource::KEYBOARD_MOUSE;
    float        _mouseMoveAccum = 0.0f;

    // counts, not flags: a step's window is FIXED_TIMESTEP / simSpeed of real time — 167ms at 0.1x —
    // and a flag would quietly fold a burst of taps in that window into one
    crButtonConfig _buttonConfig[ MAX_BUTTONS ];
    // event sources and touch are tracked apart, then OR'd: one bool would let a key release drop a
    // button a finger is still holding
    bool           _buttonEventDown[ MAX_BUTTONS ]         = {};
    bool           _buttonTouchDown[ MAX_BUTTONS ]         = {};
    SDL_FingerID   _buttonFinger[ MAX_BUTTONS ]            = {};
    int32_t        _buttonPointerSlot                      = -1;   // the action face the pointer is holding, if any
    bool           _buttonDown[ MAX_BUTTONS ]              = {};
    uint8_t        _buttonLatchPress[ MAX_BUTTONS ]        = {};
    uint8_t        _buttonLatchRelease[ MAX_BUTTONS ]      = {};
    uint8_t        _buttonPressCount[ MAX_BUTTONS ]        = {};   // this sim step's
    uint8_t        _buttonReleaseCount[ MAX_BUTTONS ]      = {};

public:
    void Init();
    void Cleanup();

    const crPointerInput& Pointer() const
    {
        return _pointer;
    }

    int32_t FingerCount() const
    {
        return _fingerCount;
    }
    const crFingerInput& Finger( int32_t index ) const
    {
        SDL_assert( ( index >= 0 ) && ( index < _fingerCount ) );
        return _fingers[ index ];
    }
    const crFingerInput* FindFinger( SDL_FingerID id ) const;   // nullptr once the touch is gone — a claim holder zeroes itself on that

    void ConfigureStick( EStickId id, const crStickConfig* config );   // Init-time; regions are in virtual UI units
    float2 Stick( EStickId id ) const
    {
        return _stickValue[ static_cast<int32_t>( id ) ];
    }
    bool IsStickTouched( EStickId id ) const   // a touch owns it right now — the visual only draws then
    {
        return _stickClaimed[ static_cast<int32_t>( id ) ];
    }
    float2 StickOrigin( EStickId id ) const;   // virtual units; where the claiming touch went down. zero when unclaimed

    void ConfigureButton( int32_t index, const crButtonConfig* config );   // Init-time; the game owns what each slot means
    bool ButtonDown( int32_t index ) const
    {
        return _buttonDown[ index ];
    }
    // edges belong to the sim step, not the render frame — BeginSimStep hands each one over exactly once
    bool ButtonPressed( int32_t index ) const
    {
        return ( _buttonPressCount[ index ] > 0 );
    }
    bool ButtonReleased( int32_t index ) const
    {
        return ( _buttonReleaseCount[ index ] > 0 );
    }
    int32_t ButtonPressCount( int32_t index ) const   // more than one when the step covered a burst
    {
        return _buttonPressCount[ index ];
    }
    int32_t ButtonReleaseCount( int32_t index ) const
    {
        return _buttonReleaseCount[ index ];
    }

    EInputSource ActiveSource() const
    {
        return _activeSource;
    }

    void UpdateInput( const crApp* app, const SDL_Event* event );   // mouse + touch + gamepad + button edges (crApp::HandleEvent)
    void PollInput( const crApp* app );                             // once per frame, ahead of the fixed-step loop — sticks and on-screen buttons
    void BeginSimStep();                                            // top of crApp::FixedUpdate — drains the button latch
    void EndFrame();                                                // clear the pressed/released edges — call after the frame consumed them

    SDL_AppResult TEST_HandleUserInput( const crApp* app, SDL_Event* event );
    void          TEST_PollUserInput( const crApp* app, float dt );   // per-frame key polling (fly mode)

private:
    static float2 FingerToPixels( const crApp* app, const SDL_TouchFingerEvent& tfinger );   // normalized touch -> viewport pixels
    static float2 PixelsToCanvas( const crApp* app, float2 posPx );                          // pointer pixels (Y-down) -> ui canvas units (Y-up)
    static float2 ApplyStickShape( float2 raw, float deadzone );   // clamp to the unit circle, drop the deadzone, remap the rest to 0..1
    int32_t       FindFingerSlot( SDL_FingerID id ) const;   // -1 when absent
    bool          IsFingerClaimed( SDL_FingerID id ) const;   // a stick or an action face holds it
    void          UpdatePointerOwner();

    void UpdateTouchStick( const crApp* app, EStickId id, float uiScale );
    bool ReadGamepadStick( EStickId id, float2* out ) const;    // false when there is no pad or it sits inside the deadzone
    bool ReadKeyboardStick( EStickId id, float2* out ) const;   // false when nothing is bound or nothing is held
    void NoteSource( EInputSource source );
    void LatchButtonEdge( SDL_Scancode key, SDL_GamepadButton pad, uint8_t mouse, bool down );   // pass unused bindings as UNKNOWN / INVALID / 0
    void ApplyButtonState( int32_t slot );   // folds the two sources together and latches the transition
    void UpdateTouchButtons( const crApp* app, float uiScale );

    static void TEST_OnUserSettingsLoaded( bool success, crUserDataBlob blob, void* context );
    static void TEST_OnUserSettingsSaved( bool success, void* context );
};
