#include "crUi.h"

#include <SDL3/SDL_assert.h>

#include "crApp.h"
#include "crSpriteAtlas.h"
#include "crCamera.h"
#include "crGraphics.h"
#include "crInputSystem.h"
#include "crMath.h"
#include "crScreenRef.h"

void crUi::Init()
{
    _sprites.Init( "ui" );
    _texts.Init();
    _lines.Init();
}
void crUi::Cleanup()
{
    _lines.Cleanup();
    _texts.Cleanup();
    _sprites.Cleanup();
}

static_assert( MAX_ACTION_SLOTS == crInputSystem::MAX_BUTTONS, "action slots must index button slots one to one" );

int32_t crUi::ActionSlotAt( float2 virtualPos ) const
{
    for( int32_t i = 0; i < MAX_ACTION_SLOTS; ++i )
    {
        const ActionRegion& region = _actionRegions[ i ];
        if( region.drawn == false )
            continue;

        if( HitTest( region.min, region.max, region.hitRadius, virtualPos ) )
            return i;
    }
    return -1;
}
bool crUi::IsOverWidget( float2 virtualPos ) const
{
    for( int32_t i = 0; i < _widgetRectCount; ++i )
    {
        const WidgetRect& rect = _widgetRects[ i ];
        if( HitTest( rect.min, rect.max, rect.hitRadius, virtualPos ) )
            return true;
    }
    return false;
}

/*static*/ float crUi::RasterScale( int2 viewport )
{
    const float aspect   = ( static_cast<float>( viewport.x ) / static_cast<float>( viewport.y ) );
    const float logicalH = ( static_cast<float>( crScreenRef::HEIGHT ) * crMath::AspectGrowth( aspect, crScreenRef::ASPECT ) );

    return ( static_cast<float>( viewport.y ) / logicalH );
}

void crUi::Begin( const crApp* app )
{
    _app = app;

    const int2 vp = app->camera->Viewport();

    _scale       = RasterScale( vp );
    _virtualSize = float2( static_cast<float>( vp.x ) / _scale, static_cast<float>( vp.y ) / _scale );

    // raw pointer (window px, top-left Y-down) -> virtual canvas (bottom-left Y-up)
    const crPointerInput& p = app->inputSys->Pointer();
    _pointerV        = float2( p.posPx.x / _scale, ( static_cast<float>( vp.y ) - p.posPx.y ) / _scale );
    _pointerDown     = p.down;
    _pointerPressed  = p.pressed;
    _pointerReleased = p.released;

    _capturedThis = false;

    _widgetRectCount = 0;   // cleared here, not in End: the sticks read last frame's layout before this runs
    for( int32_t i = 0; i < MAX_ACTION_SLOTS; ++i )
        _actionRegions[ i ].drawn = false;

    _sprites.Begin();
    _texts.Begin();
    _lines.Begin();
}

void crUi::Image( EUiAnchor anchor, float2 offset, float2 size, int32_t spriteIndex, color4 color )
{
    const float2 rectMin = ResolveRectMin( anchor, offset, size );
    AddSprite( rectMin, size, spriteIndex, color );
}

void crUi::ImageSliced( EUiAnchor anchor, float2 offset, float2 size, int32_t spriteIndex, float border, color4 color )
{
    const float2 rectMin = ResolveRectMin( anchor, offset, size );
    AddSpriteSliced( rectMin, size, spriteIndex, border, color );
}

void crUi::Label( EUiAnchor anchor, float2 offset, const char* utf8, int32_t font, color4 color )
{
    const float2 norm     = AnchorNorm( anchor );
    const float2 base     = float2( norm.x * _virtualSize.x, norm.y * _virtualSize.y );
    const float2 anchorPx = float2( ( base.x + offset.x ) * _scale, ( base.y + offset.y ) * _scale );

    // block pivot follows the anchor: corner/edge labels extend inward (AddString pivot: x 0=left..1=right, y 0=top..1=bottom)
    const float2 textPivot = float2( norm.x, 1.0f - norm.y );

    // scale 1: glyph rasters already track the resolution (crFontAtlas resScale == _scale)
    _texts.AddString( _app->graphics->FontAtlas(), utf8, anchorPx, font, color, 1.0f, textPivot, nullptr );
}

