import React, { useMemo, useState, useCallback, useEffect } from 'react';
import Plot from 'react-plotly.js';
import { defaultSensors, findSensorById } from './data/sensors.js';
import { defaultLenses } from './data/lenses.js';
import {
  selectNormalLensIdsForSensors,
  selectWideLensIdsForSensors,
  selectPortraitLensIdsForSensors,
} from './utils/lensSelection.js';
import {
  FramingModes,
  PlotTypes,
  buildBlurDataset,
  buildSeparationSamples,
  buildStatsAtCheckpoints,
  computeSubjectDistance,
} from './utils/optics.js';

const COLOR_PALETTE = ['#38bdf8', '#a855f7', '#f97316', '#22c55e', '#e11d48', '#6366f1', '#14b8a6', '#f59e0b'];
const PREVIEW_DISTANCE_MIN = 0.5;
const PREVIEW_DISTANCE_MAX = 50;
const computeInitialLensIds = () => {
  const normalLensIds = selectNormalLensIdsForSensors(defaultSensors, defaultLenses);

  if (normalLensIds.length > 0) {
    return normalLensIds;
  }

  return defaultLenses.slice(0, 3).map((lens) => lens.id);
};

const INITIAL_SELECTED_LENS_IDS = computeInitialLensIds();

const createInitialColorAssignments = (lensIds) => {
  const assignments = {};
  lensIds.forEach((lensId, index) => {
    assignments[lensId] = index % COLOR_PALETTE.length;
  });
  return assignments;
};

const reconcileColorAssignments = (selectedLensIds, previousAssignments) => {
  if (selectedLensIds.length === 0) {
    return selectedLensIds.length === Object.keys(previousAssignments).length ? previousAssignments : {};
  }

  const nextAssignments = {};
  const usedIndices = new Set();

  selectedLensIds.forEach((lensId) => {
    const priorIndex = previousAssignments[lensId];
    if (typeof priorIndex === 'number' && !usedIndices.has(priorIndex)) {
      nextAssignments[lensId] = priorIndex;
      usedIndices.add(priorIndex);
    }
  });

  const availableIndices = [];
  for (let i = 0; i < COLOR_PALETTE.length; i += 1) {
    if (!usedIndices.has(i)) {
      availableIndices.push(i);
    }
  }

  let overflowIndex = 0;
  selectedLensIds.forEach((lensId) => {
    if (typeof nextAssignments[lensId] === 'number') {
      return;
    }

    let assignedIndex;
    if (availableIndices.length > 0) {
      assignedIndex = availableIndices.shift();
    } else {
      assignedIndex = overflowIndex % COLOR_PALETTE.length;
      overflowIndex += 1;
    }

    nextAssignments[lensId] = assignedIndex;
    usedIndices.add(assignedIndex);
  });

  const previousKeys = Object.keys(previousAssignments);
  const nextKeys = Object.keys(nextAssignments);
  const assignmentsChanged =
    previousKeys.length !== nextKeys.length ||
    selectedLensIds.some((lensId) => nextAssignments[lensId] !== previousAssignments[lensId]);

  return assignmentsChanged ? nextAssignments : previousAssignments;
};

const plotTypeOptions = [
  { value: PlotTypes.DISTANCE, label: 'Absolute scene distance' },
  { value: PlotTypes.NORMALIZED, label: 'Blur vs normalized distance' },
  { value: PlotTypes.SEPARATION, label: 'Focus-relative distance' },
  { value: PlotTypes.DIOPTER, label: 'Blur vs diopter difference' },
];

const framingOptions = [
  { value: FramingModes.FIXED_DISTANCE, label: 'Fixed subject distance' },
  { value: FramingModes.MATCH_DIAGONAL, label: 'Normalize by diagonal' },
  { value: FramingModes.MATCH_HEIGHT, label: 'Normalize by height' },
  { value: FramingModes.CUSTOM, label: 'Custom framing' },
];

const parseCheckpointInput = (input) => {
  if (!input) return [3, 6, 10];
  const values = input
    .split(',')
    .map((token) => Number.parseFloat(token.trim()))
    .filter((value) => Number.isFinite(value) && value > 0)
    .sort((a, b) => a - b);
  return values.length > 0 ? values : [3, 6, 10];
};

const formatNumber = (value, digits = 2) => {
  const numeric = Number.parseFloat(value);
  if (!Number.isFinite(numeric)) return '—';
  return numeric.toFixed(digits);
};

const formatAperture = (value) => {
  const numeric = Number.parseFloat(value);
  if (!Number.isFinite(numeric)) return 'ƒ/—';
  return `ƒ/${numeric.toFixed(1)}`;
};

const getAxisConfig = (plotType) => {
  switch (plotType) {
    case PlotTypes.DISTANCE:
      return { label: 'Scene distance (m)', accessor: (point) => point.sceneDistance, ticksuffix: ' m' };
    case PlotTypes.ZOOMED:
      return { label: 'Scene distance (m)', accessor: (point) => point.sceneDistance, ticksuffix: ' m' };
    case PlotTypes.NORMALIZED:
      return { label: 'Scene distance ÷ subject distance', accessor: (point) => point.normalizedDistance };
    case PlotTypes.SEPARATION:
      return { label: 'Focus-relative distance (m)', accessor: (point) => point.focusOffset, ticksuffix: ' m' };
    case PlotTypes.DIOPTER:
      return { label: 'Diopter difference (1/m)', accessor: (point) => point.diopterDifference, ticksuffix: ' 1/m' };
    default:
      return { label: 'Scene distance (m)', accessor: (point) => point.sceneDistance, ticksuffix: ' m' };
  }
};

