Documentation
/*
==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--

Dia-Args

Copyright (C) 2018-2019, 2021-2025  Anonymous

There are several releases over multiple years,
they are listed as ranges, such as: "2018-2019".

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--
*/

//! # Kit for working with user

use {
    core::{
        fmt::{self, Display, Formatter},
        hash::{Hash, Hasher},
        str::FromStr,
    },
    std::{
        collections::HashSet,
        io::{Error, ErrorKind},
    },
    crate::Result,
};

/// # Answer
///
/// For variants which have an optional representation string:
///
/// - If you don't provide the representation strings, defaults will be used.
/// - Implementations of [`Eq`][trait:core/cmp/Eq], [`PartialEq`][trait:core/cmp/PartialEq], [`Hash`][trait:core/hash/Hash] do _NOT_ work on the
///   representation strings.
///
/// For [`UserDefined`](#variant.UserDefined):
///
/// - Implementations of [`Eq`][trait:core/cmp/Eq], [`PartialEq`][trait:core/cmp/PartialEq], [`Hash`][trait:core/hash/Hash] _work_ on data you
///   provide.
///
/// For example, if you want 2 answers of `Resume` and `Ignore`, but there are no such variants, you can do this:
///
/// ```
/// use core::fmt::{self, Display, Formatter};
/// use dia_args::Answer;
///
/// #[derive(Debug, Eq, PartialEq, Hash)]
/// enum CustomAnswer {
///     Resume,
///     Ignore,
/// }
///
/// impl Display for CustomAnswer {
///
///     fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
///         f.write_str(match self {
///             CustomAnswer::Resume => "Resume",
///             CustomAnswer::Ignore => "Ignore",
///         })
///     }
///
/// }
///
/// # // We can't test ask_user() because it will need to read data from stdin, which is impossible in current Rust tests.
/// # // Do NOT use #[cfg(test)] -- it will ignore the whole block.
/// # // Using the macro cfg!(test) will make the compiler verify what's following it.
/// # if cfg!(test) {
/// match dia_args::ask_user(
///     "What's your desire?",
///     &[
///         Answer::UserDefined(CustomAnswer::Resume),
///         Answer::UserDefined(CustomAnswer::Ignore),
///         Answer::Cancel(None),
///     ],
/// )? {
///     Answer::UserDefined(answer) => match answer {
///         CustomAnswer::Resume => {},
///         CustomAnswer::Ignore => {},
///     },
///     Answer::Cancel(_) => {},
///     _ => {},
/// };
/// # }
///
/// # dia_args::Result::Ok(())
/// ```
///
/// ## See also
///
/// [`ask_user()`][fn:ask_user].
///
/// [trait:core/cmp/Eq]: https://doc.rust-lang.org/core/cmp/trait.Eq.html
/// [trait:core/cmp/PartialEq]: https://doc.rust-lang.org/core/cmp/trait.PartialEq.html
/// [trait:core/hash/Hash]: https://doc.rust-lang.org/core/hash/trait.Hash.html
///
/// [fn:ask_user]: fn.ask_user.html
#[derive(Debug, Eq)]
pub enum Answer<'a, T> where T: Eq + PartialEq + Hash + Display {

    /// # Yes
    Yes(Option<&'a str>),

    /// # No
    No(Option<&'a str>),

    /// # Retry
    Retry(Option<&'a str>),

    /// # Next
    Next(Option<&'a str>),

    /// # Cancel
    Cancel(Option<&'a str>),

    /// # User-defined
    UserDefined(T),

}

impl<T> PartialEq for Answer<'_, T> where T: Eq + PartialEq + Hash + Display {

    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Answer::Yes(_), Answer::Yes(_)) => true,
            (Answer::No(_), Answer::No(_)) => true,
            (Answer::Retry(_), Answer::Retry(_)) => true,
            (Answer::Next(_), Answer::Next(_)) => true,
            (Answer::Cancel(_), Answer::Cancel(_)) => true,
            (Answer::UserDefined(first), Answer::UserDefined(second)) => first == second,
            _ => false,
        }
    }

}

