//-
// Copyright 2017 Jason Lingle
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

//! Strategies for generating `std::Option` values.

#![cfg_attr(feature = "cargo-clippy", allow(expl_impl_clone_on_copy))]

use core::fmt;
use core::marker::PhantomData;

use crate::std_facade::Arc;
use crate::strategy::*;
use crate::test_runner::*;

//==============================================================================
// Probability
//==============================================================================

/// Creates a `Probability` from some value that is convertible into it.
///
/// # Panics
///
/// Panics if the converted to probability would lie
/// outside interval `[0.0, 1.0]`. Consult the `Into` (or `From`)
/// implementations for more details.
pub fn prob(from: impl Into<Probability>) -> Probability {
    from.into()
}

impl Default for Probability {
    /// The default probability is 0.5, or 50% chance.
    fn default() -> Self {
        prob(0.5)
    }
}

impl From<f64> for Probability {
    /// Creates a `Probability` from a `f64`.
    ///
    /// # Panics
    ///
    /// Panics if the probability is outside interval `[0.0, 1.0]`.
    fn from(prob: f64) -> Self {
        Probability::new(prob)
    }
}

impl Probability {
    /// Creates a `Probability` from a `f64`.
    ///
    /// # Panics
    ///
    /// Panics if the probability is outside interval `[0.0, 1.0]`.
    pub fn new(prob: f64) -> Self {
        assert!(prob >= 0.0 && prob <= 1.0);
        Probability(prob)
    }

    // Don't rely on these existing internally:

    /// Merges self together with some other argument producing a product
    /// type expected by some implementations of `A: Arbitrary` in
    /// `A::Parameters`. This can be more ergonomic to work with and may
    /// help type inference.
    pub fn with<X>(self, and: X) -> product_type![Self, X] {
        product_pack![self, and]
    }

    /// Merges self together with some other argument generated with a
    /// default value producing a product type expected by some
    /// implementations of `A: Arbitrary` in `A::Parameters`.
    /// This can be more ergonomic to work with and may help type inference.
    pub fn lift<X: Default>(self) -> product_type![Self, X] {
        self.with(Default::default())
    }
}

#[cfg(feature = "frunk")]
use frunk_core::generic::Generic;

#[cfg(feature = "frunk")]
impl Generic for Probability {
    type Repr = f64;

    /// Converts the `Probability` into an `f64`.
    fn into(self) -> Self::Repr {
        self.0
    }

    /// Creates a `Probability` from a `f64`.
    ///
    /// # Panics
    ///
    /// Panics if the probability is outside interval `[0.0, 1.0]`.
    fn from(r: Self::Repr) -> Self {
        r.into()
    }
}

impl From<Probability> for f64 {
    fn from(p: Probability) -> Self {
        p.0
    }
}

/// A probability in the range `[0.0, 1.0]` with a default of `0.5`.
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Probability(f64);

//==============================================================================
// Strategies for Option
//==============================================================================

mapfn! {
    [] fn WrapSome[<T : fmt::Debug>](t: T) -> Option<T> {
        Some(t)
    }
}

#[must_use = "strategies do nothing unless used"]
struct NoneStrategy<T>(PhantomData<T>);
impl<T> Clone for NoneStrategy<T> {
    fn clone(&self) -> Self {
        *self
    }
}
impl<T> Copy for NoneStrategy<T> {}
impl<T> fmt::Debug for NoneStrategy<T> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "NoneStrategy")
    }
}
impl<T: fmt::Debug> Strategy for NoneStrategy<T> {
    type Tree = Self;
    type Value = Option<T>;

    fn new_tree(&self, _: &mut TestRunner) -> NewTree<Self> {
        Ok(*self)
    }
}
impl<T: fmt::Debug> ValueTree for NoneStrategy<T> {
    type Value = Option<T>;

    fn current(&self) -> Option<T> {
        None
    }
    fn simplify(&mut self) -> bool {
        false
    }
    fn complicate(&mut self) -> bool {
        false
    }
}

opaque_strategy_wrapper! {
    /// Strategy which generates `Option` values whose inner `Some` values are
    /// generated by another strategy.
    ///
    /// Constructed by other functions in this module.
    #[derive(Clone)]
    pub struct OptionStrategy[<T>][where T : Strategy]
        (TupleUnion<(WA<NoneStrategy<T::Value>>,
                     WA<statics::Map<T, WrapSome>>)>)
        -> OptionValueTree<T>;
    /// `ValueTree` type corresponding to `OptionStrategy`.
    pub struct OptionValueTree[<T>][where T : Strategy]
        (TupleUnionValueTree<(
            LazyValueTree<NoneStrategy<T::Value>>,
            Option<LazyValueTree<statics::Map<T, WrapSome>>>,
        )>)
        -> Option<T::Value>;
}

// XXX Unclear why this is necessary; #[derive(Debug)] *should* generate
// exactly this, but for some reason it adds a `T::Value : Debug` constraint as
// well.
impl<T: Strategy + fmt::Debug> fmt::Debug for OptionStrategy<T> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "OptionStrategy({:?})", self.0)
    }
}

impl<T: Strategy> Clone for OptionValueTree<T>
where
    T::Tree: Clone,
{
    fn clone(&self) -> Self {
        OptionValueTree(self.0.clone())
    }
}

impl<T: Strategy> fmt::Debug for OptionValueTree<T>
where
    T::Tree: fmt::Debug,
{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "OptionValueTree({:?})", self.0)
    }
}

/// Return a strategy producing `Optional` values wrapping values from the
/// given delegate strategy.
///
/// `Some` values shrink to `None`.
///
/// `Some` and `None` are each chosen with 50% probability.
pub fn of<T: Strategy>(t: T) -> OptionStrategy<T> {
    weighted(Probability::default(), t)
}

/// Return a strategy producing `Optional` values wrapping values from the
/// given delegate strategy.
///
/// `Some` values shrink to `None`.
///
/// `Some` is chosen with a probability given by `probability_of_some`, which
/// must be between 0.0 and 1.0, both exclusive.
pub fn weighted<T: Strategy>(
    probability_of_some: impl Into<Probability>,
    t: T,
) -> OptionStrategy<T> {
    let prob = probability_of_some.into().into();
    let (weight_some, weight_none) = float_to_weight(prob);

    OptionStrategy(TupleUnion::new((
        (weight_none, Arc::new(NoneStrategy(PhantomData))),
        (weight_some, Arc::new(statics::Map::new(t, WrapSome))),
    )))
}

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

    fn count_some_of_1000(s: OptionStrategy<Just<i32>>) -> u32 {
        let mut runner = TestRunner::deterministic();
        let mut count = 0;
        for _ in 0..1000 {
            count +=
                s.new_tree(&mut runner).unwrap().current().is_some() as u32;
        }

        count
    }

    #[test]
    fn probability_defaults_to_0p5() {
        let count = count_some_of_1000(of(Just(42i32)));
        assert!(count > 450 && count < 550);
    }

    #[test]
    fn probability_handled_correctly() {
        let count = count_some_of_1000(weighted(0.9, Just(42i32)));
        assert!(count > 800 && count < 950);

        let count = count_some_of_1000(weighted(0.1, Just(42i32)));
        assert!(count > 50 && count < 150);
    }

    #[test]
    fn test_sanity() {
        check_strategy_sanity(of(0i32..1000i32), None);
    }
}
