@namespace Lumeo
@implements IAsyncDisposable

@{
    var context = new ColorPickerContext(Value ?? "#000000", Open,
        EventCallback.Factory.Create<string>(this, OnColorChanged),
        EventCallback.Factory.Create<bool>(this, SetOpen),
        _wrapperId);
}

@if (Label is not null && !InsideFormField)
{
    <label class="text-sm font-medium leading-none">
        @Label
        @if (Required)
        {
            <span class="text-destructive ml-0.5">*</span>
        }
    </label>
}

<CascadingValue Value="context" IsFixed="false">
    <div id="@_wrapperId" class="@ContainerClass" @attributes="AdditionalAttributes">
        <button type="button"
                name="@Name"
                aria-required="@Required.ToString().ToLower()"
                aria-invalid="@EffectiveInvalid.ToString().ToLower()"
                class="@TriggerClass"
                @onclick="ToggleOpen"
                disabled="@Disabled"
                aria-label="@L["ColorPicker.PickColor"]">
            <span class="lumeo-cp-chip h-5 w-5 rounded-sm border border-border/40 shrink-0" style="@ChipStyle"></span>
            <span class="text-sm text-foreground font-mono">@DisplayHex</span>
            <Blazicon Svg="Lucide.ChevronDown" class="@($"h-4 w-4 text-muted-foreground ml-auto transition-transform duration-200 {(Open ? "rotate-180" : "")}")" />
        </button>

        @if (Open)
        {
            <div id="@_contentId" class="@ContentClass" role="dialog" aria-label="@L["ColorPicker.PickColor"]"
                 @onkeydown="HandleKeyDown" tabindex="-1">

                @* Saturation-Value 2D picker *@
                <div id="@_svId"
                     class="lumeo-cp-sv relative w-full rounded-md overflow-hidden cursor-crosshair touch-none select-none"
                     style="@SvBackgroundStyle">
                    @* Bug A — NOT a theme bug: rgba(255,255,255) white→transparent and rgba(0,0,0) black→transparent
                       are HSV color-space primitives (saturation and value axes). They are mathematically defined
                       constants for the SV canvas and must not use CSS theme variables. *@
                    <div class="absolute inset-0" style="background: linear-gradient(to right, rgba(255,255,255,1), rgba(255,255,255,0))"></div>
                    <div class="absolute inset-0" style="background: linear-gradient(to top, rgba(0,0,0,1), rgba(0,0,0,0))"></div>
                    @* Bug C — border-white is intentional (always-white circle contrasts against the colored SV canvas).
                       The outer shadow uses lumeo-cp-sv-indicator (CSS class) so it adapts to the page theme
                       via color-mix instead of being hardcoded rgba(0,0,0,0.35). *@
                    <div class="lumeo-cp-sv-indicator absolute h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white pointer-events-none"
                         style="@SvIndicatorStyle"></div>
                </div>

                @* Hue slider *@
                <div class="mt-3">
                    <input type="range" min="0" max="360" value="@_h"
                           class="lumeo-cp-slider lumeo-cp-hue w-full cursor-pointer"
                           aria-label="@L["ColorPicker.Hue"]" aria-valuemin="0" aria-valuemax="360" aria-valuenow="@_h"
                           @oninput="OnHueChanged" />
                </div>

                @if (ShowAlpha)
                {
                    <div class="mt-2">
                        <input type="range" min="0" max="100" value="@_alpha"
                               class="lumeo-cp-slider lumeo-cp-slider-alpha w-full cursor-pointer"
                               style="--lumeo-cp-color: @SolidColorHex"
                               aria-label="@L["ColorPicker.Opacity"]" aria-valuemin="0" aria-valuemax="100" aria-valuenow="@_alpha"
                               @oninput="OnAlphaChanged" />
                    </div>
                }

                @* Preview + Hex *@
                <div class="mt-3 flex items-center gap-2">
                    <div class="lumeo-cp-chip h-9 w-9 rounded-md border border-border/40 shrink-0" style="@ChipStyle"></div>
                    <input type="text" value="@DisplayHex"
                           class="flex h-9 w-full min-w-0 rounded-md border border-border/60 bg-transparent px-2 text-xs font-mono uppercase focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring"
                           @onchange="OnHexInput" maxlength="9"
                           aria-label="@L["ColorPicker.HexValue"]" />
                </div>

                @* RGB inputs *@
                <div class="mt-2 grid grid-cols-3 gap-1.5">
                    <div>
                        <input type="number" min="0" max="255" value="@_r"
                               class="flex h-8 w-full min-w-0 rounded-md border border-border/60 bg-transparent px-1.5 text-xs font-mono tabular-nums text-center focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring"
                               @onchange="OnRChanged" aria-label="@L["ColorPicker.Red"]" />
                        <div class="mt-0.5 text-[10px] text-muted-foreground text-center">R</div>
                    </div>
                    <div>
                        <input type="number" min="0" max="255" value="@_g"
                               class="flex h-8 w-full min-w-0 rounded-md border border-border/60 bg-transparent px-1.5 text-xs font-mono tabular-nums text-center focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring"
                               @onchange="OnGChanged" aria-label="@L["ColorPicker.Green"]" />
                        <div class="mt-0.5 text-[10px] text-muted-foreground text-center">G</div>
                    </div>
                    <div>
                        <input type="number" min="0" max="255" value="@_b"
                               class="flex h-8 w-full min-w-0 rounded-md border border-border/60 bg-transparent px-1.5 text-xs font-mono tabular-nums text-center focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring"
                               @onchange="OnBChanged" aria-label="@L["ColorPicker.Blue"]" />
                        <div class="mt-0.5 text-[10px] text-muted-foreground text-center">B</div>
                    </div>
                </div>

                @if (Presets is { Count: > 0 })
                {
                    <div class="mt-3 pt-3 border-t border-border/40">
                        <div class="flex flex-wrap gap-1.5">
                            @foreach (var preset in Presets)
                            {
                                <button type="button"
                                        class="@GetSwatchClass()"
                                        style="background-color: @preset"
                                        @onclick="() => SelectColor(preset)"
                                        title="@preset"
                                        aria-label="@preset">
                                    @if (string.Equals(Value, preset, StringComparison.OrdinalIgnoreCase))
                                    {
                                        <Blazicon Svg="Lucide.Check" class="@CheckIconClass(preset)" />
                                    }
                                </button>
                            }
                        </div>
                    </div>
                }
            </div>
        }
    </div>
