import { useEffect, useRef, useState, type CSSProperties, type InputHTMLAttributes, type KeyboardEvent, type PointerEvent } from "react";
import { cn } from "./cn.js";
import { Dropdown } from "./Dropdown.js";

export interface RangeControlProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange"> {
  label: string;
  value: number;
  min: number;
  max: number;
  formatValue?: (value: number) => string;
  onChange: (value: number) => void;
}

const clampRangeValue = (value: number, min: number, max: number) => (
  Math.max(min, Math.min(max, value))
);

const decimalPlaces = (value: number) => {
  const [coefficient, exponentText] = String(value).toLowerCase().split("e");
  const coefficientDecimals = coefficient?.split(".")[1]?.length ?? 0;
  if (exponentText === undefined) return coefficientDecimals;
  const exponent = Number.parseInt(exponentText, 10);
  if (!Number.isFinite(exponent)) return coefficientDecimals;
  return Math.max(0, coefficientDecimals - exponent);
};

const normalizeEditedRangeValue = (
  value: number,
  min: number,
  max: number,
  step: RangeControlProps["step"],
) => {
  const clamped = clampRangeValue(value, min, max);
  if (step === "any") return clamped;
  const numericStep = step === undefined ? 1 : Number(step);
  if (!Number.isFinite(numericStep) || numericStep <= 0) return clamped;
  const precision = Math.min(12, Math.max(decimalPlaces(min), decimalPlaces(numericStep)));
  const aligned = min + Math.round((clamped - min) / numericStep) * numericStep;
  return clampRangeValue(Number(aligned.toFixed(precision)), min, max);
};

const editableNumber = (value: number) => (
  Number.isInteger(value) ? String(value) : String(Number(value.toFixed(12)))
);

export function RangeControl({
  label,
  value,
  min,
  max,
  step,
  formatValue = String,
  onChange,
  className,
  disabled = false,
  tabIndex,
  ...props
}: RangeControlProps) {
  const rangeRef = useRef<HTMLInputElement>(null);
  const editorRef = useRef<HTMLInputElement>(null);
  const cancelNextBlurRef = useRef(false);
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState(() => editableNumber(value));
  const safeMax = max === min ? min + 1 : max;
  const progress = Math.max(0, Math.min(100, ((value - min) / (safeMax - min)) * 100));
  const displayValue = formatValue(value);
  const editorCharacters = Math.max(
    1,
    Math.min(14, editing ? draft.length || editableNumber(value).length : displayValue.length),
  );
  const style = {
    "--nodedc-range-progress": `${progress}%`,
    "--nodedc-range-editor-characters": editorCharacters,
  } as CSSProperties;

  useEffect(() => {
    if (!editing) setDraft(editableNumber(value));
  }, [editing, value]);

  const finishEditing = (commit: boolean, restoreRangeFocus: boolean) => {
    if (commit) {
      const parsed = Number(draft.trim().replace(",", "."));
      if (Number.isFinite(parsed)) onChange(normalizeEditedRangeValue(parsed, min, max, step));
    }
    setEditing(false);
    if (restoreRangeFocus) requestAnimationFrame(() => rangeRef.current?.focus());
  };

  const handleEditorKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
    event.stopPropagation();
    if (event.key === "Enter") {
      event.preventDefault();
      event.currentTarget.blur();
    }
    if (event.key === "Escape") {
      event.preventDefault();
      cancelNextBlurRef.current = true;
      setDraft(editableNumber(value));
      setEditing(false);
      event.currentTarget.blur();
      requestAnimationFrame(() => rangeRef.current?.focus());
    }
  };

  return (
    <div className={cn("nodedc-range", className)} style={style} data-editing={editing || undefined}>
      <input
        ref={rangeRef}
        {...props}
        type="range"
        value={value}
        min={min}
        max={safeMax}
        step={step}
        disabled={disabled}
        tabIndex={editing ? -1 : tabIndex}
        aria-label={label}
        aria-valuetext={displayValue}
        onChange={(event) => onChange(Number(event.target.value))}
      />
      <span className="nodedc-range__label">{label}</span>
      <span className="nodedc-range__value">{displayValue}</span>
      <span className="nodedc-range__fill-text" aria-hidden="true">
        <span className="nodedc-range__label">{label}</span>
        <span className="nodedc-range__value">{displayValue}</span>
      </span>
      <input
        ref={editorRef}
        className="nodedc-range__editor"
        type="text"
        inputMode="decimal"
        value={editing ? draft : editableNumber(value)}
        aria-label={`${label}: точное значение`}
        disabled={disabled}
        spellCheck={false}
        data-active={editing || undefined}
        onPointerDown={() => {
          if (!editing) {
            setDraft(editableNumber(value));
            setEditing(true);
          }
        }}
        onFocus={() => {
          if (!editing) {
            setDraft(editableNumber(value));
            setEditing(true);
          }
        }}
        onChange={(event) => setDraft(event.target.value)}
        onKeyDown={handleEditorKeyDown}
        onBlur={() => {
          if (cancelNextBlurRef.current) {
            cancelNextBlurRef.current = false;
            return;
          }
          finishEditing(true, false);
        }}
      />
    </div>
  );
}

