import {
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
  type ChangeEvent,
  type KeyboardEvent,
  type PointerEvent,
} from 'react';
import { formatTimecode } from '../media';
import { MAX_SPLIT_MARKERS } from '../limits';
import {
  renderWaveformToCanvas,
  validateWaveformPeakData,
  type WaveformPeakData,
} from '../waveform';
import './visual-editor.css';

type DragTarget =
  { kind: 'in' } | { kind: 'out' } | { kind: 'marker'; index: number };

interface ActiveDrag {
  pointerId: number;
  target: DragTarget;
}

export interface WaveformSelection {
  readonly inSeconds: number;
  readonly outSeconds: number;
  readonly markers: readonly number[];
}

export interface WaveformEditorProps {
  peaks: WaveformPeakData;
  inSeconds: number;
  outSeconds: number;
  markers: readonly number[];
  onChange: (selection: WaveformSelection) => void;
  currentTimeSeconds?: number;
  onSeek?: (timeSeconds: number) => void;
  disabled?: boolean;
  minimumClipDurationSeconds?: number;
  keyboardStepSeconds?: number;
  maximumZoom?: number;
  height?: number;
  ariaLabel?: string;
}

const MINIMUM_MARKER_GAP_SECONDS = 0.001;

/**
 * A single-track waveform editor. It renders bounded peak data to Canvas and
 * keeps trim points and split markers as accessible DOM controls above it.
 */
