import { useEffect, useState, type CSSProperties } from "react";
import {
  RegionView,
  type Points,
  type Segment,
  type Pt,
} from "./render/RegionView.js";
import { onData, onHostEvent, sendToMATLAB } from "./bridge.js";

/** Payload from the numbl script: a convex region (CCW polygon) plus the
 *  hit-and-run samples drawn from it. Mirrors what hitandrun_sampler.m sends,
 *  both as the initial `Data` and as the `data` event after "New region". */
interface HitAndRunData {
  type: "hitandrun";
  region: Points;
  samples: Points;
  n: number;
  convex?: boolean; // false → non-convex (star) region
}

function isHitAndRunData(d: unknown): d is HitAndRunData {
  return (
    !!d &&
    typeof d === "object" &&
    (d as HitAndRunData).type === "hitandrun" &&
    !!(d as HitAndRunData).region &&
    !!(d as HitAndRunData).samples
  );
}

/** `samples` event payload: fresh points for the *same* region (after Resample
 *  or moving the samples slider). */
interface SamplesEvent {
  x: number[];
  y: number[];
  n: number;
}

// Discrete sample-count choices; the slider indexes into the active array.
const SAMPLE_CHOICES = [10, 100, 1000, 10000, 100000, 1000000];
// Non-convex sampling runs in the interpreter (no JIT), so cap it lower.
const SAMPLE_CHOICES_NONCONVEX = [10, 100, 1000, 10000];
const DEFAULT_N = 10000;

// Sampling movie: reveal points one at a time. Each frame shows the chord the
// step samples along AND the point that landed on it, together.
const MOVIE_STEP_MS = 750;
const MOVIE_LAST_INDEX = 41; // animate sampling of points up to this index

interface MovieState {
  step: number; // index of the point currently being sampled
}

/** The in-region segment(s) hit-and-run samples along: the line through
 *  (px,py) with direction (dx,dy), intersected with the polygon. One segment
 *  for a convex region, possibly several for a non-convex one. Mirrors the
 *  sampler's geometry, so it reproduces each step exactly. */
function regionSegments(
  region: Points,
  px: number,
  py: number,
  dx: number,
  dy: number
): Segment[] {
  if (Math.hypot(dx, dy) < 1e-12) return [];
  const m = region.x.length;
  const ts: number[] = [];
  for (let i = 0; i < m; i++) {
    const j = (i + 1) % m;
    const ex = region.x[j] - region.x[i];
    const ey = region.y[j] - region.y[i];
    const denom = dy * ex - dx * ey;
    if (Math.abs(denom) < 1e-12) continue;
    const wx = region.x[i] - px;
    const wy = region.y[i] - py;
    const s = (dx * wy - dy * wx) / denom; // position along the edge
    if (s >= 0 && s < 1) ts.push((wy * ex - wx * ey) / denom);
  }
  ts.sort((a, b) => a - b);
  const segs: Segment[] = [];
  for (let k = 0; k < ts.length - 1; k++) {
    const tm = (ts[k] + ts[k + 1]) / 2;
    if (pointInPolygon(region, px + tm * dx, py + tm * dy)) {
      segs.push({
        x0: px + ts[k] * dx,
        y0: py + ts[k] * dy,
        x1: px + ts[k + 1] * dx,
        y1: py + ts[k + 1] * dy,
      });
    }
  }
  return segs;
}

function pointInPolygon(region: Points, x: number, y: number): boolean {
  const n = region.x.length;
  let inside = false;
  for (let i = 0, j = n - 1; i < n; j = i++) {
    const xi = region.x[i];
    const yi = region.y[i];
    const xj = region.x[j];
    const yj = region.y[j];
    if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) {
      inside = !inside;
    }
  }
  return inside;
}

const prefixPoints = (p: Points, k: number): Points => ({
  x: p.x.slice(0, k),
  y: p.y.slice(0, k),
});

