complish 0.0.1

Core library for project-aware task management with git integration
Documentation
use std::collections::HashMap;

use serde::{Deserialize, Serialize};

use crate::{
  project::Project,
  task::{Task, TaskList},
};

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct VaultIndex {
  pub next_canonical_id: u32,
  pub next_canonical_project_id: u32,
  pub next_friendly_id_by_project: HashMap<u32, u32>,
  pub next_friendly_id_by_default: u32,
  pub project_key_to_canonical_id: HashMap<String, Vec<u32>>,
  pub task_friendly_id_to_canonical_id: HashMap<String, u32>,
  pub task_id_to_list_name: HashMap<u32, String>,
  pub task_id_to_project_id: HashMap<u32, Option<u32>>,
}

impl VaultIndex {
  pub fn allocate_canonical_id(&mut self) -> u32 {
    let id = self.next_canonical_id;
    self.next_canonical_id += 1;
    id
  }

  pub fn allocate_canonical_project_id(&mut self) -> u32 {
    let id = self.next_canonical_project_id;
    self.next_canonical_project_id += 1;
    id
  }

  pub fn allocate_friendly_id_for_default(&mut self) -> u32 {
    let id = self.next_friendly_id_by_default;
    self.next_friendly_id_by_default += 1;
    id
  }

  pub fn allocate_friendly_id_for_project(&mut self, project_id: u32) -> u32 {
    let counter = self
      .next_friendly_id_by_project
      .entry(project_id)
      .or_insert(1);
    let id = *counter;
    *counter += 1;
    id
  }

  pub fn get_canonical_id_from_friendly_id(&self, friendly_id: impl Into<String>) -> Option<u32> {
    self
      .task_friendly_id_to_canonical_id
      .get(&friendly_id.into())
      .copied()
  }

  pub fn get_canonical_project_id_from_key(&self, project_key: impl Into<String>) -> Option<u32> {
    self
      .project_key_to_canonical_id
      .get(&project_key.into())
      .and_then(|ids| ids.last().copied())
  }

  pub fn get_all_canonical_project_ids_from_key(&self, project_key: impl Into<String>) -> Vec<u32> {
    self
      .project_key_to_canonical_id
      .get(&project_key.into())
      .cloned()
      .unwrap_or_default()
  }

  pub fn get_list_name_for_task(&self, task_id: u32) -> Option<&String> {
    self.task_id_to_list_name.get(&task_id)
  }

  pub fn get_project_id_for_task(&self, task_id: u32) -> Option<u32> {
    *self.task_id_to_project_id.get(&task_id).unwrap_or(&None)
  }

  pub fn peek_next_canonical_id(&self) -> u32 {
    self.next_canonical_id
  }

  pub fn peek_next_canonical_project_id(&self) -> u32 {
    self.next_canonical_project_id
  }

  pub fn peek_next_friendly_id_by_project(&self, project_id: u32) -> u32 {
    *self
      .next_friendly_id_by_project
      .get(&project_id)
      .unwrap_or(&1)
  }

  pub fn peek_next_friendly_id_by_default(&self) -> u32 {
    self.next_friendly_id_by_default
  }

  pub fn move_task_to_list(&mut self, task_id: u32, list_name: impl Into<String>) {
    self.task_id_to_list_name.insert(task_id, list_name.into());
  }

  pub fn move_task_to_project(&mut self, task_id: u32, project_id: u32) {
    self.task_id_to_project_id.insert(task_id, Some(project_id));
  }

  pub fn register_list(&mut self, task_list: &TaskList) {
    task_list.task_ids.iter().for_each(|task_id| {
      self
        .task_id_to_list_name
        .insert(*task_id, task_list.name.clone());
    });
  }

  pub fn register_project(&mut self, project: &Project) {
    self
      .project_key_to_canonical_id
      .entry(project.key.clone())
      .or_default()
      .push(project.id);

    project.task_ids.iter().for_each(|task_id| {
      self
        .task_id_to_project_id
        .insert(*task_id, Some(project.id));
    });
  }

  pub fn register_task(
    &mut self,
    task: &Task,
    list_name: impl Into<String>,
    project_id: Option<u32>,
  ) {
    self
      .task_friendly_id_to_canonical_id
      .insert(task.friendly_id.clone(), task.id);
    self.task_id_to_list_name.insert(task.id, list_name.into());

    if let Some(project_id) = project_id {
      self.task_id_to_project_id.insert(task.id, Some(project_id));
    }
  }

  pub fn remove_project(&mut self, project: &Project) {
    if let Some(ids) = self.project_key_to_canonical_id.get_mut(&project.key) {
      ids.retain(|&id| id != project.id);
      if ids.is_empty() {
        self.project_key_to_canonical_id.remove(&project.key);
      }
    }

    self.next_friendly_id_by_project.remove(&project.id);
    self
      .task_id_to_project_id
      .retain(|_, project_opt| project_opt.is_none_or(|pid| pid != project.id));
  }

  pub fn remove_task(&mut self, task: &Task) {
    self
      .task_friendly_id_to_canonical_id
      .remove(&task.friendly_id);
    self.task_id_to_list_name.remove(&task.id);
    self.task_id_to_project_id.remove(&task.id);
  }

  pub fn remove_task_from_project(&mut self, task_id: u32) {
    self.task_id_to_project_id.remove(&task_id);
  }
}

impl Default for VaultIndex {
  fn default() -> Self {
    Self {
      next_canonical_id: 1,
      next_canonical_project_id: 1,
      next_friendly_id_by_project: HashMap::new(),
      next_friendly_id_by_default: 1,
      project_key_to_canonical_id: HashMap::new(),
      task_friendly_id_to_canonical_id: HashMap::new(),
      task_id_to_list_name: HashMap::new(),
      task_id_to_project_id: HashMap::new(),
    }
  }
}