@using System.Globalization
@inject IJSRuntime JS
@implements IAsyncDisposable

<!-- Drag-shield: full-viewport overlay while dragging.
     position:fixed; inset:0; z-index:9999 so it sits above all overlays (the controls modal is z-index:1400).
     Provides the cursor hint and blocks stray clicks on underlying content during a drag.
     Event delivery is NOT via this div — setPointerCapture on the knob element routes
     pointermove/pointerup/pointercancel directly to the knob regardless of where the cursor is,
     even outside the browser window. This overlay is a belt-and-suspenders UX guard only. -->
@if (_isDragging)
{
    <div style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 9999; cursor: ns-resize;">
    </div>
}

<!-- Knob element owns pointer capture during drag.
     @onpointerdown initiates drag and calls setPointerCapture via knob.js.
     @onpointermove / @onpointerup / @onpointercancel are delivered here (not to the overlay)
     for the captured pointer, even when the cursor has left the browser window. -->
<div class="radial-knob" style="width: @(Size)px; height: @(Size)px; display: inline-block; position: relative;"
     @ref="_knobRef"
     @onpointerdown="@OnPointerDown"
     @onpointermove="@OnPointerMove"
     @onpointerup="@OnPointerUp"
     @onpointercancel="@OnPointerCancel">

    <!-- SVG Knob -->
    <svg width="@Size" height="@Size" viewBox="0 0 100 100">
        <!-- Background track (270 degree arc) -->
        <circle cx="50" cy="50" r="35" fill="none"
                stroke="var(--mud-palette-surface-variant)"
                stroke-width="6"
                stroke-dasharray="@GetBackgroundStrokeDashArray()"
                stroke-dashoffset="0"
                transform="rotate(-225 50 50)" />

        <!-- Value arc -->
        <circle cx="50" cy="50" r="35" fill="none"
                stroke="@GetKnobColor()"
                stroke-width="6"
                stroke-linecap="round"
                stroke-dasharray="@GetStrokeDashArray()"
                stroke-dashoffset="@GetStrokeDashOffset()"
                transform="rotate(-225 50 50)" />

        <!-- Center circle -->
        <circle cx="50" cy="50" r="8" fill="var(--mud-palette-surface)" stroke="@GetKnobColor()" stroke-width="2" />

        <!-- Pointer line -->
        <line x1="50" y1="50"
              x2="@GetPointerX()" y2="@GetPointerY()"
              stroke="@GetKnobColor()"
              stroke-width="3"
              stroke-linecap="round" />

        <!-- Value label inside SVG (moved down 8px from center) -->
        <text x="50" y="100" text-anchor="middle"
              font-size="22px" font-weight="bold"
              fill="var(--mud-palette-text-primary)">
            @GetDisplayLabel()
        </text>
    </svg>
</div>

<style>
    .radial-knob {
        cursor: pointer;
        user-select: none;
        -webkit-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
    }

    .radial-knob:hover svg circle:nth-child(2) {
        stroke-width: 7;
    }

    .radial-knob:active svg circle:nth-child(2) {
        stroke-width: 8;
    }
</style>

