nunc 0.2.0

WIP: time tracking application
Documentation
use std::ops::RangeBounds;
use std::slice::Iter;

use rusqlite::{
	OptionalExtension,
	types::FromSqlError,
	Result as SqlResult
};

use greg::{
	Point,
	Frame,
};
use greg::calendar::Calendar;
use greg::calendar::zone::{
	Shift,
	Steady,
	Unsteady
};

use greg_tz::{
	ZONES,
	Offset,
	Ambiguity,
	Zone as TZone
};

use crate::cli::arg::TimeArg;
use super::Database;

#[derive(Debug, Default, Clone)]
pub struct Table {
	offsets: Vec<(Point, Offset)>
}

pub struct OffsetFrames<'t> {
	offsets: Iter<'t, (Point, Offset)>,
	current: Option<(Point, Offset)>
}

impl<'t> Iterator for OffsetFrames<'t> {
	type Item = (Frame, Offset);
	fn next(&mut self) -> Option<Self::Item> {
		match (self.current.take(), self.offsets.next()) {
			(Some((curr_p, curr_off)), Some(&(next_p, next_off))) => {
				self.current = Some((next_p, next_off));
				let frame = Frame {
					start: curr_p,
					stop: next_p
				};
				Some((frame, curr_off))
			},
			(Some((last_p, last_off)), None) => Some((
				Frame {start: last_p, stop: Point::from_epoch(i64::MAX)},
				last_off
			)),
			(None, _) => None
		}
	}
}
impl<'t> ExactSizeIterator for OffsetFrames<'t> {
	fn len(&self) -> usize {
		self.offsets.len() + self.current.iter().count()
	}
}

pub trait Zone: Database {
	fn list(&self) -> SqlResult<Vec<(Point, TZone)>> {
		let mut zones = Vec::new();
		let mut stmt = self.db().prepare(
			"SELECT name, timestamp FROM time_zone \
			JOIN time_zone_encounter ON id = time_zone_id \
			ORDER BY timestamp ASC"
		)?;
		let mut rows = stmt.query([])?;
		while let Some(row) = rows.next()? {
			let (name, at): (String, Point) = row.try_into()?;
			let zone = ZONES.get(&name)
				.ok_or_else(|| format!("invalid zone {name}"))
				.map_err(Into::into)
				.map_err(FromSqlError::Other)?;
			zones.push((at, *zone));
		}

		Ok(zones)
	}

	fn change(&self, when: Point, zone: TZone) -> SqlResult<bool> {
		let zone_name = zone.name();
		let tx = self.db().unchecked_transaction()?;

		let maybe_zone_id = self.db().query_row(
			"SELECT id FROM time_zone WHERE name LIKE ?1",
			[zone_name],
			|r| r.get(0)
		).optional()?;

		let id = match maybe_zone_id {
			Some(id) => id,
			None => {
				let mut stmt = self.db().prepare(
					"INSERT INTO time_zone (name) VALUES (?1)"
				)?;
				stmt.insert([zone_name])?
			}
		};

		let mut stmt = self.db().prepare(
			"INSERT INTO time_zone_encounter \
			(timestamp, time_zone_id) VALUES (?1, ?2)"
		)?;
		stmt.insert((when, id))?;

		tx.commit().map(|()| true)
	}

	fn load_zone_table(&self) -> SqlResult<Table> {
		let mut offsets = Vec::new();
		let mut zones = Vec::new();
		let mut stmt = self.db().prepare(
			"SELECT name, timestamp FROM time_zone \
			JOIN time_zone_encounter ON id = time_zone_id \
			ORDER BY timestamp ASC"
		)?;
		let mut rows = stmt.query([])?;
		while let Some(row) = rows.next()? {
			let (name, at): (String, Point) = row.try_into()?;
			let zone = ZONES.get(&name)
				.ok_or_else(|| format!("invalid zone {name}"))
				.map_err(Into::into)
				.map_err(FromSqlError::Other)?;
			zones.push((at, zone));
		}
		let mut iter = zones.into_iter();
		let mut current = iter.next();

		for next in iter {
			let (start, zone) = current.unwrap();
			// current zone stops applying when next one starts
			let stop = next.0;

			let frames = zone.offset_frames()
				.skip_while(|(frame, _off)| !frame.contains(&start))
				.take_while(|(frame, _off)| frame.start < stop)
				.map(|(frame, off)| (frame.start.max(start), off));
			offsets.extend(frames);

			current = Some(next);
		}

		if let Some((start, zone)) = current {
			// deal with the last one (doesn't end)
			let frames = zone.offset_frames()
				.skip_while(|(frame, _off)| !frame.contains(&start))
				.map(|(frame, off)| (frame.start.max(start), off));
			offsets.extend(frames);
		}

		Ok(Table {offsets})
	}

	fn load_calendar(&self) -> SqlResult<Calendar<Table>> {
		self.load_zone_table().map(Calendar)
	}
}

