complish 0.0.1

Core library for project-aware task management with git integration
Documentation
mod project_builder;
mod project_guard;
mod task_builder;
mod task_guard;
mod vault_index;

use std::{collections::HashMap, fs, path::PathBuf};

use bincode::config;
use chrono::{DateTime, Utc};
use eyre::{Result, eyre};
use serde::{Deserialize, Serialize};
pub use vault_index::VaultIndex;

use crate::{
  path,
  project::Project,
  task::{Task, TaskList},
  vault::{
    project_builder::ProjectBuilder, project_guard::ProjectGuard, task_builder::TaskBuilder,
    task_guard::TaskGuard,
  },
};

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Vault {
  pub created_at: DateTime<Utc>,
  pub index: VaultIndex,
  #[serde(skip)]
  pub path: PathBuf,
  pub projects: HashMap<u32, Project>,
  pub task_lists: HashMap<String, TaskList>,
  pub tasks: HashMap<u32, Task>,
  pub updated_at: DateTime<Utc>,
}

impl Vault {
  pub fn load() -> Result<Self> {
    let vault_path = path::vault_file();

    if !vault_path.exists() {
      return Ok(Self::default());
    }

    let bytes = fs::read(&vault_path)?;
    let (vault, _) = bincode::serde::decode_from_slice(&bytes, config::standard())?;

    let mut vault: Vault = vault;
    vault.path = vault_path;

    Ok(vault)
  }

  pub fn add_project(&'_ mut self, name: impl Into<String>) -> ProjectBuilder<'_> {
    ProjectBuilder::new(self, name)
  }

  pub fn add_task(&'_ mut self, subject: impl Into<String>) -> TaskBuilder<'_> {
    TaskBuilder::new(self, subject)
  }

  pub fn delete_project(&mut self, project_id: u32) -> Result<()> {
    if !self.projects.contains_key(&project_id) {
      return Err(eyre!("Project {} not found", project_id));
    }

    let project = &self.projects[&project_id];
    let task_ids = project.task_ids.clone();

    for task_id in task_ids {
      if let Some(task) = self.tasks.get_mut(&task_id) {
        task.friendly_id = self.index.allocate_canonical_id().to_string();
      }
      self.index.remove_task_from_project(task_id);
    }

    let project = self.projects[&project_id].clone();
    self.projects.remove(&project_id);
    self.index.remove_project(&project);

    self.updated_at = Utc::now();
    self.save()?;
    Ok(())
  }

  pub fn delete_task(&mut self, task_id: u32) -> Result<()> {
    if !self.tasks.contains_key(&task_id) {
      return Err(eyre!("Task {} not found", task_id));
    }

    if let Some(list_name) = self.index.get_list_name_for_task(task_id)
      && let Some(list) = self.task_lists.get_mut(list_name) {
        list.task_ids.retain(|&id| id != task_id);
      }

    if let Some(project_id) = self.index.get_project_id_for_task(task_id)
      && let Some(project) = self.projects.get_mut(&project_id) {
        project.task_ids.retain(|&id| id != task_id);
      }

    let task = self.tasks[&task_id].clone();
    self.tasks.remove(&task_id);
    self.index.remove_task(&task);

    self.updated_at = Utc::now();
    self.save()?;
    Ok(())
  }

  pub fn get_project(&self, id: u32) -> Result<Project> {
    self
      .projects
      .get(&id)
      .cloned()
      .ok_or_else(|| eyre!("Project not found"))
  }

  pub fn get_project_by_key(&self, key: impl Into<String>) -> Result<Project> {
    let key = key.into();
    let id = self
      .index
      .get_canonical_project_id_from_key(&key)
      .ok_or_else(|| eyre!("Project not found"))?;
    self.get_project(id)
  }

  pub fn get_project_by_key_mut(&mut self, key: impl Into<String>) -> Result<ProjectGuard<'_>> {
    let project = self.get_project_by_key(key)?;
    self.get_project_mut(project.id)
  }

  pub fn get_project_mut(&mut self, id: u32) -> Result<ProjectGuard<'_>> {
    ProjectGuard::new(id, self)
  }

  pub fn get_task(&self, id: u32) -> Result<Task> {
    self
      .tasks
      .get(&id)
      .cloned()
      .ok_or_else(|| eyre!("Task not found"))
  }

  pub fn get_task_by_friendly_id(&self, id: impl Into<String>) -> Result<Task> {
    let friendly_id = id.into();
    let canonical_id = self
      .index
      .get_canonical_id_from_friendly_id(&friendly_id)
      .ok_or_else(|| eyre!("Task not found"))?;
    self.get_task(canonical_id)
  }

