use fmt;
use dirs;
use bufio;
use memio;
use os;
use fs;
use path;
use io;
use strings;
use format::ini;
use strconv;
use encoding::utf8;
use getopt;
use sort;
use fnmatch;
use errors;
use time;

type task = struct {
	name: str,
	path: str,
	context: (str | void),
	tags: []str,
	priority: uint,
	content: str,
	lmd: time::instant,
};

def METADATASEP: str = "----";

type rtaskerror = !(fs::error | ini::error | strconv::error | io::error |
	utf8::invalid | path::error);

fn listtasks(root: str = "tasks", context: str = "*") ([]task | rtaskerror) = {
	const dirents = os::readdir(root)?;
	defer fs::dirents_free(dirents);
	let tasks: []task = [];
	for (const dirent .. dirents) {
		if (fs::isdir(dirent.ftype)) {
			if (context == "*" || fnmatch::fnmatch(context, dirent.name)) {
				let buf = path::init()?;
				const p = path::push(&buf, root, dirent.name)?;
				append(tasks, listtasks(p)?...)!;
			};
		} else {
			let buf = path::init()?;
			const p = path::push(&buf, root, dirent.name)?;
			let t = readtask(p)?;
			append(tasks, t)!;
		};
	};
	return tasks;
};

fn readtask(taskpath: str) (task | rtaskerror) = {
	const f = os::open(taskpath)?;
	defer io::close(f)!;
	const sc = bufio::newscanner(f);
	defer bufio::finish(&sc);

	const content = memio::dynamic();
	defer io::close(&content)!;
	const meta = memio::dynamic();
	defer io::close(&meta)!;

	let currentst = &meta;
	for (let line => bufio::scan_line(&sc)?) {
		if (currentst != &content) {
			line = strings::trim(line);
		};
		if (line == METADATASEP) {
			currentst = &content;
			continue;
		};
		memio::concat(currentst, line, "\n")?;
	};
	// seek to start of memio buffer
	io::seek(&meta, 0, io::whence::SET)?;
	const isc = ini::scan(&meta);
	defer ini::finish(&isc);

	const stat = os::stat(taskpath)?;
	let t = task {
		context = void,
		tags = [],
		lmd = stat.mtime,
		...
	};
	for (let entry: ini::entry => ini::next(&isc)?) {
		switch (entry.1) {
		case "name" =>
			t.name = strings::dup(strings::trim(entry.2));
		case "priority" =>
			t.priority = strconv::stou(entry.2)?;
		case "tags" =>
			t.tags = strings::dupall(strings::split(entry.2, ","));
		case =>
			void;
		};
	};
	if (t.name == "") {
		t.name = strings::dup(path::basename(taskpath));
	};
	t.path = strings::dup(taskpath);
	t.content = strings::dup(memio::string(&content)?);
	return t;
};

fn freetask(t: *task) void = {
	free(t.name);
	free(t.path);
	free(t.content);
	if (t.context is str) {
		free(t.context as str);
	};
	strings::freeall(t.tags);
};

fn finishall(tasks: []task) void = {
	for (const task .. tasks) {
		freetask(&task);
	};
};

fn sortpriority(a: const *opaque, b: const *opaque) int = {
	const a = a: *task;
	const b = b: *task;
	return (b.priority - a.priority): int;
};

fn sortname(a: const *opaque, b: const *opaque) int = {
	const a = a: *task;
	const b = b: *task;
	return strings::compare(a.name, b.name);
};

fn sortlmd(a: const *opaque, b: const *opaque) int = {
	const a = a: *task;
	const b = b: *task;
	return time::compare(b.lmd, a.lmd);
};

fn strrtaskerror(e: rtaskerror) str = {
	return match (e) {
	case let e: fs::error =>
		yield fs::strerror(e);
	case let e: ini::error =>
		yield ini::strerror(e);
	case let e: strconv::error =>
		yield strconv::strerror(e);
	case let e: io::error =>
		yield io::strerror(e);
	case let e: utf8::invalid =>
		yield utf8::strerror(e);
	case let e: path::error =>
		yield path::strerror(e);
	};
};

fn filtertags(cfg: config, tasks: *[]task) void = {
	if (len(cfg.tags) == 0z) {
		return;
	};
	for (let i = 0z; i < len(tasks); i += 1) {
		const t = tasks[i];
		let found = false;
		 for :tagloop (let tag .. t.tags) {
			for (let tagfilter .. cfg.tags) {
				if (tag == tagfilter) {
					found = true;
					break :tagloop;
				};
			};
		};
		if (!found) {
			freetask(&tasks[i]);
			delete(tasks[i]);
			i -= 1;
		};
	};
};

export fn main() void = {
	const cmd = getopt::parse(os::args,
		"tasklist",
		('f', "path", "tasks directory"),
		('c', "context", "context filter"),
		('t', "tags", "tags filter"),
		('p', "sort by priority"),
		('m', "sort by last modification date"),
		('d', "activate debug mode"),
		("filter", ["filter tasks", "id"]: []getopt::help),
		("f", ["filter tasks", "id"]: []getopt::help),
		("show", ["show task details", "id"]: []getopt::help),
		("s", ["show task details", "id"]: []getopt::help),
		("add", ["add a task", "id"]: []getopt::help),
		("a", ["add a task", "id"]: []getopt::help),
		("write", ["write a task", "id"]: []getopt::help),
		("w", ["write a task", "id"]: []getopt::help),
		("done", ["delete a task", "id"]: []getopt::help),
		("d", ["delete a task", "id"]: []getopt::help),
		("tsv", ["print tsv of tasks"]: []getopt::help),
		("t", ["print tsv of tasks"]: []getopt::help),
	);
	defer getopt::finish(&cmd);

	const cfg: config = match (readconfig()) {
	case let cfg: config =>
		yield cfg;
	case let e: fs::error =>
		match (e) {
		case let e: errors::noentry =>
			fmt::fatal("No config file found");
		case =>
			fmt::fatal(fs::strerror(e));
		};
	case let e: cerror =>
		fmt::fatal(strcerror(e));
	};
	defer cfinish(&cfg);

	let sortfn: *sort::cmpfunc = &sortname;
	for (let opt .. cmd.opts) {
		switch (opt.0) {
		case 'f' =>
			cfg.tasksdir = strings::dup(opt.1);
		case 'c' =>
			cfg.context = strings::dup(opt.1);
		case 't' =>
			cfg.tags = strings::dupall(strings::split(opt.1, ","));
		case 'p' =>
			sortfn = &sortpriority;
		case 'm' =>
			sortfn = &sortlmd;
		case 'd' =>
			cfg.debug = true;
		case =>
			abort();
		};
	};

	if (cfg.debug) {
		fmt::errorln("config:")!;
		printconfig(cfg);
	};

	const tasks = match (listtasks(cfg.tasksdir, cfg.context)) {
	case let e: rtaskerror =>
		fmt::fatal(strrtaskerror(e));
	case let tasks: []task =>
		yield tasks;
	};
	defer finishall(tasks);
	sort::sort(tasks: []opaque, size(task), sortfn);
	filtertags(cfg, &tasks);

	const com: (str, *getopt::command) = match (cmd.subcmd) {
	case void =>
		listall(cfg, tasks);
		return;
	case let subcmd: (str, *getopt::command) =>
		yield (subcmd.0, subcmd.1);
	};

	match (execcommand(cfg, com.0, tasks, com.1)) {
	case let e: error =>
		fmt::fatal(strerror(e));
	case =>
		void;
	};
};
