#[cfg(test)]
mod tests;

#[cfg(not(target_os = "espidf"))]
pub fn page_size() -> usize {
    unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize }
}

/// Returns the value for [`confstr(key, ...)`][posix_confstr]. Currently only
/// used on Darwin, but should work on any unix (in case we need to get
/// `_CS_PATH` or `_CS_V[67]_ENV` in the future).
///
/// [posix_confstr]:
///     https://pubs.opengroup.org/onlinepubs/9699919799/functions/confstr.html
#[cfg(target_vendor = "apple")]
pub fn confstr(
    key: crate::ffi::c_int,
    size_hint: Option<usize>,
) -> crate::io::Result<crate::ffi::OsString> {
    use crate::ffi::OsString;
    use crate::io;
    use crate::os::unix::ffi::OsStringExt;

    let mut buf: Vec<u8> = Vec::with_capacity(0);
    let mut bytes_needed_including_nul = size_hint
        .unwrap_or_else(|| {
            // Treat "None" as "do an extra call to get the length". In theory
            // we could move this into the loop below, but it's hard to do given
            // that it isn't 100% clear if it's legal to pass 0 for `len` when
            // the buffer isn't null.
            unsafe { libc::confstr(key, core::ptr::null_mut(), 0) }
        })
        .max(1);
    // If the value returned by `confstr` is greater than the len passed into
    // it, then the value was truncated, meaning we need to retry. Note that
    // while `confstr` results don't seem to change for a process, it's unclear
    // if this is guaranteed anywhere, so looping does seem required.
    while bytes_needed_including_nul > buf.capacity() {
        // We write into the spare capacity of `buf`. This lets us avoid
        // changing buf's `len`, which both simplifies `reserve` computation,
        // allows working with `Vec<u8>` instead of `Vec<MaybeUninit<u8>>`, and
        // may avoid a copy, since the Vec knows that none of the bytes are needed
        // when reallocating (well, in theory anyway).
        buf.reserve(bytes_needed_including_nul);
        // `confstr` returns
        // - 0 in the case of errors: we break and return an error.
        // - The number of bytes written, iff the provided buffer is enough to
        //   hold the entire value: we break and return the data in `buf`.
        // - Otherwise, the number of bytes needed (including nul): we go
        //   through the loop again.
        bytes_needed_including_nul =
            unsafe { libc::confstr(key, buf.as_mut_ptr().cast(), buf.capacity()) };
    }
    // `confstr` returns 0 in the case of an error.
    if bytes_needed_including_nul == 0 {
        return Err(io::Error::last_os_error());
    }
    // Safety: `confstr(..., buf.as_mut_ptr(), buf.capacity())` returned a
    // non-zero value, meaning `bytes_needed_including_nul` bytes were
    // initialized.
    unsafe {
        buf.set_len(bytes_needed_including_nul);
        // Remove the NUL-terminator.
        let last_byte = buf.pop();
        // ... and smoke-check that it *was* a NUL-terminator.
        assert_eq!(last_byte, Some(0), "`confstr` provided a string which wasn't nul-terminated");
    };
    Ok(OsString::from_vec(buf))
}

#[cfg(all(target_os = "linux", target_env = "gnu"))]
pub fn glibc_version() -> Option<(usize, usize)> {
    use crate::ffi::CStr;

    unsafe extern "C" {
        fn gnu_get_libc_version() -> *const libc::c_char;
    }
    let version_cstr = unsafe { CStr::from_ptr(gnu_get_libc_version()) };
    if let Ok(version_str) = version_cstr.to_str() {
        parse_glibc_version(version_str)
    } else {
        None
    }
}

/// Returns Some((major, minor)) if the string is a valid "x.y" version,
/// ignoring any extra dot-separated parts. Otherwise return None.
#[cfg(all(target_os = "linux", target_env = "gnu"))]
fn parse_glibc_version(version: &str) -> Option<(usize, usize)> {
    let mut parsed_ints = version.split('.').map(str::parse::<usize>).fuse();
    match (parsed_ints.next(), parsed_ints.next()) {
        (Some(Ok(major)), Some(Ok(minor))) => Some((major, minor)),
        _ => None,
    }
}
