//! A safe wrapper around the `wordexp(3)` C function.
//!
//! The main entry-point for this crate is [WordExpander], which will point you
//! towards other areas of the docs.

pub mod utf8;

/// A builder-pattern struct for selecting the options to pass to `wordexp`.
///
/// # Safety
///
/// `expand` calls `wordexp(3)`, which is not thread-safe. In reality it's only
/// unsafe if you call certain functions while the call to `wordexp` is
/// occurring. See the man page for more information.
///
/// # Examples
///
/// ```
/// # use std::collections::HashSet;
/// let words = cromulent::WordExpander::default().expand("Car*l*")?;
/// let utf8 = cromulent::utf8::WordList::from(&words);
///
/// let actual = utf8.into_iter().collect::<HashSet<_>>();
/// let expected = HashSet::from(["Cargo.lock", "Cargo.toml"]);
///
/// assert_eq!(actual, expected);
/// # Ok::<(), cromulent::WordError>(())
/// ```
///
/// By default commands and undefined variables are not allowed:
///
/// ```
/// let command_words = cromulent::WordExpander::default().expand("$(echo hi)");
///
/// assert_eq!(
///     command_words,
///     Err(cromulent::WordError::CommandSubstitution)
/// );
/// ```
///
/// ```
/// # assert!(std::env::var("hopefully_this_variable_does_not_exist").is_err());
/// let var_words =
///     cromulent::WordExpander::default().expand("$hopefully_this_variable_does_not_exist");
///
/// assert_eq!(var_words, Err(cromulent::WordError::UndefinedVariable));
/// ```
/// Instead you must explicitly enable them:
///
/// ```
/// # assert!(std::env::var("hopefully_this_variable_does_not_exist").is_err());
/// let words = cromulent::WordExpander::default()
///     .allow_commands()
///     .allow_undefined()
///     .expand("$(echo hi)$hopefully_this_variable_does_not_exist")?;
/// let utf8 = cromulent::utf8::WordList::from(&words);
///
/// assert_eq!(&utf8[0], "hi");
/// # Ok::<(), cromulent::WordError>(())
/// ```
#[derive(Clone, Debug, Default)]
pub struct WordExpander {
    allow_commands: bool,
    allow_undefined: bool,
    show_stderr: bool,
}

impl WordExpander {
    /// Allow `wordexp` to run subcommands.
    ///
    /// By default `expand` will error if you try to run a subcommand, calling
    /// this function enables that functionality.
    ///
    /// # Examples
    ///
    /// ```
    /// let command_not_allowed = cromulent::WordExpander::default().expand("$(echo hi)");
    ///
    /// assert_eq!(
    ///     command_not_allowed,
    ///     Err(cromulent::WordError::CommandSubstitution)
    /// );
    /// ```
    ///
    /// ```
    /// let command_allowed = cromulent::WordExpander::default()
    ///     .allow_commands()
    ///     .expand("$(echo hi)")?;
    ///
    /// assert_eq!(command_allowed[0].to_str(), Ok("hi"));
    /// # Ok::<(), cromulent::WordError>(())
    /// ```
    pub fn allow_commands(self) -> Self {
        Self {
            allow_commands: true,
            ..self
        }
    }

    /// Allow `wordexp` to ignore undefined variables.
    ///
    /// By default `expand` will error if you try to expand a variable that is
    /// not in the environment, calling this function instead ignores it.
    ///
    /// # Examples
    ///
    /// ```
    /// # assert!(std::env::var("please_do_not_break_my_tests_by_defining_this_variable").is_err());
    /// let undefined_not_allowed = cromulent::WordExpander::default()
    ///     .expand("$please_do_not_break_my_tests_by_defining_this_variable");
    ///
    /// assert_eq!(
    ///     undefined_not_allowed,
    ///     Err(cromulent::WordError::UndefinedVariable)
    /// );
    /// ```
    ///
    /// ```
    /// # assert!(std::env::var("please_do_not_break_my_tests_by_defining_this_variable").is_err());
    /// let undefined_allowed = cromulent::WordExpander::default()
    ///     .allow_undefined()
    ///     .expand("$please_do_not_break_my_tests_by_defining_this_variable")?;
    ///
    /// assert!(undefined_allowed.is_empty());
    /// # Ok::<(), cromulent::WordError>(())
    /// ```
    pub fn allow_undefined(self) -> Self {
        Self {
            allow_undefined: true,
            ..self
        }
    }

