using DeepDrftPublic.Client.Services;
using Microsoft.AspNetCore.Components;

namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;

public partial class LevelMeterFab : ComponentBase, IAsyncDisposable
{
    [Inject] public required AudioInteropService AudioInterop { get; set; }

    [CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }

    [Parameter] public EventCallback OnClick { get; set; }

    // Calibration window for true RMS dBFS (AnalyserNode.getFloatTimeDomainData).
    // Floor/ceiling define the dB window; the linear map across this 57 dB range places
    // a steady dance track (~-14 dBFS) at ~81% fill (upper yellow) and a hot drop
    // (~-6 dBFS) at ~95% (deep orange). Attack/release coefficients are by-ear tuning values.
    private const double FloorDb = -60.0;            // fill = 0%; below this is near-silence (true RMS dBFS)
    private const double CeilingDb = -3.0;           // fill = 100%; hot peaks on commercial masters (true RMS dBFS)
    private const double SilenceFloorDb = -80.0;     // matches the analyzer's normalization window

    private const double AttackCoefficient = 0.5;    // fast rise toward a louder reading
    private const double ReleaseCoefficient = 0.12;  // slow decay so the column doesn't strobe

    private readonly string _instanceId = Guid.NewGuid().ToString();
    private bool _isAnimating;
    private string? _playerId;
    private IStreamingPlayerService? _subscribedService;

    private double _smoothedDb = SilenceFloorDb;
    private double _fillPercent;   // 0..100, the sole render state

    private string IdSuffix => _instanceId.Replace("-", "");

    private double FillHeight => 24.0 * (_fillPercent / 100.0);
    private string FillY => (24.0 - FillHeight).ToString("0.###", System.Globalization.CultureInfo.InvariantCulture);
    private string FillH => FillHeight.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture);

    protected override async Task OnParametersSetAsync()
    {
        // The cascade is IsFixed, so the provider's re-renders do NOT re-run
        // OnParametersSet here. Subscribe to the multicast StateChanged side-channel
        // so animation state stays correct independent of parent re-renders —
        // notably when the bar minimizes while a track is already playing.
        if (PlayerService != null && !ReferenceEquals(PlayerService, _subscribedService))
        {
            if (_subscribedService != null)
                _subscribedService.StateChanged -= OnPlayerStateChanged;

            PlayerService.StateChanged += OnPlayerStateChanged;
            _subscribedService = PlayerService;
        }

        if (_playerId == null && PlayerService is AudioPlayerService baseService)
        {
            _playerId = baseService.PlayerId;
        }

        await UpdateAnimationState();
    }

    private void OnPlayerStateChanged() => InvokeAsync(async () =>
    {
        if (_playerId == null && PlayerService is AudioPlayerService baseService)
        {
            _playerId = baseService.PlayerId;
        }
        await UpdateAnimationState();
    });

    private async Task UpdateAnimationState()
    {
        if (string.IsNullOrEmpty(_playerId) || PlayerService == null) return;

        var shouldAnimate = PlayerService.IsPlaying;

        if (shouldAnimate && !_isAnimating)
        {
            await StartAnimation();
        }
        else if (!shouldAnimate && _isAnimating)
        {
            await StopAnimation();
        }
    }

    private async Task StartAnimation()
    {
        if (_isAnimating || string.IsNullOrEmpty(_playerId)) return;

        _isAnimating = true;
        _smoothedDb = SilenceFloorDb;
        _fillPercent = 0;
        await AudioInterop.StartLevelAnimationAsync(_playerId, _instanceId, OnLevelData);
    }

    private async Task StopAnimation()
    {
        if (!_isAnimating || string.IsNullOrEmpty(_playerId)) return;

        _isAnimating = false;
        await AudioInterop.StopLevelAnimationAsync(_playerId, _instanceId);

        // Drop the column to empty so only the dim silhouette remains.
        if (_fillPercent != 0)
        {
            _fillPercent = 0;
            await InvokeAsync(StateHasChanged);
        }
        _smoothedDb = SilenceFloorDb;
    }

    private Task OnLevelData(double db)
    {
        // db is true RMS dBFS from getFloatTimeDomainData; -Infinity on silence.
        var instantDb = double.IsNegativeInfinity(db) || double.IsNaN(db)
            ? SilenceFloorDb
            : Math.Max(db, SilenceFloorDb);

        // Attack-fast / release-slow envelope so the column doesn't strobe at 30fps.
        var coeff = instantDb > _smoothedDb ? AttackCoefficient : ReleaseCoefficient;
        _smoothedDb += (instantDb - _smoothedDb) * coeff;

        // Linear map of smoothed dB onto a 0-100 fill across the [floor, ceiling] window.
        var next = Math.Clamp((_smoothedDb - FloorDb) / (CeilingDb - FloorDb) * 100.0, 0.0, 100.0);

        // Re-render only on a meaningful change to avoid 30fps churn over sub-pixel deltas.
        if (Math.Abs(next - _fillPercent) >= 0.5)
        {
            _fillPercent = next;
            InvokeAsync(StateHasChanged);
        }

        return Task.CompletedTask;
    }

    public async ValueTask DisposeAsync()
    {
        if (_subscribedService != null)
        {
            _subscribedService.StateChanged -= OnPlayerStateChanged;
            _subscribedService = null;
        }
        await StopAnimation();
    }
}
