{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE BangPatterns #-}

module Faker
  (
    -- * Types
    Fake(..)
  , FakerSettings
  , FakerException(..)
  , defaultFakerSettings
    -- * Setters
  , setLocale
  , setRandomGen
  , setDeterministic
  , setNonDeterministic
  , setCacheField
  , setCacheFile
  , replaceCacheField
  , replaceCacheFile
    -- * Getters
  , getRandomGen
  , getLocale
  , getDeterministic
  , getCacheField
  , getCacheFile
    -- * Generators
  , generate
  , generateWithSettings
  ) where

import Control.Exception (Exception)
import Control.Monad (ap)
import Control.Monad.IO.Class
import qualified Data.HashMap.Strict as HM
import Data.IORef
import Data.Text (Text)
import Data.Typeable
import Data.Vector (Vector)
import Data.Word (Word64)
import Data.Yaml (Value)
import Faker.Internal.Types (CacheFieldKey, CacheFileKey)
import System.Random (StdGen, mkStdGen, newStdGen, split)

data FakerSettings = FakerSettings
  { fslocale :: !Text -- ^ Locale settings for your fake data source.
  , fsrandomGen :: !StdGen -- ^ Seed to initialize random generator state
  , fsDeterministic :: !Bool -- ^ Controls whether you want
                            -- deterministic out. This overrides
                            -- 'fsrandomGen'.
  , fsCacheField :: (IORef (HM.HashMap CacheFieldKey (Vector Text)))
  , fsCacheFile :: (IORef (HM.HashMap CacheFileKey Value))
  }

newtype FakerGen = FakerGen
  { unFakerGen :: (Int, StdGen)
  } deriving (Show)

instance Show FakerSettings where
  show (FakerSettings {..}) =
    show fslocale ++ show fsrandomGen ++ show fsDeterministic

data FakerException
  = InvalidLocale String -- ^ This is thrown when it is not able to
                         -- find the fake data source for your
                         -- localization.
  | InvalidField String
                 Text -- ^ The 'String' represents the field it is
                             -- trying to resolve and the 'Text' field
                             -- is something you passed on.
  | NoDataFound FakerSettings -- ^ This is thrown when you have no
                              -- data. This may likely happen for
                              -- locales other than `en`.
  | ParseError String -- ^ This is thrown when the parsing step
                      -- fails. The 'String' represents the error
                      -- message.
  deriving (Typeable, Show)

instance Exception FakerException

-- | Default faker settings with locale of \"en\" and Deterministic output.
defaultFakerSettings :: FakerSettings
defaultFakerSettings =
  FakerSettings
    { fslocale = "en"
    , fsrandomGen = mkStdGen 10000
    , fsDeterministic = True
    , fsCacheField = error "defaultFakerSettings: fsCacheField not initialized"
    , fsCacheFile = error "defaultFakerSettings: fsCacheFile not initialized"
    }

-- | Sets the locale. Note that for any other locale apart from
-- \"en\", you need to make sure that the data is acutally present. In
-- case no data is found, 'NoDataFound' exception will be thrown. You
-- can check the presence of the data in a particular locale by
-- inspecting the `yml` file of the corresponding locale. The file
-- would be bundled along with the particular Hackage release.
setLocale :: Text -> FakerSettings -> FakerSettings
setLocale localeTxt fs = fs {fslocale = localeTxt}

-- | Sets the initial gen for random generator
setRandomGen :: StdGen -> FakerSettings -> FakerSettings
setRandomGen gen fs = fs {fsrandomGen = gen}

-- | Get the initial gen for random generator
getRandomGen :: FakerSettings -> StdGen
getRandomGen settings = fsrandomGen settings

-- | Get the Locale settings for your fake data source
getLocale :: FakerSettings -> Text
getLocale FakerSettings {..} = fslocale

-- | Set the output of fakedata to be deterministic. With this you
-- will get the same ouput for the functions every time.
--
-- @
-- λ> import qualified Faker.Name as FN
-- λ> :t FN.name
-- FN.name :: Fake Text
-- λ> generateWithSettings (setDeterministic defaultFakerSettings) FN.name
-- "Antony Langosh"
-- λ> generateWithSettings (setDeterministic defaultFakerSettings) FN.name
-- "Antony Langosh"
-- @
setDeterministic :: FakerSettings -> FakerSettings
setDeterministic fs = fs {fsDeterministic = True}

-- | Set the output of fakedata to be non deterministic. With this you
-- will get different ouput for the fake functions.
--
-- @
-- λ> generateWithSettings (setNonDeterministic defaultFakerSettings) FN.name
-- "Macy Shanahan"
-- λ> generateWithSettings (setNonDeterministic defaultFakerSettings) FN.name
-- "Rudy Dickinson II"
-- @
setNonDeterministic :: FakerSettings -> FakerSettings
setNonDeterministic fs = fs {fsDeterministic = False}

-- | Check if the fake data output is deterministic or not. A True
-- value indicates that it is deterministic.
getDeterministic :: FakerSettings -> Bool
getDeterministic FakerSettings {..} = fsDeterministic

getCacheField :: FakerSettings -> IO (HM.HashMap CacheFieldKey (Vector Text))
getCacheField FakerSettings {..} = readIORef fsCacheField

setCacheField ::
     HM.HashMap CacheFieldKey (Vector Text) -> FakerSettings -> IO ()
setCacheField cache fs = do
  writeIORef (fsCacheField fs) cache

replaceCacheField ::
     HM.HashMap CacheFieldKey (Vector Text) -> FakerSettings -> IO FakerSettings
replaceCacheField cache fs = do
  ref <- newIORef cache
  pure $ fs {fsCacheField = ref}

getCacheFile :: FakerSettings -> IO (HM.HashMap CacheFileKey Value)
getCacheFile FakerSettings {..} = readIORef fsCacheFile

setCacheFile :: HM.HashMap CacheFileKey Value -> FakerSettings -> IO ()
setCacheFile cache fs = writeIORef (fsCacheFile fs) cache

replaceCacheFile ::
     HM.HashMap CacheFileKey Value -> FakerSettings -> IO FakerSettings
replaceCacheFile cache fs = do
  ref <- newIORef cache
  pure $ fs {fsCacheFile = ref}

-- | Fake data type. This is the type you will be using to produce
-- fake values.
newtype Fake a = Fake
  { unFake :: FakerSettings -> IO a
  }

instance Functor Fake where
  fmap :: (a -> b) -> Fake a -> Fake b
  fmap f (Fake h) =
    Fake
      (\r -> do
         a <- h r
         let b = f a
         pure b)

instance Applicative Fake where
  pure x = Fake (\_ -> pure x)
  (<*>) = ap

instance Monad Fake where
  return :: a -> Fake a
  return x = Fake (\_ -> return x)
  (>>=) :: Fake a -> (a -> Fake b) -> Fake b
  (Fake h) >>= k =
    Fake
      (\settings ->
         let stdGen = getRandomGen settings
             (r1, _) = split stdGen
             m = do
               (item :: a) <- h settings
               let (Fake k1) = k item
               k1 (setRandomGen r1 settings)
          in m)

instance MonadIO Fake where
  liftIO :: IO a -> Fake a
  liftIO xs = Fake (\_ -> xs >>= pure)

-- | Generate fake value with 'defaultFakerSettings'
--
-- @
-- λ> import qualified Faker.Name as FN
-- λ> generate FN.name
-- "Antony Langosh"
-- @
generate :: Fake a -> IO a
generate (Fake f) = do
  cacheField <- newIORef HM.empty
  cacheFile <- newIORef HM.empty
  f $ defaultFakerSettings {fsCacheField = cacheField, fsCacheFile = cacheFile}

-- | Generate fake value with supplied 'FakerSettings'
--
-- @
-- λ> generateWithSettings defaultFakerSettings FN.name
-- "Antony Langosh"
-- @
generateWithSettings :: FakerSettings -> Fake a -> IO a
generateWithSettings settings (Fake f) = do
  let deterministic = getDeterministic settings
  stdGen <-
    if deterministic
      then pure $ getRandomGen settings
      else newStdGen
  let newSettings = setRandomGen stdGen settings
  cacheField <- newIORef HM.empty
  cacheFile <- newIORef HM.empty
  f $ newSettings {fsCacheField = cacheField, fsCacheFile = cacheFile}