//! Primary file copying operation.

use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;

use crate::backend::CopyDest;
use crate::backend::CopyDestFolder;
use crate::backend::CopySource;
use crate::backend::PathMeta;
use crate::backend::PathVisitor;
use crate::reporting::*;

#[derive(Debug, Default, Clone)]
pub struct Copier {}

struct CopyVisitor<'s, 'd, S: CopySource> {
    src: &'s S,
    dirs: Vec<Box<dyn CopyDestFolder<'d> + 'd>>,
    dir_idx: HashMap<PathBuf, usize>,
    n_files: i32,
    n_dirs: i32,
}

impl Copier {
    pub fn new() -> Copier {
        Self::default()
    }

    pub fn copy<S: CopySource, D: CopyDest>(&self, src: &S, dst: &D) -> Result<(), CopyError> {
        info!("copying directory {}", src);

        let mut visitor = CopyVisitor::create(src, dst.open()?);

        src.walk_paths(&mut visitor)?;

        let ndel = visitor.finish()?;

        info!(
            "copied {} files and {} directories, deleted {}",
            visitor.n_files, visitor.n_dirs, ndel
        );
        Ok(())
    }
}

impl<'s, 'd, S: CopySource> CopyVisitor<'s, 'd, S> {
    fn create(src: &'s S, root: Box<dyn CopyDestFolder<'d> + 'd>) -> Self {
        let mut visitor = CopyVisitor {
            src: src,
            dirs: Default::default(),
            dir_idx: Default::default(),
            n_files: 0,
            n_dirs: 0,
        };
        visitor.dirs.push(root);
        visitor.dir_idx.insert(PathBuf::new(), 0);
        visitor
    }
}

impl<'s, 'd, S: CopySource> CopyVisitor<'s, 'd, S> {
    fn finish(&mut self) -> Result<usize, CopyError> {
        info!("finishing copy");
        let mut n = 0;
        self.dir_idx.clear();
        while let Some(mut dir) = self.dirs.pop() {
            n += dir.finish()?;
        }
        Ok(n)
    }
}

impl<'s, 'd, S: CopySource> PathVisitor for CopyVisitor<'s, 'd, S> {
    fn visit_dir(&mut self, path: &Path, _meta: &PathMeta) -> Result<(), CopyError> {
        if path == PathBuf::new() {
            debug!("visiting root");
            return Ok(());
        }
        debug!("visiting directory {:?}", path);
        let dir = if let Some(parent) = path.parent() {
            let idx = self
                .dir_idx
                .get(parent)
                .ok_or_else(|| CopyError::Generic(format!("{:?} has unknown parent", path)))?;
            &mut self.dirs[*idx]
        } else {
            return Err(CopyError::Generic(format!("{:?} has no parent", path)));
        };
        let nd = dir.add_directory(
            path.file_name()
                .ok_or_else(|| CopyError::Generic(format!("{:?} has no name", path)))?,
        )?;
        self.dir_idx.insert(path.to_path_buf(), self.dirs.len());
        self.dirs.push(nd);
        Ok(())
    }

    fn visit_file(&mut self, path: &Path, _meta: &PathMeta) -> Result<(), CopyError> {
        debug!("visiting file {:?}", path);
        let dir = if let Some(parent) = path.parent() {
            let idx = self
                .dir_idx
                .get(parent)
                .ok_or_else(|| CopyError::Generic(format!("{:?} has unknown parent", path)))?;
            &mut self.dirs[*idx]
        } else {
            return Err(CopyError::Generic(format!("{:?} has no parent", path)));
        };
        dir.add_file(
            path.file_name()
                .ok_or_else(|| CopyError::Generic(format!("{:?} has no name", path)))?,
            self.src,
        )?;
        Ok(())
    }
}