export interface ColorFieldProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange"> {
  value: string;
  onChange: (value: string) => void;
  label?: string;
}

type Hsv = { h: number; s: number; v: number };

const palettePresets = ["#ff2f92", "#ff6caf", "#d746ff", "#7a3cff", "#4ea4ff", "#35d7c1", "#c3ff66", "#ffffff", "#404040", "#111116"];

function clamp(value: number, min: number, max: number) {
  return Math.max(min, Math.min(max, value));
}

function parseHexColor(value: string): [number, number, number] | null {
  const match = /^#?([0-9a-f]{6})$/i.exec(value.trim());
  if (!match) return null;
  const hex = match[1];
  return [
    Number.parseInt(hex.slice(0, 2), 16),
    Number.parseInt(hex.slice(2, 4), 16),
    Number.parseInt(hex.slice(4, 6), 16),
  ];
}

function hexToHsv(value: string): Hsv | null {
  const rgb = parseHexColor(value);
  if (!rgb) return null;
  const [red, green, blue] = rgb.map((channel) => channel / 255) as [number, number, number];
  const max = Math.max(red, green, blue);
  const min = Math.min(red, green, blue);
  const delta = max - min;
  let hue = 0;
  if (delta) {
    if (max === red) hue = 60 * (((green - blue) / delta) % 6);
    else if (max === green) hue = 60 * ((blue - red) / delta + 2);
    else hue = 60 * ((red - green) / delta + 4);
  }
  return { h: (hue + 360) % 360, s: max === 0 ? 0 : (delta / max) * 100, v: max * 100 };
}

function hsvToHex({ h, s, v }: Hsv): string {
  const hue = ((h % 360) + 360) % 360;
  const saturation = clamp(s, 0, 100) / 100;
  const value = clamp(v, 0, 100) / 100;
  const chroma = value * saturation;
  const component = (hue / 60) % 2;
  const match = chroma * (1 - Math.abs(component - 1));
  const base = hue < 60 ? [chroma, match, 0]
    : hue < 120 ? [match, chroma, 0]
      : hue < 180 ? [0, chroma, match]
        : hue < 240 ? [0, match, chroma]
          : hue < 300 ? [match, 0, chroma]
            : [chroma, 0, match];
  const offset = value - chroma;
  return `#${base.map((channel) => Math.round((channel + offset) * 255).toString(16).padStart(2, "0")).join("")}`;
}