export function WaveformEditor({
  peaks,
  inSeconds,
  outSeconds,
  markers,
  onChange,
  currentTimeSeconds,
  onSeek,
  disabled = false,
  minimumClipDurationSeconds = 0.001,
  keyboardStepSeconds = 0.1,
  maximumZoom = 32,
  height = 168,
  ariaLabel = 'Waveform trim and split editor',
}: WaveformEditorProps) {
  const peakData = useMemo(() => validateWaveformPeakData(peaks), [peaks]);
  validateEditorOptions(
    minimumClipDurationSeconds,
    keyboardStepSeconds,
    maximumZoom,
    height
  );

  const instanceId = useId();
  const descriptionId = `${instanceId}-waveform-description`;
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const stageRef = useRef<HTMLDivElement>(null);
  const activeDragRef = useRef<ActiveDrag | undefined>(undefined);
  const [zoom, setZoom] = useState(1);
  const [viewportStartSeconds, setViewportStartSeconds] = useState(0);
  const [renderRevision, setRenderRevision] = useState(0);
  const duration = peakData.durationSeconds;
  const effectiveMinimumClipDuration = Math.min(
    minimumClipDurationSeconds,
    duration
  );
  const effectiveZoom = clamp(zoom, 1, maximumZoom);
  const selection = useMemo(
    () =>
      normalizeSelection(
        { inSeconds, outSeconds, markers },
        duration,
        minimumClipDurationSeconds
      ),
    [duration, inSeconds, markers, minimumClipDurationSeconds, outSeconds]
  );
  const visibleDuration = duration === 0 ? 0 : duration / effectiveZoom;
  const maximumViewportStart = Math.max(0, duration - visibleDuration);
  const effectiveViewportStart = clamp(
    viewportStartSeconds,
    0,
    maximumViewportStart
  );
  const viewportEnd = effectiveViewportStart + visibleDuration;
  const currentTime =
    currentTimeSeconds === undefined
      ? undefined
      : clampFinite(currentTimeSeconds, 0, duration);

  useEffect(() => {
    const stage = stageRef.current;
    if (stage === null || typeof ResizeObserver === 'undefined') return;
    const observer = new ResizeObserver(() => {
      setRenderRevision((revision) => revision + 1);
    });
    observer.observe(stage);
    return () => observer.disconnect();
  }, []);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (canvas === null || duration === 0) return;
    const style = getComputedStyle(canvas);
    const width = clamp(Math.round(canvas.clientWidth || 640), 1, 8_192);
    const startBucket = clamp(
      Math.floor((effectiveViewportStart / duration) * peakData.bucketCount),
      0,
      Math.max(0, peakData.bucketCount - 1)
    );
    const endBucket = clamp(
      Math.ceil((viewportEnd / duration) * peakData.bucketCount),
      startBucket + 1,
      peakData.bucketCount
    );
    renderWaveformToCanvas(canvas, peakData, {
      width,
      height,
      devicePixelRatio: clamp(window.devicePixelRatio || 1, 1, 4),
      startBucket,
      endBucket,
      waveColor: style.getPropertyValue('--waveform-wave').trim() || '#6fd7ca',
      backgroundColor:
        style.getPropertyValue('--waveform-background').trim() || '#101b22',
      centerLineColor:
        style.getPropertyValue('--waveform-center').trim() ||
        'rgba(255, 255, 255, 0.22)',
      selection: {
        startFraction: timeToViewportFraction(
          selection.inSeconds,
          effectiveViewportStart,
          visibleDuration
        ),
        endFraction: timeToViewportFraction(
          selection.outSeconds,
          effectiveViewportStart,
          visibleDuration
        ),
        color:
          style.getPropertyValue('--waveform-selection').trim() ||
          'rgba(88, 201, 184, 0.18)',
      },
      accessibleLabel: `${ariaLabel}. ${describeSelection(selection)} Visible range ${formatRange(
        effectiveViewportStart,
        viewportEnd
      )}.`,
    });
  }, [
    ariaLabel,
    duration,
    effectiveViewportStart,
    height,
    peakData,
    renderRevision,
    selection,
    viewportEnd,
    visibleDuration,
  ]);

  const emit = (next: WaveformSelection) => {
    onChange(normalizeSelection(next, duration, minimumClipDurationSeconds));
  };

  const setTrimPoint = (kind: 'in' | 'out', timeSeconds: number) => {
    if (disabled || duration === 0) return;
    const next =
      kind === 'in'
        ? {
            ...selection,
            inSeconds: clamp(
              timeSeconds,
              0,
              selection.outSeconds - effectiveMinimumClipDuration
            ),
          }
        : {
            ...selection,
            outSeconds: clamp(
              timeSeconds,
              selection.inSeconds + effectiveMinimumClipDuration,
              duration
            ),
          };
    emit(next);
  };

  const setMarker = (index: number, timeSeconds: number) => {
    if (disabled) return;
    const nextMarkers = [...selection.markers];
    nextMarkers[index] = clamp(
      timeSeconds,
      selection.inSeconds + MINIMUM_MARKER_GAP_SECONDS,
      selection.outSeconds - MINIMUM_MARKER_GAP_SECONDS
    );
    emit({ ...selection, markers: nextMarkers });
  };

  const addMarker = (timeSeconds: number) => {
    if (
      disabled ||
      selection.markers.length >= MAX_SPLIT_MARKERS ||
      timeSeconds <= selection.inSeconds + MINIMUM_MARKER_GAP_SECONDS / 2 ||
      timeSeconds >= selection.outSeconds - MINIMUM_MARKER_GAP_SECONDS / 2
    ) {
      return;
    }
    emit({ ...selection, markers: [...selection.markers, timeSeconds] });
  };

  const removeMarker = (index: number) => {
    if (disabled) return;
    emit({
      ...selection,
      markers: selection.markers.filter(
        (_, markerIndex) => markerIndex !== index
      ),
    });
  };

  const timeFromPointer = (event: PointerEvent<HTMLElement>): number => {
    const bounds = stageRef.current?.getBoundingClientRect();
    if (bounds === undefined || bounds.width <= 0) {
      return effectiveViewportStart;
    }
    return clamp(
      effectiveViewportStart +
        ((event.clientX - bounds.left) / bounds.width) * visibleDuration,
      effectiveViewportStart,
      viewportEnd
    );
  };

  const beginDrag = (
    event: PointerEvent<HTMLButtonElement>,
    target: DragTarget
  ) => {
    if (disabled) return;
    event.preventDefault();
    event.stopPropagation();
    event.currentTarget.setPointerCapture?.(event.pointerId);
    activeDragRef.current = { pointerId: event.pointerId, target };
  };

  const continueDrag = (event: PointerEvent<HTMLButtonElement>) => {
    const active = activeDragRef.current;
    if (
      active === undefined ||
      active.pointerId !== event.pointerId ||
      disabled
    ) {
      return;
    }
    event.preventDefault();
    const time = timeFromPointer(event);
    if (active.target.kind === 'marker') {
      setMarker(active.target.index, time);
    } else {
      setTrimPoint(active.target.kind, time);
    }
  };

  const endDrag = (event: PointerEvent<HTMLButtonElement>) => {
    if (activeDragRef.current?.pointerId === event.pointerId) {
      event.currentTarget.releasePointerCapture?.(event.pointerId);
      activeDragRef.current = undefined;
    }
  };

  const handleTrimKey = (
    event: KeyboardEvent<HTMLButtonElement>,
    kind: 'in' | 'out'
  ) => {
    if (disabled) return;
    const current = kind === 'in' ? selection.inSeconds : selection.outSeconds;
    const next = timeFromKey(
      event,
      current,
      kind === 'in' ? 0 : selection.inSeconds + effectiveMinimumClipDuration,
      kind === 'in'
        ? selection.outSeconds - effectiveMinimumClipDuration
        : duration,
      keyboardStepSeconds
    );
    if (next === undefined) return;
    event.preventDefault();
    setTrimPoint(kind, next);
  };

  const handleMarkerKey = (
    event: KeyboardEvent<HTMLButtonElement>,
    index: number
  ) => {
    if (disabled) return;
    if (event.key === 'Delete' || event.key === 'Backspace') {
      event.preventDefault();
      removeMarker(index);
      return;
    }
    const marker = selection.markers[index];
    if (marker === undefined) return;
    const next = timeFromKey(
      event,
      marker,
      selection.inSeconds + MINIMUM_MARKER_GAP_SECONDS,
      selection.outSeconds - MINIMUM_MARKER_GAP_SECONDS,
      keyboardStepSeconds
    );
    if (next === undefined) return;
    event.preventDefault();
    setMarker(index, next);
  };

  const seekFromStage = (event: PointerEvent<HTMLDivElement>) => {
    if (disabled || onSeek === undefined || event.button !== 0) return;
    onSeek(roundSeconds(timeFromPointer(event)));
  };

  const setZoomLevel = (event: ChangeEvent<HTMLInputElement>) => {
    const nextZoom = clamp(Number(event.currentTarget.value), 1, maximumZoom);
    const center = effectiveViewportStart + visibleDuration / 2;
    const nextVisibleDuration = duration / nextZoom;
    setZoom(nextZoom);
    setViewportStartSeconds(
      clamp(
        center - nextVisibleDuration / 2,
        0,
        Math.max(0, duration - nextVisibleDuration)
      )
    );
  };

  if (duration === 0 || peakData.bucketCount === 0) {
    return (
      <section
        className="waveform-editor waveform-editor--empty"
        aria-label={ariaLabel}
      >
        <p>No waveform samples are available for this source.</p>
      </section>
    );
  }

  return (
    <section className="waveform-editor" aria-label={ariaLabel}>
      <header className="waveform-editor__header">
        <div>
          <strong>Single sequential audio track</strong>
          <span id={descriptionId}>
            Drag a handle, or focus it and use the arrow keys. Hold Shift for
            larger keyboard steps.
          </span>
        </div>
        <output aria-live="polite">{describeSelection(selection)}</output>
      </header>

      <div
        ref={stageRef}
        className="waveform-editor__stage"
        style={{ height }}
        aria-describedby={descriptionId}
        onPointerDown={seekFromStage}
      >
        <canvas ref={canvasRef} className="waveform-editor__canvas" />
        <div className="waveform-editor__overlay">
          {currentTime !== undefined &&
          isTimeVisible(currentTime, effectiveViewportStart, viewportEnd) ? (
            <span
              className="waveform-editor__playhead"
              style={{
                left: `${String(
                  timeToViewportFraction(
                    currentTime,
                    effectiveViewportStart,
                    visibleDuration
                  ) * 100
                )}%`,
              }}
              aria-hidden="true"
            />
          ) : null}
          <WaveformHandle
            kind="in"
            time={selection.inSeconds}
            viewportStart={effectiveViewportStart}
            viewportEnd={viewportEnd}
            visibleDuration={visibleDuration}
            minimum={0}
            maximum={selection.outSeconds - effectiveMinimumClipDuration}
            disabled={disabled}
            onPointerDown={(event) => beginDrag(event, { kind: 'in' })}
            onPointerMove={continueDrag}
            onPointerEnd={endDrag}
            onKeyDown={(event) => handleTrimKey(event, 'in')}
          />
          <WaveformHandle
            kind="out"
            time={selection.outSeconds}
            viewportStart={effectiveViewportStart}
            viewportEnd={viewportEnd}
            visibleDuration={visibleDuration}
            minimum={selection.inSeconds + effectiveMinimumClipDuration}
            maximum={duration}
            disabled={disabled}
            onPointerDown={(event) => beginDrag(event, { kind: 'out' })}
            onPointerMove={continueDrag}
            onPointerEnd={endDrag}
            onKeyDown={(event) => handleTrimKey(event, 'out')}
          />
          {selection.markers.map((marker, index) =>
            isTimeVisible(marker, effectiveViewportStart, viewportEnd) ? (
              <button
                key={`${String(marker)}-${String(index)}`}
                type="button"
                className="waveform-editor__marker"
                style={{
                  left: `${String(
                    timeToViewportFraction(
                      marker,
                      effectiveViewportStart,
                      visibleDuration
                    ) * 100
                  )}%`,
                }}
                role="slider"
                aria-label={`Split marker ${String(index + 1)}`}
                aria-valuemin={selection.inSeconds}
                aria-valuemax={selection.outSeconds}
                aria-valuenow={marker}
                aria-valuetext={formatTimecode(marker)}
                aria-keyshortcuts="ArrowLeft ArrowRight Delete"
                disabled={disabled}
                onPointerDown={(event) =>
                  beginDrag(event, { kind: 'marker', index })
                }
                onPointerMove={continueDrag}
                onPointerUp={endDrag}
                onPointerCancel={endDrag}
                onKeyDown={(event) => handleMarkerKey(event, index)}
              >
                <span aria-hidden="true" />
              </button>
            ) : null
          )}
        </div>
      </div>

      <div className="waveform-editor__navigation">
        <label>
          <span>Zoom</span>
          <input
            type="range"
            aria-label="Zoom waveform"
            min="1"
            max={maximumZoom}
            step="0.25"
            value={effectiveZoom}
            disabled={disabled}
            aria-valuetext={`${effectiveZoom.toFixed(2)}×`}
            onChange={setZoomLevel}
          />
          <output>{effectiveZoom.toFixed(2)}×</output>
        </label>
        <label>
          <span>Scroll</span>
          <input
            type="range"
            aria-label="Scroll waveform"
            min="0"
            max={maximumViewportStart}
            step={Math.max(0.001, visibleDuration / 200)}
            value={effectiveViewportStart}
            disabled={disabled || effectiveZoom === 1}
            aria-valuetext={formatTimecode(effectiveViewportStart)}
            onChange={(event) =>
              setViewportStartSeconds(Number(event.currentTarget.value))
            }
          />
          <output>{formatRange(effectiveViewportStart, viewportEnd)}</output>
        </label>
      </div>

      <fieldset className="waveform-editor__trim" disabled={disabled}>
        <legend>Trim selection</legend>
        <label>
          <span>In</span>
          <input
            type="number"
            min="0"
            max={selection.outSeconds - effectiveMinimumClipDuration}
            step={keyboardStepSeconds}
            value={selection.inSeconds}
            onChange={(event) =>
              setTrimPoint('in', Number(event.currentTarget.value))
            }
          />
        </label>
        <label>
          <span>Out</span>
          <input
            type="number"
            min={selection.inSeconds + effectiveMinimumClipDuration}
            max={duration}
            step={keyboardStepSeconds}
            value={selection.outSeconds}
            onChange={(event) =>
              setTrimPoint('out', Number(event.currentTarget.value))
            }
          />
        </label>
        <button
          type="button"
          disabled={currentTime === undefined}
          onClick={() => {
            if (currentTime !== undefined) setTrimPoint('in', currentTime);
          }}
        >
          Set in to current time
        </button>
        <button
          type="button"
          disabled={currentTime === undefined}
          onClick={() => {
            if (currentTime !== undefined) setTrimPoint('out', currentTime);
          }}
        >
          Set out to current time
        </button>
        <button
          type="button"
          disabled={
            currentTime === undefined ||
            selection.markers.length >= MAX_SPLIT_MARKERS
          }
          onClick={() => {
            if (currentTime !== undefined) addMarker(currentTime);
          }}
        >
          Add split at current time
        </button>
      </fieldset>

      <div className="waveform-editor__markers">
        <div>
          <strong>Split markers</strong>
          <span>
            {selection.markers.length}/{MAX_SPLIT_MARKERS}
          </span>
        </div>
        {selection.markers.length === 0 ? (
          <p>No split markers.</p>
        ) : (
          <ol>
            {selection.markers.map((marker, index) => (
              <li key={`${String(marker)}-${String(index)}`}>
                <label>
                  <span>Marker {index + 1}</span>
                  <input
                    type="number"
                    min={selection.inSeconds + MINIMUM_MARKER_GAP_SECONDS}
                    max={selection.outSeconds - MINIMUM_MARKER_GAP_SECONDS}
                    step={keyboardStepSeconds}
                    value={marker}
                    disabled={disabled}
                    onChange={(event) =>
                      setMarker(index, Number(event.currentTarget.value))
                    }
                  />
                </label>
                <button
                  type="button"
                  disabled={disabled}
                  aria-label={`Remove split marker ${String(index + 1)}`}
                  onClick={() => removeMarker(index)}
                >
                  Remove
                </button>
              </li>
            ))}
          </ol>
        )}
      </div>
    </section>
  );
}

