#include "crInputSystem.h"

#include <SDL3/SDL_log.h>
#include <imgui.h>

#include "crApp.h"

#include "crAudio.h"
#include "crCamera.h"
#include "crMath.h"
#include "crUi.h"

void crInputSystem::Init()
{

}
void crInputSystem::Cleanup()
{
    SDL_CloseGamepad( _gamepad );
    _gamepad = nullptr;
}

const crFingerInput* crInputSystem::FindFinger( SDL_FingerID id ) const
{
    const int32_t slot = FindFingerSlot( id );
    return ( slot >= 0 ) ? &_fingers[ slot ] : nullptr;
}

void crInputSystem::ConfigureStick( EStickId id, const crStickConfig* config )
{
    _stickConfig[ static_cast<int32_t>( id ) ] = *config;
}
float2 crInputSystem::StickOrigin( EStickId id ) const
{
    const int32_t s = static_cast<int32_t>( id );
    if( _stickClaimed[ s ] == false )
        return float2( 0.0f, 0.0f );

    const crFingerInput* finger = FindFinger( _stickFinger[ s ] );
    if( finger == nullptr )
        return float2( 0.0f, 0.0f );

    return finger->startPosPx;   // caller scales — this stays in the pointer's pixel space
}

void crInputSystem::ConfigureButton( int32_t index, const crButtonConfig* config )
{
    SDL_assert( ( index >= 0 ) && ( index < MAX_BUTTONS ) );
    _buttonConfig[ index ] = *config;
}
void crInputSystem::UpdateInput( const crApp* app, const SDL_Event* event )
{
    // we read fingers and the mouse natively, so SDL's synthetic copies would count one touch twice.
    // the synthesis still has to stay on: ImGui's SDL3 backend has no finger path and needs it
    const bool synthetic = ( ( event->type == SDL_EVENT_MOUSE_MOTION )      && ( event->motion.which  == SDL_TOUCH_MOUSEID ) ) ||
                           ( ( event->type == SDL_EVENT_MOUSE_BUTTON_DOWN ) && ( event->button.which  == SDL_TOUCH_MOUSEID ) ) ||
                           ( ( event->type == SDL_EVENT_MOUSE_BUTTON_UP )   && ( event->button.which  == SDL_TOUCH_MOUSEID ) ) ||
                           ( ( event->type == SDL_EVENT_FINGER_DOWN )       && ( event->tfinger.touchID == SDL_MOUSE_TOUCHID ) ) ||
                           ( ( event->type == SDL_EVENT_FINGER_MOTION )     && ( event->tfinger.touchID == SDL_MOUSE_TOUCHID ) ) ||
                           ( ( event->type == SDL_EVENT_FINGER_UP )         && ( event->tfinger.touchID == SDL_MOUSE_TOUCHID ) );
    if( synthetic )
        return;

    // NOTE: mouse coords are SDL window coordinates; finger coords are normalized and scaled by the
    // render viewport. on HiDPI displays window units and pixels can differ — verify when targeting one
    switch( event->type )
    {
    case SDL_EVENT_MOUSE_MOTION:
        if( _fingerActive == false )
            _pointer.posPx = float2( event->motion.x, event->motion.y );

        if( _activeSource != EInputSource::KEYBOARD_MOUSE )
        {
            _mouseMoveAccum += ( crMath::Abs( event->motion.xrel ) + crMath::Abs( event->motion.yrel ) );
            if( _mouseMoveAccum >= MOUSE_ACTIVATE_PX )
                NoteSource( EInputSource::KEYBOARD_MOUSE );
        }
        break;

    case SDL_EVENT_MOUSE_BUTTON_DOWN:
        NoteSource( EInputSource::KEYBOARD_MOUSE );
        if( ( event->button.button == SDL_BUTTON_LEFT ) && ( _fingerActive == false ) )
        {
            _pointer.posPx   = float2( event->button.x, event->button.y );
            _pointer.down    = true;
            _pointer.pressed = true;
        }
        // only the left button is shared with the UI, so only it defers to a menu widget. the others
        // are game controls like any key, and a key does not stop working because a cursor moved
        if( ( event->button.button != SDL_BUTTON_LEFT ) ||
            ( app->ui->IsOverWidget( PixelsToCanvas( app, float2( event->button.x, event->button.y ) ) ) == false ) )
            LatchButtonEdge( SDL_SCANCODE_UNKNOWN, SDL_GAMEPAD_BUTTON_INVALID, event->button.button, true );
        break;

    case SDL_EVENT_MOUSE_BUTTON_UP:
        if( ( event->button.button == SDL_BUTTON_LEFT ) && ( _fingerActive == false ) )
        {
            _pointer.posPx    = float2( event->button.x, event->button.y );
            _pointer.down     = false;
            _pointer.released = true;
        }
        LatchButtonEdge( SDL_SCANCODE_UNKNOWN, SDL_GAMEPAD_BUTTON_INVALID, event->button.button, false );   // an up is released wherever it happens
        break;

    case SDL_EVENT_FINGER_DOWN:
    {
        NoteSource( EInputSource::TOUCH );

        const float2 posPx = FingerToPixels( app, event->tfinger );

        if( _fingerCount < MAX_TRACKED_FINGERS )
        {
            crFingerInput& finger = _fingers[ _fingerCount ];
            finger.id         = event->tfinger.fingerID;
            finger.posPx      = posPx;
            finger.startPosPx = posPx;
            finger.down       = true;
            finger.pressed    = true;
            finger.released   = false;
            ++_fingerCount;
        }

        if( _fingerActive == false )
        {
            _fingerActive    = true;
            _activeFingerId  = event->tfinger.fingerID;
            _pointer.posPx   = posPx;
            _pointer.down    = true;
            _pointer.pressed = true;
        }
        break;
    }

    case SDL_EVENT_FINGER_MOTION:
    {
        const float2  posPx = FingerToPixels( app, event->tfinger );
        const int32_t slot  = FindFingerSlot( event->tfinger.fingerID );
        if( slot >= 0 )
            _fingers[ slot ].posPx = posPx;

        if( _fingerActive && ( event->tfinger.fingerID == _activeFingerId ) )
            _pointer.posPx = posPx;
        break;
    }

    case SDL_EVENT_FINGER_UP:
    {
        const float2  posPx = FingerToPixels( app, event->tfinger );
        const int32_t slot  = FindFingerSlot( event->tfinger.fingerID );
        if( slot >= 0 )
        {
            _fingers[ slot ].posPx    = posPx;
            _fingers[ slot ].down     = false;
            _fingers[ slot ].released = true;   // EndFrame drops the slot once the edge is consumed
        }

        if( _fingerActive && ( event->tfinger.fingerID == _activeFingerId ) )
        {
            _pointer.posPx    = posPx;
            _pointer.down     = false;
            _pointer.released = true;
            _fingerActive     = false;
        }
        break;
    }

    case SDL_EVENT_FINGER_CANCELED:
    {
        // the OS revoked the touch (system gesture, palm rejection) — no UP will follow.
        // down drops without a released edge, so a canceled press never counts as a click
        const int32_t slot = FindFingerSlot( event->tfinger.fingerID );
        if( slot >= 0 )
            _fingers[ slot ].down = false;

        if( _fingerActive && ( event->tfinger.fingerID == _activeFingerId ) )
        {
            _pointer.down = false;
            _fingerActive = false;
        }
        break;
    }

    case SDL_EVENT_KEY_DOWN:
        NoteSource( EInputSource::KEYBOARD_MOUSE );
        // auto-repeat is not filtered: the state check inside swallows it, and a repeat is the only
        // way back if the first press arrived while something else held the keyboard
        LatchButtonEdge( event->key.scancode, SDL_GAMEPAD_BUTTON_INVALID, 0, true );
        break;

    case SDL_EVENT_KEY_UP:
        LatchButtonEdge( event->key.scancode, SDL_GAMEPAD_BUTTON_INVALID, 0, false );
        break;

    case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
        NoteSource( EInputSource::GAMEPAD );
        LatchButtonEdge( SDL_SCANCODE_UNKNOWN, static_cast<SDL_GamepadButton>( event->gbutton.button ), 0, true );
        break;

    case SDL_EVENT_GAMEPAD_BUTTON_UP:
        LatchButtonEdge( SDL_SCANCODE_UNKNOWN, static_cast<SDL_GamepadButton>( event->gbutton.button ), 0, false );
        break;

    case SDL_EVENT_GAMEPAD_ADDED:
        if( _gamepad == nullptr )
        {
            _gamepad = SDL_OpenGamepad( event->gdevice.which );
            SDL_LogInfo( SDL_LOG_CATEGORY_APPLICATION, "crInputSystem: gamepad opened( %s )",
                         ( _gamepad != nullptr ) ? SDL_GetGamepadName( _gamepad ) : SDL_GetError() );
        }
        break;

    case SDL_EVENT_GAMEPAD_REMOVED:
        if( ( _gamepad != nullptr ) && ( event->gdevice.which == SDL_GetGamepadID( _gamepad ) ) )
        {
            SDL_CloseGamepad( _gamepad );
            _gamepad = nullptr;
        }
        break;

    default:
        break;
    }
}
void crInputSystem::PollInput( const crApp* app )
{
    const float uiScale = crUi::RasterScale( app->camera->Viewport() );   // px per virtual unit

    for( int32_t s = 0; s < static_cast<int32_t>( EStickId::_SIZE ); ++s )
    {
        const EStickId id = static_cast<EStickId>( s );

        UpdateTouchStick( app, id, uiScale );
        if( _stickClaimed[ s ] )
            continue;   // a held touch outranks the rest — you cannot hold a touch stick by accident

        float2 value = float2( 0.0f, 0.0f );
        if( ReadGamepadStick( id, &value ) )
            NoteSource( EInputSource::GAMEPAD );   // already past the deadzone, so this is a real push
        else if( ReadKeyboardStick( id, &value ) == false )
            value = float2( 0.0f, 0.0f );

        _stickValue[ s ] = value;
    }

    UpdateTouchButtons( app, uiScale );
    UpdatePointerOwner();   // last: it reads the claims the two calls above just settled
}
void crInputSystem::BeginSimStep()
{
    for( int32_t i = 0; i < MAX_BUTTONS; ++i )
    {
        _buttonPressCount[ i ]   = _buttonLatchPress[ i ];
        _buttonReleaseCount[ i ] = _buttonLatchRelease[ i ];

        _buttonLatchPress[ i ]   = 0;   // handed over — the next step sees nothing unless it happens again
        _buttonLatchRelease[ i ] = 0;
    }
}
void crInputSystem::EndFrame()
{
    _pointer.pressed  = false;
    _pointer.released = false;

    // stable compaction, not a swap with the tail: the slots stay in the order the touches began,
    // which is the only record of which came first
    int32_t kept = 0;
    for( int32_t i = 0; i < _fingerCount; ++i )
    {
        if( _fingers[ i ].down == false )
            continue;   // its edge was consumed this frame — the slot goes

        _fingers[ i ].pressed  = false;
        _fingers[ i ].released = false;

        if( kept != i )
            _fingers[ kept ] = _fingers[ i ];
        ++kept;
    }
    _fingerCount = kept;
}

