nunc 0.2.0

WIP: time tracking application
Documentation
use std::convert::Infallible;
use std::fmt;
use std::str::FromStr;

use conciliator::{
	Claw,
	input::Select,
	List,
	Conciliator
};
use greg::{
	Point,
	Frame,
	Span,
	real::ParseSpanError
};
use greg::calendar::{
	Date,
	Time,
	DateTime,
	Calendar,
	ParseError
};
use greg::calendar::zone::Unsteady;

use crate::Nunc;
use crate::core::{
	Log,
	Tree,
	Zone
};
use crate::data;
use crate::Error;
use crate::util::format::LookupFormatter;

/*	ACTIVITY	*/

#[derive(Clone, Debug)]
pub struct Activity(pub(crate) String);

/*	CATEGORY	*/

#[derive(Clone, Debug)]
pub struct Category(pub(crate) String);

/*	TIME	*/

#[derive(Clone, Copy, Debug)]
pub enum TimeArg {
	Date(Date),
	DateTime(DateTime),
	Time(Time),
	Relative(Span)
}

#[derive(Clone, Debug)]
pub enum ParseTimeError {
	Offset(ParseSpanError),
	Absolute(ParseError)
}


/*	SPAN	*/

pub fn parse_span(s: &str) -> Result<Span, String> {
	s.parse::<Span>().map_err(|err| err.to_string())
}

/*	ENTRY */

pub fn select_entry(
	con: &Claw,
	nunc: &Nunc,
	offset: u8,
	when: Option<TimeArg>,
	act: Option<Activity>)
	-> Result<data::Entry, Error>
{
	let act = act.map(|act| act.validate(nunc, con)).transpose()?;
	let when = when
		.map(|when| nunc.load_zone_table().map(|tb| tb.resolve(when)))
		.transpose()?;

	let mut ents = match (offset, when, act) {
		(0, None, None) => nunc.get_last_entries(9)?,
		(0, when, act) => {
			let from = when.unwrap_or_else(Point::now) - Span::HOUR * 3;
			let frame = Frame::from_point(from, Span::HOUR * 6);
			let mut ents = nunc.recap(frame)?;
			if let Some(act) = act {
				ents.retain(|e| e.activity_id == act.id);
			}
			ents
		},
		(_n, Some(_), _) |
		(_n, _, Some(_)) => ret_errmsg!("Incompatible options!"),
		(n, None, None) => return nunc
			.get_last_entries(n as usize)?
			.pop()
			.ok_or_else(|| Error::Message("No such entry".to_string())),
	};

	match ents.len() {
		0 => ret_errmsg!("No matching entries!"),
		1 => Ok(ents.pop().unwrap()),
		_ => {
			let list = List::indexed(ents.iter())
				.with_formatter(LookupFormatter::new(nunc)?)
				.with_count("entries");
			let sel = Select::new(list, "Select entry");
			Ok(ents.swap_remove(con.input(sel)))
		}
	}
}

/*
 *	ACTIVITY
 */

impl Activity {
	pub fn validate(
		self,
		nunc: &Nunc,
		con: &Claw)
		-> Result<data::Activity, Error>
	{
		let mut acts = nunc.find_activities(self.0.as_str())?;

		match acts.len() {
			0 => ret_errmsg!("Activity {:?} not found!", self.0),
			1 => Ok(acts.pop().unwrap()),
			_ => {
				con.info("Activity ")
					.push(&self.0)
					.push(" is ambiguous");
				let resolved = acts.into_iter()
					.map(|act| {
						let path = nunc.get_path_string(act.category_id)?;
						Ok(act.resolve(path))
					})
					.collect::<Result<Vec<_>, Error>>()?;

				let act = con.select(
					resolved,
					"ambiguous activities",
					"Select activity"
				).unwrap().revert();
				Ok(act)
			}
		}
	}
}

impl FromStr for Activity {
	type Err = Infallible;
	fn from_str(s: &str) -> Result<Self, Self::Err> {Ok(Self(s.to_string()))}
}

impl fmt::Display for Activity {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		f.write_str(&self.0)
	}
}

serde_as_str!(Activity, "Activity");

/*
 *	CATEGORY
 */

impl Category {
	pub fn validate(
		self,
		nunc: &Nunc,
		con: &Claw)
		-> Result<data::Category, Error>
	{
		let mut cats = nunc.find_categories(self.0.as_str())?;

		match cats.len() {
			0 => ret_errmsg!("Category {:?} not found!", self.0),
			1 => Ok(cats.pop().unwrap()),
			_ => {
				con.info("Category ")
					.push(&self.0)
					.push(" is ambiguous");
				let resolved = cats.into_iter()
					.map(|cat| {
						let path = nunc.resolve_path_string(&cat.path)?;
						Ok(cat.resolve(path))
					})
					.collect::<Result<Vec<_>, Error>>()?;

				let cat = con.select(
					resolved,
					"ambiguous categories",
					"Select category"
				).unwrap().revert();
				Ok(cat)
			}
		}
	}
}

impl FromStr for Category {
	type Err = Infallible;
	fn from_str(s: &str) -> Result<Self, Self::Err> {Ok(Self(s.to_string()))}
}

/*
 *	TIME
 */

impl TimeArg {
	pub fn resolve_with<Z: Unsteady>(self, cal: &Calendar<Z>) -> Point {
		match self {
			Self::Relative(sp) => Point::now() - sp,
			Self::DateTime(dt) => cal.resolve_lossy(dt),
			Self::Date(d) => cal.resolve_midnight_lossy(d),
			Self::Time(t) => {
				let today = cal.lookup(Point::now()).0;
				let yesterday = today.sub_day();
				let tomorrow = today.add_day();
				[yesterday, today, tomorrow].into_iter()
					.map(|d| DateTime(d, t))
					.map(|dt| cal.resolve_lossy(dt))
					.min_by_key(|&p| Point::now() - p)
					.unwrap()
			}
		}
	}
}

impl fmt::Display for TimeArg {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		match self {
			TimeArg::Date(d) => d.fmt(f),
			TimeArg::DateTime(dt) => dt.fmt(f),
			TimeArg::Time(t) => t.fmt(f),
			TimeArg::Relative(s) => s.fmt(f)
		}
	}
}

serde_as_str!(TimeArg, "absolute or relative time argument");


impl FromStr for TimeArg {
	type Err = ParseTimeError;
	fn from_str(s: &str) -> Result<Self, Self::Err> {
		match s {
			"" => Ok(Self::Relative(Span::ZERO)),
			s if s.contains(['T', ' ']) => Ok(Self::DateTime(s.parse()?)),
			s if s.contains('-') => Ok(Self::Date(s.parse()?)),
			s if s.contains(':') => Ok(Self::Time(s.parse()?)),
			s => Ok(Self::Relative(s.parse()?))
		}
	}
}

/*
 *	PARSE TIME ERROR
 */

impl From<ParseError> for ParseTimeError {
	fn from(e: ParseError) -> Self {Self::Absolute(e)}
}
impl From<ParseSpanError> for ParseTimeError {
	fn from(e: ParseSpanError) -> Self {Self::Offset(e)}
}

impl fmt::Display for ParseTimeError {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		match self {
			Self::Offset(e) => fmt::Display::fmt(e, f),
			Self::Absolute(e) => fmt::Display::fmt(e, f)
		}
	}
}
impl std::error::Error for ParseTimeError {}