interface WaveformHandleProps {
  kind: 'in' | 'out';
  time: number;
  viewportStart: number;
  viewportEnd: number;
  visibleDuration: number;
  minimum: number;
  maximum: number;
  disabled: boolean;
  onPointerDown: (event: PointerEvent<HTMLButtonElement>) => void;
  onPointerMove: (event: PointerEvent<HTMLButtonElement>) => void;
  onPointerEnd: (event: PointerEvent<HTMLButtonElement>) => void;
  onKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => void;
}

function WaveformHandle({
  kind,
  time,
  viewportStart,
  viewportEnd,
  visibleDuration,
  minimum,
  maximum,
  disabled,
  onPointerDown,
  onPointerMove,
  onPointerEnd,
  onKeyDown,
}: WaveformHandleProps) {
  if (!isTimeVisible(time, viewportStart, viewportEnd)) return null;
  return (
    <button
      type="button"
      className={`waveform-editor__trim-handle waveform-editor__trim-handle--${kind}`}
      style={{
        left: `${String(
          timeToViewportFraction(time, viewportStart, visibleDuration) * 100
        )}%`,
      }}
      role="slider"
      aria-label={`${kind === 'in' ? 'In' : 'Out'} trim handle`}
      aria-valuemin={minimum}
      aria-valuemax={maximum}
      aria-valuenow={time}
      aria-valuetext={formatTimecode(time)}
      aria-keyshortcuts="ArrowLeft ArrowRight Home End"
      disabled={disabled}
      onPointerDown={onPointerDown}
      onPointerMove={onPointerMove}
      onPointerUp={onPointerEnd}
      onPointerCancel={onPointerEnd}
      onKeyDown={onKeyDown}
    >
      <span aria-hidden="true">{kind === 'in' ? 'IN' : 'OUT'}</span>
    </button>
  );
}