bool crUi::Button( uint32_t id, EUiAnchor anchor, float2 offset, float2 size, int32_t spriteIndex, float border, const char* label, int32_t labelFont, color4 color, float hitRadius /*= 0.0f*/ )
{
    SDL_assert( id != 0 );   // 0 means "no widget" in the active tracking

    const float2 rectMin = ResolveRectMin( anchor, offset, size );
    const float2 rectMax = float2( rectMin.x + size.x, rectMin.y + size.y );

    if( _widgetRectCount < MAX_WIDGET_RECTS )
    {
        _widgetRects[ _widgetRectCount ].min       = rectMin;
        _widgetRects[ _widgetRectCount ].max       = rectMax;
        _widgetRects[ _widgetRectCount ].hitRadius = hitRadius;
        ++_widgetRectCount;
    }
    else
    {
        SDL_assert( false );   // a dropped rect lets a stick claim straight through this widget
    }

    const bool inside = HitTest( rectMin, rectMax, hitRadius, _pointerV );
    if( inside )
        _capturedThis = true;

    bool clicked = false;
    if( inside && _pointerPressed && ( _activeId == 0 ) )
        _activeId = id;
    if( ( _activeId == id ) && _pointerReleased )
    {
        clicked   = inside;   // press and release on the same widget
        _activeId = 0;
    }

    // state tint on the sprite (dedicated state sprites can come later). idle sits below 1 so hover
    // can brighten even a pure-white sprite — the pass is LDR (post-PP), values above 1 just clamp
    float tint = 0.85f;
    if( _activeId == id )
        tint = 0.65f;
    else if( inside )
        tint = 1.0f;

    const color4 tinted = color4( ( color.r * tint ), ( color.g * tint ), ( color.b * tint ), color.a );
    if( border > 0.0f )
        AddSpriteSliced( rectMin, size, spriteIndex, border, tinted );
    else
        AddSprite( rectMin, size, spriteIndex, tinted );

    if( label != nullptr )
    {
        const float2 centerPx = float2( ( rectMin.x + ( size.x * 0.5f ) ) * _scale, ( rectMin.y + ( size.y * 0.5f ) ) * _scale );
        _texts.AddString( _app->graphics->FontAtlas(), label, centerPx, labelFont, color4( 1.0f, 1.0f, 1.0f, 1.0f ), 1.0f, float2( 0.5f, 0.5f ), nullptr );
    }

    return clicked;
}

