pub use error::*;

use fs_err as fs;
use tracing::trace;
use walkdir::WalkDir;

use std::ops::Not;
use std::path::Path;

use crate::filter::DistributionFilter;


pub(crate) fn copy_dir_all(
    source_dir: impl AsRef<Path>,
    target_dir: impl AsRef<Path>,
) -> CopyDirResult<()> {
    copy_dir_impl(source_dir, target_dir, &DistributionFilter::Disabled, None)
}

pub(crate) fn copy_dir_filtered(
    source_dir: impl AsRef<Path>,
    target_dir: impl AsRef<Path>,
    target_filter: &DistributionFilter,
    prefix_to_strip_before_matching: Option<&Path>,
) -> CopyDirResult<()> {
    copy_dir_impl(source_dir, target_dir, target_filter, prefix_to_strip_before_matching)
}

pub(crate) fn copy_dir_impl(
    source_dir: impl AsRef<Path>,
    target_dir: impl AsRef<Path>,
    target_filter: &DistributionFilter,
    prefix_to_strip_before_matching: Option<&Path>,
) -> CopyDirResult<()> {
    let source_dir = source_dir.as_ref();
    let target_dir = target_dir.as_ref();
    let error = |source: CopyDirErrorSource| {
        CopyDirError::new(source_dir, target_dir, source)
    };

    if source_dir.exists().not() {
        return Err(error(CopyDirErrorSource::Message("the source directory does not exist")));
    }

    for source_path in WalkDir::new(source_dir) {
        let source_path = source_path
            .map_err(|cause| error(CopyDirErrorSource::WalkDir(Box::new(cause))))?;
        let source_path = source_path.path();

        let path_relative_to_source_dir = source_path.strip_prefix(source_dir)
            .unwrap_or_else(|_| panic!("Source path {source_path:?} was not a child of the source directory {source_dir:?} while copying this directory recursively. This should not happen, unless there is a bug."));

        let target_path = target_dir.join(path_relative_to_source_dir);

        {
            let matchable_target_path =
                if let Some(prefix_to_strip_before_matching) = prefix_to_strip_before_matching {
                    target_path.strip_prefix(prefix_to_strip_before_matching)
                        .map_err(|cause| error(CopyDirErrorSource::StripPrefix(cause)))?
                } else {
                    target_path.as_path()
                };

            if target_filter.matches_path(matchable_target_path).not() {
                trace!("Skipping inclusion of {matchable_target_path:?}, since it does not match filter: {target_filter:?}");
                continue;
            }
        }

        if source_path.is_dir() {
            fs::create_dir_all(target_path)
                .map_err(|cause| error(CopyDirErrorSource::Io(cause)))?;
        }
        else if target_path.exists() {
            return Err(error(CopyDirErrorSource::Message("the path already exists")));
        } else {
            fs::copy(source_path, target_path)
                .map_err(|cause| error(CopyDirErrorSource::Io(cause)))?;
        }
    }

    Ok(())
}

pub mod error {
    use std::{fmt, path::PathBuf};

    pub type CopyDirResult<T> = std::result::Result<T, CopyDirError>;

    #[derive(Debug)]
    pub struct CopyDirError {
        pub source_dir: PathBuf,
        pub target_dir: PathBuf,
        pub source: CopyDirErrorSource,
    }
    #[derive(Debug)]
    pub enum CopyDirErrorSource {
        Io(std::io::Error),
        WalkDir(Box<walkdir::Error>),
        StripPrefix(std::path::StripPrefixError),
        Message(&'static str),
    }

    impl CopyDirError {
        pub fn new(source_dir: impl Into<PathBuf>, target_dir: impl Into<PathBuf>, source: CopyDirErrorSource) -> Self {
            Self {
                source_dir: source_dir.into(),
                target_dir: target_dir.into(),
                source,
            }
        }
    }

    impl std::error::Error for CopyDirError {
        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
            match &self.source {
                CopyDirErrorSource::Io(error) => Some(error),
                CopyDirErrorSource::WalkDir(error) => Some(error),
                CopyDirErrorSource::StripPrefix(error) => Some(error),
                CopyDirErrorSource::Message(_) => None,
            }
        }
    }

    impl fmt::Display for CopyDirError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "Error while recursively copying directory {:?} to {:?}", self.source_dir, self.target_dir)?;
            match &self.source {
                CopyDirErrorSource::Io(_) => write!(f, " when performing IO operation"),
                CopyDirErrorSource::WalkDir(_) => write!(f, " when walking directory"),
                CopyDirErrorSource::StripPrefix(_) => write!(f, " when stripping a path prefix"),
                CopyDirErrorSource::Message(message) => write!(f, " because {message}"),
            }
        }
    }
}


#[cfg(test)]
mod tests {
    use super::*;

    use assert_fs::prelude::*;


    #[test]
    fn should_copy_a_dir_into_nonexistent_target() -> cicero_core::Result<()> {
        let temp = assert_fs::TempDir::new()?;

        let source = temp.child("source");

        source.child("subfile1")
            .write_str("subfile1")?;

        source.child("subfile2")
            .write_str("subfile2")?;

        let source_subdir = source.child("subdir");
        source_subdir.create_dir_all()?;

        source_subdir.child("subdir_subfile")
            .write_str("subdir_subfile")?;


        let target = temp.child("target");
        copy_dir_all(&source, &target)?;


        assert!(target.child("subfile1").exists());
        assert!(target.child("subfile2").exists());
        assert!(target.child("subdir").exists());
        assert!(target.child("subdir").child("subdir_subfile").exists());

        Ok(())
    }

    #[test]
    fn should_fail_if_target_path_already_exists() -> cicero_core::Result<()> {
        let temp = assert_fs::TempDir::new()?;

        let source = temp.child("source");

        source.child("subfile")
            .write_str("subfile")?;


        let target = temp.child("target");
        target.child("subfile")
            .write_str("existing_target_subfile")?;

        let result = copy_dir_all(&source, &target);
        assert!(result.is_err());

        Ok(())
    }
}