function validateEditorOptions(
  minimumDuration: number,
  keyboardStep: number,
  maximumZoom: number,
  height: number
): void {
  if (!Number.isFinite(minimumDuration) || minimumDuration <= 0) {
    throw new RangeError('Minimum clip duration must be positive.');
  }
  if (!Number.isFinite(keyboardStep) || keyboardStep <= 0) {
    throw new RangeError('Waveform keyboard step must be positive.');
  }
  if (!Number.isFinite(maximumZoom) || maximumZoom < 1 || maximumZoom > 128) {
    throw new RangeError('Waveform maximum zoom must be between 1 and 128.');
  }
  if (!Number.isFinite(height) || height < 80 || height > 1_024) {
    throw new RangeError('Waveform height must be between 80 and 1024 pixels.');
  }
}

function normalizeSelection(
  selection: WaveformSelection,
  duration: number,
  requestedMinimumDuration: number
): WaveformSelection {
  if (duration <= 0) {
    return Object.freeze({ inSeconds: 0, outSeconds: 0, markers: [] });
  }
  const minimumDuration = Math.min(requestedMinimumDuration, duration);
  const inPoint = roundSeconds(
    clampFinite(selection.inSeconds, 0, duration - minimumDuration)
  );
  const outPoint = roundSeconds(
    clampFinite(selection.outSeconds, inPoint + minimumDuration, duration)
  );
  const normalizedMarkers: number[] = [];
  for (const marker of selection.markers) {
    if (normalizedMarkers.length >= MAX_SPLIT_MARKERS) break;
    if (!Number.isFinite(marker)) continue;
    const normalized = roundSeconds(marker);
    if (
      normalized <= inPoint + MINIMUM_MARKER_GAP_SECONDS / 2 ||
      normalized >= outPoint - MINIMUM_MARKER_GAP_SECONDS / 2 ||
      normalizedMarkers.some(
        (existing) =>
          Math.abs(existing - normalized) < MINIMUM_MARKER_GAP_SECONDS
      )
    ) {
      continue;
    }
    normalizedMarkers.push(normalized);
  }
  normalizedMarkers.sort((left, right) => left - right);
  return Object.freeze({
    inSeconds: inPoint,
    outSeconds: outPoint,
    markers: Object.freeze(normalizedMarkers),
  });
}

