use std::str::FromStr;
use chrono::{Duration, NaiveDate, NaiveDateTime};
use ical::property::Property;
use super::priority::TaskPriority;
use tatuin_core::{
RichString,
project::Project as ProjectTrait,
task::{DateTimeUtc, PatchPolicy, Priority, State, Task as TaskTrait},
};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum TaskType {
Event,
#[default]
Todo,
}
#[derive(strum::EnumString, strum::Display, Clone, Debug, Default, PartialEq, Eq)]
pub enum TaskStatus {
#[strum(serialize = "TENTATIVE")]
Tentative,
#[strum(serialize = "CONFIRMED")]
#[default]
Confirmed,
#[strum(serialize = "CANCELLED")]
Cancelled,
#[strum(serialize = "NEEDS-ACTION")]
NeedsAction,
#[strum(serialize = "COMPLETED")]
Completed,
#[strum(serialize = "IN-PROCESS")]
InProcess,
#[strum(serialize = "DRAFT")]
Draft,
#[strum(serialize = "FINAL")]
Final,
}
impl From<TaskStatus> for State {
fn from(value: TaskStatus) -> Self {
match value {
TaskStatus::Tentative | TaskStatus::Confirmed | TaskStatus::NeedsAction | TaskStatus::Draft => {
State::Uncompleted
}
TaskStatus::Completed | TaskStatus::Final | TaskStatus::Cancelled => State::Completed,
TaskStatus::InProcess => State::InProgress,
}
}
}
impl From<State> for TaskStatus {
fn from(value: State) -> Self {
match value {
State::Unknown(_) | State::Uncompleted => TaskStatus::Confirmed,
State::Completed => TaskStatus::Completed,
State::InProgress => TaskStatus::InProcess,
}
}
}
#[derive(Default, Clone)]
pub struct Task {
pub provider: String,
pub properties: Vec<ical::property::Property>,
pub href: String,
pub etag: Option<String>,
pub task_type: TaskType,
pub patch_policy: PatchPolicy,
pub uid: String,
pub name: String,
pub description: Option<String>,
pub priority: TaskPriority,
pub status: TaskStatus,
pub start: Option<DateTimeUtc>,
pub end: Option<DateTimeUtc>,
pub due: Option<DateTimeUtc>,
pub completed: Option<DateTimeUtc>,
pub created: Option<DateTimeUtc>,
pub duration: Option<Duration>,
pub categories: Vec<String>,
}
impl std::fmt::Debug for Task {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Task uuid={} name={} description={:?} status={:?} priority={} start={:?} end={:?} due={:?} completed={:?} created={:?} duration={:?} categories={:?} properties={:?} href={} etag={:?} type={:?}",
self.uid,
self.name,
self.description,
self.status,
self.priority,
self.start,
self.end,
self.due,
self.completed,
self.created,
self.duration,
self.categories,
self.properties,
self.href,
self.etag,
self.task_type,
)
}
}
impl Task {
pub fn is_valid(&self) -> bool {
!self.uid.is_empty() && !self.name.is_empty()
}
pub fn set_provider(&mut self, p: &str) {
self.provider = p.to_string();
}
}
impl TaskTrait for Task {
fn id(&self) -> String {
self.uid.clone()
}
fn name(&self) -> RichString {
RichString::new(&self.name)
}
fn state(&self) -> State {
match self.status {
TaskStatus::Tentative | TaskStatus::Confirmed | TaskStatus::NeedsAction | TaskStatus::Draft => {
State::Uncompleted
}
TaskStatus::Completed | TaskStatus::Final | TaskStatus::Cancelled => State::Completed,
TaskStatus::InProcess => State::InProgress,
}
}
fn provider(&self) -> String {
self.provider.clone()
}
fn project(&self) -> Option<Box<dyn ProjectTrait>> {
None
}
fn due(&self) -> Option<DateTimeUtc> {
if self.due.is_some() { self.due } else { self.start }
}
fn completed_at(&self) -> Option<DateTimeUtc> {
self.completed
}
fn priority(&self) -> Priority {
self.priority.into()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn clone_boxed(&self) -> Box<dyn TaskTrait> {
Box::new(self.clone())
}
fn const_patch_policy(&self) -> PatchPolicy {
self.patch_policy.clone()
}
fn description(&self) -> Option<RichString> {
self.description.as_ref().map(|s| RichString::new(s))
}
fn created_at(&self) -> Option<DateTimeUtc> {
self.created
}
fn place(&self) -> String {
self.provider()
}
fn labels(&self) -> Vec<String> {
self.categories.clone()
}
}
impl From<&Vec<Property>> for Task {
fn from(properties: &Vec<Property>) -> Self {
let mut t = Task {
properties: properties.to_vec(),
..Task::default()
};
for p in properties {
match p.name.as_str() {
"UID" => t.uid = p.value.clone().unwrap_or_default(),
"SUMMARY" => t.name = p.value.clone().unwrap_or_default(),
"DESCRIPTION" => t.description = p.value.clone(),
"PRIORITY" => {
t.priority = p
.value
.as_ref()
.map(|s| TaskPriority::new(s.parse::<u8>().unwrap_or(0)))
.unwrap_or_default()
}
"STATUS" => {
t.status = p
.value
.as_ref()
.map(|s| TaskStatus::from_str(s).unwrap_or_default())
.unwrap_or_default()
}
"DUE" => t.due = dt_from_property(p),
"DTSTART" => t.start = dt_from_property(p),
"DTEND" => t.end = dt_from_property(p),
"COMPLETED" => t.completed = dt_from_property(p),
"CREATED" => t.created = dt_from_property(p),
"DURATION" => t.duration = duration_from_property(p),
"CATEGORIES" if p.value.is_some() => {
t.categories = p
.value
.as_ref()
.unwrap()
.split(",")
.map(|s| s.to_string())
.collect::<Vec<String>>()
}
_ => {}
}
}
tracing::debug!(task=?t, "New task");
t
}
}
pub fn property_to_str(value: &Property) -> String {
format!("{}:{}", value.name, value.value.as_ref().unwrap_or(&String::new()))
}
fn make_property(name: &str, value: Option<String>) -> Property {
Property {
name: name.to_string(),
params: None,
value,
}
}
fn replace_or_add(properties: &mut Vec<Property>, p: Property) {
properties.retain(|prop| prop.name != p.name);
if p.value.as_ref().is_some_and(|s| !s.is_empty()) {
properties.push(p);
}
}
impl From<&Task> for Vec<Property> {
fn from(t: &Task) -> Self {
const DT_FORMAT: &str = "%Y%m%dT%H%M%SZ";
let mut result = t.properties.clone();
replace_or_add(&mut result, make_property("SUMMARY", Some(t.name.clone())));
replace_or_add(&mut result, make_property("DESCRIPTION", t.description.clone()));
replace_or_add(&mut result, make_property("STATUS", Some(t.status.to_string())));
replace_or_add(&mut result, make_property("PRIORITY", Some(t.priority.to_string())));
replace_or_add(
&mut result,
make_property("DUE", t.due.map(|d| d.format(DT_FORMAT).to_string())),
);
replace_or_add(
&mut result,
make_property("CREATED", Some(chrono::Utc::now().format(DT_FORMAT).to_string())),
);
replace_or_add(
&mut result,
make_property("DTSTART", t.start.map(|d| d.format(DT_FORMAT).to_string())),
);
replace_or_add(
&mut result,
make_property("DTEND", t.end.map(|d| d.format(DT_FORMAT).to_string())),
);
result
}
}
fn tz_offset_from_property_params(params: &Option<Vec<(String, Vec<String>)>>) -> Option<chrono_tz::Tz> {
if let Some(params) = params {
for (n, p) in params {
if n == "TZID"
&& p.len() == 1
&& let Ok(t) = p[0].parse::<chrono_tz::Tz>()
{
return Some(t);
}
}
}
None
}
fn dt_from_property(p: &Property) -> Option<DateTimeUtc> {
let s = p.value.as_ref()?;
if let Ok(d) = NaiveDate::parse_from_str(s, "%Y%m%d") {
let dt = d.and_hms_opt(0, 0, 0)?;
return Some(DateTimeUtc::from_naive_utc_and_offset(dt, chrono::Utc));
}
if let Some(tz) = tz_offset_from_property_params(&p.params)
&& let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y%m%dT%H%M%S")
{
let dt = dt.and_local_timezone(tz).unwrap();
return Some(dt.to_utc());
}
if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y%m%dT%H%M%SZ") {
return Some(DateTimeUtc::from_naive_utc_and_offset(dt, chrono::Utc));
}
None
}
fn duration_from_property(p: &Property) -> Option<Duration> {
if let Some(v) = &p.value
&& let Ok(d) = v.parse::<iso8601_duration::Duration>()
{
return d.to_chrono();
}
None
}