/*
  Klondike Solitaire (React, no build step)
  ══════════════════════════════════════════════════════════════════════════════
  Interaction model
  - Mouse:  drag-and-drop (HTML5 DnD) + double-click to send to foundation
  - Touch:  tap to select a card (gold highlight), tap destination to move it;
            tap the same card again, or tap empty space, to deselect.
            Both models work simultaneously on hybrid devices.

  The domain logic (constants, deck, validation, reducer) is duplicated in
  game.js so the unit tests in test/game.test.js can load it without a browser.
  ══════════════════════════════════════════════════════════════════════════════
*/

// ── Domain: Card constants ────────────────────────────────────────────────────
const SUITS       = ["S", "H", "D", "C"];
const SUIT_SYMBOL = { S: "♠", H: "♥", D: "♦", C: "♣" };
const SUIT_COLOR  = { S: "black", C: "black", H: "red", D: "red" };
const RANKS       = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
const RANK_LABEL  = {
  1: "A", 2: "2",  3: "3",  4: "4",  5: "5",  6: "6",  7: "7",
  8: "8", 9: "9", 10: "10", 11: "J", 12: "Q", 13: "K",
};

// ── Domain: Deck factory ──────────────────────────────────────────────────────
function makeDeck() {
  let id = 1;
  const cards = [];
  for (const suit of SUITS)
    for (const rank of RANKS)
      cards.push({ id: id++, suit, rank, faceUp: false });
  return cards;
}

