"use client";

import { useState, type Ref } from "react";
import { cx } from "../../utils";

export interface FoxToggleQuantityProps {
  /** 주면 제어 컴포넌트가 된다 — 호출부가 소유하고, 이 요소가 스스로 바꾸지 않는다. */
  value?: number;
  /** 비제어로 쓸 때의 초기 값. `value`를 주면 무시된다. */
  defaultValue?: number;
  /**
   * 값이 바뀔 때 호출된다. 이미 `min`·`max`로 자른 뒤의 값이 넘어온다.
   *
   * ⚠️ 이름이 `onChange`가 아니다 — 네이티브 입력을 감싸는 게 아니라 버튼 둘로 만든 커스텀
   * 위젯이라 `FoxSelect.onValueChange`와 같은 쪽이다.
   */
  onValueChange?: (value: number) => void;
  /** 내려갈 수 있는 하한. 값이 여기 닿으면 빼기 버튼이 비활성된다. */
  min?: number;
  /** 올라갈 수 있는 상한. 넘기지 않으면 상한이 없어 더하기 버튼이 비활성되지 않는다. */
  max?: number;
  /** 한 번에 오르내리는 폭. */
  step?: number;
  /**
   * 폼으로 값을 보낼 이름. 주면 같은 값을 담은 `<input type="hidden">`을 함께 렌더한다.
   *
   * `FoxChip`은 이걸 열지 않는다 — 칩은 여러 개가 한 묶음으로 하나의 값을 이뤄서 이름 하나를
   * 칩 하나에 묶을 수 없기 때문이다. 이쪽은 값이 정확히 하나라 그 문제가 없다.
   */
  name?: string;
  /** 두 버튼을 모두 비활성한다. `min`·`max`에 의한 개별 비활성과는 별개다. */
  disabled?: boolean;
  /** 묶음을 읽어 줄 이름. 화면에 보이는 제목이 이미 있으면 `labelledBy`를 쓴다. */
  label?: string;
  /** 묶음 이름 역할을 하는 요소의 id. `label`보다 우선한다. */
  labelledBy?: string;
  /** 빼기 버튼을 읽어 줄 이름. */
  decreaseLabel?: string;
  /** 더하기 버튼을 읽어 줄 이름. */
  increaseLabel?: string;
  /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
  hidden?: boolean;
  /** 배치 조정용. 시안의 폭(140px)을 바꿔야 할 때도 여기로 준다. */
  className?: string;
  ref?: Ref<HTMLDivElement>;
}

/**
 * @fox 수량 조절기. 빼기 버튼 · 숫자 · 더하기 버튼이 회색 판 위에 놓인다.
 *
 * 값은 제어·비제어 둘 다 된다(`FoxSelect`와 같은 방식). `min`·`max`에 닿으면 해당 버튼이
 * 네이티브 `disabled`가 되므로, 누를 수 없다는 것이 커서와 보조기술에 함께 전달된다.
 *
 * 숫자는 입력칸이 아니라 읽기 전용 글자다 — 시안에 입력칸의 테두리도 배경도 없다. 그래서
 * `role="spinbutton"`을 붙이지 않고 `role="group"` 안의 버튼 둘로 두고, 바뀐 값은
 * `aria-live`로 알린다. 키보드로 값을 직접 치는 자리가 필요해지면 시안에 먼저 추가한다.
 *
 * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
 * (또는 개별 파티셜)로 한 번 불러와야 한다.
 */
export function FoxToggleQuantity({
  value,
  defaultValue = 0,
  onValueChange,
  min = 0,
  max,
  step = 1,
  name,
  disabled = false,
  label,
  labelledBy,
  decreaseLabel = "수량 줄이기",
  increaseLabel = "수량 늘리기",
  hidden = false,
  className,
  ref,
}: FoxToggleQuantityProps) {
  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);

  const currentValue = value ?? uncontrolledValue;

  // 훅을 전부 부른 뒤에 빠져나간다 — 위쪽에서 반환하면 훅 호출 순서가 깨진다.
  if (hidden) {
    return null;
  }

  // 호출부가 범위 밖의 값을 줄 수도 있으므로 표시 직전에 한 번 자른다 — 자르지 않으면
  // 버튼 비활성 여부와 보이는 숫자가 어긋난다.
  const clamp = (next: number) => Math.min(max ?? Infinity, Math.max(min, next));

  const atMin = currentValue <= min;
  const atMax = max !== undefined && currentValue >= max;

  function commit(next: number) {
    const clamped = clamp(next);
    if (clamped === currentValue) {
      return;
    }
    // 제어 컴포넌트일 때는 내부 값을 건드리지 않는다 — 건드리면 호출부가 값을 거부해도
    // 내부에만 반영되어 둘이 조용히 갈라진다.
    if (value === undefined) setUncontrolledValue(clamped);
    onValueChange?.(clamped);
  }

  return (
    <div
      ref={ref}
      role="group"
      aria-label={labelledBy ? undefined : label}
      aria-labelledby={labelledBy}
      className={cx("fox-toggle-quantity", className)}
    >
      <button
        type="button"
        className="fox-toggle-quantity__button"
        disabled={disabled || atMin}
        aria-label={decreaseLabel}
        onClick={() => commit(currentValue - step)}
      >
        <span className="fox-toggle-quantity__icon" aria-hidden="true">
          <MinusMark />
        </span>
      </button>

      {/* 값이 바뀐 것을 소리로도 알린다 — 버튼 이름은 "수량 줄이기"라 결과를 담지 않는다. */}
      <span className="fox-toggle-quantity__value" aria-live="polite">
        {clamp(currentValue)}
      </span>

      <button
        type="button"
        className="fox-toggle-quantity__button"
        disabled={disabled || atMax}
        aria-label={increaseLabel}
        onClick={() => commit(currentValue + step)}
      >
        <span className="fox-toggle-quantity__icon" aria-hidden="true">
          <PlusMark />
        </span>
      </button>

      {name ? <input type="hidden" name={name} value={clamp(currentValue)} /> : null}
    </div>
  );
}

// 시안 아이콘 둘. `fill`을 시안의 리터럴(#58616A) 대신 `currentColor`로 두면 색을 버튼
// 규칙이 정한다(그쪽에서 같은 값을 `primitive(neutral-60)`으로 가리킨다).
// `width`·`height`를 쓰지 않는다 — 크기는 슬롯이 정하고 SVG는 100%로 따라온다.

function MinusMark() {
  return <i className="fox-ico fox-ico-Minus" aria-hidden="true" />;
}

function PlusMark() {
  return <i className="fox-ico fox-ico-Plus" aria-hidden="true" />;
}