SDL_AppResult crInputSystem::TEST_HandleUserInput( const crApp* app, SDL_Event* event )
{
    if( event->type == SDL_EVENT_KEY_DOWN )
    {
        if( event->key.key == SDLK_F9 &&
            event->key.repeat == false )
        {
            if( ( app->userData->IsStorageReady() == true ) &&
                ( app->userData->IsPending() == false ) )
            {
                if( ( event->key.mod & SDL_KMOD_LSHIFT ) != 0 )
                {
                    crUserData_UserSettings s;
                    s.myInt    = static_cast<int64_t>( SDL_GetTicks() );
                    s.myFloat  = app->camera->Distance();
                    s.myString = SDL_strdup( "test_string" );

                    SDL_LogTrace( SDL_LOG_CATEGORY_APPLICATION, "crInputSystem: LSHIFT + F9: save requested( distance:%.3f )", s.myFloat );
                    app->userData->RequestSaveUserSettings( TEST_OnUserSettingsSaved, s, nullptr );
                    s.Free();
                }
                else
                {
                    SDL_LogTrace( SDL_LOG_CATEGORY_APPLICATION, "crInputSystem: F9: load requested" );
                    app->userData->RequestLoadUserSettings( TEST_OnUserSettingsLoaded, app->camera );
                }
            }
        }
    }

    if( devCamera == false )
        return SDL_APP_CONTINUE;   // dev free-camera off — mouse belongs to the game

    if( event->type == SDL_EVENT_MOUSE_WHEEL )
    {
        if( event->wheel.y > 0.0f )
        {
            app->camera->Dolly( 1.0f / CAMERA_ZOOM_STEP );   // wheel up = closer
        }
        else if( event->wheel.y < 0.0f )
        {
            app->camera->Dolly( CAMERA_ZOOM_STEP );
        }
    }

    if( event->type == SDL_EVENT_MOUSE_MOTION )
    {
        // Unity-editor style: Alt+LMB orbit / LMB or RMB look / MMB pan
        if( ( event->motion.state & SDL_BUTTON_LMASK ) != 0 )
        {
            if( ( SDL_GetModState() & SDL_KMOD_ALT ) != 0 )
            {
                app->camera->Orbit( event->motion.xrel * CAMERA_ORBIT_SPEED,
                                    event->motion.yrel * CAMERA_ORBIT_SPEED );
            }
            else
            {
                app->camera->Look( -event->motion.xrel * CAMERA_ORBIT_SPEED,
                                   event->motion.yrel * CAMERA_ORBIT_SPEED );
            }
        }
        else if( ( event->motion.state & SDL_BUTTON_RMASK ) != 0 )
        {
            app->camera->Look( -event->motion.xrel * CAMERA_ORBIT_SPEED,
                               event->motion.yrel * CAMERA_ORBIT_SPEED );
        }
        else if( ( event->motion.state & SDL_BUTTON_MMASK ) != 0 )
        {
            app->camera->Pan( event->motion.xrel, event->motion.yrel );
        }
    }

    return SDL_APP_CONTINUE;
}
void crInputSystem::TEST_PollUserInput( const crApp* app, float dt )
{
    if( devCamera == false )
        return;

    const ImGuiIO& io = ImGui::GetIO();
    if( io.WantCaptureMouse || io.WantCaptureKeyboard )
        return;

    float mx = 0.0f;
    float my = 0.0f;
    const SDL_MouseButtonFlags buttons = SDL_GetMouseState( &mx, &my );
    if( ( buttons & SDL_BUTTON_RMASK ) == 0 )
        return;   // fly mode only while the right button is held

    const bool* keys = SDL_GetKeyboardState( nullptr );

    float3 dir = {};
    if( keys[ SDL_SCANCODE_W ] )
        dir.z += 1.0f;
    if( keys[ SDL_SCANCODE_S ] )
        dir.z -= 1.0f;
    if( keys[ SDL_SCANCODE_D ] )
        dir.x += 1.0f;
    if( keys[ SDL_SCANCODE_A ] )
        dir.x -= 1.0f;
    if( keys[ SDL_SCANCODE_E ] )
        dir.y += 1.0f;
    if( keys[ SDL_SCANCODE_Q ] )
        dir.y -= 1.0f;

    if( ( dir.x == 0.0f ) && ( dir.y == 0.0f ) && ( dir.z == 0.0f ) )
        return;

    float speed = CAMERA_FLY_SPEED;
    if( keys[ SDL_SCANCODE_LSHIFT ] )
        speed *= CAMERA_FLY_BOOST;

    app->camera->Fly( crMath::Normalize3( dir ), speed * dt );
}