void crUi::ActionButton( int32_t slot, EUiAnchor anchor, float2 offset, float2 size, int32_t spriteIndex, float border, const char* label, int32_t labelFont, color4 color, float hitRadius /*= 0.0f*/ )
{
    SDL_assert( ( slot >= 0 ) && ( slot < MAX_ACTION_SLOTS ) );

    const float2 rectMin = ResolveRectMin( anchor, offset, size );
    const float2 rectMax = float2( rectMin.x + size.x, rectMin.y + size.y );

    _actionRegions[ slot ].min       = rectMin;
    _actionRegions[ slot ].max       = rectMax;
    _actionRegions[ slot ].hitRadius = hitRadius;
    _actionRegions[ slot ].drawn     = true;

    // deliberately NOT a _widgetRects entry: that list means "a menu widget owns this point", and a
    // click there must not reach the game. an action face is the opposite — reaching the game is its
    // whole job. sticks exclude both, and ask separately

    // no hover state: whatever pressed this may not be a pointer at all. the two levels are far
    // apart on purpose — a thumb covers the button, so the change has to read from its edge alone
    const float  tint   = _app->inputSys->ButtonDown( slot ) ? 1.0f : 0.45f;
    const color4 tinted = color4( ( color.r * tint ), ( color.g * tint ), ( color.b * tint ), color.a );

    if( border > 0.0f )
        AddSpriteSliced( rectMin, size, spriteIndex, border, tinted );
    else
        AddSprite( rectMin, size, spriteIndex, tinted );

    if( label != nullptr )
    {
        const float2 centerPx = float2( ( rectMin.x + ( size.x * 0.5f ) ) * _scale, ( rectMin.y + ( size.y * 0.5f ) ) * _scale );
        _texts.AddString( _app->graphics->FontAtlas(), label, centerPx, labelFont, color4( 1.0f, 1.0f, 1.0f, 1.0f ), 1.0f, float2( 0.5f, 0.5f ), nullptr );
    }
}
void crUi::Polyline( const float2* points, int32_t count, bool closed, float thickness, color4 color )
{
    if( count < 2 )
        return;

    float halfWidth = ( thickness * _scale * 0.5f );
    if( halfWidth < 0.5f )
        halfWidth = 0.5f;   // a sub-pixel line still has to show

    for( int32_t i = 0; i < ( count - 1 ); ++i )
        _lines.Line( ToPixel( points[ i ] ), ToPixel( points[ i + 1 ] ), halfWidth, color );

    if( closed && ( count > 2 ) )
        _lines.Line( ToPixel( points[ count - 1 ] ), ToPixel( points[ 0 ] ), halfWidth, color );
}
void crUi::End()
{
    if( _pointerDown == false )
        _activeId = 0;   // stray release, or a canceled touch (its released edge never comes)

    _capturedPrev = ( _capturedThis || ( _activeId != 0 ) );   // held widgets keep the capture while dragging

    crGraphics*    graphics = _app->graphics;
    const float4x4 proj     = _app->camera->PixelProjection();
    _sprites.Flush( graphics->Program( EShader::SPRITE ), proj, graphics );
    _texts.Flush( graphics->Program( EShader::TEXT ), proj, graphics->FontAtlas() );

    _lines.Flush( graphics->Program( EShader::LINE_2D ), proj );

    _app = nullptr;
}

