import { assertExists } from "https://deno.land/std@0.224.0/assert/assert_exists.ts";

import { Awaitable, enumerate, range } from "./utils.ts";
import { Dict } from "./dict.ts";
import { GuessResult } from "./game/game.ts";

export interface Guessing {
	guess(try_: (guess: string, known: string) => Awaitable<GuessResult>): Promise<GuessResult | null>;
}

type Knowledge = {
	letter: string;
	at: Set<number>;
	exists: "unknown" | boolean;
	not_at: Set<number>;
};

export class Guesser implements Guessing {
	length;
	dict;
	informations;

	constructor(dict: Dict) {
		this.length = dict.length;
		this.dict = dict;
		this.informations = new Map<string, Knowledge>();
		for (const letter of dict.letters.values()) {
			this.informations.set(letter, { letter, at: new Set(), not_at: new Set(), exists: "unknown" });
		}
	}

	async guess(try_: (guess: string, known: string) => Awaitable<GuessResult>) {
		const res = this.expected_result();
		if (res.completed) return await try_(res.result, res.result);

		const words = [...this.dict.words.values()];
		const scored = words.map((word) => [this.info_score_for(word), word] as const);
		const best_score = scored.reduce((acc, [next]) => Math.max(acc, next), 0);
		const bests = scored.filter(([s]) => s === best_score).map(([_, word]) => word);
		const guess = pick_random_common_word(bests);

		const result = await try_(guess, res.result);
		if (result.kind === "success") return result;
		for (const [index, info] of enumerate(result.informations)) {
			const letter = guess[index];
			if (info.kind === "there") this.learn_letter_at(letter, index);
			if (info.kind === "abscent") this.learn_does_not_exist(letter);
			if (info.kind === "somewhere") {
				this.learn_does_exist(letter);
				this.learn_letter_not_at(letter, index);
			}
		}
		return null;
	}

	learn_does_not_exist(letter: string) {
		const letter_info = this.informations.get(letter);
		assertExists(letter_info);
		letter_info.exists = false;
	}

	learn_does_exist(letter: string) {
		const letter_info = this.informations.get(letter);
		assertExists(letter_info);
		letter_info.exists = true;
		const total_known = [...this.informations.values()].filter((i) => i.exists === true).length;
		if (total_known < this.length) return;
		for (const info of this.informations.values()) if (info.exists === "unknown") info.exists = false;
	}

	learn_letter_at(letter: string, at: number) {
		const letter_info = this.informations.get(letter);
		assertExists(letter_info);
		letter_info.at.add(at);
		for (const l of this.informations.keys()) {
			if (letter === l) continue;
			this.learn_letter_not_at(l, at);
		}
	}

	learn_letter_not_at(letter: string, at: number) {
		const letter_info = this.informations.get(letter);
		assertExists(letter_info);
		letter_info.not_at.add(at);
	}

	info_score_for(word: string) {
		let total = 0;
		for (const [index, letter] of enumerate(word)) {
			const information = this.informations.get(letter);
			assertExists(information);
			if (information.exists === false) continue;
			if (information.at.has(index)) continue;
			if (information.not_at.has(index)) continue;
			if (information.exists === "unknown") total += this.length;
			if (information.exists === true) total += this.length * this.length;
		}
		const different_letters = new Set([...word]);
		total += different_letters.size;
		return total;
	}

	expected_result() {
		const known_arr = [...range(0, this.length)].map(() => null as string | null);
		for (const [letter, info] of this.informations.entries()) {
			for (const pos of info.at.values()) known_arr[pos] = letter;
		}
		const result = known_arr.map((l) => l === null ? "." : l).join("");
		const completed = !result.includes(".");
		return { completed, result };
	}
}

function pick_random_common_word(bests: string[]) {
	const index = Math.floor(Math.random() * bests.length);
	return bests[index];
}