  pub fn get_task_by_friendly_id_mut(&mut self, id: impl Into<String>) -> Result<TaskGuard<'_>> {
    let task = self.get_task_by_friendly_id(id)?;
    self.get_task_mut(task.id)
  }

  pub fn get_task_mut(&mut self, id: u32) -> Result<TaskGuard<'_>> {
    TaskGuard::new(id, self)
  }

  pub fn list_all_projects(&self) -> Vec<Project> {
    self.projects.values().cloned().collect()
  }

  pub fn list_tasks_in_list(&self, list_name: &str) -> Result<Vec<Task>> {
    let task_list = self
      .task_lists
      .get(list_name)
      .ok_or_else(|| eyre!("List '{}' not found", list_name))?;

    let tasks = task_list
      .task_ids
      .iter()
      .filter_map(|&id| self.tasks.get(&id).cloned())
      .collect();

    Ok(tasks)
  }

  pub fn list_tasks_in_project(&self, project_id: u32) -> Result<Vec<Task>> {
    let project = self.get_project(project_id)?;

    let tasks = project
      .task_ids
      .iter()
      .filter_map(|&id| self.tasks.get(&id).cloned())
      .collect();

    Ok(tasks)
  }

  pub fn move_multiple_tasks_to_list(
    &mut self,
    task_ids: &[u32],
    list_name: impl Into<String>,
  ) -> Result<()> {
    let list_name = list_name.into();

    for &task_id in task_ids {
      if !self.tasks.contains_key(&task_id) {
        return Err(eyre!("Task {} not found", task_id));
      }
    }
    if !self.task_lists.contains_key(&list_name) {
      return Err(eyre!("Invalid list '{}'", &list_name));
    }

    for &task_id in task_ids {
      let old_list_name = self.index.get_list_name_for_task(task_id);

      if let Some(old_list_name) = old_list_name
        && let Some(list) = self.task_lists.get_mut(old_list_name) {
          list.task_ids.retain(|&id| id != task_id);
        }

      self.index.move_task_to_list(task_id, &list_name);
    }

    self
      .task_lists
      .get_mut(&list_name)
      .unwrap()
      .task_ids
      .extend_from_slice(task_ids);

    self.updated_at = Utc::now();
    self.save()?;
    Ok(())
  }

  pub fn move_task_to_list(&mut self, task_id: u32, list_name: impl Into<String>) -> Result<()> {
    if !self.tasks.contains_key(&task_id) {
      return Err(eyre!("Task {} not found", task_id));
    }

    let old_list_name = self.index.get_list_name_for_task(task_id);
    let new_list_name = list_name.into();

    if !self.task_lists.contains_key(&new_list_name) {
      return Err(eyre!("Invalid list '{}'", &new_list_name));
    }

    self
      .task_lists
      .get_mut(&new_list_name)
      .unwrap()
      .task_ids
      .push(task_id);

    if let Some(old_list_name) = old_list_name
      && let Some(list) = self.task_lists.get_mut(old_list_name) {
        list.task_ids.retain(|&id| id != task_id);
      }

    self.index.move_task_to_list(task_id, &new_list_name);

    self.updated_at = Utc::now();
    self.save()?;
    Ok(())
  }

  pub fn move_task_to_project(&mut self, task_id: u32, project_id: u32) -> Result<()> {
    if !self.tasks.contains_key(&task_id) {
      return Err(eyre!("Task {} not found", task_id));
    }
    if !self.projects.contains_key(&project_id) {
      return Err(eyre!("Project {} not found", project_id));
    }

    let old_project_id = self.index.get_project_id_for_task(task_id);

    let project_key = self.projects[&project_id].key.clone();
    let new_friendly_id = format!(
      "{}-{}",
      project_key,
      self.index.allocate_friendly_id_for_project(project_id)
    );

    self.tasks.get_mut(&task_id).unwrap().friendly_id = new_friendly_id;
    self
      .projects
      .get_mut(&project_id)
      .unwrap()
      .task_ids
      .push(task_id);

    if let Some(old_project_id) = old_project_id
      && let Some(old_project) = self.projects.get_mut(&old_project_id)
    {
      old_project.task_ids.retain(|&id| id != task_id);
    }

    self.index.move_task_to_project(task_id, project_id);

    self.updated_at = Utc::now();
    self.save()?;
    Ok(())
  }

  pub fn save(&self) -> Result<()> {
    if let Some(parent) = self.path.parent() {
      fs::create_dir_all(parent)?;
    }

    let bytes = bincode::serde::encode_to_vec(self, config::standard())?;
    fs::write(&self.path, bytes)?;
    Ok(())
  }
}

impl Default for Vault {
  fn default() -> Self {
    let mut task_lists = HashMap::new();
    for name in &["today", "next", "someday"] {
      let list = TaskList::new(*name);
      task_lists.insert((*name).to_string(), list);
    }

    let now = Utc::now();

    Self {
      created_at: now,
      index: VaultIndex::default(),
      path: path::vault_file(),
      projects: HashMap::new(),
      task_lists,
      tasks: HashMap::new(),
      updated_at: now,
    }
  }
}