{-|
Module      : Flipper
Description : Main user interface for the Flipper library
-}
module Control.Flipper
    ( enabled
    , enabledFor
    , enable
    , enableFor
    , enableForPercentage
    , disable
    , toggle
    , whenEnabled
    , whenEnabledFor
    , module Control.Flipper.Types
    ) where

import           Control.Monad         (when)
import           Data.Monoid           ((<>))
import qualified Data.Set as S

import           Control.Flipper.Types

{- |
The 'whenEnabled' function calls the supplied function, 'm ()', when the given
'FeatureName' is enabled.

When the feature specified by 'FeatureName' is disabled, 'm ()' is not
evaluated.
-}
whenEnabled :: (HasFeatureFlags m)
            => FeatureName -> m () -> m ()
whenEnabled fName f = do
    featureEnabled <- enabled fName
    when featureEnabled f

{- |
The 'whenEnabledFor' function calls the supplied function, 'm ()', when the given
'FeatureName' is enabled for the given actor.

When the feature specified by 'FeatureName' is disabled for the given actor,
'm ()' is not evaluated.
-}
whenEnabledFor :: (HasFeatureFlags m, HasActorId a)
               => FeatureName -> a -> m () -> m ()
whenEnabledFor fName actor f = do
    featureEnabled <- enabledFor fName actor
    when featureEnabled f

{- |
The 'enabled' function returns a Bool indicating if the queried feature is
active.

When the queried FeatureName does not exists, 'enabled' returns False.
-}
enabled :: HasFeatureFlags m
        => FeatureName -> m Bool
enabled fName = do
    mFeature <- getFeature fName
    case mFeature of
        (Just feature) -> return $ isEnabled feature
        Nothing        -> return False

{- |
The 'enabledFor' function returns a Bool indicating if the queried feature is
active for the given enitty.

If the queried FeatureName does not exists, 'enabledFor' returns False.
-}
enabledFor :: (HasFeatureFlags m, HasActorId a)
           => FeatureName -> a -> m Bool
enabledFor fName actor = do
    mFeature <- getFeature fName
    case mFeature of
        Nothing        -> return False
        (Just feature) -> return $ isEnabledFor feature actor

{- |
The 'enable' function activates a feature globally.

When the FeatureName does not exist, it is created and set to active.
-}
enable :: ModifiesFeatureFlags m
       => FeatureName -> m ()
enable fName = upsertFeature fName True

{- |
The 'enableFor' function activates a feature for a single actor.

If the FeatureName does not exist in the store, it is created and set to active
only for the given actor.
-}
enableFor :: (ModifiesFeatureFlags m, HasActorId a)
          => FeatureName -> a -> m ()
enableFor fName actor = update fName (enableFor' actor fName)

enableFor' :: HasActorId a => a -> FeatureName -> Maybe Feature -> Maybe Feature
enableFor' actor _ (Just feature) = Just $ feature { enabledActors = S.insert (actorId actor) (enabledActors feature) }
enableFor' actor fname Nothing = Just $ (mkFeature fname) { enabledActors = S.singleton (actorId actor) }

{- |
The 'enableForPercentage' function activates a feature for a percentage of actors.

If the FeatureName does not exist in the store, it is created and set to active
only for the specified percentage.
-}
enableForPercentage :: (ModifiesFeatureFlags m)
          => FeatureName -> Percentage -> m ()
enableForPercentage fName pct
    | pct < 0   = raiseOutOfRangeError
    | pct > 100 = raiseOutOfRangeError
    | otherwise = update fName (enableForPercentage' pct fName)
    where
        raiseOutOfRangeError = error ("Invalid percentage: " <> show pct <> " Expected a value between 0 - 100")

enableForPercentage' :: Percentage -> FeatureName -> Maybe Feature -> Maybe Feature
enableForPercentage' pct _ (Just feature) = Just $ feature { enabledPercentage = pct }
enableForPercentage' pct fname Nothing = Just (mkFeature fname) { enabledPercentage = pct }

{- |
The 'disable' function deactivates a feature globally.

When the FeatureName does not exist, it is created and set to inactive.
-}
disable :: ModifiesFeatureFlags m
        => FeatureName -> m ()
disable fName = upsertFeature fName False

{- |
The 'toggle' function flips the current state of a feature globally.

When the FeatureName does not exist, it is created and set to True.
-}
toggle :: ModifiesFeatureFlags m
            => FeatureName -> m ()
toggle fName = update fName flipIt'
    where
        flipIt' :: Maybe Feature -> Maybe Feature
        flipIt' (Just feature) = Just (feature { isEnabled = not (isEnabled feature) })
        flipIt' Nothing  = Just $ (mkFeature fName) { isEnabled = True }

{- |
When the FeatureName exists in the store, it is set to the specified `isEnabled` state.

When the FeatureName does not exist, it is created and set to the specified `isEnabled` state.
-}
upsertFeature :: ModifiesFeatureFlags m
              => FeatureName -> Bool -> m ()
upsertFeature fName featureEnabled =
    update fName upsertFeature'
    where
        upsertFeature' :: (Maybe Feature -> Maybe Feature)
        upsertFeature' Nothing        = Just $ (mkFeature fName) { isEnabled = featureEnabled }
        upsertFeature' (Just feature) = Just (feature { isEnabled = featureEnabled })