function shuffle(arr) {
  const a = arr.slice();
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

function deepClone(obj) {
  return JSON.parse(JSON.stringify(obj));
}

// ── Domain: Initial game state ────────────────────────────────────────────────
function initialState() {
  const deck    = shuffle(makeDeck());
  const tableau = Array.from({ length: 7 }, () => []);
  let idx = 0;
  for (let col = 0; col < 7; col++)
    for (let row = 0; row <= col; row++) {
      const card  = deck[idx++];
      card.faceUp = (row === col);
      tableau[col].push(card);
    }
  return {
    stock:       deck.slice(idx),
    waste:       [],
    tableau,
    foundations: [[], [], [], []],
    won:         false,
    moves:       0,
  };
}

// ── Domain: Move validation ───────────────────────────────────────────────────
function canPlaceOnTableau(card, destTop) {
  if (!destTop) return card.rank === 13; // empty column: only Kings
  return SUIT_COLOR[card.suit] !== SUIT_COLOR[destTop.suit]
      && card.rank === destTop.rank - 1;
}

function canPlaceOnFoundation(card, foundation) {
  if (foundation.length === 0) return card.rank === 1; // empty: only Aces
  const top = foundation[foundation.length - 1];
  return top.suit === card.suit && card.rank === top.rank + 1;
}

// ── Domain: Auto-move helper (used by AUTO_MOVE_ALL) ─────────────────────────
function tryAutoMoveOneCard(state) {
  const tryMove = (card, removeFn) => {
    for (let f = 0; f < 4; f++) {
      if (canPlaceOnFoundation(card, state.foundations[f])) {
        removeFn();
        state.foundations[f].push(card);
        state.moves++;
        return true;
      }
    }
    return false;
  };

  if (state.waste.length)
    if (tryMove(state.waste[state.waste.length - 1], () => state.waste.pop()))
      return true;

  for (let c = 0; c < 7; c++) {
    const col  = state.tableau[c];
    const card = col[col.length - 1];
    if (!card || !card.faceUp) continue;
    if (tryMove(card, () => col.pop())) {
      const newTop = col[col.length - 1];
      if (newTop && !newTop.faceUp) newTop.faceUp = true;
      return true;
    }
  }
  return false;
}

function checkWin(state) {
  return state.foundations.reduce((sum, f) => sum + f.length, 0) === 52;
}

// ── Domain: Reducer ───────────────────────────────────────────────────────────
function gameReducer(state, action) {
  const s = deepClone(state);

  switch (action.type) {

    case "NEW_GAME":
      return initialState();

    case "DRAW": {
      if (s.stock.length) {
        const card  = s.stock.pop();
        card.faceUp = true;
        s.waste.push(card);
        s.moves++;
      } else if (s.waste.length) {
        // Recycle waste back to stock face-down, preserving draw order
        while (s.waste.length) {
          const c  = s.waste.pop();
          c.faceUp = false;
          s.stock.push(c);
        }
        s.moves++;
      }
      s.won = checkWin(s);
      return s;
    }

    case "MOVE_TO_FOUNDATION": {
      const { payload, fIndex } = action;
      const foundation = s.foundations[fIndex];

      if (payload.from === "tableau") {
        const { col, index } = payload;
        const source = s.tableau[col];
        const cards  = source.slice(index);
        if (cards.length !== 1 || !cards[0].faceUp) return state;
        if (!canPlaceOnFoundation(cards[0], foundation)) return state;
        s.tableau[col] = source.slice(0, index);
        foundation.push(cards[0]);
        const newTop = s.tableau[col][s.tableau[col].length - 1];
        if (newTop && !newTop.faceUp) newTop.faceUp = true;
        s.moves++;
      } else if (payload.from === "waste") {
        const card = s.waste[s.waste.length - 1];
        if (!card || !canPlaceOnFoundation(card, foundation)) return state;
        s.waste.pop();
        foundation.push(card);
        s.moves++;
      }
      s.won = checkWin(s);
      return s;
    }

    case "MOVE_TO_TABLEAU": {
      const { payload, destCol } = action;
      const dest    = s.tableau[destCol];
      const destTop = dest[dest.length - 1] || null;

      if (payload.from === "tableau") {
        const { col, index } = payload;
        const source = s.tableau[col];
        const moving = source.slice(index);
        if (!moving[0].faceUp) return state;
        if (!canPlaceOnTableau(moving[0], destTop)) return state;
        // Validate the whole sequence alternates colour and descends
        for (let i = 0; i < moving.length - 1; i++) {
          const a = moving[i], b = moving[i + 1];
          if (SUIT_COLOR[a.suit] === SUIT_COLOR[b.suit] || a.rank !== b.rank + 1)
            return state;
        }
        s.tableau[col] = source.slice(0, index);
        dest.push.apply(dest, moving);
        const newTop = s.tableau[col][s.tableau[col].length - 1];
        if (newTop && !newTop.faceUp) newTop.faceUp = true;
        s.moves++;
      } else if (payload.from === "waste") {
        const card = s.waste[s.waste.length - 1];
        if (!card || !canPlaceOnTableau(card, destTop)) return state;
        s.waste.pop();
        dest.push(card);
        s.moves++;
      } else if (payload.from === "foundation") {
        const { fIndex } = payload;
        const card = s.foundations[fIndex][s.foundations[fIndex].length - 1];
        if (!card || !canPlaceOnTableau(card, destTop)) return state;
        s.foundations[fIndex].pop();
        dest.push(card);
        s.moves++;
      }
      s.won = checkWin(s);
      return s;
    }

    case "SEND_TO_FOUNDATION": {
      // Move a single top card to its matching foundation (double-tap / double-click)
      const { from, col } = action;
      let card, removeFn;

      if (from === "waste") {
        if (!s.waste.length) return state;
        card     = s.waste[s.waste.length - 1];
        removeFn = () => s.waste.pop();
      } else if (from === "tableau") {
        const column = s.tableau[col];
        if (!column.length) return state;
        card = column[column.length - 1];
        if (!card.faceUp) return state;
        removeFn = () => column.pop();
      } else {
        return state;
      }

      for (let f = 0; f < 4; f++) {
        if (canPlaceOnFoundation(card, s.foundations[f])) {
          removeFn();
          s.foundations[f].push(card);
          if (from === "tableau") {
            const column = s.tableau[col];
            const newTop = column[column.length - 1];
            if (newTop && !newTop.faceUp) newTop.faceUp = true;
          }
          s.moves++;
          s.won = checkWin(s);
          return s;
        }
      }
      return state;
    }

    case "AUTO_MOVE_ALL": {
      while (tryAutoMoveOneCard(s)) {}
      s.won = checkWin(s);
      return s;
    }

    default:
      return state;
  }
}

// ── UI: Helper ────────────────────────────────────────────────────────────────
function samePayload(a, b) {
  return a && b
    && a.from   === b.from
    && a.col    === b.col
    && a.index  === b.index
    && a.fIndex === b.fIndex;
}

// ── UI: App ───────────────────────────────────────────────────────────────────
function App() {
  const [game, dispatch] = React.useReducer(gameReducer, null, initialState);
  // selected: null | { from, col?, index?, fIndex? }
  const [selected, setSelected] = React.useState(null);

  // ── Game actions ────────────────────────────────────────────────────────────
  const newGame     = () => { dispatch({ type: "NEW_GAME" }); setSelected(null); };
  const draw        = ()  => dispatch({ type: "DRAW" });
  const autoMoveAll = ()  => dispatch({ type: "AUTO_MOVE_ALL" });

  React.useEffect(() => {
    if (game.won) setTimeout(() => alert("You win in " + game.moves + " moves! \uD83C\uDF89"), 50);
  }, [game.won]);

  // ── Drag-and-drop (mouse) ───────────────────────────────────────────────────
  const onDragStart = (e, payload) => {
    setSelected(null);
    e.dataTransfer.setData("application/json", JSON.stringify(payload));
    e.dataTransfer.effectAllowed = "move";
  };

  const allowDrop = (e) => e.preventDefault();

  const onDropToTableau = (e, destCol) => {
    e.preventDefault();
    const data = e.dataTransfer.getData("application/json");
    if (data) dispatch({ type: "MOVE_TO_TABLEAU", payload: JSON.parse(data), destCol });
  };

  const onDropToFoundation = (e, fIndex) => {
    e.preventDefault();
    const data = e.dataTransfer.getData("application/json");
    if (data) dispatch({ type: "MOVE_TO_FOUNDATION", payload: JSON.parse(data), fIndex });
  };

  // ── Tap-to-select (touch & mouse) ──────────────────────────────────────────
  const isSelected = (payload) => samePayload(selected, payload);

  // All cards in a selected tableau stack share the highlight
  const isInSelectedStack = (col, cardIndex) =>
    selected !== null && selected.from === "tableau"
    && selected.col === col && cardIndex >= selected.index;

  const sourceCardFor = (sel) => {
    if (!sel) return null;
    if (sel.from === "waste")      return game.waste[game.waste.length - 1] || null;
    if (sel.from === "tableau")    return game.tableau[sel.col][sel.index] || null;
    if (sel.from === "foundation") {
      const f = game.foundations[sel.fIndex];
      return f[f.length - 1] || null;
    }
    return null;
  };

  const tryMoveToTableau = (destCol) => {
    if (!selected) return false;
    const dest       = game.tableau[destCol];
    const destTop    = dest[dest.length - 1] || null;
    const sourceCard = sourceCardFor(selected);
    if (sourceCard && canPlaceOnTableau(sourceCard, destTop)) {
      dispatch({ type: "MOVE_TO_TABLEAU", payload: selected, destCol });
      return true;
    }
    return false;
  };

  const tryMoveToFoundation = (fIndex) => {
    if (!selected) return false;
    if (selected.from === "tableau") {
      const cards = game.tableau[selected.col].slice(selected.index);
      if (cards.length !== 1) return false; // sequences can't go to foundation
    }
    const sourceCard = sourceCardFor(selected);
    if (sourceCard && canPlaceOnFoundation(sourceCard, game.foundations[fIndex])) {
      dispatch({ type: "MOVE_TO_FOUNDATION", payload: selected, fIndex });
      return true;
    }
    return false;
  };

  // ── Click handlers ──────────────────────────────────────────────────────────
  const onStockTap = (e) => {
    e.stopPropagation();
    setSelected(null);
    draw();
  };

  const onWasteTap = (e) => {
    e.stopPropagation();
    if (!game.waste.length) return;
    const payload = { from: "waste" };
    setSelected(isSelected(payload) ? null : payload);
  };

  const onFoundationTap = (e, fIndex) => {
    e.stopPropagation();
    const payload    = { from: "foundation", fIndex };
    const foundation = game.foundations[fIndex];

    if (selected) {
      if (isSelected(payload)) { setSelected(null); return; }
      const moved = tryMoveToFoundation(fIndex);
      // If move failed, select the foundation's top card (to drag back to tableau)
      setSelected(moved ? null : (foundation.length ? payload : null));
    } else {
      setSelected(foundation.length ? payload : null);
    }
  };

  const onTableauCardTap = (e, col, index, card) => {
    e.stopPropagation();
    if (!card.faceUp) { setSelected(null); return; }

    const payload = { from: "tableau", col, index };

    if (selected) {
      if (isSelected(payload)) { setSelected(null); return; }
      // Try to move the selection to this column; if invalid, pivot to new selection
      const moved = tryMoveToTableau(col);
      setSelected(moved ? null : payload);
    } else {
      setSelected(payload);
    }
  };

  const onTableauColTap = (e, col) => {
    // Fires when the empty column area is tapped (card taps stop propagation)
    if (!selected) return;
    tryMoveToTableau(col);
    setSelected(null);
  };

  // ── Render ──────────────────────────────────────────────────────────────────
  return (
    <div className="app" onClick={() => setSelected(null)}>
      <div className="topbar">
        <div className="controls">
          <button onClick={newGame}>New Game</button>
          <button onClick={(e) => { e.stopPropagation(); draw(); }}>Draw</button>
          <button onClick={autoMoveAll}>Auto Move</button>
        </div>
        <span className="move-counter">Moves: {game.moves}</span>
      </div>

      <div className="board">
        {/* ── Upper row: Stock · Waste · Foundations ────────────────────── */}
        <div className="upper">

          {/* Stock */}
          <div className="stock-slot" onClick={onStockTap} title="Tap to draw / recycle">
            <PileSlot>
              {game.stock.length > 0 && <Card card={{ faceUp: false }} style={{ left: 0 }} />}
            </PileSlot>
          </div>

          {/* Waste */}
          <div className="waste-slot" onClick={(e) => e.stopPropagation()}>
            <PileSlot>
              {game.waste.map((card, i) => {
                const isTop = i === game.waste.length - 1;
                return (
                  <Card
                    key={card.id}
                    card={card}
                    style={{ left: "calc(var(--waste-fan) * " + Math.min(i, 2) + ")" }}
                    draggable={isTop}
                    selected={isTop && isSelected({ from: "waste" })}
                    onDragStart={isTop ? (e) => onDragStart(e, { from: "waste" }) : undefined}
                    onDoubleClick={isTop
                      ? () => dispatch({ type: "SEND_TO_FOUNDATION", from: "waste" })
                      : undefined}
                    onClick={isTop ? onWasteTap : (e) => e.stopPropagation()}
                  />
                );
              })}
            </PileSlot>
          </div>

          {/* Foundations */}
          {game.foundations.map((pile, fIndex) => (
            <div
              key={fIndex}
              className="foundation-slot"
              onDragOver={allowDrop}
              onDrop={(e) => onDropToFoundation(e, fIndex)}
              onClick={(e) => onFoundationTap(e, fIndex)}
            >
              <PileSlot>
                {pile.map((card, i) => (
                  <Card
                    key={card.id}
                    card={card}
                    style={{ left: 0 }}
                    draggable={i === pile.length - 1}
                    selected={i === pile.length - 1 && isSelected({ from: "foundation", fIndex })}
                    onDragStart={i === pile.length - 1
                      ? (e) => onDragStart(e, { from: "foundation", fIndex })
                      : undefined}
                    onClick={(e) => onFoundationTap(e, fIndex)}
                  />
                ))}
              </PileSlot>
            </div>
          ))}
        </div>

        {/* ── Tableau ────────────────────────────────────────────────────── */}
        <div className="row">
          {game.tableau.map((col, colIndex) => (
            <div
              key={colIndex}
              className="tableau-col"
              onDragOver={allowDrop}
              onDrop={(e) => onDropToTableau(e, colIndex)}
              onClick={(e) => onTableauColTap(e, colIndex)}
            >
              <PileSlot>
                {col.map((card, i) => (
                  <Card
                    key={card.id}
                    card={card}
                    style={{ top: "calc(var(--stack-offset) * " + i + ")", left: 0 }}
                    draggable={card.faceUp}
                    selected={isInSelectedStack(colIndex, i)}
                    onDragStart={card.faceUp
                      ? (e) => onDragStart(e, { from: "tableau", col: colIndex, index: i })
                      : undefined}
                    onDoubleClick={i === col.length - 1 && card.faceUp
                      ? () => dispatch({ type: "SEND_TO_FOUNDATION", from: "tableau", col: colIndex })
                      : undefined}
                    onClick={(e) => onTableauCardTap(e, colIndex, i, card)}
                  />
                ))}
              </PileSlot>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// ── UI: PileSlot ──────────────────────────────────────────────────────────────
function PileSlot({ children }) {
  return (
    <div className="pile">
      <div className="slot" />
      {children}
    </div>
  );
}

// ── UI: Card ──────────────────────────────────────────────────────────────────
function Card({ card, style, draggable, selected, onDragStart, onDoubleClick, onClick }) {
  if (!card) return null;
  const faceUp  = card.faceUp;
  const color   = SUIT_COLOR[card.suit];
  const classes = ["card", faceUp ? "" : "face-down", selected ? "card--selected" : ""]
    .filter(Boolean).join(" ");

  return (
    <div
      className={classes}
      draggable={draggable && faceUp}
      onDragStart={onDragStart}
      onDoubleClick={onDoubleClick}
      onClick={onClick}
      style={style}
      title={faceUp ? RANK_LABEL[card.rank] + SUIT_SYMBOL[card.suit] : ""}
    >
      {faceUp && (
        <React.Fragment>
          <div className={"corner tl " + color}><span className="rank">{RANK_LABEL[card.rank]}</span></div>
          <div className={"corner tr " + color}><span className="suit">{SUIT_SYMBOL[card.suit]}</span></div>
          <div className={"center-suit " + color}>{SUIT_SYMBOL[card.suit]}</div>
          <div className={"corner bl " + color}><span className="suit">{SUIT_SYMBOL[card.suit]}</span></div>
          <div className={"corner br " + color}><span className="rank">{RANK_LABEL[card.rank]}</span></div>
        </React.Fragment>
      )}
    </div>
  );
}

// ── Mount ─────────────────────────────────────────────────────────────────────
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
