import * as toml from '@std/toml';

type OutputType = 'ghostty' | 'emacs';
type Output = {
	outputType: OutputType;
	dir: string;
};

const USAGE = `
pritty <input_path> <output_type:output_dir>...
`;

function printUsageAndDie() {
	console.log(USAGE);
	Deno.exit(1);
}

type RawPalette = {
	[k: string]: string | RawPalette;
};
type Palette = Record<string, string>;
function flattenPalette(
	input: RawPalette,
	prefix: string = '',
	output: Record<string, string> = {},
) {
	for (const key in input) {
		const value = input[key];
		if (typeof value === 'object') {
			flattenPalette(value, `${prefix}${key}.`, output);
		} else {
			output[`${prefix}${key}`] = value;
		}
	}

	return output;
}

type TableEntryColor = `#${string}`;
type TableEntryRef = `@${string}`;
type TableEntry = TableEntryColor | TableEntryRef;
type Tables = Partial<{
	backgrounds: Partial<{
		primary: TableEntry;
		secondary: TableEntry;
		'status-bar-active': TableEntry;
		'status-bar-inactive': TableEntry;
	}>;
	borders: Partial<{
		default: TableEntry;
		hovered: TableEntry;
		active: TableEntry;
	}>;
	text: Partial<{
		primary: TableEntry;
		muted: TableEntry;
		faint: TableEntry;
		error: TableEntry;
		warning: TableEntry;
		success: TableEntry;
		link: TableEntry;
	}>;
	syntax: Partial<{
		operator: TableEntry;
		import: TableEntry;
		function: TableEntry;
		constant: TableEntry;
		keyword: TableEntry;
		string: TableEntry;
		identifier: TableEntry;
		number: TableEntry;
		macro: TableEntry;
		other: TableEntry;
	}>;
	'terminal-16': Partial<{
		black: TableEntry;
		red: TableEntry;
		green: TableEntry;
		yellow: TableEntry;
		blue: TableEntry;
		magenta: TableEntry;
		cyan: TableEntry;
		white: TableEntry;
		'bright-black': TableEntry;
		'bright-red': TableEntry;
		'bright-green': TableEntry;
		'bright-yellow': TableEntry;
		'bright-blue': TableEntry;
		'bright-magenta': TableEntry;
		'bright-cyan': TableEntry;
		'bright-white': TableEntry;
	}>;
}>;

function get(entry: TableEntry | undefined, palette: Palette) {
	if (entry === undefined) {
		return undefined;
	}

	if (entry.startsWith('@')) {
		const c = palette[entry.substring(1)];
		if (c === undefined) {
			throw new Error(`Undefined reference ${entry}`);
		}

		return c;
	}

	return entry;
}

function bindGet(palette: Palette) {
	return (entry: TableEntry | undefined) => get(entry, palette);
}

async function generateGhostty(
	dir: string,
	name: string,
	variantName: string,
	tables: Tables,
	palette: Palette,
) {
	let output = '';
	const get = bindGet(palette);
	const w = (prop: string, v: string | undefined) => {
		if (v !== undefined) {
			output += `${prop} = ${v}\n`;
		}
	};

	const p = (index: number, v: string | undefined) => {
		if (v !== undefined) {
			output += `palette = ${index}=${v}\n`;
		}
	};

	w('background', get(tables['backgrounds']?.['primary']));
	w('foreground', get(tables['text']?.['primary']));
	w('selection-background', get(tables['backgrounds']?.['selection']));
	w('selection-foreground', get(tables['text']?.['selection']));

	p(0, get(tables['terminal-16']?.['black']));
	p(1, get(tables['terminal-16']?.['red']));
	p(2, get(tables['terminal-16']?.['green']));
	p(3, get(tables['terminal-16']?.['yellow']));
	p(4, get(tables['terminal-16']?.['blue']));
	p(5, get(tables['terminal-16']?.['magenta']));
	p(6, get(tables['terminal-16']?.['cyan']));
	p(7, get(tables['terminal-16']?.['white']));
	p(8, get(tables['terminal-16']?.['bright-black']));
	p(9, get(tables['terminal-16']?.['bright-red']));
	p(10, get(tables['terminal-16']?.['bright-green']));
	p(11, get(tables['terminal-16']?.['bright-yellow']));
	p(12, get(tables['terminal-16']?.['bright-blue']));
	p(13, get(tables['terminal-16']?.['bright-magenta']));
	p(14, get(tables['terminal-16']?.['bright-cyan']));
	p(15, get(tables['terminal-16']?.['bright-white']));

	const path = `${dir}/${name}-${variantName}.ghostty`;
	await Deno.writeTextFile(path, output);
}

