// Copyright (c) 2018 The predicates-rs Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use std::fmt;
use std::fs;
use std::io;
use std::path;

use Predicate;

#[derive(Clone, Copy, Debug, PartialEq)]
enum FileType {
    File,
    Dir,
    Symlink,
}

impl FileType {
    fn from_path(path: &path::Path) -> io::Result<FileType> {
        let file_type = path.metadata()?.file_type();
        if file_type.is_dir() {
            return Ok(FileType::Dir);
        }
        if path.is_file() {
            return Ok(FileType::File);
        }
        Ok(FileType::Symlink)
    }

    fn eval(self, ft: &fs::FileType) -> bool {
        match self {
            FileType::File => ft.is_file(),
            FileType::Dir => ft.is_dir(),
            FileType::Symlink => ft.is_symlink(),
        }
    }
}

impl fmt::Display for FileType {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let t = match *self {
            FileType::File => "file",
            FileType::Dir => "dir",
            FileType::Symlink => "symlink",
        };
        write!(f, "{}", t)
    }
}

/// Predicate that checks the `std::fs::FileType`.
///
/// This is created by the `predicate::path::is_file`, `predicate::path::is_dir`, and `predicate::path::is_symlink`.
#[derive(Clone, Debug)]
pub struct FileTypePredicate {
    ft: FileType,
    follow: bool,
}

impl FileTypePredicate {
    /// Follow symbolic links.
    ///
    /// When yes is true, symbolic links are followed as if they were normal directories and files.
    ///
    /// Default: disabled.
    pub fn follow_links(mut self, yes: bool) -> Self {
        self.follow = yes;
        self
    }

    /// Allow to create an `FileTypePredicate` from a `path`
    pub fn from_path(path: &path::Path) -> io::Result<FileTypePredicate> {
        Ok(FileTypePredicate {
            ft: FileType::from_path(path)?,
            follow: true,
        })
    }
}

impl Predicate<path::Path> for FileTypePredicate {
    fn eval(&self, path: &path::Path) -> bool {
        let metadata = if self.follow {
            path.metadata()
        } else {
            path.symlink_metadata()
        };
        metadata
            .map(|m| self.ft.eval(&m.file_type()))
            .unwrap_or(false)
    }
}

impl fmt::Display for FileTypePredicate {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "var is {}", self.ft)
    }
}

/// Creates a new `Predicate` that ensures the path points to a file.
///
/// # Examples
///
/// ```
/// use std::path::Path;
/// use predicates::prelude::*;
///
/// let predicate_fn = predicate::path::is_file();
/// assert_eq!(true, predicate_fn.eval(Path::new("Cargo.toml")));
/// assert_eq!(false, predicate_fn.eval(Path::new("src")));
/// assert_eq!(false, predicate_fn.eval(Path::new("non-existent-file.foo")));
/// ```
pub fn is_file() -> FileTypePredicate {
    FileTypePredicate {
        ft: FileType::File,
        follow: false,
    }
}

/// Creates a new `Predicate` that ensures the path points to a directory.
///
/// # Examples
///
/// ```
/// use std::path::Path;
/// use predicates::prelude::*;
///
/// let predicate_fn = predicate::path::is_dir();
/// assert_eq!(false, predicate_fn.eval(Path::new("Cargo.toml")));
/// assert_eq!(true, predicate_fn.eval(Path::new("src")));
/// assert_eq!(false, predicate_fn.eval(Path::new("non-existent-file.foo")));
/// ```
pub fn is_dir() -> FileTypePredicate {
    FileTypePredicate {
        ft: FileType::Dir,
        follow: false,
    }
}

/// Creates a new `Predicate` that ensures the path points to a symlink.
///
/// # Examples
///
/// ```
/// use std::path::Path;
/// use predicates::prelude::*;
///
/// let predicate_fn = predicate::path::is_symlink();
/// assert_eq!(false, predicate_fn.eval(Path::new("Cargo.toml")));
/// assert_eq!(false, predicate_fn.eval(Path::new("src")));
/// assert_eq!(false, predicate_fn.eval(Path::new("non-existent-file.foo")));
/// ```
pub fn is_symlink() -> FileTypePredicate {
    FileTypePredicate {
        ft: FileType::Symlink,
        follow: false,
    }
}
