import { getImage } from "./draw.ts";
import { parseFont, stringifyFont } from "./fonthelper.ts";

export type Piece =
	| { type: "text"; text: string; isBold?: boolean; isItalic?: boolean }
	| { type: "space" }
	| { type: "break" }
	| { type: "coin"; text: string }
	| { type: "debt"; text: string }
	| { type: "potion"; text: string };

type PromiseOr<T> = T | Promise<T>;

type PieceMeasure = {
	type: "content" | "space" | "break";
	width: number;
	ascent: number;
	descent: number;
};

type Line = {
	pieces: {
		piece: Piece;
		measure: PieceMeasure;
		xOffset: number;
	}[];
	width: number;
	ascent: number;
	descent: number;
};

type PieceTools = {
	measurePiece: (
		context: CanvasRenderingContext2D,
		piece: Piece
	) => PromiseOr<PieceMeasure>;
	renderPiece: (
		context: CanvasRenderingContext2D,
		piece: Piece,
		x: number,
		y: number
	) => PromiseOr<void>;
};

type PieceDef<T extends Piece["type"], M extends PieceMeasure> = {
	type: T;
	measure(
		context: CanvasRenderingContext2D,
		piece: Piece & { type: T },
		tools: PieceTools
	): PromiseOr<M>;
	render(
		context: CanvasRenderingContext2D,
		piece: Piece & { type: T },
		x: number,
		y: number,
		measure: NoInfer<M>,
		tools: PieceTools
	): PromiseOr<void>;
};

const pieceDef = <T extends Piece["type"], M extends PieceMeasure>(
	def: PieceDef<T, M>
) => {
	return def;
};

const textPiece = pieceDef({
	type: "text",
	measure(context, piece) {
		context.save();
		const fontInfo = parseFont(context.font);
		if (piece.isBold) {
			fontInfo.weight = "bold";
		}
		if (piece.isItalic) {
			fontInfo.style = "italic";
		}
		const font = stringifyFont(fontInfo);
		context.font = font;
		const metrics = context.measureText(piece.text);
		context.restore();
		return {
			type: "content",
			width: metrics.width,
			ascent: metrics.fontBoundingBoxAscent,
			descent: metrics.fontBoundingBoxDescent,
			font,
		};
	},
	render(context, piece, x, y, measure) {
		context.save();
		context.font = measure.font;
		context.fillText(piece.text, x, y);
		context.restore();
	},
});

const spacePiece = pieceDef({
	type: "space",
	measure(context, _piece) {
		const metrics = context.measureText(" ");
		return {
			type: "space",
			width: metrics.width,
			ascent: metrics.fontBoundingBoxAscent,
			descent: metrics.fontBoundingBoxDescent,
		};
	},
	render() {},
});

const breakPiece = pieceDef({
	type: "break",
	measure(context, _piece) {
		const metrics = context.measureText(" ");
		return {
			type: "break",
			width: 0,
			ascent: metrics.fontBoundingBoxAscent / 3,
			descent: metrics.fontBoundingBoxDescent / 3,
		};
	},
	render() {},
});

const coinPiece = pieceDef({
	type: "coin",
	measure(context, _piece) {
		context.save();
		const metrics = context.measureText(" ");
		const height =
			metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
		const coinImage = getImage("coin");
		context.restore();
		return {
			type: "content",
			width: coinImage.width * (height / coinImage.height),
			ascent: metrics.fontBoundingBoxAscent,
			descent: metrics.fontBoundingBoxDescent,
		};
	},
	render(context, piece, x, y, measure) {
		context.save();
		// context.fillStyle = "yellow";
		const height = measure.ascent + measure.descent;
		// context.fillRect(x, y - measure.ascent, measure.width, height);
		context.drawImage(
			getImage("coin"),
			x,
			y - measure.ascent,
			measure.width,
			height
		);
		const fontInfo = parseFont(context.font);
		fontInfo.family = ["DominionSpecial"];
		fontInfo.weight = "bold";
		fontInfo.size = parseInt(fontInfo.size.toString()) * 1.2;
		const font = stringifyFont(fontInfo);
		context.font = font;
		context.fillStyle = "black";
		context.textAlign = "center";
		context.fillText(piece.text, x + measure.width / 2, y);
		context.restore();
	},
});

