use core:io;
use markdown;
use markdown:doc;
use lang:bs:macro;

/**
 * The theme itself.
 */
class StormTheme extends Theme {
	// Template.
	private Str template;

	// HTML for the menu.
	private Str menu;

	// Url where resources are stored.
	private Url resUrl;

	// Files to copy.
	private Str[] files = ["style.css", "desktop.css", "mobile.css", "Raleway_ext.woff2", "Raleway.woff2", "images"];

	// Current version.
	Str version;

	// Current date.
	Str date;

	// Release notes to use.
	Url? releaseNotes;

	// Create.
	init() {
		var url = resUrl("storm_res");

		init {
			template = (url / "template.html").readAllText;
			resUrl = url;
			version = "0.0.0";
			date = "0000-00-00";
		}
	}

	// Consume parameters.
	void options(Str->Str options) {
		if (ver = options.at("version")) {
			version = ver;
			options.remove("version");
		} else {
			if (version = findVersion()) {
				this.version = version;
				print("WARNING: Missing --version. Using current compiler version: ${version}");
			} else {
				this.version = "0.0.0";
				print("WARNING: Missing --version. Using version 0.0.0.");
			}
		}

		if (d = options.at("date")) {
			date = d;
			options.remove("date");
		} else {
			print("WARNING: Missing --date. Using ${date}.");
		}

		if (n = options.at("notes")) {
			releaseNotes = parsePath(n).makeAbsolute(cwdUrl);
			options.remove("notes");
		}
	}

	// Initialization.
	void initialize(Tree tree) : override {
		// Create the menu.
		menu = createMenu(tree);
	}

	// Create menu.
	private Str createMenu(Tree tree) {
		Html out;
		out.html("<ul id=\"menu\">\n");
		out.indent();
		createMenu(out, tree.root);
		out.dedent();
		out.html("</ul>");
		out.toS;
	}

	// Create menu, recursive helper.
	private void createMenu(Html out, Node current) {
		for (child in current.children) {
			out.html("<li><a href=\"<?path?>");
			out.text(child.absOutputPath.format());
			out.html("\">");
			out.text(child.title);
			out.html("</a>");

			if (child.children.any) {
				out.indent();
				out.html("\n<ul>\n");
				out.indent();
				createMenu(out, child);
				out.dedent();
				out.html("\n</ul>\n");
				out.dedent();
			}
			out.html("</li>\n");
		}
	}

	// Output data.
	Str toHtml(Node node, Document document) : override {
		document.visit(StormVisitor(this, node));

		Nat relLevels = node.depth;
		if (!node.dirNode)
			relLevels--;
		Str relPath = "../" * relLevels;

		Str->Str markers;
		markers.put("path", relPath);
		markers.put("title", fullTitle(node));
		markers.put("year", year);
		markers.put("menu", replaceMarkers(menu, markers));
		markers.put("navigation", createNavigation(node));
		markers.put("content", document.toHtml.toS);

		replaceMarkers(template, markers);
	}

	// Create full title.
	private Str fullTitle(Node node) {
		Str[] titles;
		Node? current = node;
		while (c = current) {
			titles << c.title;

			current = c.parent;
		}

		titles.reverse();
		join(titles, " → ").toS();
	}

	// Create navigation.
	private Str createNavigation(Node node) {
		Html out;

		// Find the previous node:
		navigationDiv(out, "nav-prev", "Previous:", node, node.previous);

		// Find the next node:
		navigationDiv(out, "nav-next", "Next:", node, node.next);

		// Find the parent node:
		navigationDiv(out, "nav-up", "Up:", node, node.parent);

		out.toS;
	}

	// Output a navigation div.
	private void navigationDiv(Html out, Str type, Str text, Node current, Node? linkTo) {
		unless (linkTo)
			return;

		Url rel = linkTo.relativeOutputPath(current);
		Str link = rel.format();

		out.html("<div id=\"" + type + "\">");
		out.text(text);
		out.html(" <a href=\"");
		out.text(link);
		out.html("\">");
		out.text(linkTo.title);
		out.html("</a></div>\n");
	}

	// Copy files.
	void copyFiles(Url root) : override {
		copyFiles(resUrl, root, files);
	}

