import React, { useEffect, useMemo, useRef, useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { RotateCw, Shuffle, EyeOff, Download, Upload, Trash2, Lock, Unlock } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";

// Secret Spin Wheel – designed for:
// - 5 people, 5 options (unique)
// - each option can be claimed once
// - each person spins/claims privately (others can’t see what’s gone)
// - host view can be toggled with a PIN to see assignments
// - state can be saved/loaded (JSON) so you can send it to yourself or keep across devices

function clamp(n, min, max) {
  return Math.max(min, Math.min(max, n));
}

function randomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

function polarToCartesian(cx, cy, r, angleDeg) {
  const a = (Math.PI / 180) * angleDeg;
  return { x: cx + r * Math.cos(a), y: cy + r * Math.sin(a) };
}

function describeArc(cx, cy, r, startAngle, endAngle) {
  const start = polarToCartesian(cx, cy, r, endAngle);
  const end = polarToCartesian(cx, cy, r, startAngle);
  const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
  return `M ${cx} ${cy} L ${start.x} ${start.y} A ${r} ${r} 0 ${largeArcFlag} 0 ${end.x} ${end.y} Z`;
}

const palette = [
  "#2563eb", // blue
  "#16a34a", // green
  "#f59e0b", // amber
  "#ef4444", // red
  "#8b5cf6", // violet
  "#06b6d4", // cyan
  "#f97316", // orange
  "#10b981", // emerald
  "#e11d48", // rose
  "#64748b", // slate
];

const defaultOptions = ["Starter", "Side", "Dessert", "Salad", "Drinks"];

export default function SecretSpinWheelDinnerParty() {
  const [people, setPeople] = useState(["Person 1", "Person 2", "Person 3", "Person 4", "Person 5"]);
  const [options, setOptions] = useState(defaultOptions);
  const [currentPerson, setCurrentPerson] = useState(people[0]);

  // Core state
  const [remaining, setRemaining] = useState(() => defaultOptions.map((label, i) => ({ id: `${i}-${label}`, label })));
  const [assignments, setAssignments] = useState({}); // { personName: optionLabel }

  // Spin UX
  const [spinning, setSpinning] = useState(false);
  const [rotation, setRotation] = useState(0);
  const [result, setResult] = useState(null); // { person, option }
  const [toast, setToast] = useState(null);

  // Privacy
  const [revealMode, setRevealMode] = useState(false);
  const [pin, setPin] = useState("1234");
  const [pinEntry, setPinEntry] = useState("");

  // Locking (prevents edits mid-game)
  const [locked, setLocked] = useState(false);

  // SVG refs
  const svgRef = useRef(null);

  // Derived
  const remainingLabels = useMemo(() => remaining.map((r) => r.label), [remaining]);
  const usedCount = useMemo(() => Object.keys(assignments).length, [assignments]);

  useEffect(() => {
    // Keep currentPerson valid
    if (!people.includes(currentPerson)) setCurrentPerson(people[0] || "");
  }, [people, currentPerson]);

  useEffect(() => {
    // Sync remaining when options change or after reset
    setRemaining(options.map((label, i) => ({ id: `${i}-${label}`, label })));
    setAssignments({});
    setResult(null);
    setRotation(0);
    setSpinning(false);
  }, [options]);

  const segments = useMemo(() => {
    const segs = remaining.length ? remaining : [{ id: "none", label: "No options" }];
    return segs;
  }, [remaining]);

  const canSpin = useMemo(() => {
    return !spinning && !!currentPerson && !assignments[currentPerson] && remaining.length > 0;
  }, [spinning, currentPerson, assignments, remaining.length]);

  function showToast(message) {
    setToast(message);
    window.clearTimeout(showToast._t);
    showToast._t = window.setTimeout(() => setToast(null), 2800);
  }

  function resetAll() {
    setRemaining(options.map((label, i) => ({ id: `${i}-${label}`, label })));
    setAssignments({});
    setResult(null);
    setRotation(0);
    setSpinning(false);
    setRevealMode(false);
    setPinEntry("");
    showToast("Reset complete.");
  }

  function shuffleOptions() {
    const arr = [...options];
    for (let i = arr.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [arr[i], arr[j]] = [arr[j], arr[i]];
    }
    setOptions(arr);
    showToast("Options shuffled.");
  }

  function handleSpin() {
    if (!canSpin) return;

    // Pick a random remaining option
    const idx = randomInt(0, remaining.length - 1);
    const chosen = remaining[idx];

    // Calculate wheel landing rotation for visual effect
    // The wheel is drawn with segments starting at 0° on the right, we place pointer at top (-90°).
    const n = remaining.length;
    const segmentAngle = 360 / n;

    // Choose a target angle in the chosen segment (with some jitter)
    const segmentStart = idx * segmentAngle;
    const jitter = segmentAngle * (0.15 + Math.random() * 0.7);
    const target = segmentStart + jitter;

    // Add several full spins + target alignment to pointer
    const extraSpins = randomInt(4, 7) * 360;

    // Pointer at top means we want the chosen segment to end up at 270° relative orientation.
    // We rotate the wheel so that target angle aligns with 270°.
    const desired = 270;
    const delta = desired - target;

    const newRotation = rotation + extraSpins + delta;

    setSpinning(true);
    setResult(null);

    // Animate roughly 3 seconds
    setRotation(newRotation);

    window.setTimeout(() => {
      // Commit assignment after spin
      setAssignments((prev) => ({ ...prev, [currentPerson]: chosen.label }));
      setRemaining((prev) => prev.filter((_, i) => i !== idx));
      setResult({ person: currentPerson, option: chosen.label });
      setSpinning(false);
      showToast("Assigned! Tap ‘Hide result’ before handing to the next person.");
    }, 3200);
  }

  function hideResult() {
    setResult(null);
    showToast("Result hidden.");
  }

  function toggleReveal() {
    if (revealMode) {
      setRevealMode(false);
      setPinEntry("");
      showToast("Host view hidden.");
      return;
    }
    if (pinEntry === pin) {
      setRevealMode(true);
      setPinEntry("");
      showToast("Host view enabled.");
    } else {
      showToast("Incorrect PIN.");
    }
  }

  function exportState() {
    const payload = {
      v: 1,
      people,
      options,
      remaining,
      assignments,
      locked,
      pin,
      ts: new Date().toISOString(),
    };
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `secret-spinwheel-state-${Date.now()}.json`;
    a.click();
    URL.revokeObjectURL(url);
  }

  function importState(file) {
    const reader = new FileReader();
    reader.onload = () => {
      try {
        const data = JSON.parse(String(reader.result || "{}"));
        if (!data || data.v !== 1) throw new Error("Unsupported file");
        setPeople(Array.isArray(data.people) ? data.people : people);
        setOptions(Array.isArray(data.options) ? data.options : options);
        setRemaining(Array.isArray(data.remaining) ? data.remaining : remaining);
        setAssignments(typeof data.assignments === "object" && data.assignments ? data.assignments : {});
        setLocked(!!data.locked);
        setPin(typeof data.pin === "string" ? data.pin : pin);
        setResult(null);
        setRotation(0);
        setRevealMode(false);
        setPinEntry("");
        showToast("State loaded.");
      } catch (e) {
        showToast("Could not load that file.");
      }
    };
    reader.readAsText(file);
  }

  const wheelSize = 320;
  const cx = wheelSize / 2;
  const cy = wheelSize / 2;
  const r = wheelSize / 2 - 10;

  const nSeg = segments.length;
  const segAngle = 360 / nSeg;

  const pointer = (
    <div className="absolute left-1/2 -translate-x-1/2 -top-1">
      <div className="w-0 h-0 border-l-[12px] border-l-transparent border-r-[12px] border-r-transparent border-b-[20px] border-b-zinc-900" />
    </div>
  );

  const completed = usedCount === people.length || remaining.length === 0;

  return (
    <div className="min-h-screen w-full bg-gradient-to-b from-zinc-50 to-zinc-100 p-4 md:p-8">
      <div className="mx-auto max-w-5xl space-y-4">
        <motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="flex flex-col gap-2">
          <h1 className="text-2xl md:text-3xl font-semibold tracking-tight">Secret Spin Wheel (5 people / 5 dishes)</h1>
          <p className="text-sm md:text-base text-zinc-600">
            Each person spins once. Their result shows only on this screen until you tap <span className="font-medium">Hide result</span>. Others cannot see what’s
            gone. Host can unlock a private list with a PIN.
          </p>
        </motion.div>

        <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
          <Card className="lg:col-span-2 rounded-2xl shadow-sm">
            <CardContent className="p-4 md:p-6">
              <div className="flex items-center justify-between gap-2">
                <div className="flex flex-wrap items-center gap-2">
                  <Badge variant="secondary" className="rounded-full">{usedCount}/{people.length} assigned</Badge>
                  <Badge variant="secondary" className="rounded-full">{remaining.length} remaining</Badge>
                  {completed && <Badge className="rounded-full">Complete</Badge>}
                </div>
                <div className="flex items-center gap-2">
                  <Button variant="outline" className="rounded-2xl" onClick={hideResult} disabled={!result || spinning}>
                    <EyeOff className="w-4 h-4 mr-2" /> Hide result
                  </Button>
                  <Button className="rounded-2xl" onClick={handleSpin} disabled={!canSpin}>
                    <RotateCw className={`w-4 h-4 mr-2 ${spinning ? "animate-spin" : ""}`} /> Spin
                  </Button>
                </div>
              </div>

              <div className="mt-4 flex flex-col md:flex-row gap-4 md:items-center md:justify-between">
                <div className="flex items-center gap-2">
                  <div className="text-sm text-zinc-600">Current person</div>
                  <select
                    className="h-10 rounded-xl border border-zinc-200 bg-white px-3 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-zinc-300"
                    value={currentPerson}
                    onChange={(e) => setCurrentPerson(e.target.value)}
                    disabled={spinning}
                  >
                    {people.map((p) => (
                      <option key={p} value={p}>
                        {p}{assignments[p] ? " (done)" : ""}
                      </option>
                    ))}
                  </select>
                  {assignments[currentPerson] && (
                    <Badge variant="secondary" className="rounded-full">Already spun</Badge>
                  )}
                </div>

                <div className="text-sm text-zinc-600">
                  Tip: After they see their result, tap <span className="font-medium">Hide result</span> before passing the phone.
                </div>
              </div>

              <div className="mt-6 relative flex justify-center">
                {pointer}
                <div className="relative">
                  <motion.div
                    className="rounded-full"
                    animate={{ rotate: rotation }}
                    transition={{ duration: 3.1, ease: [0.12, 0, 0.11, 1] }}
                    style={{ width: wheelSize, height: wheelSize }}
                  >
                    <svg ref={svgRef} width={wheelSize} height={wheelSize} viewBox={`0 0 ${wheelSize} ${wheelSize}`}>
                      <defs>
                        <filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
                          <feDropShadow dx="0" dy="2" stdDeviation="2" floodColor="#000" floodOpacity="0.12" />
                        </filter>
                      </defs>
                      <circle cx={cx} cy={cy} r={r} fill="#fff" filter="url(#shadow)" />
                      {segments.map((seg, i) => {
                        const start = i * segAngle;
                        const end = (i + 1) * segAngle;
                        const color = palette[i % palette.length];
                        const mid = (start + end) / 2;
                        const textPos = polarToCartesian(cx, cy, r * 0.62, mid);
                        return (
                          <g key={seg.id}>
                            <path d={describeArc(cx, cy, r, start, end)} fill={color} opacity={0.92} />
                            <text
                              x={textPos.x}
                              y={textPos.y}
                              fill="#fff"
                              fontSize="14"
                              fontWeight="600"
                              textAnchor="middle"
                              dominantBaseline="middle"
                              transform={`rotate(${mid} ${textPos.x} ${textPos.y})`}
                            >
                              {seg.label}
                            </text>
                          </g>
                        );
                      })}
                      <circle cx={cx} cy={cy} r={38} fill="#111827" opacity={0.95} />
                      <text x={cx} y={cy} fill="#fff" fontSize="12" fontWeight="700" textAnchor="middle" dominantBaseline="middle">
                        SPIN
                      </text>
                    </svg>
                  </motion.div>
                </div>
              </div>

              <AnimatePresence>
                {result && (
                  <motion.div
                    initial={{ opacity: 0, y: 10 }}
                    animate={{ opacity: 1, y: 0 }}
                    exit={{ opacity: 0, y: 10 }}
                    className="mt-6 rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm"
                  >
                    <div className="text-sm text-zinc-600">Private result for</div>
                    <div className="mt-1 text-xl font-semibold">{result.person}</div>
                    <div className="mt-2 text-sm text-zinc-600">You are bringing:</div>
                    <div className="mt-1 text-2xl font-semibold">{result.option}</div>
                    <div className="mt-3 text-sm text-zinc-500">Tap <span className="font-medium">Hide result</span> before handing the device to the next person.</div>
                  </motion.div>
                )}
              </AnimatePresence>

              <AnimatePresence>
                {toast && (
                  <motion.div
                    initial={{ opacity: 0, y: 10 }}
                    animate={{ opacity: 1, y: 0 }}
                    exit={{ opacity: 0, y: 10 }}
                    className="fixed left-1/2 bottom-6 -translate-x-1/2 rounded-full bg-zinc-900 text-white px-4 py-2 text-sm shadow-lg"
                  >
                    {toast}
                  </motion.div>
                )}
              </AnimatePresence>
            </CardContent>
          </Card>

          <Card className="rounded-2xl shadow-sm">
            <CardContent className="p-4 md:p-6 space-y-4">
              <div className="flex items-center justify-between">
                <div>
                  <div className="text-lg font-semibold">Setup</div>
                  <div className="text-sm text-zinc-600">Edit names & options, then lock.</div>
                </div>
                <Button
                  variant={locked ? "secondary" : "outline"}
                  className="rounded-2xl"
                  onClick={() => setLocked((v) => !v)}
                >
                  {locked ? <Lock className="w-4 h-4 mr-2" /> : <Unlock className="w-4 h-4 mr-2" />}
                  {locked ? "Locked" : "Lock"}
                </Button>
              </div>

              <div className="space-y-2">
                <div className="text-sm font-medium">People (5)</div>
                <div className="grid grid-cols-1 gap-2">
                  {people.map((p, idx) => (
                    <Input
                      key={idx}
                      value={p}
                      disabled={locked || usedCount > 0}
                      onChange={(e) => {
                        const v = e.target.value;
                        setPeople((prev) => prev.map((x, i) => (i === idx ? v : x)));
                      }}
                      className="rounded-xl"
                    />
                  ))}
                </div>
                <div className="text-xs text-zinc-500">Editing disabled after anyone spins (to keep it fair).</div>
              </div>

              <div className="space-y-2">
                <div className="text-sm font-medium">Dish options (5, unique)</div>
                <div className="grid grid-cols-1 gap-2">
                  {options.map((o, idx) => (
                    <Input
                      key={idx}
                      value={o}
                      disabled={locked || usedCount > 0}
                      onChange={(e) => {
                        const v = e.target.value;
                        setOptions((prev) => prev.map((x, i) => (i === idx ? v : x)));
                      }}
                      className="rounded-xl"
                    />
                  ))}
                </div>
                <div className="flex flex-wrap gap-2 pt-1">
                  <Button variant="outline" className="rounded-2xl" onClick={shuffleOptions} disabled={locked || usedCount > 0}>
                    <Shuffle className="w-4 h-4 mr-2" /> Shuffle options
                  </Button>
                  <Button variant="outline" className="rounded-2xl" onClick={resetAll}>
                    <Trash2 className="w-4 h-4 mr-2" /> Reset
                  </Button>
                </div>
              </div>

              <div className="border-t border-zinc-200 pt-4 space-y-2">
                <div className="text-sm font-medium">Host view (PIN protected)</div>
                <div className="text-xs text-zinc-500">People won’t see what’s taken unless you unlock this.</div>
                <div className="flex gap-2">
                  <Input
                    type="password"
                    value={pinEntry}
                    onChange={(e) => setPinEntry(e.target.value)}
                    placeholder="Enter PIN"
                    className="rounded-xl"
                  />
                  <Button className="rounded-2xl" onClick={toggleReveal}>
                    {revealMode ? "Hide" : "Unlock"}
                  </Button>
                </div>
                <div className="flex gap-2">
                  <Input
                    type="password"
                    value={pin}
                    onChange={(e) => setPin(e.target.value)}
                    placeholder="Set PIN (default 1234)"
                    className="rounded-xl"
                    disabled={locked && usedCount > 0}
                  />
                </div>

                <AnimatePresence>
                  {revealMode && (
                    <motion.div
                      initial={{ opacity: 0, y: 8 }}
                      animate={{ opacity: 1, y: 0 }}
                      exit={{ opacity: 0, y: 8 }}
                      className="mt-2 rounded-2xl border border-zinc-200 bg-white p-3"
                    >
                      <div className="text-sm font-semibold">Assignments</div>
                      <div className="mt-2 space-y-1">
                        {people.map((p) => (
                          <div key={p} className="flex items-center justify-between text-sm">
                            <span className="text-zinc-700">{p}</span>
                            <span className="font-medium text-zinc-900">{assignments[p] || "—"}</span>
                          </div>
                        ))}
                      </div>
                      <div className="mt-3 text-xs text-zinc-500">Keep this hidden during the game.</div>
                    </motion.div>
                  )}
                </AnimatePresence>
              </div>

              <div className="border-t border-zinc-200 pt-4 space-y-2">
                <div className="text-sm font-medium">Save / Load (optional)</div>
                <div className="flex flex-wrap gap-2">
                  <Button variant="outline" className="rounded-2xl" onClick={exportState}>
                    <Download className="w-4 h-4 mr-2" /> Export
                  </Button>
                  <label className="inline-flex">
                    <input
                      type="file"
                      accept="application/json"
                      className="hidden"
                      onChange={(e) => {
                        const f = e.target.files?.[0];
                        if (f) importState(f);
                        e.target.value = "";
                      }}
                    />
                    <span className="inline-flex">
                      <Button variant="outline" className="rounded-2xl" asChild>
                        <span>
                          <Upload className="w-4 h-4 mr-2" /> Import
                        </span>
                      </Button>
                    </span>
                  </label>
                </div>
                <div className="text-xs text-zinc-500">
                  Export lets you keep the same wheel state if you move devices. Import restores it.
                </div>
              </div>
            </CardContent>
          </Card>
        </div>

        <Card className="rounded-2xl shadow-sm">
          <CardContent className="p-4 md:p-6">
            <div className="text-lg font-semibold">How to use (in 30 seconds)</div>
            <ol className="mt-2 list-decimal pl-5 space-y-1 text-sm text-zinc-700">
              <li>Enter your 5 people and 5 dish options (left panel). (Defaults are already set.)</li>
              <li>Hand the phone to Person 1, choose their name, tap <span className="font-medium">Spin</span>.</li>
              <li>They remember their dish. You tap <span className="font-medium">Hide result</span>.</li>
              <li>Repeat for each person. Nobody sees what’s already gone.</li>
              <li>If you need to check, unlock Host view with the PIN.</li>
            </ol>
            <div className="mt-4 text-sm text-zinc-600">
              Want this embedded in a Microsoft Form flow? The clean approach is: use this wheel for selection, then each person submits their result via a Form (one question:
              “What did you get?”). I can also help you wire a Power Automate flow to capture results in a SharePoint list.
            </div>
          </CardContent>
        </Card>
      </div>
    </div>
  );
}

0 comments on “Add yours →

Leave a Reply

Your email address will not be published. Required fields are marked *