------------------------------------------------------------------------------
-- | Contains definitions for generating 'MediaType's.
module Network.HTTP.Media.MediaType.Gen
    (
    -- * Generating MediaTypes
      anything
    , genMediaType
    , genSubStar
    , genMaybeSubStar
    , subStarOf
    , genConcreteMediaType
    , genWithoutParams
    , genWithParams
    , stripParams
    , genDiffMediaTypesWith
    , genDiffMediaTypeWith
    , genDiffMediaTypes
    , genDiffMediaType
    , genMatchingPair

    -- * Generating Parameters
    , genParameters
    , genMaybeParameters
    , genDiffParameters

    -- * Rendering Parameters
    , renderParameters
    ) where

import qualified Data.Map                              as Map

import           Control.Monad                         (filterM, liftM, liftM2)
import           Data.ByteString                       (ByteString)
import           Data.CaseInsensitive                  (CI, original)
import           Data.Foldable                         (foldlM)
import           Data.Map                              (fromList)
import           Data.Monoid                           ((<>))
import           Test.QuickCheck.Gen

import           Network.HTTP.Media.Gen
import           Network.HTTP.Media.MediaType.Internal


------------------------------------------------------------------------------
-- | Parameter entry for testing.
type ParamEntry = (CI ByteString, CI ByteString)


------------------------------------------------------------------------------
-- | The MediaType that matches anything.
anything :: MediaType
anything = MediaType "*" "*" Map.empty


------------------------------------------------------------------------------
-- | Generates any kind of MediaType.
genMediaType :: Gen MediaType
genMediaType = oneof [return anything, genSubStar, genConcreteMediaType]


------------------------------------------------------------------------------
-- | Generates a MediaType with just a concrete main type.
genSubStar :: Gen MediaType
genSubStar = do
    main <- genCIByteString
    return $ MediaType main "*" Map.empty


------------------------------------------------------------------------------
-- | Generates a MediaType whose sub type might be *.
genMaybeSubStar :: Gen MediaType
genMaybeSubStar = oneof [genSubStar, genConcreteMediaType]


------------------------------------------------------------------------------
-- | Strips the sub type and parameters from a MediaType.
subStarOf :: MediaType -> MediaType
subStarOf media = media { subType = "*", parameters = Map.empty }


------------------------------------------------------------------------------
-- | Generates a concrete MediaType which may have parameters.
genConcreteMediaType :: Gen MediaType
genConcreteMediaType = do
    main <- genCIByteString
    sub  <- genCIByteString
    params <- oneof [return Map.empty, genParameters]
    return $ MediaType main sub params


------------------------------------------------------------------------------
-- | Generates a concrete MediaType with no parameters.
genWithoutParams :: Gen MediaType
genWithoutParams = do
    main <- genCIByteString
    sub  <- genCIByteString
    return $ MediaType main sub Map.empty


------------------------------------------------------------------------------
-- | Generates a MediaType with at least one parameter.
genWithParams :: Gen MediaType
genWithParams = do
    main   <- genCIByteString
    sub    <- genCIByteString
    params <- genParameters
    return $ MediaType main sub params


------------------------------------------------------------------------------
-- | Strips the parameters from the given MediaType.
stripParams :: MediaType -> MediaType
stripParams media = media { parameters = Map.empty }


------------------------------------------------------------------------------
-- | Generates a different MediaType to the ones in the given list, using the
-- given generator.
genDiffMediaTypesWith :: Gen MediaType -> [MediaType] -> Gen MediaType
genDiffMediaTypesWith gen media = do
    media' <- gen
    if media' `elem` media
        then genDiffMediaTypesWith gen media
        else return media'


------------------------------------------------------------------------------
-- | Generates a different MediaType to the given one, using the given
-- generator.
genDiffMediaTypeWith :: Gen MediaType -> MediaType -> Gen MediaType
genDiffMediaTypeWith gen = genDiffMediaTypesWith gen . (: [])


------------------------------------------------------------------------------
-- | Generates a different MediaType to the ones in the given list.
genDiffMediaTypes :: [MediaType] -> Gen MediaType
genDiffMediaTypes = genDiffMediaTypesWith genMediaType


------------------------------------------------------------------------------
-- | Generates a different MediaType to the given one.
genDiffMediaType :: MediaType -> Gen MediaType
genDiffMediaType = genDiffMediaTypes . (: [])


------------------------------------------------------------------------------
-- | Reuse for 'mayParams' and 'someParams'.
mkGenParams :: (Gen ParamEntry -> Gen [ParamEntry]) -> Gen Parameters
mkGenParams = liftM fromList .
    ($ liftM2 (,) (genDiffCIByteString "q") genCIByteString)


------------------------------------------------------------------------------
-- | Generates some sort of parameters.
genMaybeParameters :: Gen Parameters
genMaybeParameters = mkGenParams listOf


------------------------------------------------------------------------------
-- | Generates at least one parameter.
genParameters :: Gen Parameters
genParameters = mkGenParams listOf1


------------------------------------------------------------------------------
-- | Generates a set of parameters that is not a submap of the given
-- parameters (but not necessarily vice versa).
genDiffParameters :: Parameters -> Gen Parameters
genDiffParameters params = do
    params' <- genParameters
    if params' `Map.isSubmapOf` params
        then genDiffParameters params
        else return params'


------------------------------------------------------------------------------
-- | Generates a set of parameters that is a strict submap of the given
-- parameters.
genSubParameters :: Parameters -> Gen (Maybe Parameters)
genSubParameters params
    | Map.null params = return Nothing
    | otherwise       = Just . Map.fromList <$> genStrictSublist
  where
    list = Map.toList params
    genStrictSublist = do
        sublist <- filterM (const $ choose (False, True)) list
        if sublist == list
            then genStrictSublist
            else return sublist


------------------------------------------------------------------------------
-- | Generates a pair of non-equal MediaType values that are in a 'matches'
-- relation, with the more specific value on the left.
genMatchingPair :: Gen (MediaType, MediaType)
genMatchingPair = do
    a <- oneof [genSubStar, genConcreteMediaType]
    b <- if subType a == "*"
        then return anything
        else oneof $ withSubParameters a : map return [subStarOf a, anything]
    return (a, b)
  where
    withSubParameters a = do
        params <- genSubParameters (parameters a)
        return $ case params of
            Just sub -> a { parameters = sub }
            Nothing  -> subStarOf a


------------------------------------------------------------------------------
-- | Render parameters with a generated amount of whitespace between the
-- semicolons. Note that there is a leading semicolon in front of the
-- parameters, as it is expected that this will always be attached to
-- a preceding 'MediaType' rendering.
renderParameters :: Parameters -> Gen ByteString
renderParameters params = foldlM pad "" (Map.toList params)
  where
    pad s (k, v) =
        (s <>) . (<> original k <> "=" <> original v) <$> padString ";"