    /// Show the error stream of subcommands.
    ///
    /// By default the error stream of subcommands is redirected to `/dev/null`,
    /// calling this function prevents that from happening.
    ///
    /// # Examples
    ///
    /// This example will not print anything to `stderr`:
    ///
    /// ```
    /// let words = cromulent::WordExpander::default()
    ///     .allow_commands()
    ///     .expand("$(>&2 echo hi)")?;
    ///
    /// assert!(words.is_empty());
    /// # Ok::<(), cromulent::WordError>(())
    /// ```
    ///
    /// This example, however, will print `"hi"` to `stderr`:
    ///
    /// ```
    /// let words = cromulent::WordExpander::default()
    ///     .allow_commands()
    ///     .show_stderr()
    ///     .expand("$(>&2 echo hi)")?;
    ///
    /// assert!(words.is_empty());
    /// # Ok::<(), cromulent::WordError>(())
    /// ```
    pub fn show_stderr(self) -> Self {
        Self {
            show_stderr: true,
            ..self
        }
    }

    /// Run `wordexp` with the configured options.
    ///
    /// See `man 3 wordexp` and [WordList].
    pub fn expand<'l, S: AsRef<str>>(self, string: S) -> Result<WordList<'l>, WordError> {
        let words = std::ffi::CString::new(string.as_ref())?;
        self.expand_cstr(&words)
    }

    /// Like [`expand`](WordExpander::expand), but avoids an allocation.
    ///
    /// This is useful if you already have a [`CString`](std::ffi::CString),
    /// otherwise one will need to be allocated to hold the null-byte.
    pub fn expand_cstr<'l>(self, string: &std::ffi::CStr) -> Result<WordList<'l>, WordError> {
        let words = string.as_ptr();

        let flags = {
            let mut flags = 0;

            if !self.allow_commands {
                flags |= wordexp_sys::WRDE_NOCMD;
            }

            if !self.allow_undefined {
                flags |= wordexp_sys::WRDE_UNDEF;
            }

            if self.show_stderr {
                flags |= wordexp_sys::WRDE_SHOWERR;
            }

            flags.try_into().expect("All flags are positive.")
        };

        let mut raw = wordexp_sys::wordexp_t {
            we_wordc: 0,
            we_wordv: std::ptr::null_mut(),
            we_offs: 0,
        };

        let code = unsafe { wordexp_sys::wordexp(words, &mut raw, flags) };

        if code == 0 {
            let wordexp_sys::wordexp_t {
                we_wordc: count,
                we_wordv: values,
                we_offs: offsets,
            } = raw;
            assert!(
                !values.is_null(),
                "This pointer should always point to at least a null pointer."
            );
            assert_eq!(offsets, 0, "Offsets can't have been set.");
            let count = count
                .try_into()
                .expect("Bindgen should have handled making sure this works");

            Ok(WordList(unsafe {
                std::slice::from_raw_parts(values as *const *const _, count)
            }))
        } else {
            unsafe { wordexp_sys::wordfree(&mut raw) };

            Err(
                match code
                    .try_into()
                    .expect("All the values it can take are positive.")
                {
                    wordexp_sys::WRDE_BADCHAR => WordError::IllegalCharacter,
                    wordexp_sys::WRDE_BADVAL => WordError::UndefinedVariable,
                    wordexp_sys::WRDE_CMDSUB => WordError::CommandSubstitution,
                    wordexp_sys::WRDE_NOSPACE => panic!("out of memory"),
                    wordexp_sys::WRDE_SYNTAX => WordError::Syntax,
                    _ => unreachable!("Only these values can be returned from the call."),
                },
            )
        }
    }
}

