use core:io;
use markdown;

/**
 * Documentation node. Corresponds to a file or directory in the source code. Each node generates
 * exactly one document in the output hierarchy.
 */
class Node {
	// Location of the source file.
	Url source;

	// Logical name of this node (i.e. the name of the directory if this is an "index" file).
	Str name;

	// Is this node a directory? (i.e. should this file be named "index" in the output?).
	Bool dirNode;

	// Depth of this node in the tree. I.e. how many parent nodes exist.
	Nat depth;

	// Tree configuration.
	TreeConfig config;

	// Parent node, if any.
	Node? parent;

	// Child nodes, if any. Sorted according to display order.
	Node[] children;

	// Loaded document.
	Document? content;

	// Create.
	init(Url file, TreeConfig config, Node? parent) {
		init {
			source = file;
			name = file.title;
			config = config;
			parent = parent;
			depth = 0;
		}

		if (parent)
			depth = parent.depth + 1;

		if (file.dir()) {
			dirNode = true;
			source = file / "index.md";
			if (!source.exists())
				throw DocError(file, "A directory should contain a file called \"index.md\".");
			findChildren(file);
		}
	}

	// Find children.
	private void findChildren(Url dir) {
		for (f in dir.children) {
			// We already account for the index file.
			if (f.title == "index")
				continue;

			if (f.dir) {
				children << Node(f, config, this);
			} else if (f.ext == "md") {
				children << Node(f, config, this);
			}
		}

		children.sort((a, b) => a.name < b.name);
	}

	// Find the ID of a child.
	Nat? findChild(Node child) {
		for (i, c in children)
			if (c is child)
				return i;
		return null;
	}

	// Find a path relative the root of the tree.
	Url absOutputPath() {
		Url path = absOutputPathI();

		if (dirNode)
			path = path / "index.html";
		else
			path = path.withExt("html");

		return path;
	}

	// Helper to find the path.
	private Url absOutputPathI() {
		if (parent) {
			Str fileTitle = name;
			if (config.stripNumbers) {
				var dash = fileTitle.find('-');
				if (dash != fileTitle.end)
					fileTitle = fileTitle.cut(dash + 1);
			}

			parent.absOutputPathI / fileTitle;
		} else {
			Url();
		}
	}

	// Find a relative path to another node.
	Url relativeOutputPath(Node current) {
		absOutputPath().relative(current.absOutputPath().parent());
	}

	// Find and resolve a link relative to this node. Fuzzy matching is supported.
	Node? resolveLink(Url url) {
		Node current = this;

		// If we're not a directory node, treat the url as relative to our parent.
		if (!dirNode)
			if (parent)
				current = parent;

		if (url.absolute) {
			unless (url.protocol as MdProtocol)
				throw InternalError("Only relative Urls and Urls with the md:// protocol are supported!");

			current = findRoot();
		}

		for (part in url) {
			unless (matching = current.findMatching(part))
				return null;

			current = matching;
		}

		return current;
	}

	// Find a child that matches the string.
	private Node? findMatching(Str name) {
		if (name == "..") {
			if (p = parent)
				return p;
			else
				return null;
		}

		for (c in children) {
			if (c.name == name)
				return c;
			var dash = c.name.find('-');
			if (dash != c.name.end) {
				if (c.name.cut(dash + 1) == name)
					return c;
			}
		}
		return null;
	}

	// Find the root node.
	Node findRoot() {
		if (parent)
			parent.findRoot();
		else
			this;
	}

	// Find the next node in the pre-order traversal.
	Node? next() {
		// If we have at least one child, continue there.
		if (children.any)
			return children[0];

		// Look at parents until we find a next element.
		Node current = this;
		while (parent = current.parent) {
			// Find the current node in the parent.
			unless (currentId = parent.findChild(current))
				return null;

			// Is there a next child?
			if (currentId + 1 < parent.children.count)
				return parent.children[currentId + 1];

			// No, go to the parent and try again.
			current = parent;
		}

		return null;
	}

	// Find the previous node in the pre-order traversal.
	Node? previous() {
		// Find the previous node:
		unless (parent)
			return null;
		unless (parentId = parent.findChild(this))
			return null;

		// Go to the last node in that tree.
		if (parentId > 0)
			return parent.children[parentId - 1].last();

		// If we were the first, return our parent.
		return parent;
	}

	// Find the last node the in-order traversal of this hierarchy.
	private Node last() {
		if (children.any)
			return children.last.last;
		return this;
	}

	// Load this node and all children. 'treePath' is the relative path in the tree.
	void load() {
		if (content.empty)
			content = loadDocument(this, source);

		for (x in children)
			x.load();
	}

	// Output to disk.
	void output(Theme theme, Url root) {
		unless (content)
			throw InternalError("'load' needs to be called before 'output'.");

		Url outUrl = (root / absOutputPath());
		if (dirNode)
			outUrl.parent.createDir();

		// Output this document.
		Str html = theme.toHtml(this, content);
		var out = Utf8Output(outUrl.write(), unixTextInfo());
		out.write(html);
		out.close();

		for (x in children)
			x.output(theme, root);
	}

	// Compute the title of this node.
	Str title() {
		StrBuf out;
		Bool firstDash = true;
		for (x in name) {
			if (x == '-') {
				if (firstDash)
					out.clear();
				else
					out << '-';
				firstDash = false;
			} else if (x == '_') {
				out << ' ';
			} else {
				out << x;
			}
		}
		out.toS();
	}

	// To string.
	void toS(StrBuf to) : override {
		to << name;
		Indent z(to);
		for (x in children)
			to << "\n" << x;
	}
}


/**
 * Configuration for the entire tree.
 */
class TreeConfig {
	// Strip numbers from md-files in the output?
	Bool stripNumbers;

	init() {
		init {}
	}
}


/**
 * Documentation tree. Contains the root node of the tree, and the associate logic to traverse the
 * tree.
 */
class Tree {
	// Root node of the tree.
	Node root;

	// Configuration.
	TreeConfig config;

	// Create the tree.
	init(Str title, Url rootDir) {
		TreeConfig config;

		init {
			root(rootDir.updated(), config, null);
			config = config;
		}

		root.name = title;
	}

	// Load all documents.
	void load() {
		root.load();
	}

	// Output to disk.
	void output(Theme theme, Url rootPath) {
		theme.initialize(this);

		rootPath.createDirTree();
		root.output(theme, rootPath);

		theme.copyFiles(rootPath);
	}

	// Output.
	void toS(StrBuf to) : override {
		to << root;
	}
}
