//! File system backend.

use std::any::Any;
use std::collections::HashSet;
use std::ffi::OsStr;
use std::ffi::OsString;
use std::fmt;
use std::fs;
use std::fs::read_dir;
use std::io;
use std::mem::replace;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};

use ignore::{WalkBuilder, overrides::OverrideBuilder};
use log::*;
use relative_path::PathExt;

use crate::backend::PathVisitor;
use crate::backend::interface::CopyDest;
use crate::backend::interface::CopyDestFolder;
use crate::backend::interface::PathMeta;
use crate::fsutil::stat;
use crate::reporting::ResultNote;
use crate::{backend::CopySource, reporting::CopyError};

/// File system backend struct.
pub struct FS {
    root: PathBuf,

    /// Follow symbolic links when traversing and copying
    follow_symlinks: bool,

    /// Include hidden files
    include_hidden: bool,

    /// Respect ignore files.
    ignore: bool,

    /// Exclude files matching glob
    exclude: Vec<String>,

    /// Delete files absent from source. Only active when used as a destination.
    delete: bool,
}

struct FSDir<'a> {
    dest: &'a FS,
    path: PathBuf,
    children: Vec<OsString>,
}

impl FS {
    pub fn create<P: AsRef<Path>>(path: P) -> FS {
        FS {
            root: path.as_ref().to_path_buf(),
            follow_symlinks: false,
            include_hidden: false,
            ignore: true,
            exclude: Vec::new(),
            delete: false,
        }
    }

    pub fn follow_symlinks(&mut self, follow: bool) -> &mut Self {
        self.follow_symlinks = follow;
        self
    }
    pub fn include_hidden(&mut self, include: bool) -> &mut Self {
        self.include_hidden = include;
        self
    }
    pub fn respect_ignore(&mut self, ignore: bool) -> &mut Self {
        self.ignore = ignore;
        self
    }
    /// Configure whether to delete extra files when used as a destination.
    pub fn delete_extra(&mut self, delete: bool) -> &mut Self {
        self.delete = delete;
        self
    }
    pub fn exclude(&mut self, pattern: &str) -> &mut Self {
        self.exclude.push(pattern.to_string());
        self
    }
}

impl fmt::Display for FS {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "directory {}", self.root.display())
    }
}

impl CopySource for FS {
    fn walk_paths(&self, visitor: &mut dyn PathVisitor) -> Result<(), CopyError> {
        let mut wb = WalkBuilder::new(&self.root);
        wb.require_git(true);
        wb.ignore(self.ignore);
        wb.git_exclude(self.ignore);
        wb.git_ignore(self.ignore);
        wb.git_global(self.ignore);
        wb.follow_links(self.follow_symlinks);
        wb.hidden(!self.include_hidden);
        wb.sort_by_file_name(|f1, f2| f1.cmp(f2));
        if !self.exclude.is_empty() {
            let mut ovb = OverrideBuilder::new(&self.root);
            for exc in self.exclude.iter() {
                ovb.add(&format!("!{}", exc))?;
            }
            wb.overrides(ovb.build()?);
        }
        let mut walk = wb.build();

        while let Some(entry) = walk.next() {
            let entry = entry?;
            debug!("found {:?}", entry.path());
            let fs_meta = match entry.metadata() {
                Ok(m) => m,
                Err(e) if e.is_io() && e.io_error().unwrap().kind() == io::ErrorKind::NotFound => {
                    warn!("{}: path disappeared while walking", entry.path().display());
                    continue;
                }
                Err(e) => {
                    error!("{}: walk meta failed: {}", entry.path().display(), e);
                    return Err(e.into());
                }
            };

            let fpath = entry.path();
            let path = fpath.strip_prefix(&self.root)?;
            let meta = PathMeta {
                size: fs_meta.size(),
                mtime: fs_meta.modified()?,
                ctime: fs_meta.modified()?,
            };

            if fs_meta.is_dir() {
                visitor.visit_dir(&path, &meta)?;
            } else if fs_meta.is_file() {
                visitor.visit_file(&path, &meta)?;
            } else {
                return Err(CopyError::Generic(format!(
                    "{}: unknown file type",
                    path.display()
                )));
            }
        }

        Ok(())
    }
}

