import { useState } from 'react';
import type { ImportedMediaAsset } from '../app/application-state';
import { formatBytes, formatDuration } from '../app/application-state';
import type {
  AvProjectV1,
  FadeCurve,
  ProjectChapter,
  ProjectMetadataSettings,
  ProjectSubtitleTrack,
  TimelineClip,
} from '../project';
import { CropOverlay } from './CropOverlay';
import { Icon } from './Icon';

type InspectorTab =
  | 'media'
  | 'transform'
  | 'audio'
  | 'metadata'
  | 'chapters'
  | 'subtitles'
  | 'thumbnails';

export interface ThumbnailRequest {
  timeSeconds: number;
  width?: number;
  height?: number;
  maintainAspect: boolean;
  format: 'png' | 'jpg' | 'webp';
  quality: number;
}

export interface InspectorProps {
  asset?: ImportedMediaAsset;
  project: AvProjectV1;
  clip?: TimelineClip;
  onClipUpdate: (changes: Partial<TimelineClip>) => void;
  onMetadataChange: (metadata: ProjectMetadataSettings) => void;
  onChaptersChange: (chapters: ProjectChapter[]) => void;
  onSubtitlesChange: (tracks: ProjectSubtitleTrack[]) => void;
  onSubtitleImport?: (file: File) => void;
  onThumbnail: (request: ThumbnailRequest) => void;
  currentPreviewTimeSeconds?: number;
  disabled?: boolean;
  drawerOpen?: boolean;
  onDrawerClose?: () => void;
}

const tabs: Array<{ id: InspectorTab; label: string }> = [
  { id: 'media', label: 'Media' },
  { id: 'transform', label: 'Transform' },
  { id: 'audio', label: 'Audio' },
  { id: 'metadata', label: 'Metadata' },
  { id: 'chapters', label: 'Chapters' },
  { id: 'subtitles', label: 'Subtitles' },
  { id: 'thumbnails', label: 'Frames' },
];

const fadeCurveOptions: ReadonlyArray<{
  value: FadeCurve;
  label: string;
}> = [
  { value: 'tri', label: 'Linear' },
  { value: 'qsin', label: 'Quarter sine' },
  { value: 'exp', label: 'Exponential' },
];

export function Inspector({
  asset,
  project,
  clip,
  onClipUpdate,
  onMetadataChange,
  onChaptersChange,
  onSubtitlesChange,
  onSubtitleImport,
  onThumbnail,
  currentPreviewTimeSeconds,
  disabled = false,
  drawerOpen = false,
  onDrawerClose,
}: InspectorProps) {
  const [tab, setTab] = useState<InspectorTab>('media');

  return (
    <aside
      id="edit-inspector-drawer"
      className={`inspector panel mobile-edit-drawer${drawerOpen ? ' is-open' : ''}`}
      aria-labelledby="inspector-title"
    >
      <header className="panel__header">
        <div>
          <p className="eyebrow">Non-destructive settings</p>
          <h2 id="inspector-title">Inspector</h2>
        </div>
        {onDrawerClose ? (
          <button
            className="icon-action mobile-edit-drawer__close"
            type="button"
            aria-label="Close inspector"
            onClick={onDrawerClose}
          >
            <Icon name="cancel" />
          </button>
        ) : null}
      </header>
      <div className="inspector__tabs" role="tablist" aria-label="Inspector">
        {tabs.map((entry) => (
          <button
            key={entry.id}
            type="button"
            role="tab"
            aria-selected={tab === entry.id}
            className={tab === entry.id ? 'is-active' : ''}
            onClick={() => setTab(entry.id)}
          >
            {entry.label}
          </button>
        ))}
      </div>
      <div className="inspector__body">
        {tab === 'media' ? <MediaInspector asset={asset} /> : null}
        {tab === 'transform' ? (
          <TransformInspector
            clip={clip}
            asset={asset}
            disabled={disabled}
            onUpdate={onClipUpdate}
          />
        ) : null}
        {tab === 'audio' ? (
          <AudioInspector
            clip={clip}
            disabled={disabled}
            onUpdate={onClipUpdate}
          />
        ) : null}
        {tab === 'metadata' ? (
          <MetadataInspector
            metadata={project.metadata}
            disabled={disabled}
            onChange={onMetadataChange}
          />
        ) : null}
        {tab === 'chapters' ? (
          <ChapterInspector
            chapters={project.chapters}
            disabled={disabled}
            onChange={onChaptersChange}
          />
        ) : null}
        {tab === 'subtitles' ? (
          <SubtitleInspector
            tracks={project.subtitles}
            disabled={disabled}
            onChange={onSubtitlesChange}
            onImport={onSubtitleImport}
          />
        ) : null}
        {tab === 'thumbnails' ? (
          <ThumbnailInspector
            duration={asset?.probe?.durationSeconds}
            currentPreviewTimeSeconds={currentPreviewTimeSeconds}
            disabled={disabled || !asset}
            onCreate={onThumbnail}
          />
        ) : null}
      </div>
    </aside>
  );
}