@code {
    [Parameter] public double Value { get; set; }
    [Parameter] public EventCallback<double> ValueChanged { get; set; }
    [Parameter] public double Min { get; set; } = 0;
    [Parameter] public double Max { get; set; } = 100;
    [Parameter] public double Step { get; set; } = 1;
    [Parameter] public string Label { get; set; } = "";
    [Parameter] public int Size { get; set; } = 50;
    [Parameter] public MudBlazor.Color Color { get; set; } = MudBlazor.Color.Primary;
    [Parameter] public bool HoldValue { get; set; } = false;

    private ElementReference _knobRef;
    private IJSObjectReference? _knobModule;

    private bool _isDragging = false;
    private double _lastMouseY = 0;
    private double _dragValue = 0;

    private double GetNormalizedValue()
    {
        // Use drag value during dragging if HoldValue is enabled, otherwise use current Value
        double currentValue = (_isDragging && HoldValue) ? _dragValue : Value;
        return Math.Max(0, Math.Min(1, (currentValue - Min) / (Max - Min)));
    }

    private string GetDisplayLabel()
    {
        // Show drag value during dragging if HoldValue is enabled, otherwise use provided Label
        if (_isDragging && HoldValue)
        {
            return _dragValue.ToString("F0");
        }
        return Label;
    }

    private string GetKnobColor()
    {
        return Color switch
        {
            MudBlazor.Color.Primary => "var(--mud-palette-primary)",
            MudBlazor.Color.Secondary => "var(--mud-palette-secondary)",
            MudBlazor.Color.Success => "var(--mud-palette-success)",
            MudBlazor.Color.Warning => "var(--mud-palette-warning)",
            MudBlazor.Color.Error => "var(--mud-palette-error)",
            MudBlazor.Color.Info => "var(--mud-palette-info)",
            _ => "var(--mud-palette-primary)"
        };
    }

    private string GetBackgroundStrokeDashArray()
    {
        double circumference = 2 * Math.PI * 35; // radius = 35
        double maxArc = circumference * 0.75; // 270 degrees
        return $"{maxArc} {circumference}";
    }

    private string GetStrokeDashArray()
    {
        double circumference = 2 * Math.PI * 35; // radius = 35
        double maxArc = circumference * 0.75; // 270 degrees
        double valueArc = maxArc * GetNormalizedValue();
        return $"{valueArc} {circumference}";
    }

    private string GetStrokeDashOffset()
    {
        // No offset needed - arc should start from the beginning and grow
        return "0";
    }

    private string GetPointerX()
    {
        double angle = -225 + (270 * GetNormalizedValue()); // -225 to +45 degrees (centered on vertical)
        double radians = angle * Math.PI / 180;
        double x = 50 + (25 * Math.Cos(radians)); // radius = 25 for pointer
        return x.ToString("F1", CultureInfo.InvariantCulture);
    }

    private string GetPointerY()
    {
        double angle = -225 + (270 * GetNormalizedValue());
        double radians = angle * Math.PI / 180;
        double y = 50 + (25 * Math.Sin(radians));
        return y.ToString("F1", CultureInfo.InvariantCulture);
    }

    private async Task OnPointerDown(PointerEventArgs e)
    {
        _isDragging = true;
        _lastMouseY = e.ClientY;
        _dragValue = Value; // Initialize drag value with current value

        // Load the JS module on first drag (lazy — avoids import cost until the user interacts).
        _knobModule ??= await JS.InvokeAsync<IJSObjectReference>(
            "import", "./_content/DeepDrftShared.Client/js/knob/knob.js");

        // Capture the pointer on the knob element so pointermove/pointerup are delivered
        // even when the cursor leaves the browser window mid-drag.
        await _knobModule.InvokeVoidAsync("capturePointer", _knobRef, e.PointerId);

        // The full-viewport capture div (rendered when _isDragging) is a belt-and-suspenders
        // guard that blocks stray clicks on underlying content while dragging.
        StateHasChanged();
    }

    // Delivered to the knob element because setPointerCapture routes the captured pointer here.
    private async Task OnPointerMove(PointerEventArgs e)
    {
        if (_isDragging)
        {
            await UpdateValueFromPointer(e);
        }
    }

    // pointerup implicitly releases pointer capture — no explicit releasePointerCapture needed.
    private async Task OnPointerUp(PointerEventArgs e)
    {
        if (_isDragging && HoldValue)
        {
            // If HoldValue is enabled, emit the final value now
            if (Math.Abs(_dragValue - Value) > 0.001)
            {
                await ValueChanged.InvokeAsync(_dragValue);
            }
        }

        _isDragging = false;
        StateHasChanged();
    }

    // Pointer capture cancelled by OS (e.g. Alt+Tab, system gesture) — end drag cleanly.
    // Delivered to the knob element (the capturing element). Release capture explicitly since
    // the implicit release on pointerup does not apply to pointercancel.
    private async Task OnPointerCancel(PointerEventArgs e)
    {
        if (_knobModule != null)
        {
            try { await _knobModule.InvokeVoidAsync("releasePointer", _knobRef, e.PointerId); }
            catch (JSException) { /* element may already be gone */ }
        }

        _isDragging = false;
        StateHasChanged();
    }

    public async ValueTask DisposeAsync()
    {
        if (_knobModule != null)
        {
            try { await _knobModule.DisposeAsync(); }
            catch (JSDisconnectedException) { /* circuit torn down */ }
        }

        GC.SuppressFinalize(this);
    }

    private async Task UpdateValueFromPointer(PointerEventArgs e)
    {
        // Calculate vertical delta from last pointer position
        double deltaY = _lastMouseY - e.ClientY; // Inverted: up = positive, down = negative
        _lastMouseY = e.ClientY;

        // Sensitivity factor (pixels to move for full range)
        double sensitivity = 100.0;

        // Calculate change in normalized value based on vertical movement
        double normalizedDelta = deltaY / sensitivity;
        double currentNormalized = GetNormalizedValue();
        double newNormalized = Math.Max(0, Math.Min(1, currentNormalized + normalizedDelta));

        // Convert to actual value
        double newValue = Min + (newNormalized * (Max - Min));

        // Apply step
        if (Step > 0)
        {
            newValue = Math.Round(newValue / Step) * Step;
        }

        if (Math.Abs(newValue - (HoldValue ? _dragValue : Value)) > 0.001) // Avoid unnecessary updates
        {
            if (HoldValue)
            {
                // Update drag value only, don't emit ValueChanged yet
                _dragValue = newValue;
                StateHasChanged(); // Update visual only
            }
            else
            {
                // Original behavior - emit ValueChanged immediately
                Value = newValue;
                await ValueChanged.InvokeAsync(Value);
            }
        }
    }
}