{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}

{- | Scotty apps with configuration and states
-}
module Web.Scottish (
    -- * Types
    Scottish, Scottish'
  , ScottishM, ScottishActionM, ScottishM', ScottishActionM'
    -- * App runners/converters
  , scottish, scottishApp, scottishOpts
  , scottish', scottishApp', scottishOpts', handleRaisedStatus
    -- * Configuratio/State accessors
    -- ** Shared by 'ScottyM' & 'ScottyActionM'
  , getConfig, getGlobalState, getLocalState, (>$<)
    -- ** 'ScottyActionM' only
  , setLocalState, modifyLocalState
    -- ** 'ScottyM' only
  , setConfig, modifyConfig
  , setGlobalState, modifyGlobalState
    -- * Re-exports from Web.Scotty.Trans
  , module Trans
  ) where

import           Control.Applicative
import           Control.Lens
import           Control.Monad.Reader
import           Control.Monad.State
import           Control.Monad.Trans.Identity

import           Data.Default
import           Data.IORef
import qualified Data.Text.Lazy as T

import           Network.HTTP.Types.Status
import           Network.Wai
import           Network.Wai.Handler.Warp (Port)

import           Web.Scotty.Trans
import qualified Web.Scotty.Trans as Trans hiding (scottyAppT, scottyOptsT,
                                                   scottyT)

data ScottishState c s s' = ScottishState { _config      :: c
                                          , _globalState :: s
                                          , _localState  :: s'
                                          }

$(makeLenses ''ScottishState)

instance (Default c, Default s, Default s')
      => Default (ScottishState c s s') where
    def = ScottishState def def def

-- | 'config' is read-only in 'ActionT', but read-write in 'ScottyT' for
-- initialization.
--
-- 'localState' is reinitialized to 'def' for every execution of each 'ActionT'.
--
-- 'globalState' may be a TVar, or other monadic mutable data types. Normally,
-- 'globalState' should not be necessary for server apps, as there may be
-- multiple instance of the server running, even across machines, and you sure
-- cannot make them share the same state. However, you may be able to do some
-- process-local caching or user interaction (say in games), with 'globalState'.
newtype Scottish config globalState localState a =
    Scottish { unScottish :: StateT (ScottishState config globalState
                                     localState) IO a }
    deriving (Functor, Monad, Applicative, MonadIO,
              MonadState (ScottishState config globalState localState))

-- | 'Scottish' monad without 'globalState'
type Scottish' c s' = Scottish c () s'

type ScottishM e c s s' = ScottyT e (Scottish c s s')
type ScottishActionM e c s s' = ActionT e (Scottish c s s')

instance ScottyError Status where
    -- for now, Scotty only `raise`s when input is bad
    stringError = either (const badRequest400) id . (toEnum<$>) . readEither
                  . T.pack
    showError   = T.pack . show

type ScottishM' c s' = ScottyT Status (Scottish' c s')
type ScottishActionM' c s' = ActionT Status (Scottish' c s')

-- | Run a scottish app with warp.
scottish :: (Default c, Default s, Default s')
         => Port
         -> ScottishM e c s s' ()
         -> IO ()
scottish p = (mkScottishRunners>>=) . flip (uncurry $ scottyT p)

-- | Turn a scottish app into a WAI one, which can be run with any WAI handler.
scottishApp :: (Default c, Default s, Default s')
            => ScottishM e c s s' ()
            -> IO Application
scottishApp = (mkScottishRunners>>=) . flip (uncurry scottyAppT)

-- | Run a scottish app with extra options.
scottishOpts :: (Default c, Default s, Default s')
             => Options -> ScottishM e c s s' () -> IO ()
scottishOpts opts = (mkScottishRunners>>=) . flip (uncurry $ scottyOptsT opts)

-- | Scottish app runner with 'Status' handler installed.
scottish' :: (Default c, Default s, Default s')
          => Port -> ScottishM Status c s s' () -> IO ()
scottish' p = (mkScottishRunners>>=) . flip (uncurry $ scottyT p)
              . handleRaisedStatus

-- | Scottish app converter with 'Status' handler installed.
scottishApp' :: (Default c, Default s, Default s')
             => ScottishM Status c s s' () -> IO Application
scottishApp' = (mkScottishRunners>>=) . flip (uncurry scottyAppT)
               . handleRaisedStatus

-- | Scottish app runner with 'Status' handler installed.
scottishOpts' :: (Default c, Default s, Default s')
              => Options -> ScottishM Status c s s' () -> IO ()
scottishOpts' opts = (mkScottishRunners>>=) . flip (uncurry $ scottyOptsT opts)
                     . handleRaisedStatus

mkScottishRunners :: (Default c, Default s, Default s', MonadIO n)
                  => IO (forall a. Scottish c s s' a -> n a,
                         Scottish c s s' Response -> IO Response)
mkScottishRunners = do
    shared <- newIORef undefined
    let initializer m = liftIO $ do
            (r, ss) <- runStateT (unScottish m) def
            writeIORef shared ss
            return r
        actionRunner m = do
            ss <- readIORef shared
            evalStateT (unScottish m) $ set localState def ss
    return (initializer, actionRunner)

getConfig :: (MonadTrans t) => t (Scottish c s s') c
getConfig = lift . use $ config

getGlobalState :: (MonadTrans t) => t (Scottish c s s') s
getGlobalState = lift . use $ globalState

setConfig :: c -> ScottishM e c s s' ()
setConfig = lift . assign config

modifyConfig :: (c -> c) -> ScottishM e c s s' ()
modifyConfig = lift . (config%=)

setGlobalState :: s -> ScottishM e c s s' ()
setGlobalState = lift . assign globalState

modifyGlobalState :: (s -> s) -> ScottishM e c s s' ()
modifyGlobalState = lift . (globalState%=)

getLocalState :: (ScottyError e) => ScottishActionM e c s s' s'
getLocalState = lift . use $ localState

setLocalState :: (ScottyError e) => s' -> ScottishActionM e c s s' ()
setLocalState = lift . assign localState

modifyLocalState:: (ScottyError e) => (s' -> s') -> ScottishActionM e c s s' ()
modifyLocalState = lift . (localState%=)

-- | 'Status' is a good candidate as an 'ScottyError' instance by itself. Call
-- this function to install a default handler to report the 'Status' when one is
-- raised.
--
-- Also, you may want to define instances of 'ScottyError' with tuples/records
-- containing 'Status', to provide more informative error pages.
handleRaisedStatus :: ScottishM Status c s s' () -> ScottishM Status c s s' ()
handleRaisedStatus = ((defaultHandler $ \e -> status e)>>)

-- | Lift a 'Scottish' function to a 'MonadTrans' wrapped 'Scottish' one.
(>$<) :: MonadTrans t
      => (a -> Scottish c s s' b)
      -> IdentityT (Scottish c s s') a
      -> t (Scottish c s s') b
infixl 4 >$<
f >$< x = lift $ runIdentityT x >>= f