use chrono::{Local, Utc};
use std::collections::HashSet;
use std::ffi::OsString;
use std::fs::DirEntry;
use std::path::{Path, PathBuf};
use std::{fs, io};

pub struct ReadDirRecursive {
    ignored: HashSet<OsString>,
    read_dir: fs::ReadDir,
    dir_stack: Vec<PathBuf>,
    files_only: bool,
}

impl ReadDirRecursive {
    /// Start the iterator for a new directory
    pub fn start<P: AsRef<Path>>(path: P) -> io::Result<Self> {
        let path = path.as_ref();
        let read_dir = path.read_dir()?;

        Ok(ReadDirRecursive {
            ignored: HashSet::new(),
            read_dir,
            dir_stack: Vec::new(),
            files_only: false,
        })
    }

    pub fn ignored<S: Into<OsString>>(mut self, s: S) -> Self {
        self.ignored.insert(s.into());

        self
    }

    pub fn files(mut self) -> Self {
        self.files_only = true;

        self
    }

    /// Tries to populate the `read_dir` field with a new `ReadDir` instance to consume.
    fn next_read_dir(&mut self) -> io::Result<bool> {
        if let Some(path) = self.dir_stack.pop() {
            self.read_dir = path.read_dir()?;

            Ok(true)
        } else {
            Ok(false)
        }
    }

    /// Convenience method to add a new directory to the stack.
    fn push_entry(&mut self, entry: &io::Result<DirEntry>) {
        if let Ok(entry) = entry {
            if entry.path().is_dir() {
                self.dir_stack.push(entry.path());
            }
        }
    }

    /// Determine whether an entry should be returned by the iterator.
    fn should_return(&self, entry: &io::Result<DirEntry>) -> bool {
        if let Ok(entry) = entry {
            let mut res = !self.ignored.contains(&entry.file_name());

            // Please just let me combine these already
            if self.files_only {
                if let Ok(file_type) = entry.file_type() {
                    res = res && file_type.is_file();
                }
                // We  couldn't determine if it's a file, so we don't return it
                else {
                    res = false;
                }
            }

            res
        } else {
            true
        }
    }
}

impl Iterator for ReadDirRecursive {
    type Item = io::Result<DirEntry>;

    fn next(&mut self) -> Option<Self::Item> {
        loop {
            // First, we try to consume the current directory's items
            while let Some(entry) = self.read_dir.next() {
                self.push_entry(&entry);

                if self.should_return(&entry) {
                    return Some(entry);
                }
            }

            // If we get an error while setting up a new directory, we return this, otherwise we
            // keep trying to consume the directories
            match self.next_read_dir() {
                Ok(true) => (),
                // There's no more directories to traverse, so the iterator is done
                Ok(false) => return None,
                Err(e) => return Some(Err(e)),
            }
        }
    }
}

pub trait PathExt {
    /// Confirm whether the file has not been modified since the given timestamp.
    ///
    /// This function will only return true if it can determine with certainty that the file hasn't
    /// been modified.
    ///
    /// # Args
    ///
    /// * `timestamp` - Timestamp to compare modified time with
    ///
    /// # Returns
    ///
    /// True if the file has not been modified for sure, false otherwise.
    fn not_modified_since(&self, timestamp: chrono::DateTime<Utc>) -> bool;

    /// An extension of the `read_dir` command that runs through the entire underlying directory
    /// structure using breadth-first search
    fn read_dir_recursive(&self) -> io::Result<ReadDirRecursive>;
}

impl PathExt for Path {
    fn not_modified_since(&self, timestamp: chrono::DateTime<Utc>) -> bool {
        self.metadata()
            .and_then(|m| m.modified())
            .map(|last_modified| {
                let t: chrono::DateTime<Utc> = last_modified.into();
                let t = t.with_timezone(&Local);

                t < timestamp
            })
            .unwrap_or(false)
    }

    fn read_dir_recursive(&self) -> io::Result<ReadDirRecursive> {
        ReadDirRecursive::start(self)
    }
}