function MediaInspector({ asset }: { asset?: ImportedMediaAsset }) {
  if (!asset) return <p className="panel-empty">Select a media source.</p>;
  const probe = asset.probe;
  return (
    <div className="inspector-section">
      <div className="inspector-file">
        <span>
          <Icon
            name={
              probe?.streams.some((stream) => stream.type === 'video')
                ? 'video'
                : 'audio'
            }
          />
        </span>
        <div>
          <strong>{asset.file.name}</strong>
          <small>{formatBytes(asset.file.size)}</small>
        </div>
      </div>
      <dl className="detail-list">
        <div>
          <dt>Container</dt>
          <dd>
            {probe?.formatLongName ?? probe?.formatNames.join(', ') ?? '—'}
          </dd>
        </div>
        <div>
          <dt>Duration</dt>
          <dd>{formatDuration(probe?.durationSeconds)}</dd>
        </div>
        <div>
          <dt>Bit rate</dt>
          <dd>
            {probe?.bitRate
              ? `${Math.round(probe.bitRate / 1_000)} kbit/s`
              : '—'}
          </dd>
        </div>
      </dl>
      {probe && Object.keys(probe.tags).length > 0 ? (
        <>
          <h3>Container tags</h3>
          <dl className="detail-list metadata-detail-list">
            {Object.entries(probe.tags).map(([key, value]) => (
              <div key={key}>
                <dt>{key}</dt>
                <dd>{value}</dd>
              </div>
            ))}
          </dl>
        </>
      ) : null}
      <h3>Streams</h3>
      <ol className="inspector-streams">
        {probe?.streams.map((stream) => (
          <li key={stream.index}>
            <span className="stream-kind">{stream.type}</span>
            <div>
              <strong>
                #{stream.index} · {stream.codecName ?? 'unknown'}
              </strong>
              {stream.type === 'video' ? (
                <>
                  <small>
                    Encoded pixels {stream.width ?? '?'}×{stream.height ?? '?'}
                    {stream.codedWidth !== undefined &&
                    stream.codedHeight !== undefined
                      ? ` · coded ${stream.codedWidth}×${stream.codedHeight}`
                      : ''}
                    {' · '}
                    {stream.frameRate?.toFixed(3) ?? '?'} fps
                  </small>
                  <small>
                    SAR {stream.sampleAspectRatio ?? '—'} · DAR{' '}
                    {stream.displayAspectRatio ?? '—'} · rotation{' '}
                    {stream.rotationDegrees === undefined
                      ? '—'
                      : `${stream.rotationDegrees}°`}
                  </small>
                </>
              ) : (
                <small>
                  {stream.type === 'audio'
                    ? `${stream.sampleRate ?? '?'} Hz · ${stream.channelLayout ?? `${stream.channels ?? '?'} channels`}`
                    : (stream.language ??
                      stream.title ??
                      'No additional details')}
                </small>
              )}
              {Object.keys(stream.disposition).length > 0 ? (
                <small>
                  Disposition:{' '}
                  {Object.entries(stream.disposition)
                    .filter(([, enabled]) => enabled)
                    .map(([name]) => name)
                    .join(', ') || 'none'}
                </small>
              ) : null}
              {Object.keys(stream.tags).length > 0 ? (
                <dl className="stream-tag-list">
                  {Object.entries(stream.tags).map(([key, value]) => (
                    <div key={key}>
                      <dt>{key}</dt>
                      <dd>{value}</dd>
                    </div>
                  ))}
                </dl>
              ) : null}
            </div>
          </li>
        )) ?? <li>Waiting for inspection.</li>}
      </ol>
      {probe && probe.chapters.length > 0 ? (
        <>
          <h3>Source chapters</h3>
          <ol className="source-chapter-list">
            {probe.chapters.map((chapter) => (
              <li key={chapter.id}>
                <strong>{chapter.title ?? `Chapter ${chapter.id + 1}`}</strong>
                <small>
                  {formatDuration(chapter.startSeconds)}–
                  {formatDuration(chapter.endSeconds)}
                  {chapter.timeBase ? ` · ${chapter.timeBase}` : ''}
                </small>
              </li>
            ))}
          </ol>
        </>
      ) : null}
      {probe && probe.warnings.length > 0 ? (
        <details className="inline-details">
          <summary>{probe.warnings.length} probe warning(s)</summary>
          <ul>
            {probe.warnings.map((warning, index) => (
              <li key={`${warning.path}-${index}`}>{warning.message}</li>
            ))}
          </ul>
        </details>
      ) : null}
      {asset.rawProbe ? (
        <details className="diagnostics">
          <summary>Validated raw ffprobe report</summary>
          <pre>{asset.rawProbe}</pre>
        </details>
      ) : null}
    </div>
  );
}