function timeFromKey(
  event: KeyboardEvent,
  value: number,
  minimum: number,
  maximum: number,
  baseStep: number
): number | undefined {
  const step = event.shiftKey ? Math.max(1, baseStep * 10) : baseStep;
  switch (event.key) {
    case 'ArrowLeft':
    case 'ArrowDown':
      return clamp(value - step, minimum, maximum);
    case 'ArrowRight':
    case 'ArrowUp':
      return clamp(value + step, minimum, maximum);
    case 'Home':
      return minimum;
    case 'End':
      return maximum;
    default:
      return undefined;
  }
}

function timeToViewportFraction(
  time: number,
  viewportStart: number,
  visibleDuration: number
): number {
  if (visibleDuration <= 0) return 0;
  return clamp((time - viewportStart) / visibleDuration, 0, 1);
}

function isTimeVisible(time: number, start: number, end: number): boolean {
  return time >= start && time <= end;
}

function describeSelection(selection: WaveformSelection): string {
  return `Selected ${formatRange(
    selection.inSeconds,
    selection.outSeconds
  )}; ${String(selection.markers.length)} split marker${
    selection.markers.length === 1 ? '' : 's'
  }.`;
}

function formatRange(start: number, end: number): string {
  return `${formatTimecode(start)}–${formatTimecode(end)}`;
}

function roundSeconds(value: number): number {
  return Math.round(value * 1_000_000) / 1_000_000;
}

function clampFinite(value: number, minimum: number, maximum: number): number {
  return Number.isFinite(value) ? clamp(value, minimum, maximum) : minimum;
}

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