/// The error type returned by [`WordExpander::expand`].
///
/// `wordexp` only returns a integer error code, so unfortunately there is not
/// much that can be said about the error itself (e.g. syntax errors won't point
/// to the invalid syntax).
#[derive(Clone, Debug, thiserror::Error, Eq, PartialEq)]
pub enum WordError {
    /// You tried to run a command without explicitly allowing it.
    ///
    /// ```
    /// assert_eq!(
    ///     cromulent::WordExpander::default().expand("$(echo hi)"),
    ///     Err(cromulent::WordError::CommandSubstitution)
    /// );
    /// ```
    ///
    /// ```
    /// assert!(cromulent::WordExpander::default()
    ///     .allow_commands()
    ///     .expand("$(echo hi)")
    ///     .is_ok());
    /// ```
    #[error("command substitution was attempted")]
    CommandSubstitution,
    /// You tried to expand a string that contains an embedded null-byte.
    ///
    /// ```
    /// assert_eq!(
    ///     cromulent::WordExpander::default().expand("a\0b"),
    ///     Err(cromulent::WordError::EmbeddedNul(
    ///         std::ffi::CString::new("a\0b").unwrap_err()
    ///     ))
    /// );
    /// ```
    #[error("the input string has a null-byte in the middle of it: {0}")]
    EmbeddedNul(#[from] std::ffi::NulError),
    /// You tried to expand a string containing certain illegal characters.
    ///
    /// See the `wordexp(3)` man page for more information on what exactly these
    /// characters are.
    ///
    /// ```
    /// assert_eq!(
    ///     cromulent::WordExpander::default().expand("{1,2,3}"),
    ///     Err(cromulent::WordError::IllegalCharacter)
    /// );
    /// ```
    #[error("an illegal character was passed in the input")]
    IllegalCharacter,
    /// You tried to expand a string containing invalid syntax.
    ///
    /// See the `wordexp(3)` man page for more information about valid syntax.
    ///
    /// ```
    /// assert_eq!(
    ///     cromulent::WordExpander::default().expand("Hello $(echo world"),
    ///     Err(cromulent::WordError::Syntax)
    /// );
    /// ```
    #[error("there was a syntax error in the input")]
    Syntax,
    /// You tried to expand a string containing a variable with no definition.
    ///
    /// Ignoring undefined variables must be explicitly enabled.
    ///
    /// ```
    /// # assert!(std::env::var("no_breaking_my_documentation").is_err());
    /// assert_eq!(
    ///     cromulent::WordExpander::default().expand("$no_breaking_my_documentation"),
    ///     Err(cromulent::WordError::UndefinedVariable)
    /// );
    /// ```
    ///
    /// ```
    /// # assert!(std::env::var("no_breaking_my_documentation").is_err());
    /// assert!(cromulent::WordExpander::default()
    ///     .allow_undefined()
    ///     .expand("$no_breaking_my_documentation")
    ///     .is_ok());
    /// ```
    #[error("a variable used in the input was undefined")]
    UndefinedVariable,
}

/// A slice-like type representing the expanded words.
///
/// Elements of this "slice" are [`CStr`](std::ffi::CStr), if you expect your
/// words to be valid UTF-8 you might prefer to use [utf8::WordList].
///
/// # Examples
///
/// All examples will use the following expansion:
///
/// ```
/// let words = cromulent::WordExpander::default()
///     .allow_commands()
///     .expand("$(seq 3)")?;
///
/// # Ok::<(), cromulent::WordError>(())
/// ```
///
/// Length-checking:
///
/// ```
/// # let words = cromulent::WordExpander::default()
/// #     .allow_commands()
/// #     .expand("$(seq 3)")?;
///
/// assert!(!words.is_empty());
/// assert_eq!(words.len(), 3);
/// assert_eq!(words[0].to_str(), Ok("1"));
///
/// # Ok::<(), cromulent::WordError>(())
/// ```
///
/// Indexing:
///
/// ```
/// # let words = cromulent::WordExpander::default()
/// #     .allow_commands()
/// #     .expand("$(seq 3)")?;
///
/// assert_eq!(words[0].to_str(), Ok("1"));
///
/// # Ok::<(), cromulent::WordError>(())
/// ```
///
/// Iteration:
///
/// ```
/// # let words = cromulent::WordExpander::default()
/// #     .allow_commands()
/// #     .expand("$(seq 3)")?;
///
/// for (i, word) in words.into_iter().enumerate() {
///     assert_eq!(word.to_str()?.parse::<usize>()?, i + 1)
/// }
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WordList<'l>(&'l [*const std::os::raw::c_char]);