function TransformInspector({
  clip,
  asset,
  disabled,
  onUpdate,
}: {
  clip?: TimelineClip;
  asset?: ImportedMediaAsset;
  disabled: boolean;
  onUpdate: (changes: Partial<TimelineClip>) => void;
}) {
  if (!clip) return <p className="panel-empty">Select a timeline clip.</p>;
  const video = asset?.probe?.streams.find((stream) => stream.type === 'video');
  const sourceWidth = video?.width;
  const sourceHeight = video?.height;
  const canShowVisualCrop =
    asset !== undefined &&
    isPositiveSafeInteger(sourceWidth) &&
    isPositiveSafeInteger(sourceHeight) &&
    sourceWidth >= 2 &&
    sourceHeight >= 2;

  return (
    <div className="inspector-section">
      <h3>Crop</h3>
      {canShowVisualCrop ? (
        <CropOverlay
          sourceWidth={sourceWidth}
          sourceHeight={sourceHeight}
          orientation={clip.rotation ?? 0}
          value={clip.crop}
          disabled={disabled}
          requireEvenDimensions
          ariaLabel={`Crop ${asset.file.name}`}
          onChange={(crop) => onUpdate({ crop })}
        >
          <video
            key={asset.id}
            src={asset.objectUrl}
            muted
            playsInline
            preload="auto"
            aria-label={`Local preview of ${asset.file.name}`}
          />
        </CropOverlay>
      ) : (
        <NumericCropEditor
          clip={clip}
          sourceWidth={sourceWidth}
          sourceHeight={sourceHeight}
          disabled={disabled}
          onUpdate={onUpdate}
        />
      )}
      <h3>Resize & rotation</h3>
      <div className="preset-buttons" aria-label="Common output resolutions">
        {[
          { label: '720p', width: 1280, height: 720 },
          { label: '1080p', width: 1920, height: 1080 },
          { label: '2160p', width: 3840, height: 2160 },
        ].map((preset) => (
          <button
            className="button button--quiet"
            type="button"
            key={preset.label}
            disabled={disabled}
            onClick={() =>
              onUpdate({
                resize: {
                  ...clip.resize,
                  width: preset.width,
                  height: preset.height,
                  mode: 'fit',
                },
              })
            }
          >
            {preset.label}
          </button>
        ))}
      </div>
      <div className="mini-grid">
        <label className="field">
          <span>Width</span>
          <input
            type="number"
            min="2"
            max="8192"
            disabled={disabled}
            value={clip.resize?.width ?? ''}
            placeholder={String(video?.width ?? 'auto')}
            onChange={(event) =>
              onUpdate({
                resize: {
                  ...clip.resize,
                  mode: clip.resize?.mode ?? 'fit',
                  width: event.currentTarget.value
                    ? Number(event.currentTarget.value)
                    : undefined,
                },
              })
            }
          />
        </label>
        <label className="field">
          <span>Height</span>
          <input
            type="number"
            min="2"
            max="8192"
            disabled={disabled}
            value={clip.resize?.height ?? ''}
            placeholder={String(video?.height ?? 'auto')}
            onChange={(event) =>
              onUpdate({
                resize: {
                  ...clip.resize,
                  mode: clip.resize?.mode ?? 'fit',
                  height: event.currentTarget.value
                    ? Number(event.currentTarget.value)
                    : undefined,
                },
              })
            }
          />
        </label>
        <label className="field">
          <span>Fit</span>
          <select
            disabled={disabled}
            value={clip.resize?.mode ?? 'fit'}
            onChange={(event) =>
              onUpdate({
                resize: {
                  ...clip.resize,
                  mode: event.currentTarget.value as 'fit' | 'fill' | 'stretch',
                },
              })
            }
          >
            <option value="fit">Fit / letterbox</option>
            <option value="fill">Fill / crop</option>
            <option value="stretch">Stretch</option>
          </select>
        </label>
        <label className="field">
          <span>Rotate</span>
          <select
            disabled={disabled}
            value={clip.rotation ?? 0}
            onChange={(event) =>
              onUpdate({
                rotation: Number(event.currentTarget.value) as
                  0 | 90 | 180 | 270,
              })
            }
          >
            <option value="0">None</option>
            <option value="90">90° clockwise</option>
            <option value="180">180°</option>
            <option value="270">90° counter-clockwise</option>
          </select>
        </label>
        <p className="field field--wide field-note">
          Frame-rate conversion is a timeline-wide output setting. Choose it in
          Timeline output below the editor; no frame interpolation is applied.
        </p>
        <label className="field">
          <span>Scale algorithm</span>
          <select
            disabled={disabled || !clip.resize}
            value={clip.resize?.algorithm ?? 'lanczos'}
            onChange={(event) =>
              onUpdate({
                resize: {
                  ...clip.resize,
                  mode: clip.resize?.mode ?? 'fit',
                  algorithm: event.currentTarget.value as NonNullable<
                    TimelineClip['resize']
                  >['algorithm'],
                },
              })
            }
          >
            <option value="fast_bilinear">Fast bilinear</option>
            <option value="bilinear">Bilinear</option>
            <option value="bicubic">Bicubic</option>
            <option value="lanczos">Lanczos</option>
          </select>
        </label>
        <label className="field">
          <span>Padding colour</span>
          <input
            type="text"
            maxLength={20}
            disabled={disabled || !clip.resize || clip.resize.mode !== 'fit'}
            value={clip.resize?.paddingColor ?? 'black'}
            onChange={(event) =>
              onUpdate({
                resize: {
                  ...clip.resize,
                  mode: clip.resize?.mode ?? 'fit',
                  paddingColor: event.currentTarget.value,
                },
              })
            }
          />
        </label>
      </div>
      <label className="switch-field">
        <input
          type="checkbox"
          checked={clip.resize?.allowUpscale !== false}
          disabled={disabled || !clip.resize}
          onChange={(event) =>
            onUpdate({
              resize: {
                ...clip.resize,
                mode: clip.resize?.mode ?? 'fit',
                allowUpscale: event.currentTarget.checked,
              },
            })
          }
        />
        <span>Allow upscaling</span>
      </label>
      <fieldset className="subpanel">
        <legend>Video fades</legend>
        <div className="mini-grid">
          <label className="field">
            <span>Fade in from black (s)</span>
            <input
              type="number"
              min="0"
              step="0.1"
              disabled={disabled}
              value={clip.video?.fadeInSeconds ?? 0}
              onChange={(event) =>
                onUpdate({
                  video: {
                    enabled: clip.video?.enabled ?? true,
                    ...clip.video,
                    fadeInSeconds: Number(event.currentTarget.value),
                  },
                })
              }
            />
          </label>
          <label className="field">
            <span>Fade out to black (s)</span>
            <input
              type="number"
              min="0"
              step="0.1"
              disabled={disabled}
              value={clip.video?.fadeOutSeconds ?? 0}
              onChange={(event) =>
                onUpdate({
                  video: {
                    enabled: clip.video?.enabled ?? true,
                    ...clip.video,
                    fadeOutSeconds: Number(event.currentTarget.value),
                  },
                })
              }
            />
          </label>
          <label className="field">
            <span>Curve</span>
            <select
              aria-label="Video fade curve"
              disabled={disabled}
              value={clip.video?.fadeCurve ?? 'tri'}
              onChange={(event) =>
                onUpdate({
                  video: {
                    enabled: clip.video?.enabled ?? true,
                    ...clip.video,
                    fadeCurve: event.currentTarget.value as FadeCurve,
                  },
                })
              }
            >
              {fadeCurveOptions.map((option) => (
                <option key={option.value} value={option.value}>
                  {option.label}
                </option>
              ))}
            </select>
          </label>
        </div>
      </fieldset>
    </div>
  );
}

