use crate::errors::InvalidParams;

#[cfg(feature = "phc")]
use password_hash::{Error, phc};

#[cfg(all(feature = "phc", doc))]
use password_hash::PasswordHasher;

/// The Scrypt parameter values.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Params {
    pub(crate) log_n: u8,
    pub(crate) r: u32,
    pub(crate) p: u32,
    #[cfg(feature = "password-hash")]
    pub(crate) len: Option<usize>,
}

impl Params {
    /// Recommended log₂ of the Scrypt parameter `N`: CPU/memory cost.
    pub const RECOMMENDED_LOG_N: u8 = 17;

    /// Recommended Scrypt parameter `r`: block size.
    pub const RECOMMENDED_R: u32 = 8;

    /// Recommended Scrypt parameter `p`: parallelism.
    pub const RECOMMENDED_P: u32 = 1;

    /// Recommended Scrypt parameter `Key length`.
    pub const RECOMMENDED_LEN: usize = 32;

    /// Recommended values according to the [OWASP cheat sheet].
    /// - `log_n = 17` (`n = 131072`)
    /// - `r = 8`
    /// - `p = 1`
    ///
    /// [OWASP cheat sheet]: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#scrypt
    pub const RECOMMENDED: Self = Self {
        log_n: Self::RECOMMENDED_LOG_N,
        r: Self::RECOMMENDED_R,
        p: Self::RECOMMENDED_P,
        #[cfg(feature = "password-hash")]
        len: None,
    };

    /// Create a new instance of [`Params`].
    ///
    /// # Arguments
    /// - `log_n` - The log₂ of the Scrypt parameter `N`
    /// - `r` - The Scrypt parameter `r`
    /// - `p` - The Scrypt parameter `p`
    ///
    /// # Conditions
    /// - `log_n` must be less than `64`
    /// - `r` must be greater than `0` and less than or equal to `4294967295`
    /// - `p` must be greater than `0` and less than `4294967295`
    ///
    /// # Errors
    /// Returns [`InvalidParams`] if one of the parameters is incorrect.
    pub fn new(log_n: u8, r: u32, p: u32) -> Result<Params, InvalidParams> {
        let cond1 = (log_n as usize) < usize::BITS as usize;
        let cond2 = size_of::<usize>() >= size_of::<u32>();
        let cond3 = usize::try_from(r).is_ok() && usize::try_from(p).is_ok();
        if !(r > 0 && p > 0 && cond1 && (cond2 || cond3)) {
            return Err(InvalidParams);
        }

        let n = 1usize << log_n;

        // check that r * 128 doesn't overflow
        let r128 = usize::try_from(r)
            .map_err(|_| InvalidParams)?
            .checked_mul(128)
            .ok_or(InvalidParams)?;

        // check that n * r * 128 doesn't overflow
        r128.checked_mul(n).ok_or(InvalidParams)?;

        // check that p * r * 128 doesn't overflow
        usize::try_from(p)
            .ok()
            .and_then(|p| r128.checked_mul(p))
            .ok_or(InvalidParams)?;

        // Note: RFC 7914 requires `n < 2^(128 * r / 8)`, i.e. `log_n < r * 16`,
        // but this upper bound is based on an error in the RFC where a bit count
        // was treated as a byte count. The correct bound from the original scrypt
        // paper (Percival, 2009) is `N < 2^(128*r)`, i.e. `log_n < r * 128`,
        // which far exceeds any practical parameter value.
        // We intentionally omit this check, consistent with the Tarsnap reference
        // implementation and Go's x/crypto/scrypt.
        // See: https://github.com/RustCrypto/password-hashes/issues/866
        // See: https://www.rfc-editor.org/errata/eid5971

        // This check required by Scrypt:
        // check: p <= ((2^32-1) * 32) / (128 * r)
        // It takes a bit of re-arranging to get the check above into this form,
        // but it is indeed the same.
        if r * p >= 0x4000_0000 {
            return Err(InvalidParams);
        }

        Ok(Params {
            log_n,
            r,
            p,
            #[cfg(feature = "password-hash")]
            len: None,
        })
    }