impl CopyDest for FS {
    fn open<'a>(&'a self) -> Result<Box<dyn CopyDestFolder<'a> + 'a>, CopyError> {
        fs::create_dir_all(&self.root)?;
        Ok(Box::new(FSDir {
            dest: self,
            path: self.root.clone(),
            children: Vec::new(),
        }))
    }
}

impl<'a> CopyDestFolder<'a> for FSDir<'a> {
    fn add_directory(
        &mut self,
        name: &OsStr,
    ) -> Result<Box<dyn CopyDestFolder<'a> + 'a>, CopyError> {
        let dir = self.path.join(name);
        let mut create = true;
        if let Some(m) = stat(&dir)? {
            if m.is_dir() {
                debug!("{}: directory already exists", dir.display());
                create = false;
            } else {
                debug!("{}: removing existing path", dir.display());
                fs::remove_file(&dir)?;
            }
        };

        if create {
            debug!("{}: creating directory", dir.display());
            let res = fs::create_dir(&dir);
            res.with_path_action("mkdir", &dir)?;
        }

        self.children.push(name.to_os_string());
        Ok(Box::new(FSDir {
            dest: self.dest,
            path: dir.clone(),
            children: Vec::new(),
        }))
    }

    fn add_file(&mut self, name: &OsStr, src: &dyn CopySource) -> Result<bool, CopyError> {
        let fs_src = (src as &dyn Any)
            .downcast_ref::<FS>()
            .ok_or_else(|| CopyError::Generic("invalid source".into()))?;
        let dst_path = self.path.join(name);
        let rel_path = dst_path.relative_to(&self.dest.root)?;
        let src_path = rel_path.to_path(&fs_src.root);

        let src_meta = fs::metadata(&src_path)?;

        self.children.push(name.to_os_string());
        if let Some(m) = stat(&dst_path)? {
            if m.is_dir() {
                warn!("{}: exists but is directory, removing", rel_path);
                fs::remove_dir_all(&dst_path).with_path_action("rmtree", &dst_path)?;
            } else if !m.is_file() {
                warn!("{}: exists but is not file, removing", rel_path);
                fs::remove_file(&dst_path).with_path_action("rm", &dst_path)?;
            } else if m.modified()? >= src_meta.modified()? && m.len() == src_meta.len() {
                debug!("{}: exists and is up-to-date", rel_path);
                return Ok(false);
            }
        }

        let tmp_path = dst_path.with_extension("rdt.tmp");
        debug!("{}: copying to temporary file", rel_path);
        fs::copy(&src_path, &tmp_path).with_path_action("copy", &tmp_path)?;
        debug!("{}: replacing destination file", rel_path);
        fs::rename(&tmp_path, &dst_path).with_path_action("rename", &dst_path)?;

        Ok(true)
    }

    fn finish(&mut self) -> Result<usize, CopyError> {
        let mut n = 0;
        if self.dest.delete {
            let kids = replace(&mut self.children, Vec::new());
            let kids: HashSet<_> = kids.into_iter().collect();
            debug!("looking for extra children of {:?}", &self.path);
            let rd = read_dir(&self.path)?;
            for kid in rd {
                let kid = kid?;
                let meta = kid.metadata()?;
                if !kids.contains(&kid.file_name()) {
                    if meta.is_dir() {
                        debug!("removing directory {}", kid.path().display());
                        fs::remove_dir_all(&kid.path())?;
                    } else {
                        debug!("removing file {}", kid.path().display());
                        fs::remove_file(&kid.path())?;
                    }
                    n += 1;
                }
            }
        }
        Ok(n)
    }
}
