mod file_tree;
mod ftp_tree;
mod gcs_tree;
mod one_drive_tree;
mod s3_tree;
mod sftp_tree;

use crate::error::Error;
use async_trait::async_trait;
use futures::{StreamExt, TryStreamExt};
use serde::{Deserialize, Serialize};
use std::time::SystemTime;

pub type TreeItems = Vec<TreeItem>;

#[async_trait]
pub trait TreeList {
  /// List items in a directory
  async fn list(&self, root_path: &str, prefix: Option<&str>) -> Result<TreeItems, Error>;

  /// List items recursively from a directory
  async fn list_tree(&self, root_path: &str, prefix: Option<&str>) -> Result<TreeItems, Error> {
    let mut items = self.list(root_path, prefix).await?;

    let folders = items
      .iter()
      .filter(|item| item.kind() == TreeItemKind::Folder);

    let mut sub_items = async_std::stream::from_iter(folders)
      .then(|folder| async move {
        self
          .list_tree(root_path, Some(folder.relative_path()))
          .await
      })
      .try_collect::<Vec<TreeItems>>()
      .await?
      .into_iter()
      .flatten()
      .collect::<TreeItems>();

    items.append(&mut sub_items);

    Ok(items)
  }

  fn get_absolute_prefix(root_path: &str, prefix: Option<&str>, separator: char) -> String {
    if let Some(prefix) = prefix {
      join_path(root_path, prefix, separator)
    } else {
      root_path
        .to_string()
        .trim_end_matches(separator)
        .to_string()
    }
  }

  fn get_absolute_path(root_path: &str, prefix: Option<&str>, separator: char) -> String {
    format!(
      "{}{}",
      separator,
      Self::get_absolute_prefix(root_path, prefix, separator).trim_start_matches(separator)
    )
  }

  fn get_delimiter(separator: char) -> String {
    separator.to_string()
  }

  fn get_root_path(root_path: &str, separator: char) -> String {
    format!(
      "{}{}",
      separator,
      root_path
        .trim_start_matches(separator)
        .trim_end_matches(separator)
    )
  }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct TreeItem {
  #[serde(rename = "type")]
  kind: TreeItemKind,
  path: String,
  root_path: String,
  size: u64,
  last_update: SystemTime,
  mime_type: Option<String>,
  separator: char,
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TreeItemKind {
  Folder,
  File,
  Link,
}

impl TreeItem {
  pub fn new(
    kind: TreeItemKind,
    path: &str,
    root_path: &str,
    size: u64,
    last_update: SystemTime,
    mime_type: Option<String>,
    separator: char,
  ) -> Self {
    Self {
      kind,
      path: path.to_string(),
      root_path: root_path.to_string(),
      size,
      last_update,
      mime_type,
      separator,
    }
  }

  pub fn kind(&self) -> TreeItemKind {
    self.kind
  }

  pub fn path(&self) -> String {
    join_path(&self.root_path, &self.path, self.separator)
  }

  pub fn relative_path(&self) -> &str {
    &self.path
  }

  pub fn root_path(&self) -> &str {
    &self.root_path
  }

  pub fn size(&self) -> u64 {
    self.size
  }

  pub fn last_update(&self) -> SystemTime {
    self.last_update
  }

  pub fn mime_type(&self) -> &Option<String> {
    &self.mime_type
  }

  pub fn separator(&self) -> char {
    self.separator
  }
}

pub fn join_path(root: &str, path: &str, separator: char) -> String {
  let root = root.trim_end_matches(separator);
  let path = path.trim_start_matches(separator);

  format!("{root}{separator}{path}")
}

pub fn strip_relative_path(prefix: &str, path: &str, separator: char) -> String {
  if prefix == separator.to_string() {
    format!(
      "{separator}{}",
      path
        .trim_start_matches(separator)
        .trim_end_matches(separator)
    )
  } else {
    path
      .trim_start_matches(separator)
      .trim_start_matches(
        prefix
          .trim_start_matches(separator)
          .trim_end_matches(separator),
      )
      .trim_end_matches(separator)
      .to_string()
  }
}