    /// Create a new instance of [`Params`], overriding the output length.
    ///
    /// Note that this length is only intended for use with the [`PasswordHasher`] API, and not with
    /// the low-level [`scrypt::scrypt`][`crate::scrypt`] API, which determines the output length
    /// using the size of the `output` slice.
    ///
    /// The allowed values for `len` are between 10 bytes (80 bits) and 64 bytes inclusive.
    /// These lengths come from the [PHC string format specification](https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md)
    /// because they are intended for use with password hash strings.
    ///
    /// # Errors
    /// Returns [`InvalidParams`] if one of the parameters is incorrect.
    #[cfg(feature = "phc")]
    pub fn new_with_output_len(
        log_n: u8,
        r: u32,
        p: u32,
        len: usize,
    ) -> Result<Params, InvalidParams> {
        if !(phc::Output::MIN_LENGTH..=phc::Output::MAX_LENGTH).contains(&len) {
            return Err(InvalidParams);
        }

        let mut ret = Self::new(log_n, r, p)?;
        ret.len = Some(len);
        Ok(ret)
    }

    /// Deprecated: recommended values according to the OWASP cheat sheet.
    #[deprecated(since = "0.12.0", note = "use Params::RECOMMENDED instead")]
    #[must_use]
    pub const fn recommended() -> Params {
        Self::RECOMMENDED
    }

    /// log₂ of the Scrypt parameter `N`, the work factor.
    ///
    /// Memory and CPU usage scale linearly with `N`. If you need `N`, use
    /// [`Params::n`] instead.
    #[must_use]
    pub const fn log_n(&self) -> u8 {
        self.log_n
    }

    /// `N` parameter: the work factor.
    ///
    /// This method returns 2 to the power of [`Params::log_n`]. Memory and CPU
    /// usage scale linearly with `N`.
    #[must_use]
    pub const fn n(&self) -> u64 {
        1 << self.log_n
    }

    /// `r` parameter: resource usage.
    ///
    /// scrypt iterates 2*r times. Memory and CPU time scale linearly
    /// with this parameter.
    #[must_use]
    pub const fn r(&self) -> u32 {
        self.r
    }

    /// `p` parameter: parallelization.
    #[must_use]
    pub const fn p(&self) -> u32 {
        self.p
    }
}

impl Default for Params {
    fn default() -> Params {
        Params::RECOMMENDED
    }
}

#[cfg(feature = "phc")]
impl TryFrom<&phc::ParamsString> for Params {
    type Error = Error;

    fn try_from(params: &phc::ParamsString) -> password_hash::Result<Self> {
        let mut log_n = Self::RECOMMENDED_LOG_N;
        let mut r = Self::RECOMMENDED_R;
        let mut p = Self::RECOMMENDED_P;

        for (ident, value) in params.iter() {
            match ident.as_str() {
                "ln" => {
                    log_n = value
                        .decimal()
                        .ok()
                        .and_then(|dec| dec.try_into().ok())
                        .ok_or(Error::ParamInvalid { name: "ln" })?;
                }
                "r" => {
                    r = value
                        .decimal()
                        .map_err(|_| Error::ParamInvalid { name: "r" })?;
                }
                "p" => {
                    p = value
                        .decimal()
                        .map_err(|_| Error::ParamInvalid { name: "p" })?;
                }
                _ => return Err(Error::ParamsInvalid),
            }
        }

        Params::new(log_n, r, p).map_err(|_| Error::ParamsInvalid)
    }
}

#[cfg(feature = "phc")]
impl TryFrom<&phc::PasswordHash> for Params {
    type Error = Error;

    fn try_from(hash: &phc::PasswordHash) -> password_hash::Result<Self> {
        if hash.version.is_some() {
            return Err(Error::Version);
        }

        let mut params = Params::try_from(&hash.params)?;

        params.len = Some(hash.hash.map_or(Self::RECOMMENDED_LEN, |out| out.len()));

        Ok(params)
    }
}

#[cfg(feature = "phc")]
impl TryFrom<Params> for phc::ParamsString {
    type Error = Error;

    fn try_from(params: Params) -> Result<phc::ParamsString, Error> {
        Self::try_from(&params)
    }
}

#[cfg(feature = "phc")]
impl TryFrom<&Params> for phc::ParamsString {
    type Error = Error;

    fn try_from(input: &Params) -> Result<phc::ParamsString, Error> {
        let mut output = phc::ParamsString::new();

        for (name, value) in [
            ("ln", phc::Decimal::from(input.log_n)),
            ("r", input.r),
            ("p", input.p),
        ] {
            output
                .add_decimal(name, value)
                .map_err(|_| Error::ParamInvalid { name })?;
        }

        Ok(output)
    }
}