/*static*/ float2 crInputSystem::FingerToPixels( const crApp* app, const SDL_TouchFingerEvent& tfinger )
{
    const int2 vp = app->camera->Viewport();
    return float2( tfinger.x * static_cast<float>( vp.x ), tfinger.y * static_cast<float>( vp.y ) );
}
/*static*/ float2 crInputSystem::PixelsToCanvas( const crApp* app, float2 posPx )
{
    const float uiScale = crUi::RasterScale( app->camera->Viewport() );
    return float2( posPx.x / uiScale, app->ui->CanvasSize().y - ( posPx.y / uiScale ) );
}
/*static*/ float2 crInputSystem::ApplyStickShape( float2 raw, float deadzone )
{
    const float length = crMath::Sqrt( ( raw.x * raw.x ) + ( raw.y * raw.y ) );
    if( length <= 0.0f )
        return float2( 0.0f, 0.0f );

    // the two axes are independent, so the raw domain is a square — a diagonal reaches 1.41
    const float clamped = ( length > 1.0f ) ? 1.0f : length;
    if( clamped <= deadzone )
        return float2( 0.0f, 0.0f );

    // remap deadzone..1 back onto 0..1, or the value would jump at the deadzone edge
    const float scale = ( ( clamped - deadzone ) / ( 1.0f - deadzone ) ) / length;
    return float2( raw.x * scale, raw.y * scale );
}
int32_t crInputSystem::FindFingerSlot( SDL_FingerID id ) const
{
    for( int32_t i = 0; i < _fingerCount; ++i )
    {
        if( _fingers[ i ].id == id )
            return i;
    }
    return -1;
}
bool crInputSystem::IsFingerClaimed( SDL_FingerID id ) const
{
    for( int32_t s = 0; s < static_cast<int32_t>( EStickId::_SIZE ); ++s )
    {
        if( _stickClaimed[ s ] && ( _stickFinger[ s ] == id ) )
            return true;
    }
    for( int32_t b = 0; b < MAX_BUTTONS; ++b )
    {
        if( _buttonTouchDown[ b ] && ( _buttonFinger[ b ] == id ) )
            return true;
    }
    return false;
}
void crInputSystem::UpdatePointerOwner()
{
    if( ( _fingerActive == false ) || ( IsFingerClaimed( _activeFingerId ) == false ) )
        return;

    // hand it to the oldest unclaimed touch (the array keeps press order), or holding a stick would
    // make every menu widget unpressable
    for( int32_t i = 0; i < _fingerCount; ++i )
    {
        if( ( _fingers[ i ].down == false ) || IsFingerClaimed( _fingers[ i ].id ) )
            continue;

        _activeFingerId  = _fingers[ i ].id;
        _pointer.posPx   = _fingers[ i ].posPx;
        _pointer.down    = true;
        _pointer.pressed = true;   // replay the press crUi never saw — it arms on that edge alone
        return;
    }

    _fingerActive = false;
    _pointer.down = false;   // no released edge to forge: crUi::End clears its active widget on its own
}

