{-# LANGUAGE CPP #-}

{-|
Stability: stable
Portability: portable

You can generate a new CUID or slug inside any IO-enabled monad using 'newCuid'
and 'newSlug'.
-}
module Web.Cuid (
    Cuid, newCuid,
    Slug, newSlug
) where

import Control.Monad (liftM)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.String (fromString)
import Data.Text (Text)
import Formatting (sformat)

import Data.Monoid (Monoid, (<>))
#if !MIN_VERSION_base(4,8,0)
import Data.Monoid (mconcat)
#endif

import Web.Cuid.Internal

-- | Convenience type so that you don't have to import Text downstream. Note that
-- this is strict Text.
type Cuid = Text

-- | Generate a new random CUID.
newCuid :: MonadIO m => m Cuid
newCuid = concatResults [c, time, count, fingerprint, random, random] where
    -- The CUID starts with a letter so it's usable in HTML element IDs.
    c = return (fromString "c")

    -- The second chunk is the timestamp. Note that this means it is possible
    -- to determine the time a particular CUID was created.
    time = liftM (sformat number) getTimestamp

    -- To avoid collisions on the same machine, add a global counter to each ID.
    count = liftM (sformat numberPadded) getNextCount

    -- To avoid collisions between separate machines, generate a 'fingerprint'
    -- from details which are hopefully unique to this machine - PID and hostname.
    fingerprint = do
        (pid, host) <- getFingerprint
        return (sformat twoOfNum pid <> sformat twoOfNum host)

    -- And some actual randomness for good measure.
    random = liftM (sformat numberPadded) getRandomValue

-- | A Slug is not a Cuid. But it is also a strict Text.
type Slug = Text

-- | A slug is a shorter piece of text generated using some of the same
-- techniques as CUIDs.
newSlug :: MonadIO m => m Slug
newSlug = concatResults [time, count, fingerprint, random] where
    time = liftM (sformat twoOfNum) getTimestamp
    count = liftM (sformat numberPadded) getNextCount
    random = liftM (sformat twoOfNum) getRandomValue
    fingerprint = do
        (pid, host) <- getFingerprint
        return (sformat firstOfNum pid <> sformat lastOfNum host)

-- Evaluate IO actions and concatenate their results.
concatResults :: (MonadIO m, Monoid a) => [IO a] -> m a
concatResults actions = liftM mconcat (liftIO $ sequence actions)