export function App() {
  const [data, setData] = useState<HitAndRunData | null>(null);
  const [n, setN] = useState(DEFAULT_N);
  const [busy, setBusy] = useState(false);
  const [movie, setMovie] = useState<MovieState | null>(null);

  useEffect(() => {
    // Initial region + samples (script → page via `Data`).
    const offData = onData(d => {
      if (isHitAndRunData(d)) {
        setData(d);
        setN(d.n);
        setMovie(null);
      }
    });
    // "New region": the script sends a full fresh payload.
    const offFull = onHostEvent("data", d => {
      if (isHitAndRunData(d)) {
        setData(d);
        setN(d.n);
        setBusy(false);
        setMovie(null);
      }
    });
    // Resample (same region): only the points change.
    const offSamples = onHostEvent("samples", ev => {
      const s = ev as SamplesEvent;
      if (!s || !Array.isArray(s.x)) return;
      setData(prev =>
        prev ? { ...prev, samples: { x: s.x, y: s.y }, n: s.n } : prev
      );
      setBusy(false);
      setMovie(null);
    });
    return () => {
      offData();
      offFull();
      offSamples();
    };
  }, []);

  // Movie clock: advance phase/step on a timer (a setTimeout chain — the effect
  // reruns whenever `movie` changes). Ends (→ full cloud) past the last index.
  useEffect(() => {
    if (!movie || !data) return;
    const lastIndex = Math.min(data.samples.x.length - 1, MOVIE_LAST_INDEX);
    const id = setTimeout(() => {
      setMovie(m => {
        if (!m) return m;
        const next = m.step + 1;
        return next > lastIndex ? null : { step: next };
      });
    }, MOVIE_STEP_MS);
    return () => clearTimeout(id);
  }, [movie, data]);

  const nonConvex = !!data && data.convex === false;
  const choices = nonConvex ? SAMPLE_CHOICES_NONCONVEX : SAMPLE_CHOICES;

  // Re-draw `count` samples in the current region (script round-trip).
  const resample = (count: number) => {
    if (!data) return;
    setBusy(true);
    sendToMATLAB("resample", {
      n: count,
      x: data.region.x,
      y: data.region.y,
      convex: data.convex !== false,
    });
  };

  // Generate a brand new region (convex or not) and sample it.
  const newRegion = (count: number, convex: boolean) => {
    setBusy(true);
    sendToMATLAB("newRegion", { n: count, convex });
  };

  // Checkbox: switch region type. Clamp N to the active set's max first.
  const setNonConvex = (makeNonConvex: boolean) => {
    const c = makeNonConvex ? SAMPLE_CHOICES_NONCONVEX : SAMPLE_CHOICES;
    const clamped = Math.min(n, c[c.length - 1]);
    setN(clamped);
    newRegion(clamped, !makeNonConvex);
  };

  const toggleMovie = () => {
    if (movie) {
      setMovie(null);
    } else if (data && data.samples.x.length >= 3) {
      setMovie({ step: 2 });
    }
  };

  // Overlay for the current movie frame (settled points + segments + marks).
  let cloud: Points = data ? data.samples : { x: [], y: [] };
  let segments: Segment[] | null = null;
  let from: Pt | null = null;
  let newPoint: Pt | null = null;
  if (movie && data) {
    const i = movie.step; // point being sampled
    const f = i - 1; // point the step starts from
    const { x, y } = data.samples;
    cloud = prefixPoints(data.samples, i); // settled points 0..i-1
    from = { x: x[f], y: y[f] };
    segments = regionSegments(data.region, x[f], y[f], x[i] - x[f], y[i] - y[f]);
    newPoint = { x: x[i], y: y[i] };
  }

  const canPlay = !!data && data.samples.x.length >= 3;

  return (
    <div style={rootStyle}>
      {data ? (
        <RegionView
          region={data.region}
          samples={cloud}
          segments={segments}
          from={from}
          newPoint={newPoint}
        />
      ) : (
        <div style={waitingStyle}>Waiting for the region from the script…</div>
      )}

      <div style={panelStyle}>
        <label style={labelStyle}>
          Samples: <b>{n.toLocaleString()}</b>
          <input
            type="range"
            min={0}
            max={choices.length - 1}
            step={1}
            value={Math.max(0, choices.indexOf(n))}
            disabled={!data || busy || !!movie}
            // Drag updates the label live; the script round-trip fires on
            // release (and on arrow-key release) to avoid flooding it.
            onChange={e => setN(choices[Number(e.target.value)])}
            onPointerUp={e => resample(choices[Number(e.currentTarget.value)])}
            onKeyUp={e => {
              if (e.key.startsWith("Arrow")) {
                resample(choices[Number(e.currentTarget.value)]);
              }
            }}
            style={sliderStyle}
          />
        </label>

        <div style={{ display: "flex", gap: 6, marginTop: 6 }}>
          <button
            style={btnStyle}
            disabled={!data || busy || !!movie}
            onClick={() => resample(n)}
            title="Draw a fresh set of samples in the same region"
          >
            Resample
          </button>
          <button
            style={btnStyle}
            disabled={busy || !!movie}
            onClick={() => newRegion(n, !nonConvex)}
            title="Generate a new region and sample it"
          >
            New region
          </button>
        </div>

        <label style={checkLabelStyle}>
          <input
            type="checkbox"
            checked={nonConvex}
            disabled={!data || busy || !!movie}
            onChange={e => setNonConvex(e.target.checked)}
          />
          non-convex region
        </label>

        <button
          style={playBtnStyle}
          disabled={!canPlay || busy}
          onClick={toggleMovie}
          title="Animate the hit-and-run steps: chord, then the sampled point"
        >
          {movie ? "■ Stop movie" : "▶ Play movie"}
        </button>

        <div style={{ fontSize: 10, color: "#64748b", marginTop: 6 }}>
          {busy
            ? "sampling…"
            : movie
              ? `movie · point ${movie.step + 1}`
              : `${data ? data.n.toLocaleString() : "—"} points`}
        </div>
      </div>
    </div>
  );
}