void crInputSystem::UpdateTouchStick( const crApp* app, EStickId id, float uiScale )
{
    const int32_t        s      = static_cast<int32_t>( id );
    const crStickConfig& config = _stickConfig[ s ];

    if( _stickClaimed[ s ] )
    {
        const crFingerInput* finger = FindFinger( _stickFinger[ s ] );
        if( ( finger == nullptr ) || ( finger->down == false ) )
        {// released, or the OS revoked it — either way the stick must fall to zero, not stick
            _stickClaimed[ s ] = false;
            _stickValue[ s ]   = float2( 0.0f, 0.0f );
            return;
        }

        // pixels are Y-down, the stick is Y-up
        const float2 raw = float2( ( finger->posPx.x - finger->startPosPx.x ) / ( config.radius * uiScale ),
                                   ( finger->startPosPx.y - finger->posPx.y ) / ( config.radius * uiScale ) );
        _stickValue[ s ] = ApplyStickShape( raw, config.deadzone );
        return;
    }

    _stickValue[ s ] = float2( 0.0f, 0.0f );

    if( config.radius <= 0.0f )
        return;   // never configured

    const float canvasH = app->ui->CanvasSize().y;

    for( int32_t i = 0; i < _fingerCount; ++i )
    {
        const crFingerInput& finger = _fingers[ i ];
        if( ( finger.down == false ) || finger.pressed )
            continue;   // one frame late on purpose: crUi publishes its capture only at End()

        bool takenByOther = false;
        for( int32_t o = 0; o < static_cast<int32_t>( EStickId::_SIZE ); ++o )
            takenByOther = takenByOther || ( _stickClaimed[ o ] && ( _stickFinger[ o ] == finger.id ) );
        if( takenByOther )
            continue;

        // both tests read where the touch landed: a widget keeps it for good, and the region is
        // only a gate on the way in — a drag may leave it freely
        const float2 at = float2( finger.startPosPx.x / uiScale, canvasH - ( finger.startPosPx.y / uiScale ) );
        if( app->ui->IsOverWidget( at ) || ( app->ui->ActionSlotAt( at ) >= 0 ) )
            continue;   // a menu widget or an action face already owns this touch

        if( ( at.x < config.regionMin.x ) || ( at.x > config.regionMax.x ) ||
            ( at.y < config.regionMin.y ) || ( at.y > config.regionMax.y ) )
            continue;

        _stickClaimed[ s ] = true;
        _stickFinger[ s ]  = finger.id;
        return;   // value stays zero — the touch is still at its own origin
    }
}
bool crInputSystem::ReadGamepadStick( EStickId id, float2* out ) const
{
    if( _gamepad == nullptr )
        return false;

    const SDL_GamepadAxis axisX = ( id == EStickId::LEFT ) ? SDL_GAMEPAD_AXIS_LEFTX : SDL_GAMEPAD_AXIS_RIGHTX;
    const SDL_GamepadAxis axisY = ( id == EStickId::LEFT ) ? SDL_GAMEPAD_AXIS_LEFTY : SDL_GAMEPAD_AXIS_RIGHTY;

    constexpr float INV_AXIS = 1.0f / 32767.0f;

    // SDL reports Y positive-down; the stick value is Y-up
    const float2 raw = float2(  static_cast<float>( SDL_GetGamepadAxis( _gamepad, axisX ) ) * INV_AXIS,
                               -static_cast<float>( SDL_GetGamepadAxis( _gamepad, axisY ) ) * INV_AXIS );

    *out = ApplyStickShape( raw, GAMEPAD_DEADZONE );
    return ( ( out->x != 0.0f ) || ( out->y != 0.0f ) );
}

