// RotorCompass — an azimuthal-equidistant rotor display (à la 4O3A RotorGenius).
//
// A world map centred on the operator's QTH (north up) fills the dial, ringed by
// a green azimuth bezel. A green needle shows the antenna heading (two needles
// when an Ultrabeam is bidirectional, the opposite one when reversed); a small
// red marker on the bezel shows the short-path bearing to the DX. Click the dial
// to turn the antenna there.
import { useEffect, useMemo, useRef, useState } from 'react';
import { geoAzimuthalEquidistant, geoPath, geoGraticule10 } from 'd3-geo';
import { feature } from 'topojson-client';
import landTopo from 'world-atlas/land-110m.json';
import { Compass, X, Play, Square } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/i18n';

// Decode the coastline outline once (≈110 m simplified land polygons).
const LAND = feature(landTopo as any, (landTopo as any).objects.land);
const GRATICULE = geoGraticule10();

interface Props {
  bearing?: number | null;       // short-path azimuth to DX (deg)
  headings: number[];            // radiating heading(s) — rotor + Ultrabeam pattern
  boomHeading?: number | null;   // mechanical boom (rotor) azimuth, shown grey when it differs
  pattern?: 'normal' | 'reverse' | 'bi' | null; // Ultrabeam pattern (for the badge)
  centerLat?: number | null;     // operator latitude (projection centre)
  centerLon?: number | null;     // operator longitude
  rotorEnabled?: boolean;
  rotors?: string[];             // logical rotor names; >1 → show a selector
  activeRotor?: number;          // index of the selected rotor (0-based)
  onSelectRotor?: (i: number) => void; // switch the active rotor
  onGoto?: (az: number) => void; // click-to-turn
  onClose?: () => void;
  // Quick-turn buttons and the azimuth box, shown only where the caller wants
  // them: Station Control draws its own GoTo/Stop around this compass, and two
  // sets of the same controls side by side would be nothing but confusing.
  presets?: { label: string; azimuth: number }[];
  onStop?: () => void;
}

const SIZE = 168;
const C = SIZE / 2;
const R = C - 6;          // outer bezel radius
const MAP_R = R - 6;      // map/clip radius (inside the bezel)

function pt(az: number, radius: number): [number, number] {
  const a = ((az - 90) * Math.PI) / 180;
  return [C + radius * Math.cos(a), C + radius * Math.sin(a)];
}

