import { h, JSX, Fragment } 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 { Row, RowState } from './Row';
import { Keyboard } from './Keyboard';
import { useLocalStorage } from './localstorage';
import { PuzzleType, PuzzleTypeDisplay } from './PuzzleType';

const MAX_GUESSES = 2;
const WORD_LENGTH = 5;

enum GameState {
    Playing,
    Won,
    Lost,
}

type History = GuessResult[];


type Puzzle = {
    history: History,
    seed: number,
    solutions: number,
}

interface GameProps {
    puzzleType: PuzzleType;
    puzzle: Puzzle;
    day: number;
    hidden: boolean;
    colorBlind: boolean;
    keyboardLayout: string;
}


type PuzzleStats = {
    tries: number,
    won: boolean,
    guesses?: GuessResult[],
};

function defaultPuzzleStats() {
    return { tries: 1, won: false };
}

type DailyStats = {
    [key in PuzzleType]: PuzzleStats
};

export function Game(props: GameProps) {
    const { puzzleType, puzzle, day } = props;

    const [gameState, setGameState] = useState(GameState.Playing);
    const [guesses, setGuesses] = useState<GuessResult[]>([]);
    const [currentGuess, setCurrentGuess] = useState<string>("");
    const [dailyStats, setDailyStats] = useLocalStorage<{ [key: number]: DailyStats }>("dailyStats", {});
    const [todayStats, setTodayStats] = useState<DailyStats>({
        2: defaultPuzzleStats(),
        3: defaultPuzzleStats(),
        4: defaultPuzzleStats(),
    });
    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),
        [puzzle]
    );
    const [gameTree, setGameTree] = useState<GuessResult[][]>();
    const reset = useCallback(() => {
        game.reset();
        setGuesses([...puzzle.history]);
        setCurrentGuess("");
        setGameState(GameState.Playing);
        setTodayStats((oldStats) => ({
            ...oldStats,
            [puzzleType]: {
                ...oldStats[puzzleType],
                guesses: [],
            },
        }));
    }, [puzzle]);

    useEffect(() => {
        const selectedPuzzle = todayStats[puzzleType];
        setGuesses([...puzzle.history, ...(selectedPuzzle.guesses || [])]);
        game.reset();
        setGameTree(undefined);
        selectedPuzzle.guesses?.forEach(
            (result) => game.guess(result.word)
        );
        if (selectedPuzzle.won) {
            setGameState(GameState.Won);
            if (selectedPuzzle.tries === 1) {
                setHint(<>
                    You won today's <PuzzleTypeDisplay type={puzzleType} />{' '}
                    puzzle on your first try! Amazing job.
                </>);
            } else {
                setHint(<>
                    You won today's <PuzzleTypeDisplay type={puzzleType} />{' '}
                    puzzle in {selectedPuzzle.tries} tries! Great job.
                </>);
            }
        } else if ((selectedPuzzle.guesses?.length || 0) >= 2) {
            setGameState(GameState.Lost);
            setHint(<p>Not quite! The answer could've been <strong>{game.possible_word().toUpperCase()}</strong>. (Enter to try again)</p>);
        } else {
            setGameState(GameState.Playing);
            setHint("");
        }
    }, [puzzle, puzzleType, todayStats]);

    useEffect(() => {
        const storedTodayStats = dailyStats[day];
        if (storedTodayStats !== undefined) {
            setTodayStats(storedTodayStats);
        }
    }, []);
    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]));
            setTodayStats((oldStats) => ({
                ...oldStats,
                [puzzleType]: {
                    ...oldStats[puzzleType],
                    guesses: [
                        ...(oldStats[puzzleType].guesses || []),
                        result,
                    ],
                },
            }));
            setCurrentGuess("");

            if (matches.every((m) => m === 2)) {
                setTodayStats((oldStats) => ({
                    ...oldStats,
                    [puzzleType]: {
                        ...oldStats[puzzleType],
                        won: true,
                        guesses: [
                            ...(oldStats[puzzleType].guesses || []),
                            result,
                        ],
                    },
                }));
            } else if (guesses.length + 1 === puzzle.history.length + MAX_GUESSES) {
                setTodayStats((oldStats) => ({
                    ...oldStats,
                    [puzzleType]: {
                        ...oldStats[puzzleType],
                        tries: oldStats[puzzleType].tries + 1,
                    },
                }));
            } 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[puzzleType].tries === 1
                                    ? "my first try!"
                                    : `try #${todayStats[puzzleType].tries}!`
                                );
                                share(
                                    "Result copied to clipboard!",
                                    `I solved ${gameName} #${puzzle.seed} (${puzzleType}🟩) 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>
    );
}