function NumericCropEditor({
  clip,
  sourceWidth,
  sourceHeight,
  disabled,
  onUpdate,
}: {
  clip: TimelineClip;
  sourceWidth?: number;
  sourceHeight?: number;
  disabled: boolean;
  onUpdate: (changes: Partial<TimelineClip>) => void;
}) {
  return (
    <>
      <p className="section-intro">
        A visual crop preview is unavailable for this source. Enter encoded
        source pixels instead.
      </p>
      <div className="mini-grid">
        {(['x', 'y', 'width', 'height'] as const).map((field) => (
          <label className="field" key={field}>
            <span>{field.toUpperCase()}</span>
            <input
              type="number"
              min={field === 'width' || field === 'height' ? 2 : 0}
              step="2"
              max={
                field === 'x' || field === 'width' ? sourceWidth : sourceHeight
              }
              disabled={disabled}
              value={
                clip.crop?.[field] ??
                (field === 'width'
                  ? (sourceWidth ?? '')
                  : field === 'height'
                    ? (sourceHeight ?? '')
                    : 0)
              }
              onChange={(event) => {
                const crop = {
                  x: clip.crop?.x ?? 0,
                  y: clip.crop?.y ?? 0,
                  width: clip.crop?.width ?? sourceWidth ?? 2,
                  height: clip.crop?.height ?? sourceHeight ?? 2,
                  [field]: toNonNegativeEvenInteger(
                    Number(event.currentTarget.value),
                    field === 'width' || field === 'height' ? 2 : 0
                  ),
                };
                onUpdate({ crop });
              }}
            />
          </label>
        ))}
      </div>
    </>
  );
}