impl<T> Hash for Answer<'_, T> where T: Eq + PartialEq + Hash + Display {

    fn hash<H>(&self, h: &mut H) where H: Hasher {
        let id = match self {
            Answer::Yes(_) => "yes",
            Answer::No(_) => "no",
            Answer::Retry(_) => "retry",
            Answer::Next(_) => "next",
            Answer::Cancel(_) => "cancel",
            Answer::UserDefined(t) => {
                t.hash(h);
                return;
            },
        };

        crate::ID.hash(h);
        id.hash(h);
    }

}

#[test]
fn test_answers() {
    let answers = &[
        Answer::Yes(Some(concat!())), Answer::Yes(None),
        Answer::No(Some(concat!())), Answer::No(None),
        Answer::Retry(Some(concat!())), Answer::Retry(None),
        Answer::Next(Some(concat!())), Answer::Next(None),
        Answer::Cancel(Some(concat!())), Answer::Cancel(None),
        Answer::UserDefined("first"), Answer::UserDefined("second"),
        Answer::UserDefined("second"), Answer::UserDefined("first"),
    ];
    assert_eq!(answers.iter().collect::<HashSet<_>>().len(), 7);
}

impl<T> Display for Answer<'_, T> where T: Eq + PartialEq + Hash + Display {

    fn fmt(&self, f: &mut Formatter) -> core::result::Result<(), fmt::Error> {
        match self {
            Answer::Yes(Some(s)) | Answer::No(Some(s)) | Answer::Retry(Some(s)) | Answer::Next(Some(s)) | Answer::Cancel(Some(s))
            => f.write_str(s),
            Answer::UserDefined(t) => f.write_str(&t.to_string()),
            Answer::Yes(None) => f.write_str("Yes"),
            Answer::No(None) => f.write_str("No"),
            Answer::Retry(None) => f.write_str("Retry"),
            Answer::Next(None) => f.write_str("Next"),
            Answer::Cancel(None) => f.write_str("Cancel"),
        }
    }

}

/// # Asks user some question
///
/// Notes:
///
/// - The question will be printed first. Then one line break. Then each answer will be printed on each line.
///
/// - The user can enter either:
///
///     + An answer index.
///     + Or some part of an answer.
///
/// - The function will start again if:
///
///     + The user enters a wrong index.
///     + The phrase entered is presented in more than one answer.
///
/// - The function returns an error if you provide duplicate answers, or no answers at all.
pub fn ask_user<'a, 'b, S, T>(question: S, answers: &'a[Answer<'b, T>]) -> Result<&'a Answer<'b, T>>
where S: AsRef<str>, T: Eq + PartialEq + Hash + Display {
    if answers.is_empty() {
        return Err(Error::new(ErrorKind::InvalidData, "There are no answers"));
    }
    if answers.iter().collect::<HashSet<_>>().len() != answers.len() {
        return Err(Error::new(ErrorKind::InvalidData, "There are duplicate answers"));
    }

    let question = question.as_ref().trim();

    loop {
        crate::lock_write_out(format!("{}\n\n", question));
        for (idx, answer) in answers.iter().enumerate() {
            crate::lock_write_out(format!("[{idx}] {answer}\n", idx=idx + 1, answer=answer));
        }

        crate::lock_write_out("\n-> ");

        let user_choice = crate::read_line::<String>()?;
        if user_choice.is_empty() {
            continue;
        }

        // Try index first
        match usize::from_str(&user_choice).map(|i| i.checked_sub(1)) {
            Ok(Some(idx)) => if idx < answers.len() {
                return Ok(&answers[idx]);
            },
            Ok(None) => continue,
            // Now try phrase
            Err(_) => {
                let user_choice = user_choice.to_lowercase();
                match answers.iter().try_fold(None, |found_answer, answer| if answer.to_string().to_lowercase().contains(&user_choice) {
                    match found_answer {
                        None => Ok(Some(answer)),
                        Some(_) => Err("User choice matches more than one answer"),
                    }
                } else {
                    Ok(found_answer)
                }) {
                    Ok(Some(answer)) => return Ok(answer),
                    _ => continue,
                };
            },
        };
    }
}