/*static*/ bool crUi::HitTest( float2 rectMin, float2 rectMax, float hitRadius, float2 at )
{
    if( hitRadius > 0.0f )
    {
        const float dx = ( at.x - ( ( rectMin.x + rectMax.x ) * 0.5f ) );
        const float dy = ( at.y - ( ( rectMin.y + rectMax.y ) * 0.5f ) );
        return ( ( ( dx * dx ) + ( dy * dy ) ) <= ( hitRadius * hitRadius ) );
    }

    return ( ( at.x >= rectMin.x ) && ( at.x <= rectMax.x ) &&
             ( at.y >= rectMin.y ) && ( at.y <= rectMax.y ) );
}
float2 crUi::ResolveRectMin( EUiAnchor anchor, float2 offset, float2 size ) const
{
    const float2 norm = AnchorNorm( anchor );
    const float2 base = float2( norm.x * _virtualSize.x, norm.y * _virtualSize.y );
    return float2( base.x + offset.x - ( norm.x * size.x ),
                   base.y + offset.y - ( norm.y * size.y ) );
}
float2 crUi::ToPixel( float2 virtualPos ) const
{
    return float2( ( virtualPos.x * _scale ), ( virtualPos.y * _scale ) );
}
void crUi::AddSprite( float2 rectMin, float2 size, int32_t spriteIndex, color4 color )
{
    const crAtlasSprite& sprite = _app->graphics->Sprite( spriteIndex );

    crBatchedSprites::SpriteInstance s;
    s.pos    = float2( ( rectMin.x + ( size.x * 0.5f ) ) * _scale, ( rectMin.y + ( size.y * 0.5f ) ) * _scale );
    s.size   = float2( size.x * _scale, size.y * _scale );
    s.cosSin = float2( 1.0f, 0.0f );
    s.color  = color;
    s.uvMin  = sprite.uvMin;
    s.uvMax  = sprite.uvMax;
    s.pivot  = float2( 0.5f, 0.5f );

    _sprites.Add( s, crGraphics::SpriteHandleAtlas( spriteIndex ) );
}
void crUi::AddSpriteSliced( float2 rectMin, float2 size, int32_t spriteIndex, float border, color4 color )
{
    const crAtlasSprite& sprite = _app->graphics->Sprite( spriteIndex );
    const int32_t        atlas  = crGraphics::SpriteHandleAtlas( spriteIndex );

    // destination corner size (virtual units, 1:1 with source px) — clamped so the corners never overlap
    float bDst = border;
    if( bDst > ( size.x * 0.5f ) )
        bDst = ( size.x * 0.5f );
    if( bDst > ( size.y * 0.5f ) )
        bDst = ( size.y * 0.5f );

    // source border as uv extents (clamped to half the sprite)
    const float bSrcX = ( border < ( sprite.sizePx.x * 0.5f ) ) ? border : ( sprite.sizePx.x * 0.5f );
    const float bSrcY = ( border < ( sprite.sizePx.y * 0.5f ) ) ? border : ( sprite.sizePx.y * 0.5f );
    const float bu    = ( ( bSrcX / sprite.sizePx.x ) * ( sprite.uvMax.x - sprite.uvMin.x ) );
    const float bv    = ( ( bSrcY / sprite.sizePx.y ) * ( sprite.uvMax.y - sprite.uvMin.y ) );

    // 3x3 grid: x/y in virtual units from rectMin (bottom-left, Y-up); v runs top-down in the image,
    // so the bottom row samples uvMax.y
    const float xs[ 4 ] = { 0.0f, bDst, ( size.x - bDst ), size.x };
    const float ys[ 4 ] = { 0.0f, bDst, ( size.y - bDst ), size.y };
    const float us[ 4 ] = { sprite.uvMin.x, ( sprite.uvMin.x + bu ), ( sprite.uvMax.x - bu ), sprite.uvMax.x };
    const float vs[ 4 ] = { sprite.uvMax.y, ( sprite.uvMax.y - bv ), ( sprite.uvMin.y + bv ), sprite.uvMin.y };

    for( int32_t r = 0; r < 3; ++r )
    {
        const float h = ( ys[ r + 1 ] - ys[ r ] );
        if( h <= 0.0f )
            continue;   // degenerate middle row (bDst clamped to half the height)

        for( int32_t c = 0; c < 3; ++c )
        {
            const float w = ( xs[ c + 1 ] - xs[ c ] );
            if( w <= 0.0f )
                continue;

            crBatchedSprites::SpriteInstance s;
            s.pos    = float2( ( rectMin.x + xs[ c ] + ( w * 0.5f ) ) * _scale, ( rectMin.y + ys[ r ] + ( h * 0.5f ) ) * _scale );
            s.size   = float2( w * _scale, h * _scale );
            s.cosSin = float2( 1.0f, 0.0f );
            s.color  = color;
            s.uvMin  = float2( us[ c ], vs[ r + 1 ] );   // instance uv is top-left..bottom-right in image space
            s.uvMax  = float2( us[ c + 1 ], vs[ r ] );
            s.pivot  = float2( 0.5f, 0.5f );

            _sprites.Add( s, atlas );
        }
    }
}

/*static*/ float2 crUi::AnchorNorm( EUiAnchor anchor )
{
    switch( anchor )
    {
    case EUiAnchor::BOTTOM_LEFT:  return float2( 0.0f, 0.0f );
    case EUiAnchor::BOTTOM:       return float2( 0.5f, 0.0f );
    case EUiAnchor::BOTTOM_RIGHT: return float2( 1.0f, 0.0f );
    case EUiAnchor::LEFT:         return float2( 0.0f, 0.5f );
    case EUiAnchor::CENTER:       return float2( 0.5f, 0.5f );
    case EUiAnchor::RIGHT:        return float2( 1.0f, 0.5f );
    case EUiAnchor::TOP_LEFT:     return float2( 0.0f, 1.0f );
    case EUiAnchor::TOP:          return float2( 0.5f, 1.0f );
    default:                      return float2( 1.0f, 1.0f );   // TOP_RIGHT
    }
}