function isPositiveSafeInteger(value: number | undefined): value is number {
  return (
    value !== undefined && Number.isSafeInteger(value) && Number(value) > 0
  );
}

function toNonNegativeEvenInteger(value: number, minimum: number): number {
  if (!Number.isFinite(value)) return minimum;
  const rounded = Math.max(minimum, Math.round(value));
  return rounded - (rounded % 2);
}

function AudioInspector({
  clip,
  disabled,
  onUpdate,
}: {
  clip?: TimelineClip;
  disabled: boolean;
  onUpdate: (changes: Partial<TimelineClip>) => void;
}) {
  if (!clip) return <p className="panel-empty">Select a timeline clip.</p>;
  const audio = clip.audio ?? { enabled: true };
  const update = (changes: Partial<typeof audio>) =>
    onUpdate({ audio: { ...audio, ...changes } });
  return (
    <div className="inspector-section">
      <label className="switch-field">
        <input
          type="checkbox"
          checked={audio.enabled}
          disabled={disabled}
          onChange={(event) => update({ enabled: event.currentTarget.checked })}
        />
        <span>Include audio</span>
      </label>
      <label className="field">
        <span>Gain (dB)</span>
        <input
          type="number"
          min="-60"
          max="30"
          step="0.1"
          value={audio.gainDb ?? 0}
          disabled={disabled || !audio.enabled}
          onChange={(event) =>
            update({ gainDb: Number(event.currentTarget.value) })
          }
        />
      </label>
      <div className="mini-grid">
        <label className="field">
          <span>Fade in (s)</span>
          <input
            type="number"
            min="0"
            step="0.1"
            value={audio.fadeInSeconds ?? 0}
            disabled={disabled || !audio.enabled}
            onChange={(event) =>
              update({ fadeInSeconds: Number(event.currentTarget.value) })
            }
          />
        </label>
        <label className="field">
          <span>Fade out (s)</span>
          <input
            type="number"
            min="0"
            step="0.1"
            value={audio.fadeOutSeconds ?? 0}
            disabled={disabled || !audio.enabled}
            onChange={(event) =>
              update({ fadeOutSeconds: Number(event.currentTarget.value) })
            }
          />
        </label>
        <label className="field">
          <span>Curve</span>
          <select
            aria-label="Audio fade curve"
            value={audio.fadeCurve ?? 'tri'}
            disabled={disabled || !audio.enabled}
            onChange={(event) =>
              update({
                fadeCurve: event.currentTarget.value as FadeCurve,
              })
            }
          >
            {fadeCurveOptions.map((option) => (
              <option key={option.value} value={option.value}>
                {option.label}
              </option>
            ))}
          </select>
        </label>
      </div>
      <fieldset className="subpanel">
        <legend>EBU R128 normalization</legend>
        <label className="switch-field">
          <input
            type="checkbox"
            checked={audio.normalization !== undefined}
            disabled={disabled || !audio.enabled}
            onChange={(event) =>
              update({
                normalization: event.currentTarget.checked
                  ? {
                      targetLufs: -16,
                      truePeakDb: -1.5,
                      loudnessRangeLufs: 11,
                    }
                  : undefined,
              })
            }
          />
          <span>Two-pass loudness analysis</span>
        </label>
        {audio.normalization ? (
          <div className="mini-grid">
            <label className="field">
              <span>Target LUFS</span>
              <input
                type="number"
                min="-70"
                max="-5"
                step="0.1"
                value={audio.normalization.targetLufs}
                onChange={(event) =>
                  update({
                    normalization: {
                      ...audio.normalization!,
                      targetLufs: Number(event.currentTarget.value),
                    },
                  })
                }
              />
            </label>
            <label className="field">
              <span>True peak (dBTP)</span>
              <input
                type="number"
                min="-9"
                max="0"
                step="0.1"
                value={audio.normalization.truePeakDb}
                onChange={(event) =>
                  update({
                    normalization: {
                      ...audio.normalization!,
                      truePeakDb: Number(event.currentTarget.value),
                    },
                  })
                }
              />
            </label>
          </div>
        ) : null}
      </fieldset>
    </div>
  );
}

