{-# LANGUAGE NoImplicitPrelude   #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Hedgehog.Gen.JSON.Constrained.Internal
  ( genBoundedInteger
  , genBoundedReal
  , genBoundedString
  , genStringFromRegexp
  , genZonedTime
  , genUniqueItems
  , filterAll
  , filterAllMaybe
  ) where

import qualified Data.HashSet                 as HashSet
import           Data.Scientific              (Scientific)
import qualified Data.Scientific              as Scientific
import qualified Data.Text                    as Text
import           Data.Time.Clock
import           Data.Time.Clock.POSIX
import           Data.Time.LocalTime
import           Data.Time.Zones
import           Data.Time.Zones.All
import           Hedgehog
import qualified Hedgehog.Gen                 as Gen
import           Hedgehog.Gen.JSON.JSONSchema
import qualified Hedgehog.Range               as Range
import           Protolude
import qualified Regex.Genex                  as Genex

-- | Generates an Integer bounded by the given constraints or by the given range, as well as a multiple-of constraint.
genBoundedInteger ::
     Maybe NumberConstraintExclusiveMinimum
  -> Maybe NumberConstraintMinimum
  -> Maybe NumberConstraintExclusiveMaximum
  -> Maybe NumberConstraintMaximum
  -> Maybe NumberConstraintMultipleOf
  -> Range Integer
  -> Gen Integer
genBoundedInteger cminEx cmin cmaxEx cmax cmult range = Gen.sized $ \sz -> filterAllMaybe filters $ Gen.integral (finalRange sz)
  where
    filters = [minExFilter, maxExFilter, minFilter, maxFilter, multipleOfFilter]
    minExFilter = ((\b -> (> b)) . truncateScientific . unNumberConstraintExclusiveMinimum) <$> cminEx
    minFilter = ((\b -> (>= b)) . truncateScientific . unNumberConstraintMinimum) <$> cmin
    maxExFilter = ((\b -> (< b)) . truncateScientific . unNumberConstraintExclusiveMaximum) <$> cmaxEx
    maxFilter = ((\b -> (<= b)) . truncateScientific . unNumberConstraintMaximum) <$> cmax
    multipleOfFilter = ((\m x -> (x `rem` m == 0)) . truncateScientific . unNumberConstraintMultipleOf) <$> cmult
    finalRange sz = Range.linear (minB sz) (maxB sz)
    minB sz =
      fromMaybe
        (Range.lowerBound sz range)
        ((maximumMay . catMaybes)
           [(truncateScientific . unNumberConstraintMinimum) <$> cmin, (truncateScientific . unNumberConstraintExclusiveMinimum) <$> cminEx])
    maxB sz =
      fromMaybe
        (Range.lowerBound sz range)
        ((minimumMay . catMaybes)
           [(truncateScientific . unNumberConstraintMaximum) <$> cmax, (truncateScientific . unNumberConstraintExclusiveMaximum) <$> cmaxEx])

-- | Generates a Double bounded by the given constraints or by the given range.
genBoundedReal ::
     Maybe NumberConstraintExclusiveMinimum
  -> Maybe NumberConstraintMinimum
  -> Maybe NumberConstraintExclusiveMaximum
  -> Maybe NumberConstraintMaximum
  -> Range Double
  -> Gen Double
genBoundedReal cminEx cmin cmaxEx cmax range = Gen.sized $ \sz -> filterAllMaybe filters $ Gen.realFloat (finalRange sz)
  where
    filters = [minExFilter, maxExFilter, minFilter, maxFilter]
    minExFilter = ((\b -> (> b)) . Scientific.toRealFloat . unNumberConstraintExclusiveMinimum) <$> cminEx
    minFilter = ((\b -> (>= b)) . Scientific.toRealFloat . unNumberConstraintMinimum) <$> cmin
    maxExFilter = ((\b -> (< b)) . Scientific.toRealFloat . unNumberConstraintExclusiveMaximum) <$> cmaxEx
    maxFilter = ((\b -> (<= b)) . Scientific.toRealFloat . unNumberConstraintMaximum) <$> cmax
    finalRange sz = Range.linearFrac (minB sz) (maxB sz)
    minB sz =
      fromMaybe
        (Range.lowerBound sz range)
        ((maximumMay . catMaybes)
           [(Scientific.toRealFloat . unNumberConstraintMinimum) <$> cmin, (Scientific.toRealFloat . unNumberConstraintExclusiveMinimum) <$> cminEx])
    maxB sz =
      fromMaybe
        (Range.lowerBound sz range)
        ((minimumMay . catMaybes)
           [(Scientific.toRealFloat . unNumberConstraintMaximum) <$> cmax, (Scientific.toRealFloat . unNumberConstraintExclusiveMaximum) <$> cmaxEx])

-- | Generates a Text bounded in size by the given constraints or by the given range.
genBoundedString :: Maybe StringConstraintMinLength -> Maybe StringConstraintMaxLength -> Range Int -> Gen Text
genBoundedString minLengthC maxLengthC range = Gen.sized $ \size -> filterAllMaybe filters $ Gen.text (Range.linear (minB size) (maxB size)) Gen.unicode
  where
    minB sz = maybe (Range.lowerBound sz range) unStringConstraintMinLength minLengthC
    maxB sz = maybe (Range.upperBound sz range) unStringConstraintMaxLength maxLengthC
    filters =
      [((\b t -> Text.length t >= b) . unStringConstraintMinLength) <$> minLengthC, ((\b t -> Text.length t <= b) . unStringConstraintMaxLength) <$> maxLengthC]

-- | Generates a Text from a given Regular Expression
genStringFromRegexp :: Text -> Gen Text
genStringFromRegexp regexp = Gen.element $ Text.pack <$> take 10 (Genex.genexPure [Text.unpack regexp])

-- | Generates unique lists of the given generator and a size range
genUniqueItems :: (Hashable a, Eq a) => Range Int -> Gen a -> Gen [a]
genUniqueItems = genUniqueItems' 100 []

-- Gives up after 100 trials
genUniqueItems' :: (Hashable a, Eq a) => Int -> [a] -> Range Int -> Gen a -> Gen [a]
genUniqueItems' s acc range gen
  | s == 0 = Gen.discard
  | otherwise =
    Gen.sized $ \size -> do
      initialList <- (makeListUnique . (++ acc)) <$> Gen.list range gen
      let l = length initialList
      if Range.lowerBound size range <= l
        then pure initialList
        else take (Range.upperBound size range) <$> genUniqueItems' (s - 1) initialList range gen

-- | Filters out values that do not satisfy all the given predicates each
filterAll :: [a -> Bool] -> Gen a -> Gen a
filterAll filters = Gen.filter (\e -> and (($ e) <$> filters))

-- | Filters out values that do not satisfy all the given not-Nothing predicates
filterAllMaybe :: [Maybe (a -> Bool)] -> Gen a -> Gen a
filterAllMaybe filters = filterAll (catMaybes filters)

--- Helpers ----
truncateScientific :: Integral a => Scientific -> a
truncateScientific x = truncate real
  where
    real :: Double = Scientific.toRealFloat x

makeListUnique :: (Eq a, Hashable a) => [a] -> [a]
makeListUnique = toList . HashSet.fromList

genZonedTime :: Range POSIXTime -> Gen ZonedTime
genZonedTime r = do
  tz <- tzByLabel <$> Gen.enumBounded
  t <- genUTCTime r
  let timezone = timeZoneForUTCTime tz t
  pure $ utcToZonedTime timezone t

genUTCTime :: Range POSIXTime -> Gen UTCTime
genUTCTime r = posixSecondsToUTCTime <$> Gen.realFrac_ r