const rootStyle: CSSProperties = {
  position: "absolute",
  inset: 0,
  overflow: "hidden",
  background: "#ffffff",
  fontFamily: "system-ui, -apple-system, Arial, sans-serif",
};

const waitingStyle: CSSProperties = {
  position: "absolute",
  inset: 0,
  display: "flex",
  alignItems: "center",
  justifyContent: "center",
  color: "#94a3b8",
};

const panelStyle: CSSProperties = {
  position: "absolute",
  top: 8,
  left: 8,
  width: 150,
  padding: "7px 9px",
  background: "rgba(255,255,255,0.9)",
  border: "1px solid #e2e8f0",
  borderRadius: 6,
  boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
  color: "#0f172a",
};

const labelStyle: CSSProperties = {
  display: "block",
  fontSize: 11,
};

const checkLabelStyle: CSSProperties = {
  display: "flex",
  alignItems: "center",
  gap: 5,
  fontSize: 11,
  marginTop: 8,
  cursor: "pointer",
};

const sliderStyle: CSSProperties = {
  width: "100%",
  marginTop: 2,
};

const btnStyle: CSSProperties = {
  flex: 1,
  padding: "3px 4px",
  fontSize: 10,
  whiteSpace: "nowrap",
  cursor: "pointer",
  background: "#f8fafc",
  border: "1px solid #cbd5e1",
  borderRadius: 5,
  color: "#0f172a",
};

const playBtnStyle: CSSProperties = {
  width: "100%",
  marginTop: 6,
  padding: "4px 6px",
  fontSize: 10,
  cursor: "pointer",
  background: "#eff6ff",
  border: "1px solid #bfdbfe",
  borderRadius: 5,
  color: "#1e3a8a",
};
