nunc 0.2.0

WIP: time tracking application
Documentation
use std::borrow::Borrow;
use std::io::Write;
use std::result::Result as StdResult;

use conciliator::{
	Buffer,
	Claw,
	Conciliator,
	print::Formatter,
	Inline,
	Print,
	Paint,
	Wrap
};
use conciliator::edit::{
	Edit,
	Edited,
	TomlEditor
};
use rusqlite::{
	Row,
	Result as SqlResult,
	Error as SqlError
};

use greg::Point;
use greg::calendar::{
	Calendar,
	DateTime
};

use serde::{Serialize, Deserialize};

use crate::{
	Nunc,
	Result,
	Error
};
use crate::cli::arg::{
	Activity,
	TimeArg
};
use crate::core::{
	Tree,
	Zone
};
use crate::core::zone::Table as ZoneTable;
use crate::util::format::LookupFormatter;

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Entry {
	pub id: u64,
	pub activity_id: u64,
	pub start: Point,
	pub stop: Option<Point>
}

pub struct List<'n, I> {
	iter: I,
	nunc: &'n Nunc,
	cal: Calendar<ZoneTable>
}

pub struct Editor<'a> {
	claw: &'a Claw,
	nunc: &'a Nunc,
	cal: Calendar<ZoneTable>,
	toml: TomlEditor<EditEntry>,
	entry: Entry
}

#[derive(Debug, Serialize, Deserialize)]
pub struct EditEntry {
	pub activity: Activity,
	pub start: TimeArg,
	pub stop: Option<TimeArg>
}

#[derive(Debug)]
pub struct InlineEntry {
	pub activity: String,
	pub start: DateTime,
	pub stop: Option<DateTime>
}

#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct SetEntry {
	// struct layout reflects Ord derive lexicography
	pub(crate) start: Point,
	pub(crate) activity_id: u64,
	pub(crate) stop: Option<Point>
}

impl<'n> Formatter<&Entry> for LookupFormatter<'n> {
	fn format(&mut self, buf: &mut Buffer, entry: &Entry) {
		let activity = self.nunc.get_activity(entry.activity_id).unwrap();
		let start = self.cal.lookup(entry.start);
		let stop = entry.stop.map(|p| self.cal.lookup(p));
		print_oneline(buf, &activity.name, start, stop);
	}
}

impl<'n> Formatter<&SetEntry> for LookupFormatter<'n> {
	fn format(&mut self, buf: &mut Buffer, entry: &SetEntry) {
		let activity = self.nunc.get_activity(entry.activity_id).unwrap();
		let start = self.cal.lookup(entry.start);
		let stop = entry.stop.map(|p| self.cal.lookup(p));
		print_oneline(buf, &activity.name, start, stop);
	}
}

impl<'n, E: Borrow<Entry>, I: Iterator<Item = E>> List<'n, I> {
	pub fn new(nunc: &'n Nunc, iter: I) -> SqlResult<Self> {
		let cal = nunc.load_calendar()?;
		Ok(Self{nunc, iter, cal})
	}
}

impl<'n, E: Borrow<Entry>, I: Iterator<Item = E>> Print for List<'n, I> {
	fn print<C: Conciliator + ?Sized>(self, con: &C) {
		let mut date = None;
		for entry in self.iter {
			let entry: &Entry = entry.borrow();
			let activity = self.nunc.get_activity(entry.activity_id).unwrap();
			let start = self.cal.lookup(entry.start);
			let stop = entry.stop.map(|p| self.cal.lookup(p));

			if date != Some(start.0) {
				con.get_line();
				con.get_line()
					.push(start.0)
					.push(' ')
					.push(self.cal.weekday(entry.start));
				con.get_line();
				date = Some(start.0);
			}

			let mut buf = con.get_line();
			print_no_date(&mut buf, &activity.name, start, stop);
		}
	}
}

pub fn print_oneline(
	buf: &mut Buffer,
	act_name: &str,
	start: DateTime,
	stop: Option<DateTime>)
{
	buf.push_iota(&start.0).push(' ');
	print_start_stop(buf, start, stop);
	buf.push(Wrap::Beta(act_name));
}

pub fn print_no_date(
	buf: &mut Buffer,
	act_name: &str,
	start: DateTime,
	stop: Option<DateTime>)
{
	print_start_stop(buf, start, stop);
	buf.push(Wrap::Beta(act_name));
}

