from typing import TypedDict
from pathlib import Path
import subprocess
import tomllib
import shutil
import json
import os
import sys

# TODO:
# list handouts without solutions
# list handouts that are not published
# lint: always commit without `nosolutions`
# use latexmk

ROOT: Path = Path(os.getcwd())

### CONFIGURATION
OUT_DIR: Path = ROOT / "output"
OUT_DIR_MANUAL: Path = ROOT / "manual"
TYPST_PATH: str = "typst"
XETEX_PATH: str = "xelatex"
### END CONFIGURATION


# If we're given an argument, build one handout.
# This places output into `OUT_DIR_MANUAL`
#
# If we're given no arguments, build everything,
# Placing output into `OUT_DIR`
target = None
if len(sys.argv) == 2:
	target = Path(sys.argv[1])
	print(f"Compiling `{target}`")

# Allow path override
_env = os.environ.get("TYPST_PATH")
if isinstance(_env, str):
	TYPST_PATH = _env


def log(msg):
	print(f"[BUILD.PY] {msg}")


log(f"Running in {ROOT}")
if not ROOT.is_dir():
	log("Root is not a directory, cannot continue")
	exit(1)


IndexEntry = TypedDict(
	"IndexEntry",
	{"title": str, "group": str, "handout_file": str, "solutions_file": str | None},
)


MetaToml = TypedDict(
	"MetaToml", {"title": str, "publish_handout": bool, "publish_solutions": bool}
)


def sanitize_file_name(file: str) -> str:
	return file.replace("?", "")


def read_meta_toml(file: Path) -> MetaToml:
	with file.open("rb") as f:
		base = tomllib.load(f)

	meta = base.get("metadata")
	if not isinstance(meta, dict):
		log("Invalid meta.toml: `metadata` should be a table")
		exit(1)

	title = meta.get("title")
	if not isinstance(title, str):
		log("Invalid meta.toml: `title` should be a string")
		exit(1)

	pub = base.get("publish")
	if not isinstance(pub, dict):
		log("Invalid meta.toml: `publish` should be a table")
		exit(1)

	pub_handout = pub.get("handout")
	if not isinstance(pub_handout, bool):
		log("Invalid meta.toml: `publish.handout` should be a boolean")
		exit(1)

	pub_solutions = pub.get("solutions")
	if not isinstance(pub_solutions, bool):
		log("Invalid meta.toml: `publish.solutions` should be a boolean")
		exit(1)

	return {
		"title": title,
		"publish_handout": pub_handout,
		"publish_solutions": pub_solutions,
	}


def log_error(res):
	stdout = res.stdout.decode("utf-8").strip()
	stderr = res.stderr.decode("utf-8").strip()

	log(f"Build failed with code {res.returncode}")

	if stdout == "":
		log("Stdout: <empty>")
	else:
		log("Stdout:")
		print(stdout)

	if stderr == "":
		log("Stderr: <empty>")
	else:
		log("Stderr:")
		print(stderr)

	exit(1)


def build_typst(
	source_dir: Path, out_subdir: Path, *, out_dir=OUT_DIR
) -> IndexEntry | None:
	if not (source_dir / "main.typ").is_file():
		# log(f"No main.typ, skipping {source_dir}")
		return None

	meta_path = source_dir / "meta.toml"
	if not meta_path.is_file():
		log(f"No meta.toml, skipping {source_dir}")
		return None
	meta = read_meta_toml(meta_path)

	handout_file = sanitize_file_name(f"{meta['title']}.pdf")
	solutions_file = sanitize_file_name(f"{meta['title']}.sols.pdf")

	# Do nothing if not published
	if not meta["publish_handout"]:
		return None

	# Build handout
	log(f"Building typst (handout)  : {source_dir}")

	out = out_dir / out_subdir
	out.mkdir(parents=True, exist_ok=True)

	res = subprocess.run(
		[
			TYPST_PATH,
			"compile",
			"--package-path",
			f"{ROOT}/lib/typst",
			"--ignore-system-fonts",
			"--input",
			"show_solutions=false",
			"main.typ",
			f"{out}/{handout_file}",
		],
		cwd=source_dir,
		stdout=subprocess.PIPE,
		stderr=subprocess.PIPE,
	)

	if res.returncode != 0:
		log_error(res)

	# Build solutions
	if meta["publish_solutions"]:
		log(f"Building typst (solutions): {source_dir}")
		res = subprocess.run(
			[
				TYPST_PATH,
				"compile",
				"--package-path",
				f"{ROOT}/lib/typst",
				"--ignore-system-fonts",
				"main.typ",
				f"{out}/{solutions_file}",
			],
			cwd=source_dir,
			stdout=subprocess.PIPE,
			stderr=subprocess.PIPE,
		)

		if res.returncode != 0:
			log_error(res)

	return {
		"title": meta["title"],
		"group": str(out_subdir),
		"handout_file": str(out / handout_file),
		"solutions_file": (
			str(out / solutions_file) if meta["publish_solutions"] else None
		),
	}