const debtPiece = pieceDef({
	type: "debt",
	measure(context, _piece) {
		context.save();
		const metrics = context.measureText(" ");
		const height =
			metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
		const coinImage = getImage("debt");
		context.restore();
		return {
			type: "content",
			width: coinImage.width * (height / coinImage.height),
			ascent: metrics.fontBoundingBoxAscent,
			descent: metrics.fontBoundingBoxDescent,
		};
	},
	render(context, piece, x, y, measure) {
		context.save();
		// context.fillStyle = "yellow";
		const height = measure.ascent + measure.descent;
		// context.fillRect(x, y - measure.ascent, measure.width, height);
		context.drawImage(
			getImage("debt"),
			x,
			y - measure.ascent,
			measure.width,
			height
		);
		const fontInfo = parseFont(context.font);
		fontInfo.family = ["DominionSpecial"];
		fontInfo.weight = "bold";
		fontInfo.size = parseInt(fontInfo.size.toString()) * 1.2;
		const font = stringifyFont(fontInfo);
		context.font = font;
		context.fillStyle = "white";
		context.textAlign = "center";
		context.fillText(piece.text, x + measure.width / 2, y);
		context.restore();
	},
});

const potionPiece = pieceDef({
	type: "potion",
	measure(context, _piece) {
		context.save();
		const metrics = context.measureText(" ");
		const height =
			metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
		const coinImage = getImage("potion");
		context.restore();
		return {
			type: "content",
			width: coinImage.width * (height / coinImage.height),
			ascent: metrics.fontBoundingBoxAscent,
			descent: metrics.fontBoundingBoxDescent,
		};
	},
	render(context, piece, x, y, measure) {
		context.save();
		// context.fillStyle = "yellow";
		const height = measure.ascent + measure.descent;
		// context.fillRect(x, y - measure.ascent, measure.width, height);
		context.drawImage(
			getImage("potion"),
			x,
			y - measure.ascent,
			measure.width,
			height
		);
		const fontInfo = parseFont(context.font);
		fontInfo.family = ["DominionSpecial"];
		fontInfo.weight = "bold";
		fontInfo.size = parseInt(fontInfo.size.toString()) * 1.2;
		const font = stringifyFont(fontInfo);
		context.font = font;
		context.fillStyle = "white";
		context.textAlign = "center";
		context.fillText(piece.text, x + measure.width / 2, y);
		context.restore();
	},
});

const pieceDefs = [
	textPiece,
	spacePiece,
	breakPiece,
	coinPiece,
	debtPiece,
	potionPiece,
];

const tools: PieceTools = {} as any;

const measurePiece = (context: CanvasRenderingContext2D, piece: Piece) => {
	const def = pieceDefs.find((def) => def.type === piece.type)!;
	return def.measure(context, piece as any, tools);
};

const renderPiece = (
	context: CanvasRenderingContext2D,
	piece: Piece,
	x: number,
	y: number
) => {
	const def = pieceDefs.find((def) => def.type === piece.type)!;
	const measure = def.measure(context, piece as any, tools);
	return def.render(context, piece as any, x, y, measure as any, tools);
};

tools.measurePiece = measurePiece;
tools.renderPiece = renderPiece;

type DominionFont = {
	font: "text" | "title";
	size: number;
	isBold: boolean;
	isItalic: boolean;
};

type PieceWithInfo = {
	piece: Piece;
	measure: PieceMeasure;
};