function MetadataInspector({
  metadata,
  disabled,
  onChange,
}: {
  metadata: ProjectMetadataSettings;
  disabled: boolean;
  onChange: (metadata: ProjectMetadataSettings) => void;
}) {
  const fields = ['title', 'artist', 'album', 'comment'] as const;
  return (
    <div className="inspector-section">
      <p className="section-intro">
        Output metadata is plain text and is escaped by the command-plan layer.
      </p>
      {fields.map((field) => (
        <label className="field" key={field}>
          <span>{field[0]?.toUpperCase() + field.slice(1)}</span>
          <input
            type="text"
            maxLength={1024}
            disabled={disabled}
            value={metadata[field] ?? ''}
            onChange={(event) =>
              onChange({ ...metadata, [field]: event.currentTarget.value })
            }
          />
        </label>
      ))}
    </div>
  );
}

function ChapterInspector({
  chapters,
  disabled,
  onChange,
}: {
  chapters: ProjectChapter[];
  disabled: boolean;
  onChange: (chapters: ProjectChapter[]) => void;
}) {
  return (
    <div className="inspector-section">
      <p className="section-intro">
        Chapters use timeline seconds and are written through generated metadata
        files. Time base is a positive fraction such as 1/1000.
      </p>
      <ol className="chapter-list">
        {chapters.map((chapter, index) => (
          <li key={chapter.id}>
            <input
              aria-label={`Chapter ${index + 1} title`}
              value={chapter.title}
              disabled={disabled}
              onChange={(event) =>
                onChange(
                  chapters.map((entry) =>
                    entry.id === chapter.id
                      ? { ...entry, title: event.currentTarget.value }
                      : entry
                  )
                )
              }
            />
            <input
              aria-label={`Chapter ${index + 1} start`}
              type="number"
              min="0"
              step="0.01"
              value={chapter.startSeconds}
              disabled={disabled}
              onChange={(event) =>
                onChange(
                  chapters.map((entry) =>
                    entry.id === chapter.id
                      ? {
                          ...entry,
                          startSeconds: Number(event.currentTarget.value),
                        }
                      : entry
                  )
                )
              }
            />
            <input
              aria-label={`Chapter ${index + 1} end`}
              type="number"
              min={chapter.startSeconds + 0.001}
              step="0.01"
              value={chapter.endSeconds}
              disabled={disabled}
              onChange={(event) =>
                onChange(
                  chapters.map((entry) =>
                    entry.id === chapter.id
                      ? {
                          ...entry,
                          endSeconds: Number(event.currentTarget.value),
                        }
                      : entry
                  )
                )
              }
            />
            <input
              aria-label={`Chapter ${index + 1} time base`}
              type="text"
              pattern="[1-9]\d*/[1-9]\d*"
              autoCapitalize="none"
              spellCheck={false}
              title="Positive integer fraction, for example 1/1000"
              value={chapter.timeBase ?? '1/1000'}
              disabled={disabled}
              onChange={(event) =>
                onChange(
                  chapters.map((entry) =>
                    entry.id === chapter.id
                      ? {
                          ...entry,
                          timeBase: event.currentTarget.value,
                        }
                      : entry
                  )
                )
              }
            />
            <div className="chapter-list__actions">
              <button
                className="icon-action"
                type="button"
                aria-label={`Move chapter ${index + 1} earlier`}
                disabled={disabled || index === 0}
                onClick={() => {
                  const next = [...chapters];
                  const previous = next[index - 1];
                  if (!previous) return;
                  next[index - 1] = chapter;
                  next[index] = previous;
                  onChange(next);
                }}
              >
                ↑
              </button>
              <button
                className="icon-action"
                type="button"
                aria-label={`Move chapter ${index + 1} later`}
                disabled={disabled || index === chapters.length - 1}
                onClick={() => {
                  const next = [...chapters];
                  const following = next[index + 1];
                  if (!following) return;
                  next[index + 1] = chapter;
                  next[index] = following;
                  onChange(next);
                }}
              >
                ↓
              </button>
              <button
                className="icon-action"
                type="button"
                aria-label={`Duplicate chapter ${index + 1}`}
                disabled={disabled}
                onClick={() =>
                  onChange([
                    ...chapters.slice(0, index + 1),
                    {
                      ...chapter,
                      id: crypto.randomUUID(),
                      title: `${chapter.title} copy`,
                    },
                    ...chapters.slice(index + 1),
                  ])
                }
              >
                <Icon name="add" />
              </button>
              <button
                className="icon-action"
                type="button"
                aria-label={`Remove chapter ${index + 1}`}
                disabled={disabled}
                onClick={() =>
                  onChange(chapters.filter((entry) => entry.id !== chapter.id))
                }
              >
                <Icon name="trash" />
              </button>
            </div>
          </li>
        ))}
      </ol>
      <div className="chapter-editor-actions">
        <button
          className="button button--secondary"
          type="button"
          disabled={disabled}
          onClick={() => {
            const last = chapters.at(-1);
            const start = last?.endSeconds ?? 0;
            onChange([
              ...chapters,
              {
                id: crypto.randomUUID(),
                startSeconds: start,
                endSeconds: start + 1,
                title: `Chapter ${chapters.length + 1}`,
                timeBase: '1/1000',
              },
            ]);
          }}
        >
          <Icon name="add" />
          Add chapter
        </button>
        <button
          className="button button--quiet"
          type="button"
          disabled={disabled || chapters.length < 2}
          onClick={() =>
            onChange(
              [...chapters].sort(
                (left, right) =>
                  left.startSeconds - right.startSeconds ||
                  left.endSeconds - right.endSeconds
              )
            )
          }
        >
          Sort by time
        </button>
      </div>
    </div>
  );
}

