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, dbg, enumerate, first, range, zip } from "../utils.ts";
import { Guessing, pick_random_common_word } from "./guesser.ts";
import {
	section,
	tip,
	top,
} from "https://git.barnulf.net/mb/profiterole/raw/commit/02d19fce2a0878abd176801cf8f2a663f6db6c16/mod.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>) {
		return await section("guess", async () => {
			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);
			if (score >= this.candidates.size) {
				const pick = pick_random_common_word([...this.candidates.values()]);
				assertExists(pick);
				const result = await try_(pick, this.candidates.size, score);
				if (result.kind === "success") return result;
				else return null;
			}
			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);
		}
		const new_candidates = new Set(next_cnd.keys());
		if (new_candidates.size === this.candidates.size) console.log({ new_candidates });
		this.candidates = new_candidates;
	}

	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>) {
	tip("word_cuts");
	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);
		}
	}
	top("word_cuts");
	return results;
}

function get_word_with_smallest_cuts(candidates: Set<string>, dict: Set<string>) {
	return section("get_word_with_smallest_cuts", () => {
		let best = null as null | [string, number];
		for (const candidate of dict.values()) {
			const cuts = word_cuts(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;
	});
}

function matches_constraints(candidate: string, constraints: [string, Info][]) {
	const letters = [...candidate];

	for (const [index, [letter, info]] of enumerate(constraints)) {
		if (info.kind !== "there") continue;
		if (letters[index] !== letter) return dbg(false, "a", index);
		letters[index] = ".";
	}

	for (const [index, [letter, info]] of enumerate(constraints)) {
		if (info.kind === "there") continue;
		if (info.kind === "abscent") {
			if (letters.includes(letter)) return dbg(false, "b", index);
			continue;
		}
		// is somewhere
		if (letters[index] === letter) return dbg(false, "c", index);
		if (!letters.includes(letter)) return dbg(false, "d", index);
		const index_ = letters.findIndex((i) => i === letter);
		letters[index_] = ".";
	}

	return true;
}

Deno.test("test_matches", () => {
	{
		const candidate = "peigne";
		const constraints = [
			["s", { kind: "abscent" }],
			["o", { kind: "abscent" }],
			["i", { kind: "there" }],
			["r", { kind: "abscent" }],
			["e", { kind: "somewhere" }],
			["e", { kind: "there" }],
		] as [string, Info][];
		assertEquals(matches_constraints(candidate, constraints), true);
	}
	{
		const candidate = "peigne";
		const constraints = [
			["g", { kind: "somewhere" }],
			["e", { kind: "there" }],
			["n", { kind: "abscent" }],
			["a", { kind: "abscent" }],
			["n", { kind: "there" }],
			["t", { kind: "abscent" }],
		] as [string, Info][];
		assertEquals(matches_constraints(candidate, constraints), true);
	}
	{
		const candidate = "arbre";
		const constraints = [
			["r", { kind: "somewhere" }],
			["e", { kind: "somewhere" }],
			["s", { kind: "abscent" }],
			["t", { kind: "abscent" }],
			["a", { kind: "somewhere" }],
		] as [string, Info][];
		assertEquals(matches_constraints(candidate, constraints), true);
	}
	{
		const candidate = "poncif";
		const constraints = [
			["p", { kind: "there" }],
			["a", { kind: "abscent" }],
			["c", { kind: "somewhere" }],
			["i", { kind: "somewhere" }],
			["n", { kind: "somewhere" }],
			["o", { kind: "somewhere" }],
		] as [string, Info][];
		assertEquals(matches_constraints(candidate, constraints), true);
	}
	{
		const candidate = "piteux";
		const constraints = [
			["s", { kind: "abscent" }],
			["o", { kind: "abscent" }],
			["i", { kind: "somewhere" }],
			["r", { kind: "abscent" }],
			["e", { kind: "somewhere" }],
			["e", { kind: "abscent" }],
		] as [string, Info][];
		assertEquals(matches_constraints(candidate, constraints), true);
	}
	{
		const candidate = "piteux";
		const constraints = [
			["l", { kind: "abscent" }],
			["a", { kind: "abscent" }],
			["t", { kind: "there" }],
			["i", { kind: "somewhere" }],
			["n", { kind: "abscent" }],
			["e", { kind: "somewhere" }],
		] as [string, Info][];
		assertEquals(matches_constraints(candidate, constraints), true);
	}
});
