    module Test.Hspec.Util (
      pluralize
    , lineBreaksAt
    , safeTry
    , Path
    , filterPredicate
    , formatRequirement
    , readMaybe
    , getEnv
    , strip
    , stdGenToInteger
    , stdGenFromInteger
    ) where

    import           Data.Int (Int32)
    import           Data.List
    import           Data.Maybe
    import           Data.Char (isSpace)
    import           Control.Applicative
    import qualified Control.Exception as E
    import qualified System.Environment as Environment
    import           System.Random (StdGen)

    -- | Create a more readable display of a quantity of something.
    --
    -- Examples:
    --
    -- >>> pluralize 0 "example"
    -- "0 examples"
    --
    -- >>> pluralize 1 "example"
    -- "1 example"
    --
    -- >>> pluralize 2 "example"
    -- "2 examples"
    pluralize :: Int -> String -> String
    pluralize 1 s = "1 " ++ s
    pluralize n s = show n ++ " " ++ s ++ "s"

    safeTry :: IO a -> IO (Either E.SomeException a)
    safeTry action = (Right <$> (action >>= E.evaluate)) `E.catches` [
      -- Re-throw AsyncException, otherwise execution will not terminate on SIGINT
      -- (ctrl-c).  All AsyncExceptions are re-thrown (not just UserInterrupt)
      -- because all of them indicate severe conditions and should not occur during
      -- normal operation.
        E.Handler $ \e -> E.throwIO (e :: E.AsyncException)

      , E.Handler $ \e -> (return . Left) (e :: E.SomeException)
      ]

    -- |
    -- A tuple that represents the location of an example within a spec.
    --
    -- It consists of a list of group descriptions and a requirement description.
    type Path = ([String], String)

    -- | A predicate that can be used to filter specs.
    filterPredicate :: String -> Path -> Bool
    filterPredicate pattern path@(groups, requirement) =
         pattern `isInfixOf` plain
      || pattern `isInfixOf` formatted
      where
        plain = intercalate "/" (groups ++ [requirement])
        formatted = formatRequirement path

    -- |
    -- Try to create a proper English sentence from a path by applying some
    -- heuristics.
    formatRequirement :: Path -> String
    formatRequirement (groups, requirement) = groups_ ++ requirement
      where
        groups_ = case break (any isSpace) groups of
          ([], ys) -> join ys
          (xs, ys) -> join (intercalate "." xs : ys)

        join xs = case xs of
          [x] -> x ++ " "
          ys  -> concatMap (++ ", ") ys

    -- NOTE: base-4.6.0.0 provides a function with that name and type.  For
    -- compatibility with earlier versions, we define our own version here.
    readMaybe :: Read a => String -> Maybe a
    readMaybe = fmap fst . listToMaybe . reads

    getEnv :: String -> IO (Maybe String)
    getEnv key = either (const Nothing) Just <$> safeTry (Environment.getEnv key)

    -- ensure that lines are not longer then given `n`, insert line breaks at word
    -- boundaries
    lineBreaksAt :: Int -> String -> [String]
    lineBreaksAt n input = case words input of
      []   -> []
      x:xs -> go (x, xs)
      where
        go :: (String, [String]) -> [String]
        go c = case c of
          (s, [])   -> [s]
          (s, y:ys) -> let r = s ++ " " ++ y in
            if length r <= n
              then go (r, ys)
              else s : go (y, ys)

    strip :: String -> String
    strip = dropWhile isSpace . reverse . dropWhile isSpace . reverse

    -- | Converts a 'StdGen' into an 'Integer'. Assumes
    --   StdGens to be encoded as two positive 'Int32's and
    --   $show (StdGen a b) = show a ++ " " ++ show b$.
    stdGenToInteger :: StdGen -> Integer
    stdGenToInteger stdGen =
      let [a, b] = map read . words $ show stdGen
      in b * fromIntegral (maxBound :: Int32) + a

    -- | Inverse of 'stdGenToInteger'.
    stdGenFromInteger :: Integer -> StdGen
    stdGenFromInteger n =
      let (a, b) = quotRem n (fromIntegral (maxBound :: Int32))
      in read (show b ++ " " ++ show a)