bool crInputSystem::ReadKeyboardStick( EStickId id, float2* out ) const
{
    // ImGui masks key *events*, but this polls SDL's key array straight through — a console
    // being typed into would drive the stick unless we bow out here
    if( ImGui::GetIO().WantCaptureKeyboard )
        return false;

    const crStickConfig& config = _stickConfig[ static_cast<int32_t>( id ) ];
    const bool*          keys   = SDL_GetKeyboardState( nullptr );

    float2 raw = float2( 0.0f, 0.0f );
    if( ( config.keyRight != SDL_SCANCODE_UNKNOWN ) && keys[ config.keyRight ] )
        raw.x += 1.0f;
    if( ( config.keyLeft != SDL_SCANCODE_UNKNOWN ) && keys[ config.keyLeft ] )
        raw.x -= 1.0f;
    if( ( config.keyUp != SDL_SCANCODE_UNKNOWN ) && keys[ config.keyUp ] )
        raw.y += 1.0f;
    if( ( config.keyDown != SDL_SCANCODE_UNKNOWN ) && keys[ config.keyDown ] )
        raw.y -= 1.0f;

    *out = ApplyStickShape( raw, 0.0f );   // a key is held or it is not — nothing to threshold
    return ( ( out->x != 0.0f ) || ( out->y != 0.0f ) );
}