def build_xetex(
	source_dir: Path, out_subdir: Path, *, out_dir=OUT_DIR
) -> IndexEntry | None:
	if not (source_dir / "main.tex").is_file():
		# log(f"No main.tex, skipping {source_dir}")
		return None

	meta_path = source_dir / "meta.toml"
	if not meta_path.is_file():
		log(f"No meta.toml, skipping {source_dir}")
		return None
	meta = read_meta_toml(meta_path)

	handout_file = sanitize_file_name(f"{meta['title']}.pdf")
	solutions_file = sanitize_file_name(f"{meta['title']}.sols.pdf")

	# Do nothing if not published
	if not meta["publish_handout"]:
		return None

	# Build handout
	log(f"Building xetex (handout)  : {source_dir}")

	out = out_dir / out_subdir
	out.mkdir(parents=True, exist_ok=True)

	res = subprocess.run(
		[
			XETEX_PATH,
			"-interaction=batchmode",
		],
		input=b"\\def\\argNoSolutions{1}\\input{main.tex}",
		cwd=source_dir,
		stdout=subprocess.PIPE,
		stderr=subprocess.PIPE,
	)

	try:
		shutil.copy(source_dir / "main.pdf", f"{out}/{handout_file}")
	except Exception as e:
		log(f"Error: {e}")
		log_error(res)

	if res.returncode != 0:
		log_error(res)

	# Build solutions
	if meta["publish_solutions"]:
		log(f"Building xetex (solutions): {source_dir}")
		res = subprocess.run(
			[
				XETEX_PATH,
				"-interaction=batchmode",
			],
			input=b"\\def\\argYesSolutions{1}\\input{main.tex}",
			cwd=source_dir,
			stdout=subprocess.PIPE,
			stderr=subprocess.PIPE,
		)

		try:
			shutil.copy(source_dir / "main.pdf", f"{out}/{solutions_file}")
		except Exception as e:
			log(f"Error: {e}")
			log_error(res)

		if res.returncode != 0:
			log_error(res)

	return {
		"title": meta["title"],
		"group": str(out_subdir),
		"handout_file": str(out / handout_file),
		"solutions_file": (
			str(out / solutions_file) if meta["publish_solutions"] else None
		),
	}


# Try to build handouts in all subdirs of `base`,
# skipping those that do not contain a handout.
#
# This method does _not_ recurse into subdirectories.
def build_dir(base: str, out_sub: str, index: list[IndexEntry]):
	builders = [build_typst, build_xetex]

	for d_str in os.listdir(base):
		d = base / Path(d_str)
		if not d.is_dir():
			continue

		# Try each builder,
		# stopping at the first success
		done = False
		for builder in builders:
			res = builder(d, Path(out_sub))
			if res is not None:
				# Check for duplicate titles
				for i in index:
					if i["title"] == res["title"]:
						log(f'Duplicate title "{i["title"]}", cannot continue')
						exit(1)
				# Save to index and exit
				index.append(res)
				done = True
			if done:
				break


if target is None:
	log(f"Output dir is {OUT_DIR}")
	if OUT_DIR.exists():
		log("Output dir exists, removing")
		shutil.rmtree(OUT_DIR)

	OUT_DIR.mkdir(parents=True)

	index: list[IndexEntry] = []
	build_dir("src/Advanced", "Advanced", index)
	build_dir("src/Intermediate", "Intermediate", index)
	build_dir("src/Warm-Ups", "Warm-Ups", index)

	with open(OUT_DIR / "index.json", "w") as f:
		f.write(json.dumps(index))

else:
	log(f"Output dir is {OUT_DIR_MANUAL}")
	# if OUT_DIR_MANUAL.exists():
	# log("Output dir exists, removing")
	# shutil.rmtree(OUT_DIR_MANUAL)
	OUT_DIR_MANUAL.mkdir(parents=True, exist_ok=True)

	builders = [build_typst, build_xetex]
	for builder in builders:
		res = builder(target, Path("."), out_dir=OUT_DIR_MANUAL)
		if res is not None:
			break