export function RotorCompass({ bearing, headings, boomHeading, pattern, centerLat, centerLon, rotorEnabled, rotors, activeRotor, onSelectRotor, onGoto, onClose, presets, onStop }: Props) {
  const { t } = useI18n();
  // Raw text, not a number: binding the input to a normalised value makes
  // Backspace fight the operator on the way from "230" to "23". It is parsed
  // when it is sent, and only then.
  const [azText, setAzText] = useState('');
  const showControls = !!(presets || onStop);

  // Which preset was just pressed, so it can light up for a moment.
  //
  // A rotor takes seconds to start moving and the needle barely twitches at
  // first, so without this the only answer to "did that register?" is to press
  // it again — which is how an antenna ends up ordered somewhere twice. The
  // acknowledgement has to come from the button itself, at once.
  const [flashIdx, setFlashIdx] = useState<number | null>(null);
  const flashTimer = useRef<number | undefined>(undefined);
  useEffect(() => () => window.clearTimeout(flashTimer.current), []);
  const pressPreset = (i: number, az: number) => {
    if (!onGoto) return;
    setFlashIdx(i);
    window.clearTimeout(flashTimer.current);
    flashTimer.current = window.setTimeout(() => setFlashIdx(null), 450);
    onGoto(az);
  };

  // Stop needs the same acknowledgement, for the same reason and one more: it
  // is pressed when something is already wrong, and a button that stays inert
  // gets hit again and again. Its own flag, so stopping does not blank a preset
  // that is still lit.
  const [stopFlash, setStopFlash] = useState(false);
  const stopTimer = useRef<number | undefined>(undefined);
  useEffect(() => () => window.clearTimeout(stopTimer.current), []);
  const pressStop = () => {
    if (!onStop) return;
    setStopFlash(true);
    window.clearTimeout(stopTimer.current);
    stopTimer.current = window.setTimeout(() => setStopFlash(false), 450);
    onStop();
  };

  // 0-359 and nothing else. 360 is refused rather than folded to 0 — it is
  // almost always a typo for 36 or 306, and a rotor swinging through north on a
  // slip of the finger is worth one rejected keypress.
  const sendAz = () => {
    const s = azText.trim();
    if (s === '' || !onGoto) return;
    const n = Number(s);
    if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0 || n > 359) return;
    onGoto(n);
    setAzText('');
  };

  const cardinals = useMemo(
    () => [ { d: 0, l: 'N' }, { d: 45, l: 'NE' }, { d: 90, l: 'E' }, { d: 135, l: 'SE' },
            { d: 180, l: 'S' }, { d: 225, l: 'SW' }, { d: 270, l: 'W' }, { d: 315, l: 'NW' } ],
    [],
  );

  // Project the world centred on the QTH (north up; antipode at the bezel).
  const { land, grat } = useMemo(() => {
    if (centerLat == null || centerLon == null) return { land: '', grat: '' };
    const proj = geoAzimuthalEquidistant()
      .rotate([-centerLon, -centerLat])
      .clipAngle(179.9)
      .scale(MAP_R / Math.PI)
      .translate([C, C]);
    const path = geoPath(proj as any);
    return { land: path(LAND as any) || '', grat: path(GRATICULE as any) || '' };
  }, [centerLat, centerLon]);

  function handleClick(e: React.MouseEvent<SVGSVGElement>) {
    if (!onGoto) return;
    const rect = e.currentTarget.getBoundingClientRect();
    const x = ((e.clientX - rect.left) / rect.width) * SIZE - C;
    const y = ((e.clientY - rect.top) / rect.height) * SIZE - C;
    let az = (Math.atan2(y, x) * 180) / Math.PI + 90;
    az = ((az % 360) + 360) % 360;
    onGoto(Math.round(az));
  }

  const headLabel = headings.length ? headings[0] : null;

  // Short and long path to the DX, in figures.
  //
  // The bezel already carries the short path as a red marker, but a marker is a
  // direction, not a number — the same pair sits in the status bar at 10px and
  // operators reported not being able to read it. It lives here because it
  // belongs to the compass: every place that draws one gets the readout, instead
  // of each caller inventing its own. Clickable when the caller can turn, like
  // the status bar's. Built as a value because it is placed in one of two
  // columns depending on whether the controls are shown.
  const pathReadout = (
    <div className="flex gap-1.5 mt-2 font-mono w-full">
      {([['SP', bearing ?? null], ['LP', bearing == null ? null : (bearing + 180) % 360]] as const).map(([lbl, az]) => (
        <button key={lbl} type="button" disabled={az == null || !onGoto}
          onClick={() => { if (az != null && onGoto) onGoto(Math.round(az)); }}
          title={az == null ? '' : `${lbl} ${Math.round(az)}°`}
          className={
            'flex-1 rounded-md border py-1 text-xs font-semibold tabular-nums transition-colors active:scale-95 ' +
            (az == null
              ? 'border-border text-muted-foreground/50 cursor-not-allowed'
              : onGoto
                ? 'border-info-border text-info-muted-foreground hover:bg-info-muted cursor-pointer'
                : 'border-border text-muted-foreground cursor-default')
          }>
          {lbl} {az == null ? '—' : `${Math.round(az)}°`}
        </button>
      ))}
    </div>
  );

  return (
    <section className="flex flex-col h-full min-h-0 rounded-lg border border-border bg-card overflow-hidden">
      {/* Header — matches the WinKeyer / Voice keyer panels. */}
      <div className="flex items-center gap-2 px-3 py-1.5 bg-muted/40 border-b border-border shrink-0">
        <Compass className="size-4 text-primary shrink-0" />
        <span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Rotor</span>
        <span className={cn('size-2 rounded-full', rotorEnabled ? 'bg-success' : 'bg-muted-foreground/40')}
          title={rotorEnabled ? 'Rotator connected' : 'Rotator disabled'} />
        <div className="flex-1" />
        {pattern && (
          <span
            className={cn('px-1 py-px rounded text-[9px] font-bold tracking-wide',
              pattern === 'reverse' ? 'bg-warning-muted text-warning-muted-foreground'
                : pattern === 'bi' ? 'bg-info-muted text-info-muted-foreground'
                : 'bg-success-muted text-success-muted-foreground')}
            title={pattern === 'reverse' ? 'Ultrabeam reversed — radiates opposite the boom'
              : pattern === 'bi' ? 'Ultrabeam bidirectional — radiates both ways'
              : 'Ultrabeam normal'}>
            {pattern === 'reverse' ? 'REV' : pattern === 'bi' ? 'BI' : 'NORM'}
          </span>
        )}
        <span className="font-mono text-sm font-bold text-success tabular-nums">
          {headLabel != null ? `${Math.round(headLabel).toString().padStart(3, '0')}°` : '—'}
        </span>
        {onClose && (
          <button className="text-muted-foreground hover:text-foreground" title="Hide rotor" onClick={onClose}>
            <X className="size-3.5" />
          </button>
        )}
      </div>

      {/* Multiple rotors — pick which one the dial shows and turns. */}
      {rotors && rotors.length > 1 && (
        <div className="flex flex-wrap gap-1 px-2 pt-1.5">
          {rotors.map((nm, i) => {
            const active = (activeRotor ?? 0) === i;
            const label = nm?.trim() || `Rotor ${i + 1}`;
            return (
              <button
                key={i}
                onClick={() => onSelectRotor?.(i)}
                className={cn(
                  'flex-1 min-w-[48px] px-1.5 py-0.5 rounded text-[10px] font-semibold truncate transition-colors',
                  active
                    ? 'bg-success text-success-foreground'
                    : 'bg-muted text-muted-foreground hover:bg-muted/70',
                )}
                title={label}
              >
                {label}
              </button>
            );
          })}
        </div>
      )}

      {/* Dial on the left, controls on the right. The controls column is sized to
          fit WITHIN the dial's height: the widget sits in a row whose height is
          set by the entry strip, so it may grow sideways but never downwards. */}
      <div className="flex items-start gap-2 p-2 min-h-0">
      {/* flex-col: the readout goes BELOW the dial. This wrapper was a row, so a
          sibling of the <svg> landed beside it. */}
      <div className="flex flex-col items-center justify-center min-h-0 shrink-0">
        <svg
          viewBox={`0 0 ${SIZE} ${SIZE}`}
          className={onGoto ? 'cursor-pointer select-none' : 'select-none'}
          style={{ width: SIZE, height: SIZE }}
          onClick={handleClick}
        >
          <defs>
            <clipPath id="rotorDial"><circle cx={C} cy={C} r={MAP_R} /></clipPath>
          </defs>
          {/* water + world map, clipped to the dial */}
          <circle cx={C} cy={C} r={MAP_R} fill="#d3e7f1" />
          <g clipPath="url(#rotorDial)">
            {grat && <path d={grat} fill="none" stroke="#9cc0d6" strokeWidth={0.4} opacity={0.7} />}
            {land && <path d={land} fill="#dfe2cf" stroke="#9aa589" strokeWidth={0.4} />}
          </g>
          {/* green azimuth bezel */}
          <circle cx={C} cy={C} r={R} fill="none" stroke="#16a34a" strokeWidth={5} />

          {/* ticks every 10°, longer at 30° */}
          {Array.from({ length: 36 }, (_, i) => i * 10).map((d) => {
            const major = d % 30 === 0;
            const [x1, y1] = pt(d, MAP_R);
            const [x2, y2] = pt(d, MAP_R - (major ? 7 : 4));
            return <line key={d} x1={x1} y1={y1} x2={x2} y2={y2} stroke="#475569" strokeWidth={major ? 1 : 0.6} opacity={0.7} />;
          })}
          {/* cardinal labels + degree numbers at 45° */}
          {cardinals.map(({ d, l }) => {
            const [x, y] = pt(d, MAP_R - 13);
            return <text key={l} x={x} y={y} textAnchor="middle" dominantBaseline="central" className="fill-slate-700" style={{ fontSize: l.length > 1 ? 7 : 9, fontWeight: 700 }}>{l}</text>;
          })}

          {/* DX short-path bearing → small red marker on the bezel */}
          {bearing != null && (() => { const [x, y] = pt(bearing, MAP_R); return (
            <circle cx={x} cy={y} r={3} fill="#dc2626" stroke="#fff" strokeWidth={1} />
          ); })()}

          {/* mechanical boom (rotor) heading — grey dashed needle, shown when the
              Ultrabeam radiates somewhere other than the boom (reverse/bi) so the
              operator sees where the antenna physically points vs where it boom-sits */}
          {boomHeading != null && pattern && pattern !== 'normal' && (() => {
            const [x, y] = pt(boomHeading, MAP_R - 2);
            return (
              <g>
                <title>Boom (rotor) {Math.round(boomHeading)}°</title>
                <line x1={C} y1={C} x2={x} y2={y} stroke="#64748b" strokeWidth={2} strokeDasharray="3 3" strokeLinecap="round" />
                <circle cx={x} cy={y} r={3} fill="#64748b" stroke="#fff" strokeWidth={1} />
              </g>
            );
          })()}

          {/* radiating heading needle(s) — green; two when bidirectional */}
          {headings.map((h, i) => { const [x, y] = pt(h, MAP_R - 2); return (
            <g key={i}>
              <line x1={C} y1={C} x2={x} y2={y} stroke="#15803d" strokeWidth={3} strokeLinecap="round" opacity={i === 0 ? 1 : 0.55} />
              <polygon points={`${x},${y} ${pt(h - 5, MAP_R - 12).join(',')} ${pt(h + 5, MAP_R - 12).join(',')}`} fill="#15803d" opacity={i === 0 ? 1 : 0.55} />
            </g>
          ); })}
          <circle cx={C} cy={C} r={3.5} fill="#15803d" stroke="#fff" strokeWidth={1} />
        </svg>

        {/* With the controls column present the readout goes at the FOOT OF IT
            instead: the dial sets the widget's height, the controls are shorter
            than the dial, and that leftover space is exactly the right size for
            the pair. Under the dial it would push the whole widget taller. */}
        {!showControls && pathReadout}
      </div>

      {/* Quick turns + free azimuth + Stop. */}
      {showControls && (
        <div className="flex flex-col gap-1.5 flex-1 min-w-0">
          {/* Two columns so six regions fit beside the dial rather than under
              it. An operator with fewer keeps the same compact block. */}
          {!!presets?.length && (
            <div className="grid grid-cols-2 gap-1">
              {presets.map((p, i) => (
                <button
                  key={`${p.label}-${i}`}
                  type="button"
                  disabled={!onGoto}
                  onClick={() => pressPreset(i, p.azimuth)}
                  title={`${p.label} — ${p.azimuth}°`}
                  className={cn(
                    'rounded-md border py-1 text-xs font-semibold truncate transition-all duration-150 active:scale-95',
                    flashIdx === i
                      // Lit, and showing the azimuth it just sent: the label alone
                      // would only say the press landed, not what was ordered.
                      ? 'border-success bg-success text-success-foreground scale-95'
                      : 'border-border bg-muted/40',
                    onGoto && flashIdx !== i ? 'hover:bg-muted' : '',
                    !onGoto ? 'opacity-50 cursor-not-allowed' : '',
                  )}
                >
                  {flashIdx === i ? `${p.azimuth}°` : p.label}
                </button>
              ))}
            </div>
          )}

          {/* Free azimuth: Enter sends, so the whole thing is type-three-digits
              -and-go without reaching for the mouse. */}
          <div className="flex gap-1">
            <input
              type="text"
              inputMode="numeric"
              value={azText}
              onChange={(e) => setAzText(e.target.value.replace(/[^0-9]/g, '').slice(0, 3))}
              onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); sendAz(); } }}
              placeholder={t('rotor.azPh')}
              title={t('rotor.azTitle')}
              disabled={!onGoto}
              className="min-w-0 flex-1 rounded-md border border-border bg-background px-2 py-1 text-xs font-mono tabular-nums text-center disabled:opacity-50"
            />
            <button
              type="button"
              onClick={sendAz}
              disabled={!onGoto || azText.trim() === ''}
              title={t('rotor.go')}
              className="flex items-center gap-1 rounded-md border border-success/60 bg-success-muted px-2 py-1 text-xs font-bold text-success-muted-foreground transition-transform hover:bg-success/25 active:scale-95 disabled:opacity-40 disabled:cursor-not-allowed"
            >
              <Play className="size-3" /> {t('rotor.go')}
            </button>
          </div>

          {onStop && (
            <button
              type="button"
              onClick={pressStop}
              title={t('rotor.stop')}
              className={cn(
                'flex items-center justify-center gap-1.5 rounded-md border py-1 text-xs font-bold transition-all duration-150 active:scale-95',
                stopFlash
                  // Solid, not a tint: STOP reads the same in both languages, so
                  // the fill is the whole acknowledgement.
                  ? 'border-destructive bg-destructive text-destructive-foreground scale-95'
                  : 'border-destructive/60 bg-destructive/15 text-destructive hover:bg-destructive/25',
              )}
            >
              <Square className="size-3 fill-current" /> {t('rotor.stop')}
            </button>
          )}

          {/* mt-auto: the readout sits at the FOOT of the column, level with the
              bottom of the dial, instead of floating under the Stop button. */}
          <div className="mt-auto">{pathReadout}</div>
        </div>
      )}
      </div>
    </section>
  );
}
