// Copyright 2016-2019 Cargo-Bundle developers <https://github.com/burtonageo/cargo-bundle>
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use std::{
  fs::{create_dir, create_dir_all, read_dir, remove_dir_all},
  path::{Path, PathBuf},
};

/// Directory options.
#[derive(Default, Clone)]
pub struct DirOpts {
  pub depth: u64,
}

/// File options.
pub struct FileOpts {
  pub overwrite: bool,
  pub skip: bool,
  #[allow(dead_code)]
  pub buffer_size: usize,
}

/// Copy options.
#[derive(Clone)]
pub struct Options {
  pub overwrite: bool,
  pub skip: bool,
  pub buffer_size: usize,
  pub copy_files: bool,
  pub content_only: bool,
  pub depth: u64,
}

/// Directory information descriptor
pub struct DirInfo {
  pub size: u64,
  pub files: Vec<String>,
  pub directories: Vec<String>,
}

impl Default for Options {
  fn default() -> Options {
    Options {
      overwrite: false,
      skip: false,
      buffer_size: 64000,
      copy_files: false,
      content_only: false,
      depth: 0,
    }
  }
}

impl Default for FileOpts {
  fn default() -> FileOpts {
    FileOpts {
      overwrite: false,
      skip: false,
      buffer_size: 64000,
    }
  }
}

/// Creates the given directory path,
/// erasing it first if specified.
pub fn create<P>(path: P, erase: bool) -> crate::Result<()>
where
  P: AsRef<Path>,
{
  if erase && path.as_ref().exists() {
    remove(&path)?;
  }
  Ok(create_dir(&path)?)
}

/// Creates all of the directories of the specified path,
/// erasing it first if specified.
pub fn create_all<P>(path: P, erase: bool) -> crate::Result<()>
where
  P: AsRef<Path>,
{
  if erase && path.as_ref().exists() {
    remove(&path)?;
  }
  Ok(create_dir_all(&path)?)
}

/// Removes the directory if it exists.
pub fn remove<P: AsRef<Path>>(path: P) -> crate::Result<()> {
  if path.as_ref().exists() {
    Ok(remove_dir_all(path)?)
  } else {
    Ok(())
  }
}

/// Copy file with the given options.
pub fn copy_file<P, Q>(from: P, to: Q, options: &FileOpts) -> crate::Result<u64>
where
  P: AsRef<Path>,
  Q: AsRef<Path>,
{
  let from = from.as_ref();
  if !from.exists() {
    if let Some(msg) = from.to_str() {
      let msg = format!("Path \"{msg}\" does not exist or you don't have access");
      return Err(crate::Error::PathUtilError(msg));
    }
    return Err(crate::Error::PathUtilError(
      "Path does not exist or you don't have access!".to_owned(),
    ));
  }

  if !from.is_file() {
    if let Some(msg) = from.to_str() {
      let msg = format!("Path \"{msg}\" is not a file!");
      return Err(crate::Error::PathUtilError(msg));
    }
    return Err(crate::Error::PathUtilError(
      "Path is not a file!".to_owned(),
    ));
  }
  if !options.overwrite && to.as_ref().exists() {
    if options.skip {
      return Ok(0);
    }

    if let Some(msg) = to.as_ref().to_str() {
      let msg = format!("Path \"{msg}\" is exist");
      return Err(crate::Error::PathUtilError(msg));
    }
  }

  Ok(std::fs::copy(from, to)?)
}

/// Copies the directory with the given options.
#[allow(dead_code)]
pub fn copy<P, Q>(from: P, to: Q, options: &Options) -> crate::Result<u64>
where
  P: AsRef<Path>,
  Q: AsRef<Path>,
{
  let from = from.as_ref();
  if !from.exists() {
    if let Some(msg) = from.to_str() {
      let msg = format!("Path \"{msg}\" does not exist or you don't have access!");
      return Err(crate::Error::PathUtilError(msg));
    }
    return Err(crate::Error::PathUtilError(
      "Path does not exist or you don't have access".to_owned(),
    ));
  }
  if !from.is_dir() {
    if let Some(msg) = from.to_str() {
      let msg = format!("Path \"{msg}\" is not a directory!");
      return Err(crate::Error::PathUtilError(msg));
    }
    return Err(crate::Error::PathUtilError(
      "Path is not a directory".to_owned(),
    ));
  }

  let dir_name = if let Some(val) = from.components().last() {
    val.as_os_str()
  } else {
    return Err(crate::Error::PathUtilError(
      "Invalid Folder form".to_owned(),
    ));
  };
  let mut to: PathBuf = to.as_ref().to_path_buf();
  if !options.content_only && (!options.copy_files || to.exists()) {
    to.push(dir_name);
  }

  let mut read_options = DirOpts::default();
  if options.depth > 0 {
    read_options.depth = options.depth;
  }

  let dir_content = get_dir_info(from, &read_options)?;
  for directory in dir_content.directories {
    let tmp_to = Path::new(&directory).strip_prefix(from)?;
    let dir = to.join(tmp_to);
    if !dir.exists() {
      if options.copy_files {
        create_all(dir, false)?;
      } else {
        create(dir, false)?;
      }
    }
  }
  let mut result: u64 = 0;
  for file in dir_content.files {
    let to = to.to_path_buf();
    let tp = Path::new(&file).strip_prefix(from)?;
    let path = to.join(tp);

    let file_options = FileOpts {
      overwrite: options.overwrite,
      skip: options.skip,
      buffer_size: options.buffer_size,
    };
    let mut result_copy: crate::Result<u64>;
    let mut work = true;

    while work {
      #[allow(clippy::needless_borrow)]
      {
        result_copy = copy_file(&file, &path, &file_options);
      }
      match result_copy {
        Ok(val) => {
          result += val;
          work = false;
        }
        Err(err) => {
          let err_msg = err.to_string();
          return Err(crate::Error::PathUtilError(err_msg));
        }
      }
    }
  }
  Ok(result)
}

/// Gets the DirInfo from the directory path with the given options.
pub fn get_dir_info<P>(path: P, options: &DirOpts) -> crate::Result<DirInfo>
where
  P: AsRef<Path>,
{
  let depth = if options.depth == 0 {
    0
  } else {
    options.depth + 1
  };

  _get_dir_info(path, depth)
}

/// Gets the DirInfo from the directory with the given depth.
fn _get_dir_info<P>(path: P, mut depth: u64) -> crate::Result<DirInfo>
where
  P: AsRef<Path>,
{
  let mut directories = Vec::new();
  let mut files = Vec::new();
  let mut size = 0;
  let item = path.as_ref().to_str();
  if item.is_none() {
    return Err(crate::Error::PathUtilError("Invalid Path".to_owned()));
  }
  let item = item.expect("Item had no data").to_string();

  if path.as_ref().is_dir() {
    directories.push(item);
    if depth == 0 || depth > 1 {
      if depth > 1 {
        depth -= 1;
      }
      for entry in read_dir(&path)? {
        let _path = entry?.path();

        match _get_dir_info(_path, depth) {
          Ok(items) => {
            let mut _files = items.files;
            let mut _directories = items.directories;
            size += items.size;
            files.append(&mut _files);
            directories.append(&mut _directories);
          }
          Err(err) => return Err(err),
        }
      }
    }
  } else {
    size = path.as_ref().metadata()?.len();
    files.push(item);
  }
  Ok(DirInfo {
    size,
    files,
    directories,
  })
}