</CascadingValue>

@if (!InsideFormField)
{
    @if (EffectiveInvalid && ErrorText is not null)
    {
        <p class="text-xs text-destructive">@ErrorText</p>
    }
    else if (HelperText is not null)
    {
        <p class="text-xs text-muted-foreground">@HelperText</p>
    }
}

@inject ComponentInteropService Interop
@inject Lumeo.Services.Localization.ILumeoLocalizer L

@code {
    [CascadingParameter] private FormField.FormFieldContext? FormField { get; set; }

    [Parameter] public bool Required { get; set; }
    [Parameter] public bool Invalid { get; set; }
    [Parameter] public string? ErrorText { get; set; }
    [Parameter] public string? HelperText { get; set; }
    [Parameter] public string? Label { get; set; }
    [Parameter] public string? Name { get; set; }

    private bool EffectiveInvalid => Invalid || FormField?.HasError == true;
    private bool InsideFormField => FormField is not null;

    [Parameter] public string? Value { get; set; }
    [Parameter] public EventCallback<string> ValueChanged { get; set; }
    [Parameter] public bool ShowAlpha { get; set; }
    [Parameter] public List<string>? Presets { get; set; }
    [Parameter] public bool Disabled { get; set; }
    [Parameter] public bool Open { get; set; }
    [Parameter] public EventCallback<bool> OpenChanged { get; set; }

    [Obsolete("Use Open instead. IsOpen will be removed in a future release.")]
    [Parameter] public bool IsOpen { get => Open; set => Open = value; }

    [Obsolete("Use OpenChanged instead. IsOpenChanged will be removed in a future release.")]
    [Parameter] public EventCallback<bool> IsOpenChanged { get => OpenChanged; set => OpenChanged = value; }

    [Parameter] public string? Class { get; set; }
    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }

    private readonly string _wrapperId = $"colorpicker-{Guid.NewGuid():N}";
    private readonly string _contentId = $"colorpicker-content-{Guid.NewGuid():N}";
    private readonly string _svId = $"colorpicker-sv-{Guid.NewGuid():N}";
    private bool _registered;

    private int _h;
    private int _s = 100;
    private int _v = 100;
    private int _alpha = 100;
    private int _r, _g, _b;
    private string? _lastSyncedValue;

    private string ContainerClass => $"relative inline-block {Class}".Trim();

    private string TriggerClass => $"flex items-center gap-2 h-9 px-3 rounded-md border {(EffectiveInvalid ? "border-destructive ring-1 ring-destructive/20" : "border-border/60")} bg-background text-sm hover:bg-accent hover:text-accent-foreground transition-colors disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-[1.5px] {(EffectiveInvalid ? "focus-visible:ring-destructive" : "focus-visible:ring-ring")}";

    private const string ContentClass = "fixed z-50 w-72 rounded-md border border-border/40 bg-popover p-3 text-popover-foreground shadow-lg animate-fade-in";

    private string ChipStyle => $"background-color: {DisplayColor}";
    private string DisplayColor => ShowAlpha && _alpha < 100
        ? $"rgba({_r}, {_g}, {_b}, {(_alpha / 100.0).ToString("0.##", System.Globalization.CultureInfo.InvariantCulture)})"
        : SolidColorHex;
    private string SolidColorHex => $"#{_r:X2}{_g:X2}{_b:X2}";
    private string DisplayHex => Value ?? SolidColorHex;

    private string SvBackgroundStyle => $"background-color: hsl({_h}, 100%, 50%)";
    private string SvIndicatorStyle => $"left: {_s.ToString(System.Globalization.CultureInfo.InvariantCulture)}%; top: {(100 - _v).ToString(System.Globalization.CultureInfo.InvariantCulture)}%";

    private static string GetSwatchClass() =>
        "h-6 w-6 rounded-sm border border-border/40 cursor-pointer hover:scale-110 transition-transform flex items-center justify-center focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring";

    // Bug B — NOT a theme bug: text-black / text-white here is contrast-against-the-swatch-color, not against the
    // page background. The luminance check picks whichever extreme (black or white) is most readable on top of the
    // specific preset swatch. This is correct regardless of the page's light/dark theme.
    private static string CheckIconClass(string color) =>
        $"h-3 w-3 {(IsLight(color) ? "text-black" : "text-white")} drop-shadow-[0_1px_1px_rgba(0,0,0,0.35)]";

    protected override void OnParametersSet()
    {
        var current = Value ?? "#000000";
        if (!string.Equals(current, _lastSyncedValue, StringComparison.OrdinalIgnoreCase))
        {
            SyncStateFromHex(current);
            _lastSyncedValue = current;
        }
    }

    private async Task ToggleOpen() { if (!Disabled) await SetOpen(!Open); }

    private async Task SetOpen(bool value)
    {
        Open = value;
        await OpenChanged.InvokeAsync(value);
    }

    private async Task SelectColor(string color) => await ApplyColor(color);

    private async Task OnColorChanged(string color) => await ApplyColor(color);

    private async Task ApplyColor(string color)
    {
        Value = color;
        SyncStateFromHex(color);
        _lastSyncedValue = color;
        await ValueChanged.InvokeAsync(color);
    }

    private async Task OnHueChanged(ChangeEventArgs e)
    {
        if (int.TryParse(e.Value?.ToString(), out var h))
        {
            _h = Math.Clamp(h, 0, 360);
            await ApplyHsv();
        }
    }

    private void OnAlphaChanged(ChangeEventArgs e)
    {
        if (int.TryParse(e.Value?.ToString(), out var a)) _alpha = Math.Clamp(a, 0, 100);
    }

    private async Task OnRChanged(ChangeEventArgs e) { if (int.TryParse(e.Value?.ToString(), out var n)) { _r = Math.Clamp(n, 0, 255); await ApplyRgb(); } }
    private async Task OnGChanged(ChangeEventArgs e) { if (int.TryParse(e.Value?.ToString(), out var n)) { _g = Math.Clamp(n, 0, 255); await ApplyRgb(); } }
    private async Task OnBChanged(ChangeEventArgs e) { if (int.TryParse(e.Value?.ToString(), out var n)) { _b = Math.Clamp(n, 0, 255); await ApplyRgb(); } }

    private async Task ApplyHsv()
    {
        (_r, _g, _b) = HsvToRgb(_h, _s, _v);
        Value = $"#{_r:X2}{_g:X2}{_b:X2}";
        _lastSyncedValue = Value;
        await ValueChanged.InvokeAsync(Value);
    }

    private async Task ApplyRgb()
    {
        Value = $"#{_r:X2}{_g:X2}{_b:X2}";
        (_h, _s, _v) = RgbToHsv(_r, _g, _b);
        _lastSyncedValue = Value;
        await ValueChanged.InvokeAsync(Value);
    }

    private async Task OnHexInput(ChangeEventArgs e)
    {
        var hex = e.Value?.ToString()?.Trim() ?? "";
        if (!hex.StartsWith('#')) hex = "#" + hex;
        if (hex.Length is 4 or 7 or 9 && IsValidHex(hex))
        {
            await ApplyColor(hex);
        }
    }

    private async Task HandleKeyDown(KeyboardEventArgs e)
    {
        if (e.Key == "Escape") await SetOpen(false);
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (Open && !_registered)
        {
            await Interop.PositionFixed(_contentId, _wrapperId, "start", false, "bottom");
            await Interop.RegisterClickOutside(_contentId, _wrapperId, async () =>
            {
                await SetOpen(false);
                await InvokeAsync(StateHasChanged);
            });
            await Interop.RegisterSvDrag(_svId, async (s, v) =>
            {
                _s = (int)Math.Round(Math.Clamp(s, 0, 100));
                _v = (int)Math.Round(Math.Clamp(v, 0, 100));
                await ApplyHsv();
                await InvokeAsync(StateHasChanged);
            });
            _registered = true;
        }
        else if (!Open && _registered)
        {
            await Cleanup();
        }
    }

    private async Task Cleanup()
    {
        try { await Interop.UnpositionFixed(_contentId); } catch (JSDisconnectedException) { }
        try { await Interop.UnregisterClickOutside(_contentId); } catch (JSDisconnectedException) { }
        try { await Interop.UnregisterSvDrag(_svId); } catch (JSDisconnectedException) { }
        _registered = false;
    }

    public async ValueTask DisposeAsync()
    {
        if (_registered) await Cleanup();
    }

    private void SyncStateFromHex(string hex)
    {
        if (!TryParseHex(hex, out var r, out var g, out var b)) return;
        _r = r; _g = g; _b = b;
        (_h, _s, _v) = RgbToHsv(r, g, b);
    }

    private static bool IsValidHex(string hex)
    {
        if (string.IsNullOrEmpty(hex) || hex[0] != '#') return false;
        if (hex.Length is not (4 or 7 or 9)) return false;
        for (int i = 1; i < hex.Length; i++)
            if (!Uri.IsHexDigit(hex[i])) return false;
        return true;
    }

    private static bool TryParseHex(string hex, out int r, out int g, out int b)
    {
        r = g = b = 0;
        if (!IsValidHex(hex)) return false;
        if (hex.Length == 4)
        {
            r = int.Parse($"{hex[1]}{hex[1]}", System.Globalization.NumberStyles.HexNumber);
            g = int.Parse($"{hex[2]}{hex[2]}", System.Globalization.NumberStyles.HexNumber);
            b = int.Parse($"{hex[3]}{hex[3]}", System.Globalization.NumberStyles.HexNumber);
        }
        else
        {
            r = int.Parse(hex.Substring(1, 2), System.Globalization.NumberStyles.HexNumber);
            g = int.Parse(hex.Substring(3, 2), System.Globalization.NumberStyles.HexNumber);
            b = int.Parse(hex.Substring(5, 2), System.Globalization.NumberStyles.HexNumber);
        }
        return true;
    }

    private static bool IsLight(string hex)
    {
        if (!TryParseHex(hex, out var r, out var g, out var b)) return false;
        var luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255.0;
        return luminance > 0.6;
    }

    private static (int r, int g, int b) HsvToRgb(int h, int s, int v)
    {
        double hd = h / 360.0;
        double sd = s / 100.0;
        double vd = v / 100.0;

        if (sd < 0.0001)
        {
            var n = (int)Math.Round(vd * 255);
            return (n, n, n);
        }

        double sector = hd * 6;
        int i = (int)Math.Floor(sector);
        double f = sector - i;
        double p = vd * (1 - sd);
        double q = vd * (1 - sd * f);
        double t = vd * (1 - sd * (1 - f));

        double r, g, b;
        switch (i % 6)
        {
            case 0: r = vd; g = t;  b = p;  break;
            case 1: r = q;  g = vd; b = p;  break;
            case 2: r = p;  g = vd; b = t;  break;
            case 3: r = p;  g = q;  b = vd; break;
            case 4: r = t;  g = p;  b = vd; break;
            default: r = vd; g = p; b = q;  break;
        }
        return ((int)Math.Round(r * 255), (int)Math.Round(g * 255), (int)Math.Round(b * 255));
    }

    private static (int h, int s, int v) RgbToHsv(int r, int g, int b)
    {
        double rd = r / 255.0, gd = g / 255.0, bd = b / 255.0;
        double max = Math.Max(rd, Math.Max(gd, bd));
        double min = Math.Min(rd, Math.Min(gd, bd));
        double d = max - min;

        double h = 0;
        if (d > 0.0001)
        {
            if (max == rd)      h = 60 * (((gd - bd) / d) % 6);
            else if (max == gd) h = 60 * ((bd - rd) / d + 2);
            else                h = 60 * ((rd - gd) / d + 4);
            if (h < 0) h += 360;
        }
        double s = max < 0.0001 ? 0 : d / max;
        double v = max;
        return ((int)Math.Round(h), (int)Math.Round(s * 100), (int)Math.Round(v * 100));
    }

    public record ColorPickerContext(string Value, bool IsOpen, EventCallback<string> OnColorChanged, EventCallback<bool> SetOpen, string WrapperId);
}
