import { h, JSX } from 'preact';
import { useState, useRef, useMemo, useCallback, useEffect } from 'preact/hooks';

import * as wasm from 'web-optimle';
import { clue, describeClue, type Clue, type GuessResult } from './clue';
import { gameName, speak } from './util';

import { COMMON_WORDS, UNCOMMON_WORDS } from './dictionary';
import * as PUZZLES from './puzzles.json';
import { Row, RowState } from './Row';
import { Keyboard } from './Keyboard';
import { useLocalStorage } from './localstorage';

const MAX_GUESSES = 2;
const WORD_LENGTH = 5;
const EPOCH = new Date(2022, 1, 27);

enum GameState {
    Playing,
    Won,
    Lost,
}

interface GameProps {
    hidden: boolean;
    colorBlind: boolean;
    keyboardLayout: string;
}

type History = GuessResult[];

type DailyStats = {
    tries: number,
    won: boolean,
    winningGuesses?: GuessResult[],
};

export function Game(props: GameProps) {
    const day = useMemo(() => {
        let date = new Date();
        date.setHours(date.getHours() - 6); // treat 6 AM as the start of the day
        date.setHours(0); // now set it to midnight of the day
        return Math.round((date.getTime() - EPOCH.getTime()) / (24 * 60 * 60 * 1000));
    }, []);
    const puzzle = PUZZLES[day];
    const [gameState, setGameState] = useState(GameState.Playing);
    const [guesses, setGuesses] = useState<GuessResult[]>([...puzzle.history]);
    const [currentGuess, setCurrentGuess] = useState<string>("");
    const [dailyStats, setDailyStats] = useLocalStorage<{ [key: number]: DailyStats }>("dailyTries", {});
    const [todayStats, setTodayStats] = useState<DailyStats>({ tries: 1, won: false });
    const [hint, setHint] = useState<JSX.Element | string>(
        <p>
            Try to guarantee a win in <strong>2 guesses</strong>!
        </p>
    );
    const tableRef = useRef<HTMLTableElement>(null);
    const game = useMemo(() => wasm.Game.new(COMMON_WORDS, puzzle.history), []);
    const [gameTree, setGameTree] = useState<GuessResult[][]>();
    const reset = useCallback(() => {
        game.reset();
        setGuesses([...puzzle.history]);
        setCurrentGuess("");
        setGameState(GameState.Playing);
        setTodayStats((oldStats) => ({
            ...oldStats,
            tries: oldStats.tries + 1,
        }));
    }, []);

    useEffect(() => {
        const storedTodayStats = dailyStats[day];
        if (storedTodayStats !== undefined) {
            setTodayStats(storedTodayStats);
            if (storedTodayStats.won) {
                setGameState(GameState.Won);
                setHint("You already won today's puzzle!");
                if (storedTodayStats.winningGuesses) {
                    setGuesses([
                        ...puzzle.history,
                        ...storedTodayStats.winningGuesses,
                    ]);
                    storedTodayStats.winningGuesses.forEach(
                        (result) => game.guess(result.word)
                    );
                }
            }
        }
    }, []);
    useEffect(() => {
        setDailyStats({
            ...dailyStats,
            [day]: todayStats,
        });
    }, [todayStats]);

    async function share(copiedHint: string, text?: string) {
        const url = window.location.origin + window.location.pathname;
        const body = `${text}\n${url}`;
        if (
            /android|iphone|ipad|ipod|webos/i.test(navigator.userAgent) &&
            !/firefox/i.test(navigator.userAgent)
        ) {
            try {
                await navigator.share({ text: body });
                return;
            } catch (e) {
                console.warn("navigator.share failed:", e);
            }
        }
        try {
            await navigator.clipboard.writeText(body);
            setHint(copiedHint);
            return;
        } catch (e) {
            console.warn("navigator.clipboard.writeText failed:", e);
        }
        setHint(url);
    }

    const onKey = (key: string) => {
        if (gameState !== GameState.Playing) {
            if (key === "Enter" && gameState === GameState.Lost) {
                reset();
            }
            return;
        }
        if (guesses.length === MAX_GUESSES) return;
        if (/^[a-z]$/i.test(key)) {
            // We're about to update the current guess, but
            // we can't rely on the value updating in this render
            const newGuess = currentGuess + key.toLowerCase();
            setCurrentGuess((guess) =>
                (guess + key.toLowerCase()).slice(0, WORD_LENGTH)
            );
            tableRef.current?.focus();
            if (newGuess.length === WORD_LENGTH
                && !COMMON_WORDS.includes(newGuess)
                && UNCOMMON_WORDS.includes(newGuess)
            ) {
                setHint(`(The word ${newGuess.toUpperCase()} isn't in the secret list)`);
            } else {
                setHint("");
            }
        } else if (key === "Backspace") {
            setCurrentGuess((guess) => guess.slice(0, -1));
            setHint("");
        } else if (key === "Enter") {
            if (currentGuess.length !== WORD_LENGTH) {
                setHint("Too short");
                return;
            }
            if (!COMMON_WORDS.includes(currentGuess)
                && !UNCOMMON_WORDS.includes(currentGuess)
            ) {
                setHint("Not a valid word");
                return;
            }
            const matches: number[] = game.guess(currentGuess);
            const result: GuessResult = {
                word: currentGuess,
                matches: matches,
            }
            setGuesses((guesses) => guesses.concat([result]));
            setCurrentGuess("");

            if (matches.every((m) => m === 2)) {
                if (todayStats.tries === 1) {
                    setHint("You won on your first try! Amazing job.");
                } else {
                    setHint(`You won in ${todayStats.tries} tries! Great job.`);
                }
                setGameState(GameState.Won);
                setTodayStats((oldStats) => ({
                    ...oldStats,
                    won: true,
                    winningGuesses: [
                        ...guesses.slice(puzzle.history.length),
                        result,
                    ],
                }))
            } else if (guesses.length + 1 === puzzle.history.length + MAX_GUESSES) {
                setHint(<p>Not quite! The answer could've been <strong>{game.possible_word().toUpperCase()}</strong>. (Enter to try again)</p>);
                setGameState(GameState.Lost);
            } else {
                speak(describeClue(clue(result)));
            }
        }
    };

    useEffect(() => {
        const onKeyDown = (e: KeyboardEvent) => {
            if (!e.ctrlKey && !e.metaKey) {
                onKey(e.key);
            }
            if (e.key === "Backspace") {
                e.preventDefault();
            }
        };
        document.addEventListener("keydown", onKeyDown);
        return () => {
            document.removeEventListener("keydown", onKeyDown);
        };
    }, [currentGuess, gameState]);

    let letterInfo = new Map<string, Clue>();
    const tableRows = Array(puzzle.history.length + MAX_GUESSES)
        .fill(undefined)
        .map((_, i) => {
            const isPartOfPuzzle = i < puzzle.history.length;
            const result = [
                ...guesses,
                { word: currentGuess, matches: [0, 0, 0, 0, 0] },
            ][i] ?? { word: "", matches: [0, 0, 0, 0, 0] };
            const cluedLetters = clue(result);
            const lockedIn = i < guesses.length;
            if (lockedIn) {
                for (const { clue, letter } of cluedLetters) {
                    if (clue === undefined) break;
                    const old = letterInfo.get(letter);
                    if (old === undefined || clue > old) {
                        letterInfo.set(letter, clue);
                    }
                }
            }
            return (
                <Row
                    key={i}
                    wordLength={WORD_LENGTH}
                    rowState={
                        lockedIn
                            ? RowState.LockedIn
                            : i === guesses.length
                                ? RowState.Editing
                                : RowState.Pending
                    }
                    cluedLetters={cluedLetters}
                    isPartOfPuzzle={isPartOfPuzzle}
                />
            );
        });

    return (
        <div className="Game" style={{ display: props.hidden ? "none" : "block" }}>
            <table
                className="Game-rows"
                tabIndex={0}
                aria-label="Table of guesses"
                ref={tableRef}
            >
                <tbody>{tableRows}</tbody>
            </table>
            <p
                role="alert"
                style={{
                    userSelect: "none",
                    whiteSpace: "pre-wrap",
                }}
            >
                {hint || `\u00a0`}
            </p>
            <Keyboard
                layout={props.keyboardLayout}
                letterInfo={letterInfo}
                onKey={onKey}
            />
            <p>
                {gameState === GameState.Won && (
                    <div class="win-buttons">
                        <button
                            onClick={() => {
                                const emoji = props.colorBlind
                                    ? ["⬛", "🟦", "🟧"]
                                    : ["⬛", "🟨", "🟩"];
                                const score = todayStats.tries === 1 ? "my first try!" : `try #${todayStats.tries}!`;
                                share(
                                    "Result copied to clipboard!",
                                    `I solved ${gameName} #${puzzle.seed} on ${score}\n` +
                                    guesses
                                        .slice(puzzle.history.length)
                                        .map((result) =>
                                            clue(result)
                                                .map((c) => emoji[c.clue ?? 0])
                                                .join("")
                                        )
                                        .join("\n")
                                );
                            }}
                        >
                            Share results
                        </button>
                        <button onClick={() => setGameTree(game.explore_game_tree())}>
                            Explore game tree
                        </button>
                    </div>
                )}
            </p>
            {gameTree && (
                <div className="game-tree">
                    <p>Here are all the ways the game could've responded:</p>
                    {gameTree?.map((option) =>
                        <div className="game-tree-option">
                            {option.map((guessResult, i) => {
                                const cluedLetters = clue(guessResult);
                                return (
                                    <Row
                                        key={i}
                                        wordLength={WORD_LENGTH}
                                        rowState={RowState.LockedIn}
                                        cluedLetters={cluedLetters}
                                        isPartOfPuzzle={false}
                                    />
                                );
                            })}
                        </div>
                    )}
                </div>
            )}
            <p>
                Today's puzzle (#{puzzle.seed}) has {puzzle.solutions} path{puzzle.solutions === 1 ? '' : 's'} to a win.
            </p>
        </div>
    );
}