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: "hr" }
	| {
			type: "symbol";
			symbol: "coin" | "debt" | "potion" | "vp" | "vp-token" | "sun";
			isBig?: boolean;
			prefix?: string;
			text: string;
			textColor: 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 hrPiece = pieceDef({
	type: "hr",
	measure(context, _piece) {
		const metrics = context.measureText(" ");
		const h =
			(metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent) /
			3;
		return {
			type: "content",
			width: 750,
			ascent: h / 2,
			descent: h / 2,
		};
	},
	render(context, _piece, x, y, measure) {
		context.save();
		context.beginPath();
		context.moveTo(x, y);
		context.lineTo(x + measure.width, y);
		context.lineWidth = 8;
		context.stroke();
		context.restore();
	},
});

const symbolPiece = pieceDef({
	type: "symbol",
	measure(context, piece) {
		context.save();
		const metrics = context.measureText(" ");
		const height =
			metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
		const prefixMetrics = context.measureText(piece.prefix ?? "");
		const coinImage = getImage(piece.symbol);
		context.restore();
		const { isBig } = piece;
		const scale = isBig ? 2.5 : 1;
		return {
			type: "content",
			width:
				scale *
				(prefixMetrics.width +
					coinImage.width * (height / coinImage.height)),
			ascent: scale * metrics.fontBoundingBoxAscent,
			descent: scale * metrics.fontBoundingBoxDescent,
			prefixWidth: scale * prefixMetrics.width,
			scale,
		};
	},
	render(context, piece, x, y, measure) {
		if (piece.isBig) {
			console.log("big", piece, measure);
		}
		context.save();
		// context.fillStyle = "yellow";
		const height = measure.ascent + measure.descent;
		// context.fillRect(x, y - measure.ascent, measure.width, height);
		context.drawImage(
			getImage(piece.symbol),
			x + measure.prefixWidth,
			y - measure.ascent,
			measure.width - measure.prefixWidth,
			height
		);

		context.save();
		const prefixFontInfo = parseFont(context.font);
		prefixFontInfo.weight = "bold";
		prefixFontInfo.size =
			parseInt(prefixFontInfo.size.toString()) * measure.scale;
		const prefixFont = stringifyFont(prefixFontInfo);
		context.font = prefixFont;
		context.fillText(piece.prefix ?? "", x, y);
		context.restore();

		const fontInfo = parseFont(context.font);
		fontInfo.family = ["DominionSpecial"];
		fontInfo.weight = "bold";
		fontInfo.size =
			parseInt(fontInfo.size.toString()) * 1.2 * measure.scale;
		const font = stringifyFont(fontInfo);
		context.font = font;
		context.fillStyle = piece.textColor;
		context.textAlign = "center";
		context.fillText(
			piece.text,
			x + measure.prefixWidth + (measure.width - measure.prefixWidth) / 2,
			y
		);
		context.restore();
	},
});

const pieceDefs = [textPiece, spacePiece, breakPiece, symbolPiece, hrPiece];

// deno-lint-ignore no-explicit-any
const tools: PieceTools = {} as any;

const measurePiece = (context: CanvasRenderingContext2D, piece: Piece) => {
	const def = pieceDefs.find((def) => def.type === piece.type)!;
	// deno-lint-ignore no-explicit-any
	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)!;
	// deno-lint-ignore no-explicit-any
	const measure = def.measure(context, piece as any, tools);
	// deno-lint-ignore no-explicit-any
	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),
		}))
	);
	let 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
				);
			}
		}
	}
	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;
	});
	return {
		lines,
		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,
	options?: { isDescription: boolean }
): Piece[] => {
	const { isDescription = false } = options ?? {};
	const pieces: Piece[] = [];
	const symbolMap = {
		"$": { symbol: "coin", textColor: "black" },
		"@": { symbol: "debt", textColor: "white" },
		"^": { symbol: "potion", textColor: "white" },
		"%": { symbol: "vp", textColor: "white" },
		"#": { symbol: "vp-token", textColor: "black" },
		"*": { symbol: "sun", textColor: "black" },
	} as const;
	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 in symbolMap) {
			const c = char as keyof typeof symbolMap;
			const end = text.slice(i).match(new RegExp(`\\${c}[^ \n.,;]*`))![0]
				.length;
			const isBig =
				isDescription &&
				["\n", undefined].includes(text[i - 1]) &&
				["\n", undefined].includes(text[i + end]);
			pieces.push({
				type: "symbol",
				...symbolMap[c],
				text: text.slice(i + 1, i + end),
				isBig,
			});
			i += end - 1;
		} else if (char === "+") {
			const match = text.slice(i).match(/\+\d*( \w+)?/);
			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 if (
			char === "-" &&
			text[i - 1] === "\n" &&
			text[i + 1] === "\n"
		) {
			pieces.push({ type: "hr" });
		} else if (/\d/.test(char)) {
			const match = text.slice(i).match(
				new RegExp(
					`\\d+(${Object.keys(symbolMap)
						.map((s) => `\\${s}`)
						.join("|")})`
				)
			);
			if (match) {
				const end = match[0].length;
				const symbolChar = match[1] as keyof typeof symbolMap;
				const isBig =
					isDescription &&
					["\n", undefined].includes(text[i - 1]) &&
					["\n", undefined].includes(text[i + end]);
				pieces.push({
					type: "symbol",
					...symbolMap[symbolChar],
					prefix: text.slice(i, i + end - 1),
					text: "",
					isBig,
				});
				i += end - 1;
			} else {
				const end = text.slice(i).match(/\d+/)![0].length;
				pieces.push({ type: "text", text: text.slice(i, i + end) });
				i += end - 1;
			}
		} else {
			const end = text.slice(i).match(
				new RegExp(
					`[^${Object.keys(symbolMap)
						.map((s) => `\\${s}`)
						.join("")} \n]+`
				)
			)![0].length;
			pieces.push({ type: "text", text: text.slice(i, i + end) });
			i += end - 1;
		}
	}
	return pieces;
};
