using System.Globalization;
using System.Text;
using DeepDrftModels.DTOs;
using DeepDrftPublic.Client.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

namespace DeepDrftPublic.Client.Controls;

/// <summary>
/// Renders a Mix release's stored loudness profile as a full-page background silhouette. Standalone
/// and reusable: give it a <see cref="ReleaseId"/> and it fetches its own datum. Visually distinct
/// from the player-bar spectrum/level idiom by design — this is a single continuous mirrored wave,
/// not discrete peak bars.
/// </summary>
public partial class MixWaveformVisualizer : ComponentBase
{
    [Inject] public required IReleaseDataService ReleaseData { get; set; }
    [Inject] public required ILogger<MixWaveformVisualizer> Logger { get; set; }

    /// <summary>The Mix release whose waveform datum to fetch and render.</summary>
    [Parameter] public required long ReleaseId { get; set; }

    /// <summary>
    /// Normalized playback head in [0, 1]. Two-way bindable so a future click-to-seek can write back
    /// through it; today it is read-only input that drives the played-portion wash. The seam exists
    /// now so wiring click-to-seek later is a pure addition, not a signature change.
    /// </summary>
    [Parameter] public double PlaybackPosition { get; set; }

    [Parameter] public EventCallback<double> PlaybackPositionChanged { get; set; }

    /// <summary>
    /// Fired when the user seeks by interacting with the waveform. Unused until click-to-seek ships;
    /// present now to lock the seek seam into the public contract.
    /// </summary>
    [Parameter] public EventCallback<double> OnSeek { get; set; }

    // Fixed SVG coordinate width. The path is computed in this space, then stretched to the
    // viewport via preserveAspectRatio="none".
    private const int ViewBoxWidth = 1000;

    private readonly string _clipId = $"mix-wf-clip-{Guid.NewGuid():N}";

    private WaveformProfileDto? _profile;
    private string _silhouettePath = string.Empty;
    private long? _loadedReleaseId;

    private double ClampedPosition => Math.Clamp(PlaybackPosition, 0d, 1d);

    protected override async Task OnParametersSetAsync()
    {
        // ReleaseId is the only fetch input; fetch once per id. A PlaybackPosition update re-renders
        // but must not refetch — and a release with no datum must not refetch either, so the guard
        // keys on the fetched id, not on whether a profile came back.
        if (_loadedReleaseId == ReleaseId)
            return;

        _loadedReleaseId = ReleaseId;

        var result = await ReleaseData.GetMixWaveform(ReleaseId);
        if (result is { Success: true, Value: { } profile } && profile.BucketCount > 0)
        {
            _profile = profile;
            try
            {
                _silhouettePath = BuildSilhouettePath(profile);
            }
            catch (Exception ex)
            {
                Logger.LogWarning(ex, "MixWaveformVisualizer: failed to decode waveform profile for release {ReleaseId}; rendering empty backdrop.", ReleaseId);
                _profile = null;
                _silhouettePath = string.Empty;
            }
        }
        else
        {
            // No datum (not generated yet, or not a Mix) — leave the background empty; the detail
            // page still renders its content over a plain backdrop.
            _profile = null;
            _silhouettePath = string.Empty;
        }
    }

    // Builds a closed, vertically mirrored silhouette path across the buckets. Loudness bytes are
    // [0, 255]; mapped to a half-height amplitude around the vertical midline (y=50). The top edge
    // runs left-to-right, the bottom edge mirrors right-to-left, and the path closes — yielding a
    // filled continuous wave shape rather than separate bars.
    private static string BuildSilhouettePath(WaveformProfileDto profile)
    {
        var data = Convert.FromBase64String(profile.Data);
        int n = data.Length;
        if (n == 0) return string.Empty;

        const double midline = 50d;
        const double maxAmplitude = 48d; // leave a 2-unit margin top and bottom
        double step = n > 1 ? (double)ViewBoxWidth / (n - 1) : ViewBoxWidth;

        var sb = new StringBuilder();

        // Top edge, left to right.
        for (int i = 0; i < n; i++)
        {
            double x = i * step;
            double amp = data[i] / 255d * maxAmplitude;
            double y = midline - amp;
            sb.Append(i == 0 ? 'M' : 'L');
            AppendPoint(sb, x, y);
        }

        // Bottom edge, right to left (mirror).
        for (int i = n - 1; i >= 0; i--)
        {
            double x = i * step;
            double amp = data[i] / 255d * maxAmplitude;
            double y = midline + amp;
            sb.Append('L');
            AppendPoint(sb, x, y);
        }

        sb.Append('Z');
        return sb.ToString();
    }

    private static void AppendPoint(StringBuilder sb, double x, double y)
    {
        sb.Append(x.ToString("0.##", CultureInfo.InvariantCulture));
        sb.Append(' ');
        sb.Append(y.ToString("0.##", CultureInfo.InvariantCulture));
        sb.Append(' ');
    }
}
