"use client";

import { useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
import rawQuizData from "./quiz-data.json";

type ResultChoice = { label: string; type: "result"; ideology: string };
type QuestionChoice = { label: string; type: "question"; id: string };
type Choice = ResultChoice | QuestionChoice;
type QuestionNode = { question: string; choices: Choice[] };
type RouteStep = { id: string; question: string; answer: string };
type Route = { ideology: string; path: RouteStep[] };
type Ideology = {
  name: string;
  quote: string;
  author: string;
  flag: string;
  thumbnail: string;
  image: string;
  source: "both" | "quark" | "left" | "curated";
  aliases: string[];
  family: string;
  branch: "Pro-private property" | "Anti-private property";
};
type QuizData = {
  version: string;
  counts: {
    ideologies: number;
    questions: number;
    routes: number;
    shared: number;
    quarkOnly: number;
    leftOnly: number;
    curated: number;
  };
  start: string;
  graph: Record<string, QuestionNode>;
  ideologies: Record<string, Ideology>;
};
type View = "home" | "quiz" | "result" | "results" | "tree" | "about";
type HistoryItem = { nodeId: string; answer: string };

const quizData = rawQuizData as QuizData;
const ideologyNames = Object.keys(quizData.ideologies).sort((a, b) => a.localeCompare(b));

const quarkIconLabels = new Set([
  "aggressively", "apathy", "armed forces", "biology", "birthright", "bureaucrats", "business",
  "charisma", "clerics", "commandment", "connections", "cooperation", "coup", "direct democracy",
  "economic leverage", "elected officials", "election", "fair competition", "fair outcomes", "god",
  "group", "guerrilla", "identity", "ideology", "illegal trade", "inheritance", "institutions",
  "insurrection", "judgment", "labor", "land ownership", "management", "military honors",
  "military readiness", "mythology", "national", "no", "parliament", "peacefully", "politics",
  "regional", "revolution", "selection", "share holding", "skill", "social elites", "sovereign",
  "spirits", "spiritual", "state", "state officials", "strongman", "technology", "temporal", "terror",
  "terrorism", "values", "vanguard", "variety", "warriors", "wisdom", "yes",
]);
const leftIconLabels = new Set([
  "christianity", "confucianism", "islam", "judaism", "military", "none", "strength", "trade",
]);
const answerIconOverrides: Record<string, string> = {
  "q_authMarkSoc:single-party state": "/assets/quark/buttons/vanguard.svg",
  "q_authMarkSoc:plural or decentralized state": "/assets/quark/buttons/parliament.svg",
  "q_authMarkSoc:no state": "/assets/quark/buttons/nostate.svg",
  "q_control:conditioned conformity": "/assets/quark/buttons/apathy.svg",
  "q_control:surveillance and terror": "/assets/quark/buttons/terror.svg",
  "q_counterEcon:counter-economic trade": "/assets/left/buttons/trade.svg",
  "q_fumivFascfu:spiritual reconstruction": "/assets/quark/buttons/spiritual.svg",
  "q_fumivFascfu:religious abolition": "/assets/left/buttons/none.svg",
  "q_khrushBrezhnev:planned development": "/assets/left/buttons/business.svg",
  "q_khrushBrezhnev:organic development": "/assets/left/buttons/home.svg",
  "q_muhCapitalistRoaders:mao's original revolutionary line": "/assets/quark/buttons/ideology.svg",
  "q_muhCapitalistRoaders:maoism as a universal third stage": "/assets/quark/buttons/ideology.svg",
  "q_muhCapitalistRoaders:reform and opening within the socialist road": "/assets/left/buttons/trade.svg",
  "q_nazbol:revolutionary vanguard": "/assets/quark/buttons/vanguard.svg",
  "q_nazbol:representative parliament": "/assets/quark/buttons/parliament.svg",
  "q_organic:organic centralism": "/assets/quark/buttons/group.svg",
  "q_organic:formal democratic organization": "/assets/quark/buttons/direct democracy.svg",
  "q_organic:no prescriptive organizational model": "/assets/left/buttons/none.svg",
  "q_organic:the revolution does not have an organizational model": "/assets/left/buttons/no.svg",
  "q_partyElites:an independent political organization": "/assets/quark/buttons/vanguard.svg",
  "q_partyElites:councils alone": "/assets/quark/buttons/direct democracy.svg",
  "q_trotskyCurrent:orthodox trotskyism": "/assets/quark/buttons/ideology.svg",
  "q_trotskyCurrent:posadism": "/assets/quark/buttons/ideology.svg",
  "q_utopianSchool:cooperation": "/assets/quark/buttons/cooperation.svg",
  "q_utopianSchool:variety": "/assets/quark/buttons/variety.svg",
  "q_utopianSchool:skill": "/assets/quark/buttons/skill.svg",
};

function answerIcon(nodeId: string, label: string) {
  const key = label.toLowerCase();
  const override = answerIconOverrides[`${nodeId}:${key}`];
  if (override) return override;
  if (quarkIconLabels.has(key)) return `/assets/quark/buttons/${key}.svg`;
  if (leftIconLabels.has(key)) return `/assets/left/buttons/${key}.svg`;
  if (key === "the state should not exist" || key === "no state") return "/assets/quark/buttons/nostate.svg";
  if (key.includes("election")) return "/assets/quark/buttons/election.svg";
  if (key.includes("revolution")) return "/assets/quark/buttons/revolution.svg";
  if (key.includes("council") || key.includes("direct democracy")) return "/assets/quark/buttons/direct democracy.svg";
  if (key.includes("party") || key.includes("vanguard") || key.includes("organization")) return "/assets/quark/buttons/vanguard.svg";
  if (key.includes("trade") || key.includes("mutual")) return "/assets/left/buttons/trade.svg";
  if (key.includes("cooperat") || key.includes("association")) return "/assets/quark/buttons/cooperation.svg";
  if (key.includes("socialist") || key.includes("communist") || key.includes("mao") || key.includes("trotsky")) return "/assets/quark/buttons/ideology.svg";
  return "/assets/quark/buttons/missing.svg";
}

function deriveRoutes(start: string) {
  const routes = new Map<string, Route>();
  const active = new Set<string>();

  function walk(id: string, path: RouteStep[]) {
    if (active.has(id)) throw new Error(`Cycle detected at ${id}`);
    const current = quizData.graph[id];
    if (!current) throw new Error(`Missing question ${id}`);
    active.add(id);
    for (const choice of current.choices) {
      const nextPath = [...path, { id, question: current.question, answer: choice.label }];
      if (choice.type === "question") walk(choice.id, nextPath);
      else routes.set(choice.ideology, { ideology: choice.ideology, path: nextPath });
    }
    active.delete(id);
  }

  walk(start, []);
  return routes;
}

const primaryRoutes = deriveRoutes(quizData.start);
const ambientProperties = ["--ambient-a", "--ambient-b", "--ambient-c", "--ambient-d", "--ambient-e"];
const answerAmbientColors = [
  "hsl(120 63% 38%)",
  "hsl(0 62% 42%)",
  "hsl(280 54% 34%)",
  "hsl(40 78% 38%)",
  "hsl(205 65% 36%)",
];
const viewAmbientPalettes: Partial<Record<View, string[]>> = {
  home: ["#858a8f", "#555c63", "#a1a4a7"],
  results: ["#466578", "#655071", "#597564"],
  tree: ["#38634f", "#435d78", "#806f43"],
  about: ["#496472", "#5b6372", "#7a7180"],
  result: ["#6a7279", "#4d565e", "#8b9094"],
};
const resultPaletteCache = new Map<string, string[]>();

type SampledColor = {
  red: number;
  green: number;
  blue: number;
  count: number;
  lightness: number;
  saturation: number;
  score: number;
};

function setAmbientPalette(colors: string[]) {
  const palette = colors.filter(Boolean).slice(0, ambientProperties.length);
  if (!palette.length || typeof document === "undefined") return;
  ambientProperties.forEach((property, index) => {
    document.documentElement.style.setProperty(property, palette[index % palette.length]);
  });
  const background = document.getElementById("ambient-background");
  if (background) background.dataset.count = String(palette.length);
}

function colorMetrics(red: number, green: number, blue: number) {
  const maximum = Math.max(red, green, blue) / 255;
  const minimum = Math.min(red, green, blue) / 255;
  const lightness = (maximum + minimum) / 2;
  const delta = maximum - minimum;
  const saturation = delta === 0 ? 0 : delta / (1 - Math.abs(2 * lightness - 1));
  return { lightness, saturation };
}

function colorDistance(left: SampledColor, right: SampledColor) {
  return Math.hypot(left.red - right.red, left.green - right.green, left.blue - right.blue);
}

function toHex({ red, green, blue }: SampledColor) {
  return `#${[red, green, blue].map((channel) => Math.round(channel).toString(16).padStart(2, "0")).join("")}`;
}

function sampleImagePalette(image: HTMLImageElement) {
  const canvas = document.createElement("canvas");
  canvas.width = 32;
  canvas.height = 20;
  const context = canvas.getContext("2d", { willReadFrequently: true });
  if (!context) return [];
  try {
    context.drawImage(image, 0, 0, canvas.width, canvas.height);
    const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
    const buckets = new Map<string, { red: number; green: number; blue: number; count: number }>();
    for (let index = 0; index < pixels.length; index += 4) {
      if (pixels[index + 3] < 128) continue;
      const red = pixels[index];
      const green = pixels[index + 1];
      const blue = pixels[index + 2];
      const key = `${red >> 5}:${green >> 5}:${blue >> 5}`;
      const bucket = buckets.get(key) ?? { red: 0, green: 0, blue: 0, count: 0 };
      bucket.red += red;
      bucket.green += green;
      bucket.blue += blue;
      bucket.count += 1;
      buckets.set(key, bucket);
    }
    const candidates: SampledColor[] = [...buckets.values()].map((bucket) => {
      const red = bucket.red / bucket.count;
      const green = bucket.green / bucket.count;
      const blue = bucket.blue / bucket.count;
      const { lightness, saturation } = colorMetrics(red, green, blue);
      const middleWeight = 0.65 + (1 - Math.abs(lightness - 0.5) * 2) * 0.35;
      return { red, green, blue, count: bucket.count, lightness, saturation, score: bucket.count * (0.45 + saturation * 1.8) * middleWeight };
    });
    if (!candidates.length) return [];
    const chromatic = candidates
      .filter((color) => color.saturation >= 0.18 && color.lightness > 0.05 && color.lightness < 0.95)
      .sort((left, right) => right.score - left.score);
    const ranked = [...candidates].sort((left, right) => right.score - left.score);
    const chosen = [chromatic[0] ?? ranked[0]];
    while (chosen.length < 5) {
      const remaining = ranked
        .filter((candidate) => !chosen.includes(candidate))
        .map((candidate) => ({
          candidate,
          distance: Math.min(...chosen.map((color) => colorDistance(candidate, color))),
        }))
        .filter(({ distance }) => distance >= 34)
        .sort((left, right) => (right.candidate.score * (0.35 + right.distance / 255)) - (left.candidate.score * (0.35 + left.distance / 255)));
      if (!remaining.length) break;
      chosen.push(remaining[0].candidate);
    }
    return chosen.map(toHex);
  } catch (error) {
    console.warn("Could not sample result artwork colors", error);
    return [];
  }
}

function lengthBand(text: string, mediumAt: number, longAt: number) {
  if (text.length >= longAt) return "long";
  if (text.length >= mediumAt) return "medium";
  return "short";
}

function resetPageScroll() {
  const reset = () => {
    document.documentElement.scrollTop = 0;
    document.body.scrollTop = 0;
    window.scrollTo(0, 0);
  };
  reset();
  requestAnimationFrame(reset);
}

export default function Home() {
  const [view, setView] = useState<View>("home");
  const [navOpen, setNavOpen] = useState(false);
  const [currentNode, setCurrentNode] = useState(quizData.start);
  const [history, setHistory] = useState<HistoryItem[]>([]);
  const [selectedName, setSelectedName] = useState(ideologyNames[0]);
  const [resultOrigin, setResultOrigin] = useState<View>("home");
  const [resultPath, setResultPath] = useState<RouteStep[]>([]);
  const [search, setSearch] = useState("");
  const [branchFilter, setBranchFilter] = useState<"all" | Ideology["branch"]>("all");
  const [familyFilter, setFamilyFilter] = useState("all");
  const [treeSelection, setTreeSelection] = useState("");
  const resultImageRef = useRef<HTMLImageElement>(null);

  const families = useMemo(
    () => [...new Set(Object.values(quizData.ideologies).map((item) => item.family))].sort(),
    [],
  );
  const filteredIdeologies = useMemo(() => {
    const query = search.trim().toLowerCase();
    return ideologyNames
      .map((name) => quizData.ideologies[name])
      .filter((item) => {
        const searchable = [item.name, item.family, item.author, ...item.aliases].join(" ").toLowerCase();
        return (
          (!query || searchable.includes(query)) &&
          (branchFilter === "all" || item.branch === branchFilter) &&
          (familyFilter === "all" || item.family === familyFilter)
        );
      });
  }, [search, branchFilter, familyFilter]);

  const node = quizData.graph[currentNode];
  const ideology = quizData.ideologies[selectedName];

  useEffect(() => {
    if (view === "quiz") {
      setAmbientPalette(answerAmbientColors.slice(0, node.choices.length));
      return;
    }
    if (view !== "result") {
      setAmbientPalette(viewAmbientPalettes[view] ?? viewAmbientPalettes.home!);
      return;
    }
    const image = resultImageRef.current;
    const cachedPalette = resultPaletteCache.get(ideology.name);
    if (cachedPalette) {
      setAmbientPalette(cachedPalette);
      return;
    }
    if (!image) return;
    let active = true;
    const applyImagePalette = () => {
      if (!active) return;
      const palette = sampleImagePalette(image);
      if (!palette.length) return;
      resultPaletteCache.set(ideology.name, palette);
      setAmbientPalette(palette);
    };
    if (image.complete && image.naturalWidth) requestAnimationFrame(applyImagePalette);
    else image.addEventListener("load", applyImagePalette, { once: true });
    return () => {
      active = false;
      image.removeEventListener("load", applyImagePalette);
    };
  }, [view, node.choices.length, ideology.name]);

  function changeView(next: View) {
    setView(next);
    setNavOpen(false);
    resetPageScroll();
  }

  function startQuiz() {
    setHistory([]);
    setCurrentNode(quizData.start);
    changeView("quiz");
  }

  function startAtNode(id: string) {
    setHistory([]);
    setCurrentNode(id);
    changeView("quiz");
  }

  function choose(choice: Choice) {
    const nextHistory = [...history, { nodeId: currentNode, answer: choice.label }];
    setHistory(nextHistory);
    if (choice.type === "question") {
      setCurrentNode(choice.id);
      resetPageScroll();
      return;
    }
    setSelectedName(choice.ideology);
    setResultPath(
      nextHistory.map((item) => ({
        id: item.nodeId,
        question: quizData.graph[item.nodeId].question,
        answer: item.answer,
      })),
    );
    setResultOrigin("quiz");
    changeView("result");
  }

  function quizBack() {
    if (!history.length) {
      changeView("home");
      return;
    }
    const previous = history.at(-1)!;
    setCurrentNode(previous.nodeId);
    setHistory(history.slice(0, -1));
  }

  function openResult(name: string, origin: View) {
    setSelectedName(name);
    setResultOrigin(origin);
    setResultPath(primaryRoutes.get(name)?.path ?? []);
    changeView("result");
  }

  function resultBack() {
    if (resultOrigin === "quiz") {
      const previous = history.at(-1);
      if (previous) {
        setCurrentNode(previous.nodeId);
        setHistory(history.slice(0, -1));
        changeView("quiz");
      } else {
        changeView("home");
      }
      return;
    }
    changeView(resultOrigin);
  }

  function cycleResult(direction: -1 | 1) {
    const index = ideologyNames.indexOf(selectedName);
    const next = (index + direction + ideologyNames.length) % ideologyNames.length;
    const name = ideologyNames[next];
    setSelectedName(name);
    setResultPath(primaryRoutes.get(name)?.path ?? []);
  }

  return (
    <>
      <div className="ambient-background" id="ambient-background" data-count="3" aria-hidden="true">
        <span className="ambient-orb ambient-orb-a" />
        <span className="ambient-orb ambient-orb-b" />
        <span className="ambient-orb ambient-orb-c" />
        <span className="ambient-orb ambient-orb-d" />
        <span className="ambient-orb ambient-orb-e" />
      </div>
      <main>
      <nav className="floating-nav" aria-label="Primary navigation">
        <button
          className="nav-button nav-toggle"
          onClick={() => setNavOpen((open) => !open)}
          aria-expanded={navOpen}
          aria-label={navOpen ? "Close navigation" : "Open navigation"}
        >
          <img src="/assets/quark/buttons/navigation.svg" alt="" />
        </button>
        <div className={`nav-items ${navOpen ? "nav-items-open" : ""}`}>
          <button className="nav-button" onClick={() => changeView("home")} aria-label="Home">
            <img src="/assets/quark/buttons/home.svg" alt="" />
          </button>
          <button className="nav-button" onClick={startQuiz} aria-label="Start quiz">
            <img src="/assets/quark/buttons/yes.svg" alt="" />
          </button>
          <button className="nav-button" onClick={() => changeView("results")} aria-label="All results">
            <img src="/assets/quark/buttons/ideology.svg" alt="" />
          </button>
          <button className="nav-button" onClick={() => changeView("about")} aria-label="About">
            <img src="/assets/quark/buttons/about.svg" alt="" />
          </button>
          <button className="nav-button" onClick={() => changeView("tree")} aria-label="Tree explorer">
            <img src="/assets/quark/buttons/tree.svg" alt="" />
          </button>
        </div>
      </nav>

      <header className="site-title">
        <span className="version-spacer" aria-hidden="true" />
        <span className="wordmark">Ideology Sorter</span>
        <span className="version-label" title="Site edition">{quizData.version}</span>
      </header>

      {view === "home" && (
        <section className="home-view view-enter">
          <p className="eyebrow">One decision tree · {quizData.counts.ideologies} outcomes</p>
          <h1 className="intro">Discover your political ideology across {quizData.counts.ideologies} distinct outcomes</h1>
          <div className="home-buttons">
            <button className="button start-button" onClick={startQuiz}>
              Start <img src="/assets/quark/buttons/start.svg" alt="" />
            </button>
            <button className="button small-button" onClick={() => changeView("results")}>
              <img src="/assets/quark/buttons/ideology.svg" alt="" /> Results
            </button>
            <button className="button small-button" onClick={() => changeView("about")}>
              <img src="/assets/quark/buttons/about.svg" alt="" /> About
            </button>
            <button className="button small-button" onClick={() => changeView("tree")}>
              <img src="/assets/quark/buttons/tree.svg" alt="" /> Tree
            </button>
          </div>
          <div className="merge-summary" aria-label="Sorter summary">
            <div><strong>{quizData.counts.ideologies}</strong><span>ideologies</span></div>
            <div><strong>{quizData.counts.questions}</strong><span>questions</span></div>
            <div><strong>{quizData.counts.routes}</strong><span>complete paths</span></div>
          </div>
          <p className="home-note">A single audited sorter with one complete route to every ideology.</p>
          <p className="home-credit">Interface based on <a href="https://quark88.github.io/ideosorter/" target="_blank" rel="noreferrer">Quark&apos;s original IdeoSorter</a>.</p>
        </section>
      )}

      {view === "quiz" && node && (
        <section className="quiz-view view-enter" key={currentNode} aria-live="polite">
          <div className="quiz-meta"><span>Question {history.length + 1}</span><span>{quizData.counts.ideologies} possible outcomes</span></div>
          <h1 className="question" data-length={lengthBand(node.question, 70, 105)}>{node.question}</h1>
          <div className="answers">
            {node.choices.map((choice, index) => (
              <button
                className={`button answer-button answer-${index + 1}`}
                data-length={choice.label.length >= 30 ? "long" : "short"}
                style={{ "--answer-index": index } as CSSProperties}
                key={`${choice.label}-${choice.type === "question" ? choice.id : choice.ideology}`}
                onClick={() => choose(choice)}
              >
                <img
                  src={answerIcon(currentNode, choice.label)}
                  onError={(event) => { event.currentTarget.src = "/assets/quark/buttons/missing.svg"; }}
                  alt=""
                />
                <span>{choice.label}</span>
              </button>
            ))}
          </div>
          <button className="button back-button" onClick={quizBack}>
            <img src="/assets/quark/buttons/back.svg" alt="" /> Back
          </button>
          <div className="path-dots" aria-label={`${history.length} answers completed`}>
            {history.slice(-12).map((item, index) => <span key={`${item.nodeId}-${index}`} title={item.answer} />)}
          </div>
        </section>
      )}

      {view === "result" && ideology && (
        <section className="result-view view-enter" key={selectedName}>
          <article className="result-card">
            <h1 className="match" data-length={lengthBand(ideology.name, 22, 38)}>{ideology.name}</h1>
            <div className="result-switchbar">
              {resultOrigin !== "quiz" ? (
                <button className="button switch-button" onClick={() => cycleResult(-1)} aria-label="Previous ideology">
                  <img src="/assets/quark/buttons/left.svg" alt="" />
                </button>
              ) : <span className="switch-placeholder" />}
              <img
                className="flag"
                ref={resultImageRef}
                src={ideology.image}
                alt={`${ideology.name} flag`}
                decoding="async"
                onError={(event) => { event.currentTarget.src = "/assets/quark/flags/Missing.svg"; }}
              />
              {resultOrigin !== "quiz" ? (
                <button className="button switch-button" onClick={() => cycleResult(1)} aria-label="Next ideology">
                  <img src="/assets/quark/buttons/right.svg" alt="" />
                </button>
              ) : <span className="switch-placeholder" />}
            </div>
            <blockquote className="quote">{ideology.quote}</blockquote>
            <p className="author">{ideology.author}</p>
            <div className="result-details"><span>{ideology.family}</span><span>{ideology.branch}</span></div>
            {!!ideology.aliases.length && (
              <p className="aliases">Also known as: <span>{ideology.aliases.join(" · ")}</span></p>
            )}
          </article>
          <details className="route-details">
            <summary>View the {resultPath.length}-answer route</summary>
            <RouteTimeline path={resultPath} />
          </details>
          <div className="result-actions">
            <button className="button back-button" onClick={resultBack}>
              <img src="/assets/quark/buttons/back.svg" alt="" /> Back
            </button>
            <button className="button back-button retake-button" onClick={startQuiz}>
              <img src="/assets/quark/buttons/start.svg" alt="" /> Retake
            </button>
          </div>
        </section>
      )}

      {view === "results" && (
        <section className="catalog-view view-enter">
          <p className="eyebrow">Complete ideology index</p>
          <h1 className="notice">All {quizData.counts.ideologies} results</h1>
          <div className="filters">
            <label className="search-label">
              <span>Search ideologies, aliases or authors</span>
              <input value={search} onChange={(event) => setSearch(event.target.value)} placeholder="Search…" />
            </label>
            <label>
              <span>Property branch</span>
              <select value={branchFilter} onChange={(event) => setBranchFilter(event.target.value as typeof branchFilter)}>
                <option value="all">Both branches</option><option value="Pro-private property">Pro-private property</option>
                <option value="Anti-private property">Anti-private property</option>
              </select>
            </label>
            <label>
              <span>Ideology family</span>
              <select value={familyFilter} onChange={(event) => setFamilyFilter(event.target.value)}>
                <option value="all">All families</option>
                {families.map((family) => <option key={family} value={family}>{family}</option>)}
              </select>
            </label>
          </div>
          <p className="catalog-count">Showing {filteredIdeologies.length} outcomes</p>
          <div className="result-grid">
            {filteredIdeologies.map((item) => (
              <button className="result-tile" key={item.name} onClick={() => openResult(item.name, "results")}>
                <img
                  src={item.thumbnail}
                  alt=""
                  width="360"
                  height="240"
                  loading="lazy"
                  decoding="async"
                  fetchPriority="low"
                  onError={(event) => { event.currentTarget.src = "/assets/quark/flags/Missing.svg"; }}
                />
                <span className="tile-overlay">
                  <strong>{item.name}</strong>
                  <small>{item.family}</small>
                </span>
              </button>
            ))}
          </div>
          {!filteredIdeologies.length && <p className="empty-state">No ideology matches those filters.</p>}
        </section>
      )}

      {view === "tree" && (
        <section className="tree-view view-enter">
          <div className="tree-info">
            <div className="tree-info-half">
              <h2 className="tree-tip">All {ideologyNames.length} possible results, alphabetically</h2>
              <select
                className="tree-matches"
                value={treeSelection}
                onChange={(event) => {
                  const name = event.target.value;
                  setTreeSelection(name);
                  if (name) openResult(name, "tree");
                }}
                aria-label="Select a result to view"
              >
                <option value="">Select result to view...</option>
                {ideologyNames.map((name) => <option key={name} value={name}>{name}</option>)}
              </select>
            </div>
            <div className="tree-info-half">
              <h2 className="tree-tip">Legend (click a box in the tree to view it)</h2>
              <div className="tree-legend" aria-label="Tree legend">
                <div className="tree-legend-box tree-legend-start">Start</div>
                <div className="tree-legend-box tree-legend-question">Question</div>
                <div
                  className="tree-legend-box tree-legend-result"
                  style={{ backgroundImage: `url(${quizData.ideologies[treeSelection || ideologyNames[0]].thumbnail})` }}
                >Result</div>
              </div>
            </div>
          </div>
          <h2 className="notice tree-heading">Pro-private property tree</h2>
          <TreeCanvas
            branch="pro"
            rootId="q_constitution"
            label="Pro-private property decision tree"
            onQuestion={startAtNode}
            onResult={(name) => openResult(name, "tree")}
          />
          <h2 className="notice tree-heading">Anti-private property tree</h2>
          <TreeCanvas
            branch="anti"
            rootId="q_markets"
            label="Anti-private property decision tree"
            onQuestion={startAtNode}
            onResult={(name) => openResult(name, "tree")}
          />
          <button className="button back-button" onClick={() => changeView("home")}>
            <img src="/assets/quark/buttons/back.svg" alt="" /> Back
          </button>
        </section>
      )}

      {view === "about" && (
        <section className="about-view view-enter">
          <p className="eyebrow">About the sorter</p>
          <h1 className="notice">One tree, many traditions</h1>
          <div className="about-grid">
            <article>
              <h2>How does it work?</h2>
              <p>
                Each answer marks a substantive political position and narrows the remaining possibilities. Every branch ends at one ideology, and every ideology has one complete route through the tree.
              </p>
            </article>
            <article>
              <h2>How are distinctions handled?</h2>
              <p>
                Mere spelling variants appear once, while genuinely different currents remain separate. Questions are written to distinguish positions without announcing the result in advance, and both acceptance and rejection mark meaningful positions.
              </p>
            </article>
            <article>
              <h2>What exactly is here?</h2>
              <p>
                {quizData.counts.ideologies} distinct outcomes, {quizData.counts.questions} reachable questions and {quizData.counts.routes} complete routes. Every result has a flag, quotation, author, family, property branch and inspectable answer path.
              </p>
            </article>
            <article>
              <h2>Important limitation</h2>
              <p>
                This is an exploratory classification game, not a validated psychological or political-science measure. Its labels and distinctions necessarily simplify contested traditions; inclusion does not imply endorsement.
              </p>
            </article>
          </div>
          <article className="license-panel">
            <h2>License and source</h2>
            <p>
              This work is distributed under the GNU GPL v3. <a href="/source/ideology-sorter-source.zip">Download the corresponding source</a>.
            </p>
          </article>
        </section>
      )}

      <footer>
        <p>Ideology Sorter · GPL-3.0 · 2026</p>
      </footer>
      </main>
    </>
  );
}

const TREE_NODE_WIDTH = 190;
const TREE_NODE_HEIGHT = 82;
const TREE_COLUMN_GAP = 310;
const TREE_ROW_GAP = 108;
const TREE_PADDING = 34;
const TREE_ARROW_COLORS = ["#21a63a", "#bd3030", "#74329a", "#b57a13", "#166b9f"];

type TreeVisualNode = {
  key: string;
  type: "question" | "result";
  label: string;
  x: number;
  y: number;
  isRoot?: boolean;
  id?: string;
  ideology?: string;
};
type TreeVisualEdge = {
  key: string;
  from: string;
  to: string;
  label: string;
  colorIndex: number;
};
type TreeLayout = {
  nodes: TreeVisualNode[];
  edges: TreeVisualEdge[];
  width: number;
  height: number;
};

function buildTreeLayout(rootId: string): TreeLayout {
  const nodes: TreeVisualNode[] = [];
  const edges: TreeVisualEdge[] = [];
  const positions = new Map<string, TreeVisualNode>();
  const active = new Set<string>();
  let leafIndex = 0;
  let maxDepth = 0;

  function addResult(ideology: string, depth: number) {
    const key = `result:${ideology}`;
    const visual: TreeVisualNode = {
      key,
      type: "result",
      label: ideology,
      ideology,
      x: TREE_PADDING + depth * TREE_COLUMN_GAP,
      y: TREE_PADDING + leafIndex * TREE_ROW_GAP + TREE_NODE_HEIGHT / 2,
    };
    leafIndex += 1;
    maxDepth = Math.max(maxDepth, depth);
    nodes.push(visual);
    positions.set(key, visual);
    return key;
  }

  function addQuestion(id: string, depth: number, isRoot = false): string {
    if (active.has(id)) throw new Error(`Cycle detected while laying out ${id}`);
    const current = quizData.graph[id];
    if (!current) throw new Error(`Missing question ${id}`);
    active.add(id);
    maxDepth = Math.max(maxDepth, depth);
    const childKeys = current.choices.map((choice) => (
      choice.type === "question"
        ? addQuestion(choice.id, depth + 1)
        : addResult(choice.ideology, depth + 1)
    ));
    const childPositions = childKeys.map((key) => positions.get(key)!);
    const key = `question:${id}`;
    const visual: TreeVisualNode = {
      key,
      type: "question",
      label: current.question,
      id,
      isRoot,
      x: TREE_PADDING + depth * TREE_COLUMN_GAP,
      y: childPositions.reduce((sum, child) => sum + child.y, 0) / childPositions.length,
    };
    nodes.push(visual);
    positions.set(key, visual);
    current.choices.forEach((choice, index) => {
      edges.push({
        key: `${key}:${index}`,
        from: key,
        to: childKeys[index],
        label: choice.label,
        colorIndex: index % TREE_ARROW_COLORS.length,
      });
    });
    active.delete(id);
    return key;
  }

  addQuestion(rootId, 0, true);
  return {
    nodes,
    edges,
    width: TREE_PADDING * 2 + maxDepth * TREE_COLUMN_GAP + TREE_NODE_WIDTH,
    height: TREE_PADDING * 2 + Math.max(TREE_NODE_HEIGHT, (leafIndex - 1) * TREE_ROW_GAP + TREE_NODE_HEIGHT),
  };
}

function TreeCanvas({
  branch,
  rootId,
  label,
  onQuestion,
  onResult,
}: {
  branch: "pro" | "anti";
  rootId: string;
  label: string;
  onQuestion: (id: string) => void;
  onResult: (name: string) => void;
}) {
  const layout = useMemo(() => buildTreeLayout(rootId), [rootId]);
  const scroller = useRef<HTMLDivElement>(null);
  const positions = useMemo(
    () => new Map(layout.nodes.map((node) => [node.key, node])),
    [layout],
  );
  useEffect(() => {
    const root = layout.nodes.find((node) => node.isRoot);
    if (root && scroller.current) {
      scroller.current.scrollTop = Math.max(0, root.y - scroller.current.clientHeight / 2);
      scroller.current.scrollLeft = 0;
    }
  }, [layout]);

  return (
    <div ref={scroller} className="tree-scroller" tabIndex={0} aria-label={`${label}; scroll in both directions to explore`}>
      <div className="tree-canvas" style={{ width: layout.width, height: layout.height }}>
        <svg className="tree-links" width={layout.width} height={layout.height} aria-hidden="true">
          <defs>
            {TREE_ARROW_COLORS.map((color, index) => (
              <marker
                id={`${branch}-arrow-${index}`}
                key={color}
                viewBox="0 0 10 10"
                refX="8"
                refY="5"
                markerWidth="7"
                markerHeight="7"
                orient="auto-start-reverse"
              >
                <path d="M 0 0 L 10 5 L 0 10 z" fill={color} />
              </marker>
            ))}
          </defs>
          {layout.edges.map((edge) => {
            const from = positions.get(edge.from)!;
            const to = positions.get(edge.to)!;
            const startX = from.x + TREE_NODE_WIDTH;
            const endX = to.x - 8;
            const middleX = startX + (endX - startX) / 2;
            return (
              <path
                key={edge.key}
                d={`M ${startX} ${from.y} C ${middleX} ${from.y}, ${middleX} ${to.y}, ${endX} ${to.y}`}
                fill="none"
                stroke={TREE_ARROW_COLORS[edge.colorIndex]}
                strokeWidth="8"
                markerEnd={`url(#${branch}-arrow-${edge.colorIndex})`}
              />
            );
          })}
        </svg>
        {layout.edges.map((edge) => {
          const from = positions.get(edge.from)!;
          const to = positions.get(edge.to)!;
          const startX = from.x + TREE_NODE_WIDTH;
          const endX = to.x;
          return (
            <span
              className="tree-edge-label"
              key={`${edge.key}:label`}
              style={{
                left: startX + (endX - startX) / 2 - 57,
                top: (from.y + to.y) / 2 - 13,
                backgroundColor: TREE_ARROW_COLORS[edge.colorIndex],
              }}
            >
              {edge.label}
            </span>
          );
        })}
        {layout.nodes.map((visual) => {
          const style = { left: visual.x, top: visual.y - TREE_NODE_HEIGHT / 2 };
          if (visual.type === "question") {
            return (
              <button
                className={`tree-cell tree-question-cell ${visual.isRoot ? "tree-start-cell" : ""}`}
                style={style}
                key={visual.key}
                title={visual.id}
                onClick={() => onQuestion(visual.id!)}
              >
                {visual.label}
              </button>
            );
          }
          const item = quizData.ideologies[visual.ideology!];
          return (
            <button
              className="tree-cell tree-result-cell"
              style={style}
              key={visual.key}
              title={`${item.name} · ${item.family}`}
              onClick={() => onResult(item.name)}
            >
              <img
                src={item.thumbnail}
                alt=""
                width="360"
                height="240"
                loading="lazy"
                decoding="async"
                fetchPriority="low"
                onError={(event) => { event.currentTarget.src = "/assets/quark/flags/Missing.svg"; }}
              />
              <span>{item.name}</span>
            </button>
          );
        })}
      </div>
    </div>
  );
}

function RouteTimeline({ path }: { path: RouteStep[] }) {
  if (!path.length) return <p className="empty-state">No route is available.</p>;
  return (
    <ol className="route-timeline">
      {path.map((step, index) => (
        <li key={`${step.id}-${index}`}>
          <span className="route-number">{index + 1}</span>
          <div><p>{step.question}</p><strong>{step.answer}</strong></div>
        </li>
      ))}
    </ol>
  );
}