function SubtitleInspector({
  tracks,
  disabled,
  onChange,
  onImport,
}: {
  tracks: ProjectSubtitleTrack[];
  disabled: boolean;
  onChange: (tracks: ProjectSubtitleTrack[]) => void;
  onImport?: (file: File) => void;
}) {
  return (
    <div className="inspector-section">
      <p className="section-intro">
        SRT, WebVTT and ASS sources can be muxed, converted, or burned in.
      </p>
      <ol className="subtitle-list">
        {tracks.map((track, index) => (
          <li key={track.id}>
            <div>
              <strong>{track.sourceName ?? `Subtitle ${index + 1}`}</strong>
              <small>
                {track.sourceFormat?.toUpperCase() ?? 'Text subtitle'} ·{' '}
                {track.sourceStatus ?? 'source not attached'}
              </small>
            </div>
            <select
              aria-label={`Subtitle ${index + 1} output mode`}
              value={track.mode}
              disabled={disabled}
              onChange={(event) =>
                onChange(
                  tracks.map((entry) =>
                    entry.id === track.id
                      ? {
                          ...entry,
                          mode: event.currentTarget
                            .value as ProjectSubtitleTrack['mode'],
                        }
                      : entry
                  )
                )
              }
            >
              <option value="exclude">Exclude</option>
              <option value="mux">Soft subtitle</option>
              <option value="burn-in">Burn in</option>
            </select>
            <input
              aria-label={`Subtitle ${index + 1} language`}
              value={track.language ?? ''}
              maxLength={16}
              placeholder="Language"
              disabled={disabled}
              onChange={(event) =>
                onChange(
                  tracks.map((entry) =>
                    entry.id === track.id
                      ? { ...entry, language: event.currentTarget.value }
                      : entry
                  )
                )
              }
            />
            <input
              aria-label={`Subtitle ${index + 1} title`}
              value={track.title ?? ''}
              maxLength={128}
              placeholder="Title"
              disabled={disabled}
              onChange={(event) =>
                onChange(
                  tracks.map((entry) =>
                    entry.id === track.id
                      ? { ...entry, title: event.currentTarget.value }
                      : entry
                  )
                )
              }
            />
            <label>
              Shift (s)
              <input
                type="number"
                min="-86400"
                max="86400"
                step="0.01"
                value={track.offsetSeconds ?? 0}
                disabled={disabled}
                onChange={(event) =>
                  onChange(
                    tracks.map((entry) =>
                      entry.id === track.id
                        ? {
                            ...entry,
                            offsetSeconds: Number(event.currentTarget.value),
                          }
                        : entry
                    )
                  )
                }
              />
            </label>
            <label className="check-field">
              <input
                type="checkbox"
                checked={track.default ?? false}
                disabled={disabled}
                onChange={(event) =>
                  onChange(
                    tracks.map((entry) =>
                      entry.id === track.id
                        ? { ...entry, default: event.currentTarget.checked }
                        : entry
                    )
                  )
                }
              />
              Default
            </label>
            <label className="check-field">
              <input
                type="checkbox"
                checked={track.forced ?? false}
                disabled={disabled}
                onChange={(event) =>
                  onChange(
                    tracks.map((entry) =>
                      entry.id === track.id
                        ? { ...entry, forced: event.currentTarget.checked }
                        : entry
                    )
                  )
                }
              />
              Forced
            </label>
            <button
              className="icon-action"
              type="button"
              aria-label={`Remove subtitle ${index + 1}`}
              disabled={disabled}
              onClick={() =>
                onChange(tracks.filter((entry) => entry.id !== track.id))
              }
            >
              <Icon name="trash" />
            </button>
          </li>
        ))}
      </ol>
      <label className="button button--secondary file-button">
        <Icon name="add" />
        Import subtitle
        <input
          className="visually-hidden"
          type="file"
          accept=".srt,.vtt,.ass,text/vtt"
          disabled={disabled}
          onChange={(event) => {
            const file = event.currentTarget.files?.[0];
            if (!file) return;
            if (onImport) {
              onImport(file);
            } else {
              const extension = file.name.split('.').at(-1)?.toLowerCase();
              onChange([
                ...tracks,
                {
                  id: crypto.randomUUID(),
                  sourceName: file.name,
                  sourceFormat:
                    extension === 'vtt' || extension === 'ass'
                      ? extension
                      : 'srt',
                  mode: 'mux',
                  sourceStatus: 'attached',
                },
              ]);
            }
            event.currentTarget.value = '';
          }}
        />
      </label>
    </div>
  );
}