async function generateEmacs(
	dir: string,
	name: string,
	variantName: string,
	tables: Tables,
	palette: Palette,
) {
	let output = `(deftheme ${name}-${variantName})\n`;
	output += `(custom-theme-set-faces '${name}-${variantName}\n`;

	const get = bindGet(palette);
	const f = (faceName: string, props: Record<string, boolean | string | undefined>) => {
		let out = '';
		for (const [prop, value] of Object.entries(props)) {
			if (value !== undefined) {
				if (out === '') {
					out += `  \`(${faceName} ((t (`;
				}

				if (typeof value === 'string') {
					out += `:${prop} "${value}" `;
				} else if (typeof value === 'boolean') {
					const v = value ? 't' : 'nil';
					out += `:${prop} ${v} `;
				}
			}
		}

		if (out !== '') {
			out += '))))\n';
		}

		output += out;
	};

	f('default', {
		foreground: get(tables['text']?.['primary']),
		background: get(tables['backgrounds']?.['primary']),
	});
	f('mode-line-active', {
		foreground: get(tables['text']?.['primary']),
		background: get(tables['backgrounds']?.['status-bar-active']),
		box: true,
	});
	f('mode-line-inactive', {
		foreground: get(tables['text']?.['primary']),
		background: get(tables['backgrounds']?.['status-bar-inactive']),
		box: true,
	});
	f('cursor', {
		foreground: get(tables['text']?.['cursor']),
		background: get(tables['backgrounds']?.['cursor']),
	});
	f('region', {
		foreground: get(tables['text']?.['selection']),
		background: get(tables['backgrounds']?.['selection']),
	});
	f('vertico-current', {
		foreground: get(tables['text']?.['selection']),
		background: get(tables['backgrounds']?.['selection']),
	});
	f('whitespace-tab', { foreground: get(tables['text']?.['faint']) });

	const s = (faceName: string, syntaxEntry: string) => {
		const v = get(tables['syntax']?.[syntaxEntry]) ?? get(tables['text']?.[syntaxEntry]);

		f(faceName, { foreground: v });
	};
	const syntaxMappings = [
		['font-lock-type-face', 'identifier'],
		['font-lock-constant-face', 'constant'],
		['font-lock-builtin-face', 'other'],
		['font-lock-preprocessor-face', 'macro'],
		['font-lock-doc-face', 'comment'],
		['font-lock-function-name-face', 'function'],
		['font-lock-keyword-face', 'keyword'],
		['font-lock-variable-name-face', 'identifier'],
		['font-lock-string-face', 'string'],
		['font-lock-variable-use-face', 'identifier'],
		['font-lock-negation-char-face', 'operator'],
		['font-lock-property-use-face', 'identifier'],
		['font-lock-punctuation-face', 'operator'],
		['font-lock-doc-markup-face', 'comment'],
		['font-lock-comment-delimiter-face', 'comment'],
		['font-lock-delimiter-face', 'operator'],
		['font-lock-number-face', 'constant'],
		['font-lock-function-call-face', 'function'],
		['font-lock-comment-face', 'comment'],
		['font-lock-operator-face', 'operator'],
		['font-lock-property-name-face', 'identifier'],
		['font-lock-misc-punctuation-face', 'operator'],
		['font-lock-escape-face', 'other'],

		['font-lock-warning-face', 'warning'],
		['font-lock-error-face', 'error'],
	] as const;

	for (const [faceName, syntaxEntry] of syntaxMappings) {
		s(faceName, syntaxEntry);
	}

	output += `)\n`;

	const path = `${dir}/${name}-${variantName}-theme.el`;
	await Deno.writeTextFile(path, output);
}

async function generateOutput(
	output: Output,
	name: string,
	variants: Record<string, Tables>,
	palette: Palette,
) {
	const { outputType, dir } = output;
	await Deno.mkdir(dir, { recursive: true });

	const genFunctions = {
		'ghostty': generateGhostty,
		'emacs': generateEmacs,
	};
	const genFunction = genFunctions[outputType];
	if (genFunction === undefined) {
		throw new Error(`Unsupported output type "${outputType}"`);
	}

	for (const [variantName, tables] of Object.entries(variants)) {
		await genFunction(dir, name, variantName, tables, palette);
	}
}

async function main() {
	const inputPath = Deno.args[1];
	if (inputPath === undefined) {
		printUsageAndDie();
	}

	const outputs_ = Deno.args.slice(2);
	if (outputs_.length === 0) {
		printUsageAndDie();
	}

	const outputs: Output[] = outputs_.map((o) => {
		const [outputType, dir] = o.split(':');

		return { outputType: outputType as OutputType, dir };
	});

	const input = await Deno.readTextFile(inputPath);
	const parsed = toml.parse(input);

	const { name, palette: palette_, ...variants } = parsed;
	const palette = flattenPalette(palette_ as RawPalette);

	for (const output of outputs) {
		await generateOutput(
			output,
			name as string,
			variants as Record<string, Tables>,
			palette,
		);
	}
}

await main();
