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

import { Dict } from "../dict.ts";
import { GuessResult, Info } from "../game/game.ts";
import { info_of_guess } from "../game/simulator.ts";
import { Awaitable, enumerate, first, range, zip } from "../utils.ts";
import { Guessing } from "./guesser.ts";

export class ReducingGuesser implements Guessing {
	length;
	words;
	candidates;

	public constructor(words: Dict) {
		this.words = words.words;
		this.length = words.length;
		this.candidates = new Set(this.words.values());
	}

	public declare_properties() {
		return ["candidates", "best score"];
	}

	public async guess(try_: (guess: string, candidates: number, score: number) => Awaitable<GuessResult>) {
		if (this.candidates.size === 1) return await try_(first(this.candidates)!, this.candidates.size, 1);
		const [guess, score] = get_word_with_smallest_cuts(this.candidates, this.words);
		const result = await try_(guess, this.candidates.size, score);
		if (result.kind === "success") return result;
		this.learn(guess, result.informations);
		return null;
	}

	learn(word: string, infos: Info[]) {
		let next_cnd = new Map([...this.candidates.values()].map((value) => [value, value]));
		// TODO : should treat all there before somewheres
		for (const [i, [info, c]] of enumerate(zip(infos, word))) {
			if (info.kind === "abscent") next_cnd = filter_map(next_cnd, (_, r) => !r.includes(c));
			if (info.kind === "somewhere") next_cnd = filter_map(next_cnd, (_, r) => r.includes(c) ? consume(r, c) : null);
			if (info.kind === "there") next_cnd = filter_map(next_cnd, (_, r) => r[i] === c ? consume(r, c) : null);
		}
		this.candidates = new Set(next_cnd.keys());
	}

	restraint_candidates(constraints: [string, Info][]) {
		const new_cand = new Set<string>();
		for (const cand of this.candidates.values()) if (matches_constraints(cand, constraints)) new_cand.add(cand);
		return new_cand;
	}
}

function filter_map<K, T>(
	map: Map<K, T>,
	predicate: (k: K, v: T) => boolean | null | T,
) {
	const new_set = new Map<K, T>();
	for (const [key, value] of map.entries()) {
		const result = predicate(key, value);
		if (result === null) continue;
		if (result === false) continue;
		if (result === true) new_set.set(key, value);
		else new_set.set(key, result);
	}
	return new_set;
}

function consume(word: string, letter: string, at: number | null = null, replace = ".") {
	if (at !== null) {
		if (word[at] !== letter) return word;
		else return word.slice(0, at) + replace + word.slice(at + 1);
	}
	return word.replace(letter, replace);
}

/*
note : The algorithm must proceed as follow :

1. establish a list of possible words
2. loop :
	2.1 for each word, for each letter, estimate by how much this letter cuts possibilities for all three possible outcomes.
	(2.1-bis weigts those scores by the frequency of the word ?)
	2.2 play the word which individual letters cuts most of the possible space.
 */

function word_cuts(word: string, dict: Set<string>) {
	const result = [] as [Set<string>, Set<string>, Set<string>][];
	const valued = [...dict.values()].map((value) => ({ value, rest: value }));
	// note : first pass to detect corrects.
	for (const [index, letter] of enumerate(word)) {
		const [theres, somewheres, abscents] = [new Set<string>(), new Set<string>(), new Set<string>()];
		for (const option of valued) {
			if (option.rest[index] === letter) {
				theres.add(option.value);
				option.rest = consume(option.value, letter, index);
			}
			result.push([theres, somewheres, abscents]);
		}
	}
	// note : second pass to detect rest.
	for (const letter of word) {
		for (const [option, [theres, somewheres, abscents]] of zip(valued, result)) {
			if (option.rest.includes(letter)) {
				somewheres.add(option.value);
				option.rest = consume(option.rest, letter);
			} else if (!theres.has(option.value)) abscents.add(option.value);
		}
	}
	return result;
}

function word_cuts_alt(word: string, dict: Set<string>) {
	const results = [...range(0, word.length)]
		.map(() => [new Set<string>(), new Set<string>(), new Set<string>()] as const);
	for (const option of dict) {
		const infos = info_of_guess(word, option);
		for (const [index, info] of enumerate(infos)) {
			if (info.kind === "there") results[index][0].add(option);
			if (info.kind === "somewhere") results[index][1].add(option);
			if (info.kind === "abscent") results[index][2].add(option);
		}
	}
	return results;
}

function get_word_with_smallest_cuts(candidates: Set<string>, dict: Set<string>) {
	let best = null as null | [string, number];
	for (const candidate of dict.values()) {
		const cuts = word_cuts_alt(candidate, candidates);
		let max_part_size = 0;
		for (const cut of cuts) for (const part of cut) if (part.size > max_part_size) max_part_size = part.size;
		if (best === null) best = [candidate, max_part_size];
		else if (max_part_size < best[1]) best = [candidate, max_part_size];
	}
	assertExists(best);
	return best;
}

Deno.test("test_word_cuts", () => {
	{
		const cuts = word_cuts("a", new Set(["a", "b"]));
		assertEquals(cuts, [[new Set(["a"]), new Set([]), new Set(["b"])]]);
	}
	{
		const cuts = word_cuts("aa", new Set(["aa", "ab", "ba", "bb"]));
		assertEquals(cuts, [
			[new Set(["aa", "ab"]), new Set(["ba"]), new Set(["bb"])],
			[new Set(["aa", "ba"]), new Set(["ab"]), new Set(["bb"])],
		]);
	}
});

Deno.test("test_smallest_cuts", () => {
	const [best] = get_word_with_smallest_cuts(
		new Set(["aa", "ab", "ac", "ba", "bc"]),
		new Set(["ab", "ba"]),
	);
	assertEquals(best, "ba");
});

function matches_constraints(cand: string, constraints: [string, Info][]) {
	// TODO
	return false;
}