export const measureDominionText = async (
	context: CanvasRenderingContext2D,
	pieces: Piece[],
	maxWidth = Infinity
) => {
	const data: PieceWithInfo[] = await Promise.all(
		pieces.map(async (piece) => ({
			piece,
			measure: await measurePiece(context, piece),
		}))
	);
	const lines: Line[] = [{ pieces: [], width: 0, ascent: 0, descent: 0 }];
	for (const pieceInfo of data) {
		const line = lines[lines.length - 1]!;
		if (pieceInfo.measure.type === "break") {
			line.pieces.push({ ...pieceInfo, xOffset: line.width });
			line.width += pieceInfo.measure.width;
			line.ascent = Math.max(line.ascent, pieceInfo.measure.ascent);
			line.descent = Math.max(line.descent, pieceInfo.measure.descent);
			lines.push({ pieces: [], width: 0, ascent: 0, descent: 0 });
		} else {
			if (line.width + pieceInfo.measure.width > maxWidth) {
				lines.push({
					pieces: [{ ...pieceInfo, xOffset: 0 }],
					width: pieceInfo.measure.width,
					ascent: pieceInfo.measure.ascent,
					descent: pieceInfo.measure.descent,
				});
			} else {
				line.pieces.push({
					...pieceInfo,
					xOffset: line.width,
				});
				line.width += pieceInfo.measure.width;
				line.ascent = Math.max(line.ascent, pieceInfo.measure.ascent);
				line.descent = Math.max(
					line.descent,
					pieceInfo.measure.descent
				);
			}
		}
	}
	return {
		lines: lines.map((line) => {
			while (
				line.pieces[line.pieces.length - 1] &&
				line.pieces[line.pieces.length - 1]!.measure.type === "space"
			) {
				line.pieces = line.pieces.slice(0, -1);
			}
			line.width = line.pieces
				.map((piece) => piece.measure.width)
				.reduce((a, b) => a + b, 0);
			return line;
		}),
		width: Math.max(...lines.map((line) => line.width)),
		height: lines
			.map((line) => line.ascent + line.descent)
			.reduce((a, b) => a + b, 0),
	};
};

const debug = false;

export const renderDominionText = async (
	context: CanvasRenderingContext2D,
	pieces: Piece[],
	x: number,
	y: number,
	maxWidth = Infinity
) => {
	const { lines, height } = await measureDominionText(
		context,
		pieces,
		maxWidth
	);
	let yOffset = 0;
	for (const line of lines) {
		yOffset += line.ascent;
		for (const { piece, xOffset } of line.pieces) {
			await renderPiece(
				context,
				piece,
				x - line.width / 2 + xOffset,
				y - height / 2 + yOffset
			);
			if (debug) {
				context.save();
				context.strokeStyle = "blue";
				context.lineWidth = 5;
				const pieceMeasure = await measurePiece(context, piece);
				context.strokeRect(
					x - line.width / 2 + xOffset,
					y - height / 2 - line.ascent + yOffset,
					pieceMeasure.width,
					pieceMeasure.ascent + pieceMeasure.descent
				);
				context.strokeStyle = "red";
				context.beginPath();
				context.moveTo(
					x - line.width / 2 + xOffset - 5,
					y - height / 2 + yOffset
				);
				context.lineTo(
					x - line.width / 2 + xOffset + 5,
					y - height / 2 + yOffset
				);
				context.stroke();
				context.restore();
			}
		}
		yOffset += line.descent;
	}
};

export const parse = (text: string): Piece[] => {
	const pieces: Piece[] = [];
	for (let i = 0; i < text.length; i++) {
		const char = text[i];
		if (char === " ") {
			pieces.push({ type: "space" });
		} else if (char === "\n") {
			pieces.push({ type: "break" });
		} else if (char === "$") {
			const end = text.slice(i).match(/\$\d*/)![0].length;
			pieces.push({ type: "coin", text: text.slice(i + 1, i + end) });
			i += end - 1;
		} else if (char === "@") {
			const end = text.slice(i).match(/@\d*/)![0].length;
			pieces.push({ type: "debt", text: text.slice(i + 1, i + end) });
			i += end - 1;
		} else if (char === "^") {
			const end = text.slice(i).match(/\^\d*/)![0].length;
			pieces.push({ type: "potion", text: text.slice(i + 1, i + end) });
			i += end - 1;
		} else if (char === "+") {
			const match = text.slice(i).match(/\+\d* \S+/);
			if (match) {
				const end = match[0].length;
				pieces.push({
					type: "text",
					isBold: true,
					text: text.slice(i, i + end),
				});
				i += end - 1;
			} else {
				pieces.push({
					type: "text",
					isBold: true,
					text: "+",
				});
			}
		} else {
			const end = text.slice(i).match(/[^$ \n]+/)![0].length;
			pieces.push({ type: "text", text: text.slice(i, i + end) });
			i += end - 1;
		}
	}
	return pieces;
};