{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}

-----------------------------------------------------------------------------
-- |
-- Module      :  Webcrank.Dispatch
-- Copyright   :  (C) 2015 Richard Wallace
-- License     :  BSD-style (see the file LICENSE)
-- Maintainer  :  Richard Wallace <rwallace@thewallacepack.net>
-- Stability   :  provisional
-----------------------------------------------------------------------------

module Webcrank.Dispatch
  ( -- * Paths
    -- ** Building Paths
    root
  , (</>)
  , param
  , RR.Path
    -- ** Rendering Paths
  , renderPath
  , params
  , HBuild'(..)
    -- * Dispatching
  , (==>)
  , dispatch
  , Dispatcher
  ) where

import Control.Monad.Identity
import qualified Data.HashMap.Strict as HM
import Data.HVect as HV
import Data.Maybe
import Data.Monoid
import Data.Text (Text)
import Data.Typeable (Typeable)
import Web.PathPieces
import Web.Routing.AbstractRouter
import qualified Web.Routing.SafeRouting as RR

-- | The simplest @'Path'@ is the @root@ path, which is equivalent to @/@.
root :: RR.Path '[]
root = RR.root

-- | Other routes can be built with @</>@:
--
-- @
-- docsPath = "package" \<\/> "webcrank-dispatch-0.1" \<\/> "docs"
-- @
(</>) :: RR.Path as -> RR.Path bs -> RR.Path (Append as bs)
(</>) = (RR.</>)

-- | Paths can contain parameters.  To create a parameterized path, use
-- @param@ as a path component:
--
-- @
-- docsPath :: Path '[String]
-- docsPath = "package" \<\/> param \<\/> "docs"
-- @
--
-- Paths can contain as many parameters of varying types as needed:
--
-- @
-- wat :: Path '[String, Int, Bool, Int, String]
-- wat :: "this" \<\/> param \<\/> param \<\/> "crazyness" \<\/> param \<\/> "ends" \<\/> param \<\/> param
-- @
--
-- Path parameters can be of any type that have instances for @'Typeable'@
-- and @'PathPiece'@.
param :: (Typeable a, PathPiece a) => RR.Path (a ': '[])
param = RR.var

-- | @Path@s can be rendered using @'renderPath'@ and
-- @'params'@.
--
-- >>> renderPath root params
-- ["/"]
--
-- >>> renderPath docsPath $ params "webcrank-dispatch-0.1"
-- ["package", "webcrank-dispatch-0.1", "docs"]
--
-- >>> renderPath wat $ params "down is up" 42 False 7 "up is down"
-- ["this", "down is up", "42", "crazyness", "False", "ends", "7", "up is down"]
--
-- Note in the last example that no encoding is done by @renderPath@.
renderPath :: RR.Path l -> HVect l -> [Text]
renderPath = RR.renderRoute'

params :: (HBuild' '[] r) => r
params = hBuild' HNil

class HBuild' l r where
  hBuild' :: HVect l -> r

instance (l' ~ ReverseLoop l '[]) => HBuild' l (HVect l') where
  hBuild' l = HV.reverse l

instance HBuild' (a ': l) r => HBuild' l (a -> r) where
  hBuild' l x = hBuild' (x :&: l)

-- | An elementary @'Dispatcher'@ can be built using @'==>'@.
--
-- @disp = root ==> \"Dispatched\"@
--
-- @Dispatcher@s form a @'Monoid'@, so more interesting dispatchers can
-- be built with @'<>'@ or @'mconcat'@.
--
-- @
-- disp = mconcat
--   [ root ==> "Welcome!"
--   , "echo" </> param ==> id
--   ]
-- @
(==>) :: RR.Path as -> HVectElim as a -> Dispatcher a
(==>) p r = Dispatcher $ hookRoute () (SafeRouterPath p) (RR.HVectElim' r)
infixr 8 ==>

-- | Dispatching requests is done with @'dispatch'@. It turns a
-- @Dispatcher@ into a function from a list of decoded path components
-- to a possible handler.
--
-- >>> dispatch (root ==> "Welcome!") [""]
-- Just "Welcome!"
--
-- >>> dispatch (root ==> "Welcome!") ["echo", "Goodbye!"]
-- Nothing
--
-- >>> dispatch (root ==> "Welcome!" <> "echo" </> param ==> id) ["echo", "Goodbye!"]
-- Just "Goodbye!"
dispatch :: Dispatcher a -> [Text] -> Maybe a
dispatch (Dispatcher r) = case runIdentity $ runRegistry SafeRouter r of
  (_, f, _) -> fmap snd . listToMaybe . f ()

newtype Dispatcher a = Dispatcher (RegistryT (SafeRouter a) () () Identity ())

instance Monoid (Dispatcher a) where
  mempty = Dispatcher $ return ()
  mappend (Dispatcher x) (Dispatcher y) = Dispatcher $ x >> y

data SafeRouter a = SafeRouter

instance AbstractRouter (SafeRouter a) where
  newtype Registry (SafeRouter a) = SafeRouterReg (RR.PathMap a, [[Text] -> a])
  newtype RoutePath (SafeRouter a) xs = SafeRouterPath (RR.Path xs)
  type RouteAction (SafeRouter a) = RR.HVectElim' a
  type RouteAppliedAction (SafeRouter a) = a
  subcompCombine (SafeRouterPath p1) (SafeRouterPath p2) =
    SafeRouterPath $ p1 </> p2
  emptyRegistry = SafeRouterReg (RR.emptyPathMap, [])
  rootPath = SafeRouterPath RR.Empty
  defRoute (SafeRouterPath path) action (SafeRouterReg (a, cAll)) =
    SafeRouterReg
      ( RR.insertPathMap' path (HV.uncurry $ RR.flipHVectElim action) a
      , cAll
      )
  fallbackRoute routeDef (SafeRouterReg (a, cAll)) =
    SafeRouterReg (a, cAll <> [routeDef])
  matchRoute (SafeRouterReg (a, cAll)) pathPieces =
    let matches = RR.match a pathPieces
        matches' =
            if Prelude.null matches
            then matches <> fmap (\f -> f pathPieces) cAll
            else matches
    in zip (replicate (Prelude.length matches') HM.empty) matches'