impl<Db: Database + ?Sized> Zone for Db {}

/*
 *	ZONE TABLE
 */


impl Table {
	pub fn offset_frames(&'_ self) -> OffsetFrames<'_> {
		let offsets = self.offsets.iter();
		let current = Some((Point::from_epoch(i64::MIN), Offset::UTC));
		OffsetFrames {offsets, current}
	}

	pub fn offset_at(&self, point: Point) -> Offset {
		self.offsets.iter()
			.rev()
			.find(|&&(p, _offset)| p <= point)
			.map(|&(_p, offset)| offset)
			.unwrap_or(Offset::UTC)
	}

	pub(crate) fn revert_frame(&self, frame: Frame) -> Frame {
		let start = self.revert_lossy(frame.start);
		let stop = self.revert_lossy(frame.stop);
		Frame {start, stop}
	}

	pub fn resolve(&self, arg: TimeArg) -> Point {
		arg.resolve_with(&Calendar(self))
	}
}


impl Shift for Table {
	fn apply(&self, point: Point) -> Point {
		self.offset_at(point).apply(point)
	}
}

impl Unsteady for Table {
	type Ambiguity = Ambiguity;
	fn revert_lossy(&self, point: Point) -> Point {
		for (frame, offset) in self.offset_frames() {
			let reverted = offset.revert(point);
			if frame.start <= reverted && frame.stop > reverted {
				return reverted;
			}
			else if frame.start > reverted {
				//skipped
				return frame.start;
			}

		}
		unreachable!()
	}
	fn try_revert(&self, point: Point) -> Result<Point, Ambiguity> {
		let mut found = None;
		for (frame, offset) in self.offset_frames() {
			let reverted = offset.revert(point);
			match frame.contains(&reverted) {
				true => match found {
					Some(previous) => return Err(Ambiguity::Repeated(
						[previous, (reverted, offset)]
					)),
					None => found = Some((reverted, offset))
				},
				false => match found {
					Some((point, _)) => return Ok(point),
					None if frame.start > reverted => return Err(
						Ambiguity::Skipped(frame.start)
					),
					None => continue
				}
			}
		}
		unreachable!()
	}
}
impl Shift for &Table {
	fn apply(&self, point: Point) -> Point {
		(*self).apply(point)
	}
}

impl Unsteady for &Table {
	type Ambiguity = <Table as Unsteady>::Ambiguity;
	fn revert_lossy(&self, point: Point) -> Point {
		(*self).revert_lossy(point)
	}
	fn try_revert(&self, point: Point) -> Result<Point, Self::Ambiguity> {
		(*self).try_revert(point)
	}
}

#[test]
fn offsets() -> SqlResult<()> {
	use greg::utc;

	// get a db
	let nunc = crate::Nunc::testing().unwrap();

	// insert zone entries:
	assert!(nunc.change(utc!(2020-01-01), TZone::Europe__Berlin)?);
	assert!(nunc.change(utc!(2021-01-01), TZone::Iceland)?);

	// load table
	let table = nunc.load_zone_table()?;

	let cet = TZone::Europe__Berlin.offset_at(utc!(2020-01-01));
	let cest = TZone::Europe__Berlin.offset_at(utc!(2020-06-01));

	// check that 4 (5?) offsets exist:
	// utc until 2020
	//assert_eq!(table.offsets[0], (Point::from_epoch(i64::MIN), Offset::UTC));
	// then CET (winter time) until march
	assert_eq!(table.offsets[0], (utc!(2020-01-01), cet));
	// then CEST until october
	assert_eq!(table.offsets[1], (utc!(2020-03-29 01:00:00), cest));
	// then CET again until 2021
	assert_eq!(table.offsets[2], (utc!(2020-10-25 01:00:00), cet));
	// then GMT (Iceland time)
	assert_eq!(table.offsets[3], (utc!(2021-01-01), Offset::GMT));

	assert_eq!(table.offsets.len(), 4);

	assert_eq!(
		table.try_revert(utc!(2020-01-01 00:30:00)),
		Err(Ambiguity::Skipped(utc!(2020-01-01)))
	);

	assert_eq!(
		table.try_revert(utc!(2020-03-29 02:00:00)),
		Err(Ambiguity::Skipped(utc!(2020-03-29 01:00:00)))
	);

	assert_eq!(
		table.try_revert(utc!(2020-10-25 02:00:00)),
		Err(Ambiguity::Repeated([
			(utc!(2020-10-25 00:00:00), cest),
			(utc!(2020-10-25 01:00:00), cet)
		]))
	);
	assert_eq!(
		table.try_revert(utc!(2021-01-01)),
		Err(Ambiguity::Repeated([
			(utc!(2020-12-31 23:00:00), cet),
			(utc!(2021-01-01), Offset::GMT)
		]))
	);

	Ok(())
}