UI/SlotMachineUI.razor.cs
using System;
using System.Linq;
using Sandbox;
using Sandbox.UI;
using Casino.RpSlot.Economy;

namespace Casino.RpSlot;

public partial class SlotMachineUI
{
    public static event Action<Transform>? OnPlayerEngaged;
    public static event Action? OnPlayerDisengaged;

    public SlotMachine Machine { get; set; }
    private SlotSymbol[] _displayed = new SlotSymbol[9];
    private bool _isEngaged;
    private bool[] _reelSpinning = new bool[3];
    private float[] _reelStopTimes = new float[3];
    private float _spinStartTime;
    private SlotState _prevState = SlotState.Idle;
    private System.Collections.Generic.List<int> _sortedWinningLines = new();
    private int _currentDisplayedLine = -1;
    private float _lineDisplayTimer = 0f;
    private int _lastSeenWinSignature = -1;
    private string _currentTab = "play";
    private int _maxGainSession = 0;
    private float _insufficientBalanceTimer = 0f;

    private bool IsBusy => Machine != null && Machine.State != SlotState.Idle;

    public bool IsEngaged
    {
        get => _isEngaged;
        set
        {
            if ( _isEngaged == value ) return;
            _isEngaged = value;
            Mouse.Visible = value; // TODO: API obsolete, remplacer quand l'équivalent est stable

            if ( value )
            {
                var viewpoint = Machine?.GameObject.Children
                    .FirstOrDefault( c => c.Name == "PlayerViewpoint" );

                if ( viewpoint != null )
                    OnPlayerEngaged?.Invoke( new Transform( viewpoint.WorldPosition, viewpoint.WorldRotation ) );

                Machine?.GameObject.Components.Get<SlotMachineFx>()?.StartAmbient();
            }
            else
            {
                OnPlayerDisengaged?.Invoke();
                Machine?.GameObject.Components.Get<SlotMachineFx>()?.StopAmbient();
            }
        }
    }

    protected override void OnAwake()
    {
        base.OnAwake();
        Machine = GameObject.Components.GetInParentOrSelf<SlotMachine>();

        var rng = new System.Random();
        for ( int i = 0; i < _displayed.Length; i++ )
            _displayed[i] = (SlotSymbol)rng.Next( 0, 7 );
    }

    protected override void OnUpdate()
    {
        if ( Machine == null ) return;

        var state = Machine.State;

        if ( state == SlotState.Spinning && _prevState != SlotState.Spinning )
        {
            _spinStartTime = Time.Now;
            var rng = new Random();

            for ( int i = 0; i < 3; i++ )
            {
                _reelSpinning[i] = true;
                _reelStopTimes[i] = Machine.SpinDuration * (0.5f + (float)rng.NextDouble() * 0.5f);
            }
        }

        if ( state == SlotState.Spinning )
        {
            var elapsed = Time.Now - _spinStartTime;
            var wasSpinning = new bool[_reelSpinning.Length];
            for ( int i = 0; i < _reelSpinning.Length; i++ )
                wasSpinning[i] = _reelSpinning[i];

            for ( int col = 0; col < 3; col++ )
            {
                if ( elapsed >= _reelStopTimes[col] )
                    _reelSpinning[col] = false;
            }

            for ( int col = 0; col < 3; col++ )
            {
                for ( int row = 0; row < 3; row++ )
                {
                    int idx = col * 3 + row;
                    if ( _reelSpinning[col] )
                        _displayed[idx] = (SlotSymbol)((int)(Time.Now * 12 + col * 5 + row * 3) % 7);
                    else if ( wasSpinning[col] && Machine.ReelResults.Count > idx )
                        _displayed[idx] = (SlotSymbol)Machine.ReelResults[idx];
                }
            }
        }
        else
        {
            for ( int i = 0; i < 3; i++ ) _reelSpinning[i] = false;

            if ( Machine.ReelResults.Count >= 9 )
            {
                for ( int i = 0; i < 9; i++ )
                    _displayed[i] = (SlotSymbol)Machine.ReelResults[i];
            }
        }

        _prevState = state;

        if ( _insufficientBalanceTimer > 0f )
            _insufficientBalanceTimer -= Time.Delta;

        if ( state == SlotState.Idle && Machine.ReelResults.Count == 9 )
        {
            int sig = HashCode.Combine(
                Machine.LastPayout,
                Machine.WinningLines?.Count ?? 0,
                Machine.ReelResults.Count >= 9 ? Machine.ReelResults[0] : -1,
                Machine.ReelResults.Count >= 9 ? Machine.ReelResults[4] : -1,
                Machine.ReelResults.Count >= 9 ? Machine.ReelResults[8] : -1
            );
            if ( sig != _lastSeenWinSignature )
            {
                _lastSeenWinSignature = sig;
                _sortedWinningLines.Clear();
                _currentDisplayedLine = -1;
                _lineDisplayTimer = 0f;

                if ( Machine.WinningLines != null )
                {
                    foreach ( var l in Machine.WinningLines )
                        _sortedWinningLines.Add( l );
                }

                if ( _sortedWinningLines.Count > 0 )
                    _currentDisplayedLine = 0;

                if ( Machine.LastPayout > _maxGainSession )
                    _maxGainSession = Machine.LastPayout;
            }

            if ( _sortedWinningLines.Count > 0 )
            {
                _lineDisplayTimer += Time.Delta;
                if ( _lineDisplayTimer >= 1.5f )
                {
                    _lineDisplayTimer = 0f;
                    _currentDisplayedLine = ( _currentDisplayedLine + 1 ) % _sortedWinningLines.Count;
                }
            }
        }
        else
        {
            _lastSeenWinSignature = -1;
        }
    }

