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

import { Guessing } from "./guesser/guesser.ts";
import { Gaming, GuessResult } from "./game/game.ts";
import { last, wait, zip } from "./utils.ts";
import {
	tip,
	top,
} from "https://git.barnulf.net/mb/profiterole/raw/commit/02d19fce2a0878abd176801cf8f2a663f6db6c16/mod.ts";

export class Runner<Ga extends Gaming, Gu extends Guessing> {
	game;
	guesser;
	delay_ms;
	turns;
	logging;
	max;

	constructor(game: Ga, guesser: Gu, logging: LoggingStrategy, delay_ms = 0, max: number | undefined = undefined) {
		this.game = game;
		this.guesser = guesser;
		this.delay_ms = delay_ms;
		this.turns = [] as Turn[];
		this.logging = logging;
		this.max = max;
	}

	async play_once() {
		tip("play_once");
		const res = await this.guesser.guess(async (guess, ...properties) => {
			const result = await this.game.guess(guess);
			this.turns.push({ guess, result });
			this.logging.on_guess(guess, result, ...properties);
			return result;
		});
		top("play_once");
		return res;
	}

	async play_all() {
		this.logging.on_start(this.game.length(), ...this.guesser.declare_properties());
		while (true) {
			const result = await this.play_once();
			if (result !== null) break;
			if (this.max !== undefined) { if (this.turns.length >= this.max) return this.turns; }
			await wait(this.delay_ms);
		}
		this.logging.on_finish(this.turns);
		return this.turns;
	}
}

type Turn = { guess: string; result: GuessResult };

function format_result(result: GuessResult) {
	if (result.kind === "success") return "success";
	let line = "";
	for (const info of result.informations) {
		if (info.kind === "abscent") line += ".";
		if (info.kind === "somewhere") line += "*";
		if (info.kind === "there") line += "#";
	}
	return line;
}

export interface LoggingStrategy {
	on_start(length: number, ...properties: string[]): void;
	on_guess(guess: string, result: GuessResult, ...properties: unknown[]): void;
	on_finish(turns: Turn[]): void;
}

export class VerboseLogging implements LoggingStrategy {
	properties;
	length;

	constructor() {
		this.length = 0;
		this.properties = [] as string[];
	}

	on_start(_length: number, ...properties: string[]) {
		this.length = Math.max(8, ...properties.map((p) => p.length));
		this.properties = properties;
	}

	on_guess(guess: string, result: GuessResult, ...properties: unknown[]): void {
		for (const [name, value] of zip(this.properties, properties)) this.log_entry(name, value);
		this.log_entry("Guessing", guess);
		this.log_entry("Got", format_result(result));
		console.log();
	}

	on_finish(turns: Turn[]): void {
		const last_ = last(turns);
		assertExists(last_);
		console.log("Stopped on", last_.guess, ".");
		console.log("With", last_.result.kind, ".");
		console.log("In", turns.length, "steps");
	}

	log_entry(name: string, value: unknown) {
		console.log(name.padStart(this.length) + ": " + value);
	}
}

export class TableLogging implements LoggingStrategy {
	columns;

	constructor() {
		this.columns = [] as (readonly [string, number])[];
	}

	on_start(length: number, ...properties: string[]): void {
		this.columns = properties.map((p) => [p, p.length] as const);
		this.columns.splice(0, 0, ["guess", length], ["result", Math.max(7, length)]);
		let line = "";
		for (const [name, width] of this.columns) line += name.padStart(width) + "  ";
		console.log(line);
		console.log();
	}

	on_guess(guess: string, result: GuessResult, ...properties: unknown[]): void {
		const properties_ = [guess, format_result(result), ...properties];
		let line = "";
		for (const [[_, width], value] of zip(this.columns, properties_)) line += `${value}`.padStart(width) + "  ";
		console.log(line);
	}

	on_finish(turns: Turn[]): void {
		const last_ = last(turns);
		assertExists(last_);
		console.log();
		console.log(last_.result.kind, "in", turns.length, "steps");
	}
}

export class GameLogging implements LoggingStrategy {
	on_finish(turns: Turn[]): void {
		const last_ = last(turns);
		assertExists(last_);
		console.log();
		console.log(last_.result.kind, "in", turns.length, "steps");
	}

	on_guess(_guess: string, result: GuessResult, ..._properties: unknown[]): void {
		console.log(this.result_to_display(result));
	}

	on_start(_length: number, ..._properties: string[]): void {
	}

	result_to_display(result: GuessResult) {
		if (result.kind === "success") return "  success";

		let line = "  ";
		for (const info of result.informations) {
			if (info.kind === "abscent") line += ".";
			if (info.kind === "somewhere") line += "-";
			if (info.kind === "there") line += "+";
		}
		return line;
	}
}