	// Get the year from the date.
	Str year() {
		var sep = date.find('-');
		date.cut(date.begin, sep);
	}

}

// Find the current compiler version. Robust for development builds where the version file does not
// exist.
private Str? findVersion() on Compiler {
	var infoPkg = named{core:info};
	if (ver = infoPkg.find(lang:SimplePart("COMPILER"), lang:Scope()) as core:info:VersionTag)
		return ver.version.toS;
	return null;
}

/**
 * Custom traversal of some elements.
 */
class StormVisitor extends markdown:Visitor {

	// Theme.
	StormTheme theme;

	// Current node.
	Node node;

	// Create.
	init(StormTheme theme, Node node) {
		init { theme = theme; node = node; }
	}

	// Visit elements, highligt source blocks.
	Element visit(Element elem) : override {
		if (elem as CodeBlock) {
			if (elem.language == "stormdoc")
				return formatStormDoc(node.source, elem.code);
			else if (elem.language == "storm:releasenotes")
				return formatReleaseNotes(node);

			resolveIncludes(elem);

			if (highlighted = highlight:highlightSource(node.source, elem))
				return highlighted;
		}
		elem;
	}

	// Visit text, add custom info and code.
	TextSpan visit(TextSpan elem) : override {
		if (elem as CustomText) {
			var colon = elem.text.find(':');
			if (colon == elem.text.end)
				return elem;

			Str name = elem.text.cut(elem.text.begin, colon);
			Str param = elem.text.cut(colon + 1);

			if (name == "downloadbutton")
				return createDownload(param);
			if (name == "storm")
				return createStormInfo(param);
			if (name == "stormname") {
				Bool full = param.startsWith("full:");
				if (full)
					param = param.cut(param.begin + 5);
				return PackedText(formatName(node.source, param, full));
			}
		}
		return elem;
	}

	// Create download button.
	TextSpan createDownload(Str link) {
		Html html;
		html.html("<div class=\"buttonborder\">");
		html.html("<a class=\"button\" href=\"");
		if (link.startsWith("md:")) {
			if (target = node.resolveLink(parseMdUrl(link.cut(link.begin + 3))))
				html.text(target.relativeOutputPath(node).format());
			else
				print("WARNING: In ${node.source}: Broken markdown link in download button: ${link}");
		} else {
			html.text(link);
		}
		html.html("\">");
		html.html("Download Storm<br/>");
		html.html("<span class=\"filedate\">Latest version: ");
		html.text(theme.version);
		html.html(" (");
		html.text(theme.date);
		html.html(")</span></a></div>");

		CustomInlineHtml(html.toS);
	}

	// Create info node.
	TextSpan createStormInfo(Str param) {
		Str text = "";
		if (param == "version")
			text = theme.version;
		else if (param == "date")
			text = theme.date;

		return PlainTextSpan(text);
	}

	Element formatReleaseNotes(Node node) {
		Url url = if (n = theme.releaseNotes) {
			n;
		} else {
			Url p = node.findRoot().source().parent() / "release_notes.txt";
			if (!p.exists()) {
				print("WARNING: No --notes flag, nor does \"release_notes.txt\" exist. Release notes will be missing.");
				return Paragraph(FormattedText("No release notes available."));
			}
			p;
		};

		Str data = url.readAllText();
		var doc = markdown:parse(data);

		PackedElements elements;
		elements.elements = doc.elements;

		elements;
	}

	// Resolve includes in source listings.
	void resolveIncludes(CodeBlock code) {
		unless (code.code.count == 1)
			return;

		Str line = code.code[0];
		unless (line.startsWith("<?include:"))
			return;

		var end = line.find("?>");
		unless (end != line.end)
			return;

		unless (rootPath = lang:rootPkg.url)
			return;

		Str path = line.cut(line.begin + 10, end);
		Url file = parsePath(path).makeAbsolute(rootPath);
		if (!file.exists) {
			print("WARNING: ${node.source}: File ${file} does not exist.");
			return;
		}

		Str text = file.readAllText;
		code.code.clear();
		code.code << text;
	}
}

// Entry point.
Theme storm() {
	StormTheme();
}