export function ColorField({ value, onChange, label = "Цвет", className, disabled = false, ...props }: ColorFieldProps) {
  const validColor = hexToHsv(value);
  const [hsv, setHsv] = useState<Hsv>(() => validColor ?? { h: 330, s: 82, v: 100 });
  const style = { "--nodedc-color-value": validColor ? hsvToHex(validColor) : hsvToHex(hsv) } as CSSProperties;

  useEffect(() => {
    if (validColor) setHsv(validColor);
  }, [value, validColor?.h, validColor?.s, validColor?.v]);

  const commit = (next: Hsv) => {
    const normalized = { h: clamp(next.h, 0, 360), s: clamp(next.s, 0, 100), v: clamp(next.v, 0, 100) };
    setHsv(normalized);
    onChange(hsvToHex(normalized));
  };

  const updateSpectrum = (event: PointerEvent<HTMLDivElement>) => {
    const rect = event.currentTarget.getBoundingClientRect();
    commit({ ...hsv, s: ((event.clientX - rect.left) / rect.width) * 100, v: 100 - ((event.clientY - rect.top) / rect.height) * 100 });
  };

  return (
    <div className={cn("nodedc-color-field", className)} style={style}>
      <Dropdown
        className="nodedc-color-field__dropdown"
        placement="bottom-start"
        minWidth={296}
        width={296}
        disabled={disabled}
        surfaceRole="dialog"
        surfaceClassName="nodedc-color-palette-surface"
        trigger={({ open, toggle, setTriggerRef, surfaceId }) => (
          <button
            ref={setTriggerRef}
            type="button"
            className="nodedc-color-field__picker"
            aria-label={`Выбрать ${label}`}
            aria-haspopup="dialog"
            aria-controls={surfaceId}
            aria-expanded={open}
            data-open={open ? "true" : undefined}
            disabled={disabled}
            onClick={toggle}
          ><span aria-hidden="true" /></button>
        )}
      >
        <div className="nodedc-color-palette" aria-label={`Палитра: ${label}`}>
          <div
            className="nodedc-color-palette__spectrum"
            role="slider"
            tabIndex={disabled ? -1 : 0}
            aria-label={`${label}: насыщенность и яркость`}
            aria-valuetext={hsvToHex(hsv)}
            style={{ "--nodedc-color-hue": hsvToHex({ h: hsv.h, s: 100, v: 100 }), "--nodedc-color-s": `${hsv.s}%`, "--nodedc-color-v": `${100 - hsv.v}%` } as CSSProperties}
            onPointerDown={(event) => {
              if (disabled) return;
              event.currentTarget.setPointerCapture(event.pointerId);
              updateSpectrum(event);
            }}
            onPointerMove={(event) => {
              if (event.currentTarget.hasPointerCapture(event.pointerId)) updateSpectrum(event);
            }}
            onPointerUp={(event) => {
              if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
            }}
            onKeyDown={(event) => {
              const delta = event.shiftKey ? 10 : 2;
              if (event.key === "ArrowLeft") { event.preventDefault(); commit({ ...hsv, s: hsv.s - delta }); }
              if (event.key === "ArrowRight") { event.preventDefault(); commit({ ...hsv, s: hsv.s + delta }); }
              if (event.key === "ArrowDown") { event.preventDefault(); commit({ ...hsv, v: hsv.v - delta }); }
              if (event.key === "ArrowUp") { event.preventDefault(); commit({ ...hsv, v: hsv.v + delta }); }
            }}
          ><span className="nodedc-color-palette__marker" aria-hidden="true" /></div>
          <label className="nodedc-color-palette__hue">
            <span>Оттенок</span>
            <input type="range" min={0} max={360} value={Math.round(hsv.h)} aria-label={`${label}: оттенок`} disabled={disabled} onChange={(event) => commit({ ...hsv, h: Number(event.target.value) })} />
          </label>
          <div className="nodedc-color-palette__presets" aria-label="Быстрые цвета">
            {palettePresets.map((preset) => (
              <button
                key={preset}
                type="button"
                aria-label={`Выбрать ${preset}`}
                aria-pressed={hsvToHex(hsv).toLowerCase() === preset}
                style={{ "--nodedc-palette-swatch": preset } as CSSProperties}
                onClick={() => {
                  const next = hexToHsv(preset);
                  if (next) commit(next);
                }}
              />
            ))}
          </div>
        </div>
      </Dropdown>
      <input
        className="nodedc-color-field__text"
        value={value}
        aria-label={`${label}: HEX`}
        disabled={disabled}
        spellCheck={false}
        onChange={(event) => onChange(event.target.value)}
        {...props}
      />
    </div>
  );
}