pub fn print_start_stop(
	buf: &mut Buffer,
	start: DateTime,
	stop: Option<DateTime>)
{
	buf.push_plain("[")
		.push_plain(&start.1)
		.push_plain("] ");

	match stop {
		Some(stop) if start.0.add_day() == stop.0 => buf
			.push_iota("→")
			.push_plain(" ")
			.push_iota("{")
			.push_plain(&stop.1)
			.push_iota("}")
			.push_plain(": "),
		Some(stop) if start.0 == stop.0 => buf
			.push_plain("→ [")
			.push_plain(&stop.1)
			.push_plain("]: "),
		Some(stop) => buf
			.push_plain("→ ")
			.push_iota(&stop.0)
			.push_plain(" [")
			.push_plain(&stop.1)
			.push_plain("]: "),
		None => buf.push_plain("→ [  ····  ]: ")
	};
}

/*
 *	ENTRY
 */

impl Entry {
	pub fn edit(&self, claw: &Claw, nunc: &Nunc) -> Result<Option<Entry>> {
		let cal = nunc.load_calendar()?;

		let &Self { id: _, activity_id, start, stop } = self;
		let activity = nunc.get_activity(activity_id)?.name;
		let start = cal.lookup(start);
		let stop = stop.map(|s| cal.lookup(s));
		let inline = InlineEntry { activity, start, stop };
		claw.info(&inline);

		let editor = Editor {
			claw,
			nunc,
			cal,
			toml: TomlEditor::new(inline.into_edit()),
			entry: self.clone()
		};

		match claw.edit(editor) {
			Edited::Ok((edited, inline)) => {
				claw.info(&inline);
				match claw.confirm(false, "Apply these changes?") {
					true => Ok(Some(edited)),
					false => {
						claw.info("Entry not updated!");
						Ok(None)
					}
				}
			},
			Edited::Cancelled => {
				claw.info("Edit aborted");
				Ok(None)
			},
			Edited::Err(err) => Err(err)
		}
	}
}

impl<'r> TryFrom<&Row<'r>> for Entry {
	type Error = SqlError;

	fn try_from(value: &Row<'r>) -> StdResult<Self, Self::Error> {
		value.try_into().map(|(id, activity_id, start, stop)|
			Self {id, activity_id, start, stop}
		)
	}
}

/*
 *	EDIT ENTRY
 */

impl<'a> Edit for Editor<'a> {
	const EXTENSION: &'static str = TomlEditor::<()>::EXTENSION;
	type T = (Entry, InlineEntry);
	type Err = Error;
	fn write<W: Write>(&self, w: &mut W) -> Result<()> {
		self.toml.write(w)?;
		Ok(())
	}
	fn read<C>(&self, content: String, con: &C) -> Option<Self::T>
			where C: Conciliator + ?Sized
	{
		let edit_entry = self.toml.read(content, con)?;
		let EditEntry {activity, start, stop} = edit_entry;
		let try_block = || -> Result<(Entry, String)>{
			let activity = activity.validate(self.nunc, self.claw)?;

			let start = start.resolve_with(&self.cal);
			let stop = stop.map(|s| s.resolve_with(&self.cal));
			let edited = Entry {
				id: self.entry.id,
				activity_id: activity.id,
				start,
				stop
			};
			Ok((edited, activity.name))
		};
		let (edited, activity) = match try_block() {
			Ok((ent, act)) => (ent, act),
			Err(e) => {
				con.error(e);
				return None;
			}
		};

		if edited == self.entry {
			con.warn("Entry has not been changed!");
			None
		}
		else {
			let start = self.cal.lookup(edited.start);
			let stop = edited.stop.map(|p| self.cal.lookup(p));
			let inline_entry = InlineEntry {activity, start, stop};
			Some((edited, inline_entry))
		}
	}
}

impl InlineEntry {
	pub fn into_edit(self) -> EditEntry {
		let Self { activity, start, stop } = self;
		let activity = Activity(activity);
		let start = TimeArg::DateTime(start);
		let stop = stop.map(TimeArg::DateTime);
		EditEntry { activity, start, stop }
	}
}

impl Inline for InlineEntry {
	fn inline(&self, buffer: &mut Buffer) {
		let Self { ref activity, start, stop } = *self;
		print_oneline(buffer, activity, start, stop);
	}
}