const filterDatasetForPlotType = (dataset, plotType) => {
  if (plotType === PlotTypes.ZOOMED) {
    return dataset.filter((point) => Math.abs(point.focusOffset) <= 3);
  }
  return dataset;
};

const PreviewPanel = ({ lensCurves, previewDistanceMeters }) => {
  const previewItems = lensCurves.map((curve) => {
    const targetDistance = previewDistanceMeters;
    const safeDistance = Number.isFinite(targetDistance) ? Math.max(targetDistance, PREVIEW_DISTANCE_MIN) : targetDistance;
    const targetSeparation = Number.isFinite(safeDistance) ? safeDistance - curve.subjectDistance : Infinity;

    let stat = curve.checkpointStats.find((entry) => {
      if (!Number.isFinite(targetSeparation)) {
        return !Number.isFinite(entry.separation);
      }
      return Number.isFinite(entry.separation) && Math.abs(entry.separation - targetSeparation) < 1e-6;
    });

    if (!stat) {
      const derivedStats = buildStatsAtCheckpoints({
        lens: curve.lens,
        sensor: curve.sensor,
        aperture: curve.apertureUsed,
        subjectDistance: curve.subjectDistance,
        checkpoints: [targetSeparation],
      });
      stat = derivedStats[0];
    }

    const blurMm = stat?.blurMm ?? 0;

    return {
      lens: curve.lens,
      blurMm,
      sensorDiagonal: curve.sensor?.diagonal ?? 1,
      colorIndex: curve.colorIndex,
    };
  });

  const maxBlurMm = previewItems.reduce((maxValue, item) => Math.max(maxValue, item.blurMm), 0);
  const denominator = maxBlurMm > 0 ? maxBlurMm : 1;
  const distanceLabel = Number.isFinite(previewDistanceMeters)
    ? `${formatNumber(previewDistanceMeters, previewDistanceMeters >= 10 ? 0 : 1)} m`
    : '∞';

  return (
    <div className="rounded-lg border border-slate-800 bg-slate-900/60 p-4">
      <div className="mb-3 flex items-center justify-between">
        <h2 className="text-sm font-semibold uppercase tracking-wide text-slate-400">Bokeh preview</h2>
        <span className="text-[10px] uppercase tracking-wide text-slate-500">Scene distance: {distanceLabel}</span>
      </div>
      {previewItems.length === 0 ? (
        <div className="text-sm text-slate-500">Select at least one lens to preview its blur.</div>
      ) : (
        <div className="flex flex-wrap justify-center gap-4">
          {previewItems.map((item) => {
            const normalized = Math.min(item.blurMm / denominator, 1);
            const diameter = Math.max(18, 30 + normalized * 70);
            const paletteColor = COLOR_PALETTE[item.colorIndex % COLOR_PALETTE.length];

            return (
              <div key={item.lens.id} className="flex w-32 flex-col items-center gap-2 text-center">
                <div
                  className="flex items-center justify-center rounded-full border"
                  style={{
                    width: `${diameter}px`,
                    height: `${diameter}px`,
                    borderColor: paletteColor,
                    background: `${paletteColor}1A`,
                    boxShadow: `0 0 12px ${paletteColor}33`,
                  }}
                >
                  <span className="sr-only">{item.lens.name}</span>
                </div>
                <div className="text-xs text-slate-200">
                  <div className="font-medium text-slate-100">{item.lens.name}</div>
                  <div className="text-[10px] uppercase tracking-wide text-slate-500">
                    Blur {formatNumber(item.blurMm, 2)} mm
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
};

const PreviewDistanceControl = ({ value, onChange, max }) => {
  const isInfinity = value >= max;
  const displayValue = isInfinity ? '∞' : `${formatNumber(value, value >= 10 ? 0 : 1)} m`;

  return (
    <div className="rounded-lg border border-slate-800 bg-slate-900/60 p-4">
      <label className="flex items-center justify-between text-xs uppercase tracking-wide text-slate-400">
        <span>Preview distance</span>
        <span className="text-slate-200">{displayValue}</span>
      </label>
      <input
        type="range"
        min={PREVIEW_DISTANCE_MIN}
        max={max}
        step="0.5"
        value={value}
        onChange={(event) => onChange(Number.parseFloat(event.target.value))}
        className="mt-3 w-full"
      />
      <div className="mt-2 text-[11px] text-slate-500">Drag to compare blur at absolute scene distances. Set to max for infinity.</div>
    </div>
  );
};

const PlotSection = ({ lensCurves, plotType, setPlotType, previewDistanceMeters }) => {
  const axisConfig = getAxisConfig(plotType);

  const plotData = lensCurves.map((curve, index) => {
    const filteredDataset = filterDatasetForPlotType(curve.dataset, plotType);
    const xValues = filteredDataset.map(axisConfig.accessor);
    const yValues = filteredDataset.map((point) => point.blurPercentOfDiagonal);
    const paletteIndex =
      typeof curve.colorIndex === 'number' ? curve.colorIndex % COLOR_PALETTE.length : index % COLOR_PALETTE.length;

    return {
      x: xValues,
      y: yValues,
      customdata: filteredDataset.map((point) => [
        point.blurPercentOfDiagonal,
        point.blurMm,
        point.sceneDistance,
        point.focusOffset,
        point.normalizedDistance,
        point.diopterDifference,
      ]),
      mode: 'lines',
      name: `${curve.lens.name} (${formatAperture(curve.apertureUsed)})`,
      line: {
        width: 2,
        color: COLOR_PALETTE[paletteIndex],
      },
      hovertemplate: `<b>%{text}</b><br>${axisConfig.label}: %{x:.2f}<br>Blur: %{customdata[0]:.2f}% diag (%{customdata[1]:.2f} mm)<extra></extra>`,
      text: Array(filteredDataset.length).fill(curve.lens.name),
    };
  });

  const maxY = Math.max(
    0.1,
    ...lensCurves.flatMap((curve) => curve.dataset.map((point) => point.blurPercentOfDiagonal))
  );

  const shouldShowPreviewLine =
    Number.isFinite(previewDistanceMeters) && (plotType === PlotTypes.DISTANCE || plotType === PlotTypes.ZOOMED);
  const previewLineShape = shouldShowPreviewLine
    ? [
        {
          type: 'line',
          xref: 'x',
          yref: 'paper',
          x0: previewDistanceMeters,
          x1: previewDistanceMeters,
          y0: 0,
          y1: 1,
          line: {
            color: 'rgba(148, 163, 184, 0.35)',
            width: 1,
            dash: 'dot',
          },
          layer: 'below',
        },
      ]
    : undefined;

  return (
    <div className="flex flex-col gap-4 p-4">
      <div className="flex flex-wrap items-center justify-between gap-3">
        <div>
          <h2 className="text-lg font-semibold text-slate-100">Blur comparison</h2>
          <p className="text-xs uppercase tracking-wide text-slate-400">Blur radius as % of sensor diagonal</p>
        </div>
        <div className="flex flex-wrap items-center gap-3">
          <select
            value={plotType}
            onChange={(event) => setPlotType(event.target.value)}
            className="rounded border border-slate-700 bg-slate-900 px-3 py-2 text-sm focus:border-accent focus:outline-none"
          >
            {plotTypeOptions.map((option) => (
              <option key={option.value} value={option.value}>
                {option.label}
              </option>
            ))}
          </select>
        </div>
      </div>

      <div className="h-[420px] min-h-[320px] overflow-hidden rounded-lg border border-slate-800 bg-slate-900/60 lg:h-[calc(100vh-18rem)]">
        {lensCurves.length > 0 ? (
          <Plot
            data={plotData}
            layout={{
              paper_bgcolor: 'rgba(15, 23, 42, 1)',
              plot_bgcolor: 'rgba(15, 23, 42, 1)',
              font: { color: '#e2e8f0' },
              margin: { l: 60, r: 20, t: 30, b: 50 },
              legend: { orientation: 'h', yanchor: 'bottom', y: 1.02 },
              xaxis: {
                title: axisConfig.label,
                gridcolor: 'rgba(148, 163, 184, 0.2)',
                zerolinecolor: 'rgba(148, 163, 184, 0.4)',
                ...(axisConfig.ticksuffix ? { ticksuffix: axisConfig.ticksuffix } : {}),
              },
              yaxis: {
                title: 'Blur radius (% of sensor diagonal)',
                type: 'linear',
                gridcolor: 'rgba(148, 163, 184, 0.2)',
                zerolinecolor: 'rgba(148, 163, 184, 0.4)',
                range: [0, maxY * 1.1],
                ticksuffix: ' %',
              },
              shapes: previewLineShape,
            }}
            useResizeHandler
            style={{ width: '100%', height: '100%' }}
            config={{ responsive: true, displaylogo: false, modeBarButtonsToRemove: ['select2d', 'lasso2d'] }}
          />
        ) : (
          <div className="flex h-full items-center justify-center text-sm text-slate-400">Select at least one lens to render a curve.</div>
        )}
      </div>
    </div>
  );
};

const ControlsPanel = ({
  selectedLensIds,
  toggleLensSelection,
  activeLensId,
  setActiveLensId,
  lensSensorFilter,
  setLensSensorFilter,
  sensorFilterOptions,
  applyLensPreset,
}) => {
  const lensesToRender = useMemo(() => {
    const sensorCache = new Map();
    const getSensor = (sensorId) => {
      if (sensorCache.has(sensorId)) {
        return sensorCache.get(sensorId);
      }
      const sensor = findSensorById(sensorId);
      sensorCache.set(sensorId, sensor);
      return sensor;
    };

    const lensList =
      lensSensorFilter === 'all'
        ? defaultLenses
        : defaultLenses.filter((lens) => lens.sensorId === lensSensorFilter);

    return [...lensList].sort((lensA, lensB) => {
      const sensorA = getSensor(lensA.sensorId);
      const sensorB = getSensor(lensB.sensorId);

      const cropFactorA = Number.isFinite(sensorA?.cropFactor) ? sensorA.cropFactor : 1;
      const cropFactorB = Number.isFinite(sensorB?.cropFactor) ? sensorB.cropFactor : 1;

      const effectiveFocalLengthA = lensA.focalLength * cropFactorA;
      const effectiveFocalLengthB = lensB.focalLength * cropFactorB;

      if (effectiveFocalLengthA !== effectiveFocalLengthB) {
        return effectiveFocalLengthA - effectiveFocalLengthB;
      }

      if (lensA.focalLength !== lensB.focalLength) {
        return lensA.focalLength - lensB.focalLength;
      }

      return lensA.name.localeCompare(lensB.name);
    });
  }, [lensSensorFilter]);

  return (
    <div className="space-y-6 p-4">
      <section>
        <h2 className="text-sm font-semibold uppercase tracking-wide text-slate-400 mb-2">Lenses & sensors</h2>
        <p className="mb-3 text-xs text-slate-500">Each lens automatically pairs with its native sensor format for accurate comparisons.</p>
        <div className="mb-3 flex flex-wrap gap-2">
          <button
            type="button"
            onClick={() => applyLensPreset('normal')}
            className="rounded border border-slate-800 bg-slate-900/50 px-2 py-1 text-xs uppercase tracking-wide text-slate-300 transition hover:border-accent/40 hover:text-slate-100"
            title="Select a go-to normal lens for each sensor"
          >
            Normal picks
          </button>
          <button
            type="button"
            onClick={() => applyLensPreset('wide')}
            className="rounded border border-slate-800 bg-slate-900/50 px-2 py-1 text-xs uppercase tracking-wide text-slate-300 transition hover:border-accent/40 hover:text-slate-100"
            title="Focus on wider fields of view"
          >
            Wide picks
          </button>
          <button
            type="button"
            onClick={() => applyLensPreset('portrait')}
            className="rounded border border-slate-800 bg-slate-900/50 px-2 py-1 text-xs uppercase tracking-wide text-slate-300 transition hover:border-accent/40 hover:text-slate-100"
            title="Highlight portrait-friendly focal lengths"
          >
            Portrait picks
          </button>
        </div>
        <div className="mb-3 flex flex-wrap gap-2">
          <button
            type="button"
            onClick={() => setLensSensorFilter('all')}
            className={`rounded border px-2 py-1 text-xs uppercase tracking-wide transition ${
              lensSensorFilter === 'all'
                ? 'border-accent/60 bg-slate-900/80 text-slate-100 shadow-[0_0_0_1px_rgba(56,189,248,0.35)]'
                : 'border-slate-800 bg-slate-900/50 text-slate-400 hover:border-accent/40 hover:text-slate-200'
            }`}
          >
            All
          </button>
          {sensorFilterOptions.map((sensor) => {
            const isActive = lensSensorFilter === sensor.id;
            const label = sensor.shortLabel ?? sensor.name;
            return (
              <button
                key={sensor.id}
                type="button"
                onClick={() => setLensSensorFilter(sensor.id)}
                className={`rounded border px-2 py-1 text-xs uppercase tracking-wide transition ${
                  isActive
                    ? 'border-accent/60 bg-slate-900/80 text-slate-100 shadow-[0_0_0_1px_rgba(56,189,248,0.35)]'
                    : 'border-slate-800 bg-slate-900/50 text-slate-400 hover:border-accent/40 hover:text-slate-200'
                }`}
                title={sensor.name}
              >
                {label}
              </button>
            );
          })}
        </div>
        <div className="flex flex-col gap-2">
        {lensesToRender.map((lens) => {
          const isSelected = selectedLensIds.includes(lens.id);
          const isActive = activeLensId === lens.id;
          const sensor = findSensorById(lens.sensorId);
          const sensorLabel = sensor ? sensor.name : 'Unknown sensor';
          const lensMetaParts = [
            lens.brand,
            `${lens.focalLength}mm`,
            `ƒ/${lens.maxAperture}`,
            sensorLabel,
          ].filter(Boolean);
          const lensMeta = lensMetaParts.join(' · ');

          return (
            <label
              key={lens.id}
              onMouseEnter={() => setActiveLensId(lens.id)}
              onFocus={() => setActiveLensId(lens.id)}
              className={`flex cursor-pointer items-center justify-between rounded border px-3 py-2 text-sm transition ${
                isActive
                  ? 'border-accent/70 bg-slate-900/80 text-slate-100 shadow-[0_0_0_1px_rgba(56,189,248,0.35)]'
                  : isSelected
                  ? 'border-accent/40 bg-slate-900/60 text-slate-100'
                  : 'border-slate-700 bg-slate-900/40 text-slate-400'
              }`}
            >
              <span className="flex flex-col">
                <span className="font-medium text-slate-100">{lens.name}</span>
                <span className="text-xs text-slate-400">{lensMeta}</span>
              </span>
              <input
                type="checkbox"
                checked={isSelected}
                onChange={() => {
                  toggleLensSelection(lens.id);
                  if (!isSelected) {
                    setActiveLensId(lens.id);
                  }
                }}
                className="h-4 w-4 accent-accent"
              />
            </label>
          );
          })}
        </div>
      </section>

    </div>
  );
};

const StatsTable = ({ lensStats, checkpoints }) => (
  <div className="overflow-x-auto rounded-lg border border-slate-800 bg-slate-900/60">
    <table className="min-w-full divide-y divide-slate-800 text-sm">
      <thead className="text-left text-xs uppercase tracking-wide text-slate-400">
        <tr>
          <th className="px-3 py-2">Lens</th>
          <th className="px-3 py-2">Sensor</th>
          <th className="px-3 py-2">Subject distance</th>
          <th className="px-3 py-2">Entrance pupil</th>
          {checkpoints.map((checkpoint) => (
            <th key={checkpoint} className="px-3 py-2 text-right">
              {formatNumber(checkpoint, checkpoint >= 1 ? 0 : 2)} m
            </th>
          ))}
        </tr>
      </thead>
      <tbody className="divide-y divide-slate-900/60">
        {lensStats.length === 0 ? (
          <tr>
            <td colSpan={4 + checkpoints.length} className="px-3 py-6 text-center text-sm text-slate-400">
              Select at least one lens to compare blur metrics.
            </td>
          </tr>
        ) : (
          lensStats.map(({ lens, sensor, apertureUsed, subjectDistance, entrancePupil, checkpointStats }) => {
            const lensSummaryParts = [lens.brand, `${lens.focalLength}mm`, formatAperture(apertureUsed)];
            const lensSummary = lensSummaryParts.filter(Boolean).join(' · ');
            return (
              <tr key={lens.id} className="hover:bg-slate-900/60">
                <td className="px-3 py-2">
                  <div className="font-medium text-slate-100">{lens.name}</div>
                  <div className="text-xs text-slate-400">{lensSummary}</div>
                </td>
                <td className="px-3 py-2 text-slate-200">{sensor?.name ?? '—'}</td>
                <td className="px-3 py-2 text-slate-200">{formatNumber(subjectDistance, 2)} m</td>
                <td className="px-3 py-2 text-slate-200">{formatNumber(entrancePupil, 1)} mm</td>
                {checkpointStats.map((stat) => (
                  <td key={stat.separation} className="px-3 py-2 text-right text-slate-100">
                    <div>{formatNumber(stat.blurPercentOfDiagonal, 2)}%</div>
                    <div className="text-xs text-slate-400">{formatNumber(stat.blurMm, 2)} mm</div>
                  </td>
                ))}
              </tr>
            );
          })
        )}
      </tbody>
    </table>
  </div>
);


const FramingPanel = ({
  framingMode,
  setFramingMode,
  subjectHeight,
  setSubjectHeight,
  frameFill,
  setFrameFill,
  fixedSubjectDistance,
  setFixedSubjectDistance,
  customSubjectDistance,
  setCustomSubjectDistance,
  customDimension,
  setCustomDimension,
}) => {
  const distanceValue = framingMode === FramingModes.FIXED_DISTANCE ? fixedSubjectDistance : customSubjectDistance;

  return (
    <div className="rounded-lg border border-slate-800 bg-slate-900/60 p-4">
      <h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-slate-400">Framing mode</h2>
      <div className="space-y-3">
        <div className="space-y-1">
          <label className="text-xs uppercase tracking-wide text-slate-400">Mode</label>
          <select
            value={framingMode}
            onChange={(event) => setFramingMode(event.target.value)}
            className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-sm focus:border-accent focus:outline-none"
          >
            {framingOptions.map((option) => (
              <option key={option.value} value={option.value}>
                {option.label}
              </option>
            ))}
          </select>
        </div>

        {(framingMode === FramingModes.FIXED_DISTANCE || framingMode === FramingModes.CUSTOM) && (
          <div className="space-y-1">
            <label className="flex items-center justify-between text-xs uppercase tracking-wide text-slate-400">
              <span>{framingMode === FramingModes.FIXED_DISTANCE ? 'Subject distance' : 'Custom subject distance'}</span>
              <span className="text-slate-200">{formatNumber(distanceValue, 2)} m</span>
            </label>
            <input
              type="range"
              min="0.5"
              max="15"
              step="0.1"
              value={distanceValue}
              onChange={(event) => {
                const value = Number.parseFloat(event.target.value);
                setCustomSubjectDistance(value);
                setFixedSubjectDistance(value);
              }}
              className="w-full"
            />
          </div>
        )}

        {(framingMode === FramingModes.MATCH_HEIGHT || framingMode === FramingModes.MATCH_DIAGONAL || framingMode === FramingModes.CUSTOM) && (
          <>
            <div className="space-y-1">
              <label className="flex items-center justify-between text-xs uppercase tracking-wide text-slate-400">
                <span>Subject height</span>
                <span className="text-slate-200">{formatNumber(subjectHeight, 2)} m</span>
              </label>
              <input
                type="range"
                min="0.5"
                max="2.5"
                step="0.01"
                value={subjectHeight}
                onChange={(event) => setSubjectHeight(Number.parseFloat(event.target.value))}
                className="w-full"
              />
            </div>
            <div className="space-y-1">
              <label className="flex items-center justify-between text-xs uppercase tracking-wide text-slate-400">
                <span>Frame fill</span>
                <span className="text-slate-200">{formatNumber(frameFill * 100, 0)}%</span>
              </label>
              <input
                type="range"
                min="0.2"
                max="0.95"
                step="0.01"
                value={frameFill}
                onChange={(event) => setFrameFill(Number.parseFloat(event.target.value))}
                className="w-full"
              />
            </div>
          </>
        )}

        {framingMode === FramingModes.CUSTOM && (
          <div className="space-y-1">
            <label className="text-xs uppercase tracking-wide text-slate-400">Custom framing dimension</label>
            <select
              value={customDimension}
              onChange={(event) => setCustomDimension(event.target.value)}
              className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-sm focus:border-accent focus:outline-none"
            >
              <option value="height">Match frame height</option>
              <option value="diagonal">Match frame diagonal</option>
            </select>
          </div>
        )}
      </div>
    </div>
  );
};

const OpticsPanel = ({
  foregroundRange,
  setForegroundRange,
  maxSeparation,
  setMaxSeparation,
  checkpointsInput,
  setCheckpointsInput,
  foregroundRangeLimit,
  maxSeparationLimit,
}) => (
  <div className="rounded-lg border border-slate-800 bg-slate-900/60 p-4">
    <h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-slate-400">Optics</h2>
    <div className="space-y-3">
        <div className="space-y-1">
          <label className="flex items-center justify-between text-xs uppercase tracking-wide text-slate-400">
            <span>Foreground range</span>
            <span className="text-slate-200">{formatNumber(foregroundRange, 1)} m</span>
          </label>
          <input
            type="range"
            min="0"
            max={foregroundRangeLimit}
            step="0.1"
            value={foregroundRange}
            onChange={(event) => setForegroundRange(Number.parseFloat(event.target.value))}
            className="w-full"
          />
          <div className="text-xs text-slate-500">Shared span: 0 – {formatNumber(foregroundRangeLimit, 1)} m</div>
        </div>
        <div className="space-y-1">
          <label className="flex items-center justify-between text-xs uppercase tracking-wide text-slate-400">
            <span>Max background range</span>
            <span className="text-slate-200">{formatNumber(maxSeparation, 1)} m</span>
          </label>
          <input
            type="range"
            min="1"
            max={maxSeparationLimit}
            step="1"
            value={maxSeparation}
            onChange={(event) => setMaxSeparation(Number.parseFloat(event.target.value))}
            className="w-full"
          />
          <div className="text-xs text-slate-500">Shared span: 0 – {formatNumber(maxSeparationLimit, 0)} m</div>
        </div>
        <div className="space-y-1">
          <label className="text-xs uppercase tracking-wide text-slate-400">Stats checkpoints (m, comma separated)</label>
          <input
            type="text"
            value={checkpointsInput}
            onChange={(event) => setCheckpointsInput(event.target.value)}
            placeholder="3,6,10"
            className="w-full rounded border border-slate-700 bg-slate-900 px-3 py-2 text-sm focus:border-accent focus:outline-none"
          />
        </div>
    </div>
  </div>
);


const App = () => {
  const [selectedLensIds, setSelectedLensIds] = useState(INITIAL_SELECTED_LENS_IDS);
  const [lensSensorFilter, setLensSensorFilter] = useState('all');
  const [framingMode, setFramingMode] = useState(FramingModes.MATCH_HEIGHT);
  const [subjectHeight, setSubjectHeight] = useState(1.7);
  const [frameFill, setFrameFill] = useState(0.75);
  const [fixedSubjectDistance, setFixedSubjectDistance] = useState(2);
  const [customSubjectDistance, setCustomSubjectDistance] = useState(2);
  const [customDimension, setCustomDimension] = useState('height');
  const [foregroundRange, setForegroundRange] = useState(1.5);
  const [foregroundRangeLimit, setForegroundRangeLimit] = useState(5);
  const [plotType, setPlotType] = useState(PlotTypes.DISTANCE);
  const [maxSeparation, setMaxSeparation] = useState(25);
  const [maxSeparationLimit, setMaxSeparationLimit] = useState(80);
  const [checkpointsInput, setCheckpointsInput] = useState('3,6,10');
  const [activeLensId, setActiveLensId] = useState(null);
  const [lensColors, setLensColors] = useState(() => createInitialColorAssignments(INITIAL_SELECTED_LENS_IDS));
  const [previewDistance, setPreviewDistance] = useState(PREVIEW_DISTANCE_MAX);

  const sensorFilterOptions = useMemo(() => {
    const lensSensorIds = new Set(defaultLenses.map((lens) => lens.sensorId));
    return defaultSensors.filter((sensor) => lensSensorIds.has(sensor.id));
  }, []);

  const previewDistanceMeters = useMemo(
    () => (previewDistance >= PREVIEW_DISTANCE_MAX ? Infinity : previewDistance),
    [previewDistance]
  );

  const checkpoints = useMemo(() => {
    const userValues = parseCheckpointInput(checkpointsInput);
    const withInfinity = userValues.includes(Infinity) ? userValues : [...userValues, Infinity];
    return withInfinity;
  }, [checkpointsInput]);
  const separationSamples = useMemo(
    () => buildSeparationSamples({ maxSeparation, foregroundRange }),
    [maxSeparation, foregroundRange]
  );

  const selectedLenses = useMemo(
    () => defaultLenses.filter((lens) => selectedLensIds.includes(lens.id)),
    [selectedLensIds]
  );

  const toggleLensSelection = useCallback((lensId) => {
    setSelectedLensIds((current) => {
      if (current.includes(lensId)) {
        return current.filter((id) => id !== lensId);
      }
      return [...current, lensId];
    });
  }, []);

  const applyLensPreset = useCallback(
    (presetKey) => {
      const sensorsForFilter =
        lensSensorFilter === 'all'
          ? defaultSensors
          : defaultSensors.filter((sensor) => sensor.id === lensSensorFilter);

      const effectiveSensors = sensorsForFilter.length > 0 ? sensorsForFilter : defaultSensors;

      let nextLensIds = [];
      switch (presetKey) {
        case 'wide':
          nextLensIds = selectWideLensIdsForSensors(effectiveSensors, defaultLenses);
          break;
        case 'portrait':
          nextLensIds = selectPortraitLensIdsForSensors(effectiveSensors, defaultLenses);
          break;
        case 'normal':
        default:
          nextLensIds = selectNormalLensIdsForSensors(effectiveSensors, defaultLenses);
          break;
      }

      if (nextLensIds.length === 0) {
        const fallback = selectNormalLensIdsForSensors(defaultSensors, defaultLenses);
        if (fallback.length === 0) {
          return;
        }
        nextLensIds = fallback;
      }

      setSelectedLensIds(nextLensIds);
      setActiveLensId((current) => (current && nextLensIds.includes(current) ? current : nextLensIds[0] ?? null));
    },
    [lensSensorFilter, setSelectedLensIds, setActiveLensId]
  );

  useEffect(() => {
    setLensColors((currentAssignments) => reconcileColorAssignments(selectedLensIds, currentAssignments));
  }, [selectedLensIds]);

  const lensCurves = useMemo(() => {
    return selectedLenses
      .map((lens) => {
        const sensor = findSensorById(lens.sensorId);
        if (!sensor) {
          return null;
        }

        const apertureUsed = lens.maxAperture;
        const subjectDistance = computeSubjectDistance({
          framingMode,
          lens,
          sensor,
          fixedSubjectDistance,
          subjectHeight,
          frameFill,
          customSubjectDistance,
          customDimension,
        });

        const dataset = buildBlurDataset({
          lens,
          sensor,
          aperture: apertureUsed,
          subjectDistance,
          separationSamples,
        });

        const checkpointStats = buildStatsAtCheckpoints({
          lens,
          sensor,
          aperture: apertureUsed,
          subjectDistance,
          checkpoints,
        });

        return {
          lens,
          sensor,
          subjectDistance,
          apertureUsed,
          dataset,
          checkpointStats,
          entrancePupil: checkpointStats[0]?.entrancePupil ?? 0,
          colorIndex: lensColors[lens.id] ?? 0,
        };
      })
      .filter(Boolean);
  }, [selectedLenses, framingMode, fixedSubjectDistance, subjectHeight, frameFill, customSubjectDistance, customDimension, separationSamples, checkpoints, lensColors]);

  const statsPayload = lensCurves.map((curve) => ({
    lens: curve.lens,
    sensor: curve.sensor,
    subjectDistance: curve.subjectDistance,
    apertureUsed: curve.apertureUsed,
    entrancePupil: curve.checkpointStats[0]?.entrancePupil ?? 0,
    checkpointStats: curve.checkpointStats,
  }));

  useEffect(() => {
    if (!lensCurves.length) {
      setActiveLensId(null);
      return;
    }

    if (!activeLensId || !lensCurves.some((curve) => curve.lens.id === activeLensId)) {
      setActiveLensId(lensCurves[0].lens.id);
    }
  }, [lensCurves, activeLensId]);

  useEffect(() => {
    if (!lensCurves.length) {
      return;
    }

    let minFocusOffset = 0;
    let maxFocusOffset = 0;

    lensCurves.forEach((curve) => {
      curve.dataset.forEach((point) => {
        if (!Number.isFinite(point.focusOffset)) {
          return;
        }
        if (point.focusOffset < minFocusOffset) {
          minFocusOffset = point.focusOffset;
        }
        if (point.focusOffset > maxFocusOffset) {
          maxFocusOffset = point.focusOffset;
        }
      });
    });

    const requiredForeground = Math.abs(minFocusOffset);
    const requiredBackground = maxFocusOffset;

    if (requiredForeground > foregroundRangeLimit + 1e-6) {
      const nextLimit = Math.max(foregroundRangeLimit, Math.ceil(requiredForeground * 10) / 10);
      setForegroundRangeLimit(nextLimit);
    }

    if (requiredBackground > maxSeparationLimit + 1e-6) {
      const nextLimit = Math.max(maxSeparationLimit, Math.ceil(requiredBackground));
      setMaxSeparationLimit(nextLimit);
    }

    if (requiredForeground > foregroundRange + 1e-6) {
      setForegroundRange(Number(requiredForeground.toFixed(2)));
    }

    if (requiredBackground > maxSeparation + 1e-6) {
      setMaxSeparation(Number(requiredBackground.toFixed(2)));
    }
  }, [lensCurves, foregroundRange, maxSeparation, foregroundRangeLimit, maxSeparationLimit]);

  return (
    <div className="min-h-screen bg-slate-950 text-slate-100">
      <header className="border-b border-slate-900/70 bg-slate-950/90 px-6 py-4 backdrop-blur">
        <div className="flex w-full flex-col gap-2 md:flex-row md:items-center md:justify-between">
          <div>
            <h1 className="text-2xl font-semibold text-slate-100">Lens & Sensor Blur Comparison</h1>
            <p className="text-sm text-slate-400">
              Explore blur radius as % of sensor diagonal across lens and sensor combinations.
            </p>
          </div>
        </div>
      </header>

      <div className="flex min-h-[calc(100vh-4.5rem)] w-full flex-col lg:flex-row">
        <aside className="w-full border-b border-slate-900/60 bg-slate-950/60 lg:w-72 lg:border-b-0 lg:border-r">
          <ControlsPanel
            selectedLensIds={selectedLensIds}
            toggleLensSelection={toggleLensSelection}
            activeLensId={activeLensId}
            setActiveLensId={setActiveLensId}
            lensSensorFilter={lensSensorFilter}
            setLensSensorFilter={setLensSensorFilter}
            sensorFilterOptions={sensorFilterOptions}
            applyLensPreset={applyLensPreset}
          />
        </aside>

        <main className="flex flex-1 min-h-0 flex-col bg-slate-950/40 overflow-hidden">
          <PlotSection
            lensCurves={lensCurves}
            plotType={plotType}
            setPlotType={setPlotType}
            previewDistanceMeters={previewDistanceMeters}
          />

          <div className="space-y-6 px-4 pb-6 lg:hidden">
            <PreviewPanel lensCurves={lensCurves} previewDistanceMeters={previewDistanceMeters} />
            <PreviewDistanceControl
              value={previewDistance}
              onChange={setPreviewDistance}
              max={PREVIEW_DISTANCE_MAX}
            />
            <FramingPanel
              framingMode={framingMode}
              setFramingMode={setFramingMode}
              subjectHeight={subjectHeight}
              setSubjectHeight={setSubjectHeight}
              frameFill={frameFill}
              setFrameFill={setFrameFill}
              fixedSubjectDistance={fixedSubjectDistance}
              setFixedSubjectDistance={setFixedSubjectDistance}
              customSubjectDistance={customSubjectDistance}
              setCustomSubjectDistance={setCustomSubjectDistance}
              customDimension={customDimension}
              setCustomDimension={setCustomDimension}
            />

            <OpticsPanel
              foregroundRange={foregroundRange}
              setForegroundRange={setForegroundRange}
              maxSeparation={maxSeparation}
              setMaxSeparation={setMaxSeparation}
              checkpointsInput={checkpointsInput}
              setCheckpointsInput={setCheckpointsInput}
              foregroundRangeLimit={foregroundRangeLimit}
              maxSeparationLimit={maxSeparationLimit}
            />
          </div>

          <div className="px-4 pb-6">
            <div className="space-y-3">
              <h2 className="text-sm font-semibold uppercase tracking-wide text-slate-400">Blur check points</h2>
              <StatsTable lensStats={statsPayload} checkpoints={checkpoints} />
            </div>
          </div>
        </main>

        <aside className="hidden w-full max-w-xs border-l border-slate-900/60 bg-slate-950/60 lg:block">
          <div className="space-y-6 px-4 py-6">
            <PreviewPanel lensCurves={lensCurves} previewDistanceMeters={previewDistanceMeters} />
            <PreviewDistanceControl
              value={previewDistance}
              onChange={setPreviewDistance}
              max={PREVIEW_DISTANCE_MAX}
            />
            <FramingPanel
              framingMode={framingMode}
              setFramingMode={setFramingMode}
              subjectHeight={subjectHeight}
              setSubjectHeight={setSubjectHeight}
              frameFill={frameFill}
              setFrameFill={setFrameFill}
              fixedSubjectDistance={fixedSubjectDistance}
              setFixedSubjectDistance={setFixedSubjectDistance}
              customSubjectDistance={customSubjectDistance}
              setCustomSubjectDistance={setCustomSubjectDistance}
              customDimension={customDimension}
              setCustomDimension={setCustomDimension}
            />

            <OpticsPanel
              foregroundRange={foregroundRange}
              setForegroundRange={setForegroundRange}
              maxSeparation={maxSeparation}
              setMaxSeparation={setMaxSeparation}
              checkpointsInput={checkpointsInput}
              setCheckpointsInput={setCheckpointsInput}
              foregroundRangeLimit={foregroundRangeLimit}
              maxSeparationLimit={maxSeparationLimit}
            />
          </div>
        </aside>
      </div>

      <footer className="border-t border-slate-900/70 bg-slate-950/80 px-6 py-3 text-xs text-slate-500">
        <p>
          Questions or feedback? Reach out at{' '}
          <a className="text-accent hover:underline" href="mailto:lensblur@posteo.de">
            lensblur@posteo.de
          </a>
          .
        </p>
      </footer>
    </div>
  );
};

export default App;