    private string GetDisplayedSymbol( int i ) => _displayed[i] switch
    {
        SlotSymbol.Cherry  => "🍒",
        SlotSymbol.Lemon   => "🍋",
        SlotSymbol.Orange  => "🍊",
        SlotSymbol.Bell    => "🔔",
        SlotSymbol.Bar     => "🍫",
        SlotSymbol.Seven   => "7",
        SlotSymbol.Jackpot => "💎",
        _ => "?"
    };

    private int GetPlayerBalance()
    {
        foreach ( var go in Scene.GetAllObjects( false ) )
            foreach ( var c in go.Components.GetAll() )
                if ( c is IRpEconomy eco ) return eco.Balance;
        return 0;
    }

    private string GetSymbolClass( int idx ) => _displayed[idx] switch
    {
        SlotSymbol.Seven => "is-seven",
        SlotSymbol.Bar   => "is-bar",
        _                => ""
    };

    private string GetWinningColor( int idx )
    {
        if ( Machine == null ) return "";
        if ( Machine.State != SlotState.Idle ) return "";
        if ( _currentDisplayedLine < 0 || _sortedWinningLines.Count == 0 ) return "";

        var lineIdx = _sortedWinningLines[_currentDisplayedLine];
        var line = SlotPaylines.Lines[lineIdx];
        if ( line[0] != idx && line[1] != idx && line[2] != idx ) return "";

        return lineIdx switch
        {
            0 => "win-top",
            1 => "win-middle",
            2 => "win-bottom",
            3 => "win-diag-back",
            4 => "win-diag-fwd",
            _ => ""
        };
    }

    private void PlayClick()
    {
        Machine?.GameObject.Components.Get<SlotMachineFx>()?.PlayClick();
    }

    private void SetTab( string tab )
    {
        PlayClick();
        _currentTab = tab;
    }

    private string GetResultClass()
    {
        if ( _insufficientBalanceTimer > 0f ) return "active";
        if ( Machine == null ) return "";
        if ( Machine.State != SlotState.Idle ) return "";
        if ( Machine.ReelResults.Count != 9 ) return "";
        if ( Machine.LastSpinWasJackpot ) return "active jackpot";
        if ( Machine.LastPayout > 0 ) return "active win";
        return "active lose";
    }

    private string FormatMult( float m )
    {
        if ( m < 1f ) return m.ToString( "0.0" );
        return m.ToString( "0" );
    }

    private void Spin()
    {
        PlayClick();
        if ( !IsEngaged ) return;
        if ( IsBusy ) return;

        GameObject economyGo = null;
        foreach ( var go in Scene.GetAllObjects( false ) )
        {
            foreach ( var c in go.Components.GetAll() )
            {
                if ( c is IRpEconomy )
                {
                    economyGo = go;
                    break;
                }
            }
            if ( economyGo != null ) break;
        }

        if ( economyGo == null ) return;

        var economy = economyGo.Components.GetAll().OfType<IRpEconomy>().FirstOrDefault();
        if ( economy != null && economy.Balance < Machine.CurrentBet )
        {
            _insufficientBalanceTimer = 2f;
            return;
        }

        Machine.RequestSpin( economyGo.Id, Machine.CurrentBet );
    }

    private void IncreaseBet()
    {
        if ( IsBusy ) return;
        PlayClick();
        Machine.CurrentBet = Math.Min( Machine.CurrentBet + 10, Machine.MaxBet );
    }

    private void DecreaseBet()
    {
        if ( IsBusy ) return;
        PlayClick();
        Machine.CurrentBet = Math.Max( Machine.CurrentBet - 10, Machine.MinBet );
    }

    private void IncreaseBet100()
    {
        if ( IsBusy ) return;
        PlayClick();
        Machine.CurrentBet = System.Math.Min( Machine.CurrentBet + 100, Machine.MaxBet );
    }

    private void DecreaseBet100()
    {
        if ( IsBusy ) return;
        PlayClick();
        Machine.CurrentBet = System.Math.Max( Machine.CurrentBet - 100, Machine.MinBet );
    }

    private void Close()
    {
        PlayClick();
        IsEngaged = false;
    }

    protected override int BuildHash()
    {
        var hash = new HashCode();
        hash.Add( Machine?.State );
        hash.Add( Machine?.CurrentBet );
        hash.Add( Machine?.LastPayout );
        for ( int i = 0; i < 9; i++ ) hash.Add( _displayed[i] );
        hash.Add( _isEngaged );
        hash.Add( _reelSpinning[0] );
        hash.Add( _reelSpinning[1] );
        hash.Add( _reelSpinning[2] );
        hash.Add( Machine?.WinningLines?.Count );
        hash.Add( _currentDisplayedLine );
        hash.Add( _currentTab );
        hash.Add( _maxGainSession );
        hash.Add( GetPlayerBalance() );
        hash.Add( Machine?.LastSpinWasJackpot );
        hash.Add( _insufficientBalanceTimer > 0f );
        return hash.ToHashCode();
    }
}