use libc::{c_char, c_int, size_t};
use std::cmp::Ordering;
use std::ffi::{CString, OsStr, OsString};
use std::iter::IntoIterator;
use std::path::{Component, Path, PathBuf};

use crate::{raw, Error};

#[doc(hidden)]
pub trait IsNull {
    fn is_ptr_null(&self) -> bool;
}
impl<T> IsNull for *const T {
    fn is_ptr_null(&self) -> bool {
        self.is_null()
    }
}
impl<T> IsNull for *mut T {
    fn is_ptr_null(&self) -> bool {
        self.is_null()
    }
}

#[doc(hidden)]
pub trait Binding: Sized {
    type Raw;

    unsafe fn from_raw(raw: Self::Raw) -> Self;
    fn raw(&self) -> Self::Raw;

    unsafe fn from_raw_opt<T>(raw: T) -> Option<Self>
    where
        T: Copy + IsNull,
        Self: Binding<Raw = T>,
    {
        if raw.is_ptr_null() {
            None
        } else {
            Some(Binding::from_raw(raw))
        }
    }
}

/// Converts an iterator of repo paths into a git2-compatible array of cstrings.
///
/// Only use this for repo-relative paths or pathspecs.
///
/// See `iter2cstrs` for more details.
pub fn iter2cstrs_paths<T, I>(
    iter: I,
) -> Result<(Vec<CString>, Vec<*const c_char>, raw::git_strarray), Error>
where
    T: IntoCString,
    I: IntoIterator<Item = T>,
{
    let cstrs = iter
        .into_iter()
        .map(|i| fixup_windows_path(i.into_c_string()?))
        .collect::<Result<Vec<CString>, _>>()?;
    iter2cstrs(cstrs)
}

/// Converts an iterator of things into a git array of c-strings.
///
/// Returns a tuple `(cstrings, pointers, git_strarray)`. The first two values
/// should not be dropped before `git_strarray`.
pub fn iter2cstrs<T, I>(
    iter: I,
) -> Result<(Vec<CString>, Vec<*const c_char>, raw::git_strarray), Error>
where
    T: IntoCString,
    I: IntoIterator<Item = T>,
{
    let cstrs = iter
        .into_iter()
        .map(|i| i.into_c_string())
        .collect::<Result<Vec<CString>, _>>()?;
    let ptrs = cstrs.iter().map(|i| i.as_ptr()).collect::<Vec<_>>();
    let raw = raw::git_strarray {
        strings: ptrs.as_ptr() as *mut _,
        count: ptrs.len() as size_t,
    };
    Ok((cstrs, ptrs, raw))
}

#[cfg(unix)]
pub fn bytes2path(b: &[u8]) -> &Path {
    use std::os::unix::prelude::*;
    Path::new(OsStr::from_bytes(b))
}
#[cfg(windows)]
pub fn bytes2path(b: &[u8]) -> &Path {
    use std::str;
    Path::new(str::from_utf8(b).unwrap())
}

/// A class of types that can be converted to C strings.
///
/// These types are represented internally as byte slices and it is quite rare
/// for them to contain an interior 0 byte.
pub trait IntoCString {
    /// Consume this container, converting it into a CString
    fn into_c_string(self) -> Result<CString, Error>;
}

impl<'a, T: IntoCString + Clone> IntoCString for &'a T {
    fn into_c_string(self) -> Result<CString, Error> {
        self.clone().into_c_string()
    }
}

impl<'a> IntoCString for &'a str {
    fn into_c_string(self) -> Result<CString, Error> {
        Ok(CString::new(self)?)
    }
}

impl IntoCString for String {
    fn into_c_string(self) -> Result<CString, Error> {
        Ok(CString::new(self.into_bytes())?)
    }
}

impl IntoCString for CString {
    fn into_c_string(self) -> Result<CString, Error> {
        Ok(self)
    }
}

impl<'a> IntoCString for &'a Path {
    fn into_c_string(self) -> Result<CString, Error> {
        let s: &OsStr = self.as_ref();
        s.into_c_string()
    }
}

impl IntoCString for PathBuf {
    fn into_c_string(self) -> Result<CString, Error> {
        let s: OsString = self.into();
        s.into_c_string()
    }
}

impl<'a> IntoCString for &'a OsStr {
    fn into_c_string(self) -> Result<CString, Error> {
        self.to_os_string().into_c_string()
    }
}

impl IntoCString for OsString {
    #[cfg(unix)]
    fn into_c_string(self) -> Result<CString, Error> {
        use std::os::unix::prelude::*;
        let s: &OsStr = self.as_ref();
        Ok(CString::new(s.as_bytes())?)
    }
    #[cfg(windows)]
    fn into_c_string(self) -> Result<CString, Error> {
        match self.to_str() {
            Some(s) => s.into_c_string(),
            None => Err(Error::from_str(
                "only valid unicode paths are accepted on windows",
            )),
        }
    }
}

impl<'a> IntoCString for &'a [u8] {
    fn into_c_string(self) -> Result<CString, Error> {
        Ok(CString::new(self)?)
    }
}

impl IntoCString for Vec<u8> {
    fn into_c_string(self) -> Result<CString, Error> {
        Ok(CString::new(self)?)
    }
}