impl<'l> Drop for WordList<'l> {
    fn drop(&mut self) {
        let mut raw = wordexp_sys::wordexp_t {
            we_wordc: self
                .0
                .len()
                .try_into()
                .expect("Bindgen should have handled making this work."),
            we_wordv: self.0.as_ptr() as *mut _,
            we_offs: 0,
        };

        unsafe { wordexp_sys::wordfree(&mut raw) };
    }
}

impl<'l> std::ops::Index<usize> for WordList<'l> {
    type Output = std::ffi::CStr;

    fn index(&self, index: usize) -> &'l Self::Output {
        unsafe { std::ffi::CStr::from_ptr(self.0[index]) }
    }
}

impl<'l> WordList<'l> {
    /// Return `true` if no words were expanded.
    ///
    /// # Examples
    ///
    /// ```
    /// let words = cromulent::WordExpander::default().expand("")?;
    /// assert!(words.is_empty());
    ///
    /// # Ok::<(), cromulent::WordError>(())
    /// ```
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// Return the number of words that the input expanded into.
    ///
    /// # Examples
    ///
    /// ```
    /// let words = cromulent::WordExpander::default().expand("")?;
    /// assert_eq!(words.len(), 0);
    ///
    /// # Ok::<(), cromulent::WordError>(())
    /// ```
    ///
    /// ```
    /// let words = cromulent::WordExpander::default()
    ///     .allow_commands()
    ///     .expand("$(seq 3)")?;
    /// assert_eq!(words.len(), 3);
    ///
    /// # Ok::<(), cromulent::WordError>(())
    /// ```
    pub fn len(&self) -> usize {
        self.0.len()
    }

    /// Return the word at a given index without panicking.
    ///
    /// # Examples
    ///
    /// ```
    /// let words = cromulent::WordExpander::default()
    ///     .allow_commands()
    ///     .expand("$(seq 3)")?;
    ///
    /// assert_eq!(words.get(1), Some(&words[1]));
    /// assert_eq!(words.get(3), None);
    ///
    /// # Ok::<(), cromulent::WordError>(())
    /// ```
    pub fn get(&self, index: usize) -> Option<&<Self as std::ops::Index<usize>>::Output> {
        if index < self.len() {
            Some(&self[index])
        } else {
            None
        }
    }
}

pub struct Iter<'r, 'l: 'r> {
    word_list: &'r WordList<'l>,
    index: usize,
}

impl<'r, 'l: 'r> IntoIterator for &'r WordList<'l> {
    type IntoIter = Iter<'r, 'l>;
    type Item = <Self::IntoIter as Iterator>::Item;

    fn into_iter(self) -> Self::IntoIter {
        Iter {
            word_list: self,
            index: 0,
        }
    }
}

impl<'r, 'l: 'r> Iterator for Iter<'r, 'l> {
    type Item = &'r <WordList<'l> as std::ops::Index<usize>>::Output;

    fn next(&mut self) -> Option<Self::Item> {
        if self.index >= self.word_list.len() {
            None
        } else {
            let result = &self.word_list[self.index];
            self.index += 1;
            Some(result)
        }
    }
}
