{-# LANGUAGE CPP                        #-}
{-# LANGUAGE BangPatterns               #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ImpredicativeTypes         #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE FlexibleInstances          #-}

#ifndef MIN_VERSION_comonad
#define MIN_VERSION_comonad(x,y,z) 1
#endif

module Snap.Snaplet.Internal.Types where

import           Control.Applicative
import           Control.Error
import           Control.Lens
import           Control.Monad.CatchIO hiding (Handler)
import           Control.Monad.Reader
import           Control.Monad.State.Class
import           Control.Monad.Trans.Writer hiding (pass)
import           Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as B
import           Data.Configurator.Types
import           Data.IORef
import           Data.Monoid
import           Data.Text (Text)

import           Snap.Core
import qualified Snap.Snaplet.Internal.LensT as LT
import qualified Snap.Snaplet.Internal.Lensed as L


------------------------------------------------------------------------------
-- | An opaque data type holding internal snaplet configuration data.  It is
-- exported publicly because the getOpaqueConfig function in MonadSnaplet
-- makes implementing new instances of MonadSnaplet more convenient.
data SnapletConfig = SnapletConfig
    { _scAncestry        :: [Text]
    , _scFilePath        :: FilePath
    , _scId              :: Maybe Text
    , _scDescription     :: Text
    , _scUserConfig      :: Config
    , _scRouteContext    :: [ByteString]
    , _scRoutePattern    :: Maybe ByteString
        -- ^ Holds the actual route pattern passed to addRoutes for the
        -- current handler.  Nothing during initialization and before route
        -- dispatech.
    , _reloader          :: IO (Either Text Text) -- might change
        -- ^ This is the universal reload action for the top-level site.  We
        -- can't update this in place to be a reloader for each individual
        -- snaplet because individual snaplets can't be reloaded in isolation
        -- without losing effects that subsequent hooks may have had.
    }

makeLenses ''SnapletConfig


------------------------------------------------------------------------------
-- | Joins a reversed list of directories into a path.
buildPath :: [ByteString] -> ByteString
buildPath ps = B.intercalate "/" $ filter (not . B.null) $ reverse ps


------------------------------------------------------------------------------
-- | Joins a reversed list of directories into a path.
getRootURL :: SnapletConfig -> ByteString
getRootURL sc = buildPath $ _scRouteContext sc


------------------------------------------------------------------------------
-- | Snaplet's type parameter 's' here is user-defined and can be any Haskell
-- type.  A value of type @Snaplet s@ countains a couple of things:
--
-- * a value of type @s@, called the \"user state\".
--
-- * some bookkeeping data the framework uses to plug things together, like
--   the snaplet's configuration, the snaplet's root directory on the
--   filesystem, the snaplet's root URL, and so on.
data Snaplet s = Snaplet
    { _snapletConfig   :: SnapletConfig
    , _snapletModifier :: s -> IO ()
        -- ^ See the _reloader comment for why we have to use this to reload
        -- single snaplets in isolation.  This action won't actually run the
        -- initializer at all.  It will only modify the existing state.  It is
        -- the responsibility of the snaplet author to avoid using this in
        -- situations where it will destroy data in its state that was created
        -- by subsequent hook actions.
    , _snapletValue    :: s
    }

makeLenses ''Snaplet

--instance Functor Snaplet where
--  fmap f (Snaplet c r a) = Snaplet c r (f a)
--
--instance Foldable Snaplet where
--  foldMap f (Snaplet _ _ a) = f a
--
--instance Traversable Snaplet where
--  traverse f (Snaplet c r a) = Snaplet c r <$> f a
--
--instance Comonad Snaplet where
--  extract (Snaplet _ _ a) = a
--
-- #if !(MIN_VERSION_comonad(3,0,0))
-- instance Extend Snaplet where
-- #endif
--   extend f w@(Snaplet c r _) = Snaplet c r (f w)

{-
------------------------------------------------------------------------------
-- | A lens referencing the opaque SnapletConfig data type held inside
-- Snaplet.
snapletConfig :: SimpleLens (Snaplet a) SnapletConfig


------------------------------------------------------------------------------
-- | A lens referencing the user-defined state type wrapped by a Snaplet.
snapletValue :: SimpleLens (Snaplet a) a
-}

type SnapletLens s a = ALens' s (Snaplet a)

------------------------------------------------------------------------------
-- | Transforms a lens of the type you get from makeLenses to an similar lens
-- that is more suitable for internal use.
subSnaplet :: SnapletLens a b
           -> SnapletLens (Snaplet a) b
subSnaplet l = snapletValue . l


------------------------------------------------------------------------------
-- | The m type parameter used in the MonadSnaplet type signatures will
-- usually be either Initializer or Handler, but other monads may sometimes be
-- useful.
--
-- Minimal complete definition:
--
-- * 'withTop'', 'with'', 'getLens', and 'getOpaqueConfig'.
--
class MonadSnaplet m where
    -- | Runs a child snaplet action in the current snaplet's context.  If you
    -- think about snaplet lenses using a filesystem path metaphor, the lens
    -- supplied to this snaplet must be a relative path.  In other words, the
    -- lens's base state must be the same as the current snaplet.
    with :: SnapletLens v v'
             -- ^ A relative lens identifying a snaplet
         -> m b v' a
             -- ^ Action from the lense's snaplet
         -> m b v a
    with l = with' (subSnaplet l)

    -- | Like 'with' but doesn't impose the requirement that the action
    -- being run be a descendant of the current snaplet.  Using our filesystem
    -- metaphor again, the lens for this function must be an absolute
    -- path--it's base must be the same as the current base.
    withTop :: SnapletLens b v'
                -- ^ An \"absolute\" lens identifying a snaplet
            -> m b v' a
                -- ^ Action from the lense's snaplet
            -> m b v a
    withTop l = withTop' (subSnaplet l)

    -- | A variant of 'with' accepting a lens from snaplet to snaplet.  Unlike
    -- the lens used in the above 'with' function, this lens formulation has
    -- an identity, which makes it useful in certain circumstances.  The
    -- lenses generated by 'makeLenses' will not work with this function,
    -- however the lens returned by 'getLens' will.
    --
    -- @with = with' . subSnaplet@
    with' :: SnapletLens (Snaplet v) v'
          -> m b v' a -> m b v a

    -- Not providing a definition for this function in terms of withTop'
    -- allows us to avoid extra Monad type class constraints, making the type
    -- signature easier to read.
    -- with' l m = flip withTop m . (l .) =<< getLens

    -- | The absolute version of 'with''
    withTop' :: SnapletLens (Snaplet b) v'
             -> m b v' a -> m b v a

    -- | Gets the lens for the current snaplet.
    getLens :: m b v (SnapletLens (Snaplet b) v)

    -- | Gets the current snaplet's opaque config data type.  You'll only use
    -- this function when writing MonadSnaplet instances.
    getOpaqueConfig :: m b v SnapletConfig
    -- NOTE: We can't just use a MonadState (Snaplet v) instance for this
    -- because Initializer has SnapletConfig, but doesn't have a full Snaplet.


------------------------------------------------------------------------------
-- | Gets a list of the names of snaplets that are direct ancestors of the
-- current snaplet.
getSnapletAncestry :: (Monad (m b v), MonadSnaplet m) => m b v [Text]
getSnapletAncestry = return . _scAncestry =<< getOpaqueConfig


------------------------------------------------------------------------------
-- | Gets the snaplet's path on the filesystem.
getSnapletFilePath :: (Monad (m b v), MonadSnaplet m) => m b v FilePath
getSnapletFilePath = return . _scFilePath =<< getOpaqueConfig


------------------------------------------------------------------------------
-- | Gets the current snaple's name.
getSnapletName :: (Monad (m b v), MonadSnaplet m) => m b v (Maybe Text)
getSnapletName = return . _scId =<< getOpaqueConfig


------------------------------------------------------------------------------
-- | Gets a human readable description of the snaplet.
getSnapletDescription :: (Monad (m b v), MonadSnaplet m) => m b v Text
getSnapletDescription = return . _scDescription =<< getOpaqueConfig


------------------------------------------------------------------------------
-- | Gets the config data structure for the current snaplet.
getSnapletUserConfig :: (Monad (m b v), MonadSnaplet m) => m b v Config
getSnapletUserConfig = return . _scUserConfig =<< getOpaqueConfig


------------------------------------------------------------------------------
-- | Gets the base URL for the current snaplet.  Directories get added to
-- the current snaplet path by calls to 'nestSnaplet'.
getSnapletRootURL :: (Monad (m b v), MonadSnaplet m) => m b v ByteString
getSnapletRootURL = liftM getRootURL getOpaqueConfig


------------------------------------------------------------------------------
-- | Constructs a url relative to the current snaplet.
snapletURL :: (Monad (m b v), MonadSnaplet m)
           => ByteString -> m b v ByteString
snapletURL suffix = do
    cfg <- getOpaqueConfig
    return $ buildPath (cleanSuffix : _scRouteContext cfg)
  where
    dropSlash = B.dropWhile (=='/')
    cleanSuffix = B.reverse $ dropSlash $ B.reverse $ dropSlash suffix


------------------------------------------------------------------------------
-- | Snaplet infrastructure is available during runtime request processing
-- through the Handler monad.  There aren't very many standalone functions to
-- read about here, but this is deceptive.  The key is in the type class
-- instances.  Handler is an instance of 'MonadSnap', which means it is the
-- monad you will use to write all your application routes.  It also has a
-- 'MonadSnaplet' instance, which gives you all the functionality described
-- above.
newtype Handler b v a =
    Handler (L.Lensed (Snaplet b) (Snaplet v) Snap a)
  deriving ( Monad
           , Functor
           , Applicative
           , MonadIO
           , MonadPlus
           , MonadCatchIO
           , Alternative
           , MonadSnap)


------------------------------------------------------------------------------
-- | Gets the @Snaplet v@ from the current snaplet's state.
getSnapletState :: Handler b v (Snaplet v)
getSnapletState = Handler get


------------------------------------------------------------------------------
-- | Puts a new @Snaplet v@ in the current snaplet's state.
putSnapletState :: Snaplet v -> Handler b v ()
putSnapletState = Handler . put


------------------------------------------------------------------------------
-- | Modifies the @Snaplet v@ in the current snaplet's state.
modifySnapletState :: (Snaplet v -> Snaplet v) -> Handler b v ()
modifySnapletState f = do
    s <- getSnapletState
    putSnapletState (f s)


------------------------------------------------------------------------------
-- | Gets the @Snaplet v@ from the current snaplet's state and applies a
-- function to it.
getsSnapletState :: (Snaplet v -> b) -> Handler b1 v b
getsSnapletState f = do
    s <- getSnapletState
    return (f s)


------------------------------------------------------------------------------
-- | The MonadState instance gives you access to the current snaplet's state.
instance MonadState v (Handler b v) where
    get = getsSnapletState _snapletValue
    put v = modifySnapletState (set snapletValue v)


------------------------------------------------------------------------------
-- | The MonadState instance gives you access to the current snaplet's state.
instance MonadReader v (Handler b v) where
    ask = getsSnapletState _snapletValue
    local f m = do
        cur <- ask
        put (f cur)
        res <- m
        put cur
        return res


instance MonadSnaplet Handler where
    getLens = Handler ask
    with' !l (Handler !m) = Handler $ L.with l m
    withTop' !l (Handler m) = Handler $ L.withTop l m
    getOpaqueConfig = Handler $ gets _snapletConfig


------------------------------------------------------------------------------
-- | Like 'runBase', but it doesn't require an MVar to be executed.
runPureBase :: Handler b b a -> Snaplet b -> Snap a
runPureBase (Handler m) b = do
        (!a, _) <- L.runLensed m id b
        return $! a


------------------------------------------------------------------------------
-- | Gets the route pattern that matched for the handler.  This lets you find
-- out exactly which of the strings you used in addRoutes matched.
getRoutePattern :: Handler b v (Maybe ByteString)
getRoutePattern =
    withTop' id $ liftM _scRoutePattern getOpaqueConfig


------------------------------------------------------------------------------
-- | Sets the route pattern that matched for the handler.  Use this when to
-- override the default pattern which is the key to the alist passed to
-- addRoutes.
setRoutePattern :: ByteString -> Handler b v ()
setRoutePattern p = withTop' id $
    modifySnapletState (set (snapletConfig . scRoutePattern) (Just p))


------------------------------------------------------------------------------
-- | Pass if the request is not coming from localhost.
failIfNotLocal :: MonadSnap m => m b -> m b
failIfNotLocal m = do
    -- FIXME: this moves to auth once control-panel is done
    rip <- liftM rqRemoteAddr getRequest
    if not $ elem rip [ "127.0.0.1"
                      , "localhost"
                      , "::1" ]
      then pass
      else m


------------------------------------------------------------------------------
-- | Handler that reloads the site.
reloadSite :: Handler b v ()
reloadSite = failIfNotLocal $ do
    cfg <- getOpaqueConfig
    !res <- liftIO $ _reloader cfg
    either bad good res
  where
    bad msg = do
        writeText $ "Error reloading site!\n\n"
        writeText msg
    good msg = do
        writeText msg
        writeText $ "Site successfully reloaded.\n"


------------------------------------------------------------------------------
-- | This function brackets a Handler action in resource acquisition and
-- release.  Like 'bracketSnap',  this is provided because MonadCatchIO's
-- 'bracket' function doesn't work properly in the case of a short-circuit
-- return from the action being bracketed.
--
-- In order to prevent confusion regarding the effects of the
-- aquisition and release actions on the Handler state, this function
-- doesn't accept Handler actions for the acquire or release actions.
--
-- This function will run the release action in all cases where the
-- acquire action succeeded.  This includes the following behaviors
-- from the bracketed Snap action.
--
-- 1. Normal completion
--
-- 2. Short-circuit completion, either from calling 'fail' or 'finishWith'
--
-- 3. An exception being thrown.
bracketHandler :: IO a -> (a -> IO x) -> (a -> Handler b v c) -> Handler b v c
bracketHandler begin end f = Handler . L.Lensed $ \l v b -> do
    bracketSnap begin end $ \a -> case f a of Handler m -> L.unlensed m l v b


------------------------------------------------------------------------------
-- | Information about a partially constructed initializer.  Used to
-- automatically aggregate handlers and cleanup actions.
data InitializerState b = InitializerState
    { _isTopLevel      :: Bool
    , _cleanup         :: IORef (IO ())
    , _handlers        :: [(ByteString, Handler b b ())]
        -- ^ Handler routes built up and passed to route.
    , _hFilter         :: Handler b b () -> Handler b b ()
        -- ^ Generic filtering of handlers
    , _curConfig       :: SnapletConfig
        -- ^ This snaplet config is the incrementally built config for
        -- whatever snaplet is currently being constructed.
    , _initMessages    :: IORef Text
    , _environment     :: String
    , masterReloader   :: (Snaplet b -> Snaplet b) -> IO ()
        -- ^ We can't just hae a simple MVar here because MVars can't be
        -- chrooted.
    }


------------------------------------------------------------------------------
-- | Wrapper around IO actions that modify state elements created during
-- initialization.
newtype Hook a = Hook (Snaplet a -> EitherT Text IO (Snaplet a))


instance Monoid (Hook a) where
    mempty = Hook return
    (Hook a) `mappend` (Hook b) = Hook (a >=> b)


------------------------------------------------------------------------------
-- | Monad used for initializing snaplets.
newtype Initializer b v a =
    Initializer (LT.LensT (Snaplet b)
                          (Snaplet v)
                          (InitializerState b)
                          (WriterT (Hook b) IO)
                          a)
  deriving (Applicative, Functor, Monad, MonadIO)

makeLenses ''InitializerState


instance MonadSnaplet Initializer where
    getLens = Initializer ask
    with' !l (Initializer !m) = Initializer $ LT.with l m
    withTop' !l (Initializer m) = Initializer $ LT.withTop l m
    getOpaqueConfig = Initializer $ liftM _curConfig LT.getBase


------------------------------------------------------------------------------
-- | Opaque newtype which gives us compile-time guarantees that the user is
-- using makeSnaplet and either nestSnaplet or embedSnaplet correctly.
newtype SnapletInit b v = SnapletInit (Initializer b v (Snaplet v))