void crInputSystem::NoteSource( EInputSource source )
{
    if( _activeSource == source )
        return;

    _activeSource   = source;
    _mouseMoveAccum = 0.0f;
}
void crInputSystem::LatchButtonEdge( SDL_Scancode key, SDL_GamepadButton pad, uint8_t mouse, bool down )
{
    for( int32_t i = 0; i < MAX_BUTTONS; ++i )
    {
        const crButtonConfig& config = _buttonConfig[ i ];

        const bool match = ( ( key   != SDL_SCANCODE_UNKNOWN )       && ( config.key   == key ) ) ||
                           ( ( pad   != SDL_GAMEPAD_BUTTON_INVALID ) && ( config.pad   == pad ) ) ||
                           ( ( mouse != 0 )                          && ( config.mouse == mouse ) );
        if( match == false )
            continue;

        _buttonEventDown[ i ] = down;
        ApplyButtonState( i );
    }
}
void crInputSystem::ApplyButtonState( int32_t slot )
{
    const bool down = ( _buttonEventDown[ slot ] || _buttonTouchDown[ slot ] || ( _buttonPointerSlot == slot ) );

    // edges come from the state change, never from the raw event: a lost DOWN (ImGui held the
    // keyboard, focus moved) would otherwise leave its UP to latch a release with no press.
    // counts saturate — wrapping to zero would erase the whole step instead of under-reporting it
    if( down )
    {
        if( ( _buttonDown[ slot ] == false ) && ( _buttonLatchPress[ slot ] < UINT8_MAX ) )
            ++_buttonLatchPress[ slot ];
    }
    else
    {
        if( _buttonDown[ slot ] && ( _buttonLatchRelease[ slot ] < UINT8_MAX ) )
            ++_buttonLatchRelease[ slot ];
    }

    _buttonDown[ slot ] = down;
}
void crInputSystem::UpdateTouchButtons( const crApp* app, float uiScale )
{
    const float canvasH = app->ui->CanvasSize().y;

    for( int32_t i = 0; i < MAX_BUTTONS; ++i )
    {
        if( _buttonTouchDown[ i ] )
        {// held until the finger ends — sliding off keeps it down, the same rule a stick claim uses
            const crFingerInput* finger = FindFinger( _buttonFinger[ i ] );
            if( ( finger != nullptr ) && finger->down )
                continue;

            _buttonTouchDown[ i ] = false;
            ApplyButtonState( i );
        }
    }

    {// the mouse presses an action face the same way a finger does, and holds it the same way too
        if( _buttonPointerSlot >= 0 )
        {
            if( _pointer.down == false )
            {
                const int32_t slot  = _buttonPointerSlot;
                _buttonPointerSlot  = -1;
                ApplyButtonState( slot );
            }
        }
        else if( _pointer.pressed && ( _fingerActive == false ) )
        {
            const int32_t slot = app->ui->ActionSlotAt( PixelsToCanvas( app, _pointer.posPx ) );
            if( slot >= 0 )
            {
                _buttonPointerSlot = slot;
                ApplyButtonState( slot );
            }
        }
    }

    for( int32_t f = 0; f < _fingerCount; ++f )
    {
        const crFingerInput& finger = _fingers[ f ];
        if( ( finger.down == false ) || ( finger.pressed == false ) )
            continue;   // claimed where it landed, on the frame it landed — no reason to wait

        const float2  at   = float2( finger.startPosPx.x / uiScale, canvasH - ( finger.startPosPx.y / uiScale ) );
        const int32_t slot = app->ui->ActionSlotAt( at );
        if( ( slot < 0 ) || _buttonTouchDown[ slot ] )
            continue;   // first finger holds it; a second on the same face changes nothing

        _buttonTouchDown[ slot ] = true;
        _buttonFinger[ slot ]    = finger.id;
        ApplyButtonState( slot );
    }
}

/*static*/ void crInputSystem::TEST_OnUserSettingsLoaded( bool success, crUserDataBlob blob, void* context )
{
    crUserData_UserSettings settings;

    if( success && settings.FromBlob( blob ) )   // FromBlob consumes the blob — no blob.Free() here
    {
        SDL_LogTrace( SDL_LOG_CATEGORY_APPLICATION, "crInputSystem: userData loaded( myInt:%" SDL_PRIs64 ", myFloat:%.3f, myString:%s )",
                                                    settings.myInt,
                                                    settings.myFloat,
                                                    ( settings.myString != nullptr ) ? settings.myString : "(null)" );

        crCamera* camera = static_cast<crCamera*>( context );
        camera->SetDistance( settings.myFloat );
    }
    else
    {
        SDL_LogError( CR_LOG_CATEGORY_SAVELOAD, "crInputSystem: userData load failed." );
    }

    settings.Free();
}
/*static*/ void crInputSystem::TEST_OnUserSettingsSaved( bool success, void* context )
{
    ( void )context;

    SDL_LogTrace( CR_LOG_CATEGORY_SAVELOAD, "crInputSystem: userData saved( success:%d )", success );
}