pub fn into_opt_c_string<S>(opt_s: Option<S>) -> Result<Option<CString>, Error>
where
    S: IntoCString,
{
    match opt_s {
        None => Ok(None),
        Some(s) => Ok(Some(s.into_c_string()?)),
    }
}

pub fn c_cmp_to_ordering(cmp: c_int) -> Ordering {
    match cmp {
        0 => Ordering::Equal,
        n if n < 0 => Ordering::Less,
        _ => Ordering::Greater,
    }
}

/// Converts a path to a CString that is usable by the libgit2 API.
///
/// Checks if it is a relative path.
///
/// On Windows, this also requires the path to be valid unicode, and translates
/// back slashes to forward slashes.
pub fn path_to_repo_path(path: &Path) -> Result<CString, Error> {
    macro_rules! err {
        ($msg:literal, $path:expr) => {
            return Err(Error::from_str(&format!($msg, $path.display())))
        };
    }
    match path.components().next() {
        None => return Err(Error::from_str("repo path should not be empty")),
        Some(Component::Prefix(_)) => err!(
            "repo path `{}` should be relative, not a windows prefix",
            path
        ),
        Some(Component::RootDir) => err!("repo path `{}` should be relative", path),
        Some(Component::CurDir) => err!("repo path `{}` should not start with `.`", path),
        Some(Component::ParentDir) => err!("repo path `{}` should not start with `..`", path),
        Some(Component::Normal(_)) => {}
    }
    #[cfg(windows)]
    {
        match path.to_str() {
            None => {
                return Err(Error::from_str(
                    "only valid unicode paths are accepted on windows",
                ))
            }
            Some(s) => return fixup_windows_path(s),
        }
    }
    #[cfg(not(windows))]
    {
        path.into_c_string()
    }
}

pub fn cstring_to_repo_path<T: IntoCString>(path: T) -> Result<CString, Error> {
    fixup_windows_path(path.into_c_string()?)
}

#[cfg(windows)]
fn fixup_windows_path<P: Into<Vec<u8>>>(path: P) -> Result<CString, Error> {
    let mut bytes: Vec<u8> = path.into();
    for i in 0..bytes.len() {
        if bytes[i] == b'\\' {
            bytes[i] = b'/';
        }
    }
    Ok(CString::new(bytes)?)
}

#[cfg(not(windows))]
fn fixup_windows_path(path: CString) -> Result<CString, Error> {
    Ok(path)
}

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

    macro_rules! assert_err {
        ($path:expr, $msg:expr) => {
            match path_to_repo_path(Path::new($path)) {
                Ok(_) => panic!("expected `{}` to err", $path),
                Err(e) => assert_eq!(e.message(), $msg),
            }
        };
    }

    macro_rules! assert_repo_path_ok {
        ($path:expr) => {
            assert_repo_path_ok!($path, $path)
        };
        ($path:expr, $expect:expr) => {
            assert_eq!(
                path_to_repo_path(Path::new($path)),
                Ok(CString::new($expect).unwrap())
            );
        };
    }

    #[test]
    #[cfg(windows)]
    fn path_to_repo_path_translate() {
        assert_repo_path_ok!("foo");
        assert_repo_path_ok!("foo/bar");
        assert_repo_path_ok!(r"foo\bar", "foo/bar");
        assert_repo_path_ok!(r"foo\bar\", "foo/bar/");
    }

    #[test]
    fn path_to_repo_path_no_weird() {
        assert_err!("", "repo path should not be empty");
        assert_err!("./foo", "repo path `./foo` should not start with `.`");
        assert_err!("../foo", "repo path `../foo` should not start with `..`");
    }

    #[test]
    #[cfg(not(windows))]
    fn path_to_repo_path_no_absolute() {
        assert_err!("/", "repo path `/` should be relative");
        assert_repo_path_ok!("foo/bar");
    }

    #[test]
    #[cfg(windows)]
    fn path_to_repo_path_no_absolute() {
        assert_err!(
            r"c:",
            r"repo path `c:` should be relative, not a windows prefix"
        );
        assert_err!(
            r"c:\",
            r"repo path `c:\` should be relative, not a windows prefix"
        );
        assert_err!(
            r"c:temp",
            r"repo path `c:temp` should be relative, not a windows prefix"
        );
        assert_err!(
            r"\\?\UNC\a\b\c",
            r"repo path `\\?\UNC\a\b\c` should be relative, not a windows prefix"
        );
        assert_err!(
            r"\\?\c:\foo",
            r"repo path `\\?\c:\foo` should be relative, not a windows prefix"
        );
        assert_err!(
            r"\\.\COM42",
            r"repo path `\\.\COM42` should be relative, not a windows prefix"
        );
        assert_err!(
            r"\\a\b",
            r"repo path `\\a\b` should be relative, not a windows prefix"
        );
        assert_err!(r"\", r"repo path `\` should be relative");
        assert_err!(r"/", r"repo path `/` should be relative");
        assert_err!(r"\foo", r"repo path `\foo` should be relative");
        assert_err!(r"/foo", r"repo path `/foo` should be relative");
    }
}
