{-|
Module      : Generate arbitrary Intervals
Description : Functions for generating arbitrary intervals
Copyright   : (c) NoviSci, Inc 2020
License     : BSD3
Maintainer  : bsaul@novisci.com
Stability   : experimental
-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE Safe              #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ScopedTypeVariables #-}


module IntervalAlgebra.Arbitrary
  ( arbitraryWithRelation
  ) where

import           Control.Applicative            ( (<$>)
                                                , liftA2
                                                )
import           Control.Monad                  ( liftM2 )
import           Data.Bool
import           Data.Fixed
import           Data.Function                  ( ($)
                                                , (.)
                                                , flip
                                                )
import           Data.Maybe                     ( Maybe(Just, Nothing) )
import           Data.Ord
import qualified Data.Set                       ( Set
                                                , difference
                                                , null
                                                , singleton
                                                )
import           Data.Time                     as DT
                                                ( Day(ModifiedJulianDay)
                                                , DiffTime
                                                , NominalDiffTime
                                                , UTCTime(..)
                                                , picosecondsToDiffTime
                                                , secondsToDiffTime
                                                , secondsToNominalDiffTime
                                                , toModifiedJulianDay
                                                )
import           GHC.Float
import           GHC.Int                        ( Int )
import           GHC.Num
import           GHC.Real
import           IntervalAlgebra                ( Interval
                                                , IntervalRelation(..)
                                                , IntervalSizeable
                                                , Intervallic
                                                , PairedInterval
                                                , beginerval
                                                , converse
                                                , duration
                                                , makePairedInterval
                                                , moment
                                                , predicate
                                                , strictWithinRelations
                                                )
import           Prelude                        ( (==)
                                                , Eq
                                                )
import           Test.QuickCheck                ( Arbitrary(arbitrary, shrink)
                                                , Gen
                                                , NonNegative
                                                , arbitrarySizedNatural
                                                , elements
                                                , resize
                                                , sized
                                                , suchThat
                                                )

-- NOTE: the default size for arbitrary :: Gen Int appears to be 30
arbitrarySizedPositive :: Integral a => Gen a
arbitrarySizedPositive = (+ 1) <$> arbitrarySizedNatural

-- resize in utctDayTime is to avoid rare leap-seconds-related failure, in
-- which e.g.  1858-12-31 00:00:00 UTC /= 1858-12-30 23:59:60 UTC
maxDiffTime :: Int
maxDiffTime = 86399

instance Arbitrary DT.Day where
  arbitrary = sized (\s -> DT.ModifiedJulianDay <$> s `resize` arbitrary)
  shrink    = (DT.ModifiedJulianDay <$>) . shrink . DT.toModifiedJulianDay

instance Arbitrary DT.NominalDiffTime where
  arbitrary = sized
    (\s -> fromInteger <$> (min s maxDiffTime `resize` arbitrarySizedNatural))

instance Arbitrary DT.DiffTime where
  arbitrary = sized
    (\s -> fromInteger <$> (min s maxDiffTime `resize` arbitrarySizedNatural))

instance Arbitrary DT.UTCTime  where
  arbitrary = liftA2 UTCTime arbitrary arbitrary

-- | Conditional generation of intervals relative to a reference.  If the
-- reference @iv@ is of 'moment' duration, it is not possible to generate
-- intervals from the strict enclose relations StartedBy, Contains, FinishedBy.
-- If @iv@ and @rs@ are such that no possible relations can be generated, this
-- function returns `Nothing`. Otherwise, it returns `Just` an interval that
-- satisfies at least one of the possible relations in @rs@ relative to
-- @iv@.
--
-- @
-- > import Test.QuickCheck (generate)
-- > import Data.Set (fromList)
-- > isJust $ generate $ arbitraryWithRelation (beginerval 10 (0::Int)) (fromList [Before])
-- Just (20, 22)
-- > generate $ arbitraryWithRelation (beginerval 1 (0::Int)) (fromList [StartedBy])
-- Nothing
-- > generate $ arbitraryWithRelation (beginerval 1 (0::Int)) (fromList [StartedBy, Before])
-- Just (4, 13)
-- @
--
arbitraryWithRelation
  :: forall i a b
   . (IntervalSizeable a b, Intervallic i, Arbitrary (i a))
  => i a -- ^ reference interval
  -> Data.Set.Set IntervalRelation -- ^ set of `IntervalRelation`s, of which at least one will hold for the generated interval relative to the reference
  -> Gen (Maybe (i a))
arbitraryWithRelation iv rs
  | rs == Data.Set.singleton Equals = elements [Just iv]
  | isEnclose && isMom = elements [Nothing]
  | isMom = Just <$> arbitrary `suchThat` predicate notStrictEnclose iv
  | otherwise = Just <$> arbitrary `suchThat` predicate rs iv
 where
  notStrictEnclose = Data.Set.difference rs (converse strictWithinRelations)
  isEnclose        = Data.Set.null notStrictEnclose
  isMom            = duration iv == moment @a