function ThumbnailInspector({
  duration,
  currentPreviewTimeSeconds,
  disabled,
  onCreate,
}: {
  duration?: number;
  currentPreviewTimeSeconds?: number;
  disabled: boolean;
  onCreate: (request: ThumbnailRequest) => void;
}) {
  const [timeSeconds, setTimeSeconds] = useState(0);
  const [width, setWidth] = useState('1280');
  const [height, setHeight] = useState('');
  const [maintainAspect, setMaintainAspect] = useState(true);
  const [format, setFormat] = useState<ThumbnailRequest['format']>('jpg');
  const [quality, setQuality] = useState(85);
  return (
    <div className="inspector-section">
      <p className="section-intro">
        Extract one exact frame. Series and contact sheets use the same typed
        plan builders.
      </p>
      <button
        className="button button--quiet"
        type="button"
        disabled={
          disabled ||
          currentPreviewTimeSeconds === undefined ||
          !Number.isFinite(currentPreviewTimeSeconds)
        }
        onClick={() =>
          setTimeSeconds(
            Math.min(
              duration ?? currentPreviewTimeSeconds ?? 0,
              Math.max(0, currentPreviewTimeSeconds ?? 0)
            )
          )
        }
      >
        Use preview time
      </button>
      <label className="field">
        <span>Time (seconds)</span>
        <input
          type="number"
          min="0"
          max={duration}
          step="0.01"
          value={timeSeconds}
          disabled={disabled}
          onChange={(event) =>
            setTimeSeconds(Number(event.currentTarget.value))
          }
        />
      </label>
      <div className="mini-grid">
        <label className="field">
          <span>Width</span>
          <input
            type="number"
            min="64"
            max="8192"
            value={width}
            disabled={disabled}
            onChange={(event) => setWidth(event.currentTarget.value)}
          />
        </label>
        <label className="field">
          <span>Height (optional)</span>
          <input
            type="number"
            min="64"
            max="8192"
            value={height}
            placeholder="Auto"
            disabled={disabled}
            onChange={(event) => setHeight(event.currentTarget.value)}
          />
        </label>
        <label className="field">
          <span>Format</span>
          <select
            value={format}
            disabled={disabled}
            onChange={(event) =>
              setFormat(event.currentTarget.value as ThumbnailRequest['format'])
            }
          >
            <option value="jpg">JPEG</option>
            <option value="png">PNG</option>
            <option value="webp">WebP</option>
          </select>
        </label>
      </div>
      <label className="check-field">
        <input
          type="checkbox"
          checked={maintainAspect}
          disabled={disabled}
          onChange={(event) => setMaintainAspect(event.currentTarget.checked)}
        />
        Preserve source aspect ratio
      </label>
      <label className="field">
        <span>Quality</span>
        <input
          type="range"
          min="1"
          max="100"
          value={quality}
          disabled={disabled || format === 'png'}
          onChange={(event) => setQuality(Number(event.currentTarget.value))}
        />
        <output>{format === 'png' ? 'Lossless PNG' : `${quality}%`}</output>
      </label>
      <button
        className="button button--primary"
        type="button"
        disabled={
          disabled ||
          (!width && !height) ||
          (width !== '' &&
            (!Number.isSafeInteger(Number(width)) ||
              Number(width) < 1 ||
              Number(width) > 8192)) ||
          (height !== '' &&
            (!Number.isSafeInteger(Number(height)) ||
              Number(height) < 1 ||
              Number(height) > 8192))
        }
        onClick={() =>
          onCreate({
            timeSeconds,
            ...(width ? { width: Number(width) } : {}),
            ...(height ? { height: Number(height) } : {}),
            maintainAspect,
            format,
            quality,
          })
        }
      >
        <Icon name="sparkles" />
        Extract frame
      </button>
    </div>
  );
}
