use std::io::{self, Write};
use std::path::Path;
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
use std::fs::{read_dir, symlink_metadata, Metadata};
use std::os::unix::fs::MetadataExt;
use libc::{AT_FDCWD, R_OK, X_OK, AT_SYMLINK_NOFOLLOW};
use libc;

macro_rules! reportln {
    () => (writeln!(io::stderr()).unwrap_or(()));
    ($($arg:tt)*) => (writeln!(io::stderr(), $($arg)*).unwrap_or(()));
}

fn need_access_check() -> bool
{
    let uid : libc::uid_t;
    let euid : libc::uid_t;
    let gid : libc::gid_t;
    let egid : libc::gid_t;
    unsafe {
        uid = libc::getuid();
        euid = libc::geteuid();
        gid = libc::getgid();
        egid = libc::getegid();
    }
    return uid != euid || gid != egid;
}

fn check_path(path: &Path, mode: libc::c_int) -> bool
{
    
    let ospath = path.as_os_str();
    if ospath.is_empty() {
        return true;
    } else {
        let cstring = CString::new(ospath.as_bytes()).unwrap();
        let result : libc::c_int;
        unsafe {
            result = libc::faccessat(AT_FDCWD, cstring.as_ptr(),
                                     mode, AT_SYMLINK_NOFOLLOW);
        }
        println!("Checking {:?} -> {:?}", path, result);
        return result == 0;
    }
}

fn check_full_path(path: &Path, mode: libc::c_int) -> bool
{
    let mut result = check_path(path, mode);
    let mut parent = path;
    loop {
        match parent.parent() {
            Some(p) => {
                result &= check_path(p, R_OK|X_OK);
                parent = p;
            },
            _ => break
        }
    }
    println!("Checking {:?} {}", path, result);
    return result;
}

fn inner_visit(path: &Path,
               metadata: &Metadata,
               checkpath: bool,
               cb: &Fn(&Path, &Metadata)) -> io::Result<()>
{
    if checkpath {
        if metadata.file_type().is_dir() {
            check_full_path(&path, R_OK|X_OK);
        } else {
            check_full_path(&path, R_OK);
        }
    }
    let access = if ! checkpath {
        true
    } else {
        if metadata.file_type().is_dir() {
            check_path(&path, R_OK|X_OK)
        } else {
            check_path(&path, R_OK)
        }
    };
    if ! access {
        reportln!("Could not access {:?}", path);
        return Ok(());
    } else {
        cb(&path, &metadata);
        if ! metadata.file_type().is_symlink() && metadata.is_dir() {
            match read_dir(path) {
                Ok(readdir) => {
                    let mut paths: Vec<_> =
                        readdir.map(|r| r.unwrap()).collect();   
                    paths.sort_by_key(|entry| entry.file_name());
                    for entry in paths {
                        let p = entry.path();
                        match symlink_metadata(&p) {
                            Ok(m) => {
                                if metadata.dev() != m.dev() {
                                    reportln!("Warning: Skipping mountpoint {:?}", p);
                                    continue;
                                }
                                inner_visit(&p, &m, checkpath, cb).unwrap();
                            },
                            Err(e) => {
                                reportln!("Error: {:?} [{}]", p, e)
                            }
                        }
                    }
                },
                Err(e) => {
                    reportln!("Error: {:?} {}", path, e);
                }
            }            
        }
    }
    Ok(())
}

pub fn visit(path: &Path, cb: &Fn(&Path, &Metadata)) -> io::Result<()>
{
    let checkpath = need_access_check();
    let metadata = symlink_metadata(path);
    let access = match metadata {
        Ok(ref m) => {
            if ! checkpath {
                true
            } else {
                if m.file_type().is_dir() {
                    check_full_path(&path, R_OK|X_OK)
                } else {
                    check_full_path(&path, R_OK)
                }
            }
        },
        _ => false
    };
    if ! access {
        reportln!("Could not access {:?}", path);
        return Ok(());
    } else {
        let metadata = metadata.unwrap();
        try!(inner_visit(&path, &metadata, checkpath, cb));
    }
    Ok(())
}