{-# LANGUAGE GADTs #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE DeriveFunctor #-}

module UrlPath.Types where

import Data.String
import Data.Monoid
import Control.Applicative
import Control.Monad
import Control.Monad.Trans
import Control.Monad.Reader.Class

-- | A Url string - a target page and GET parameters. We only require a 
-- constraint of @IsString@ so that construction can be convenient, but 
-- rendering will require a monomorphic type.
data UrlString a where
  UrlString :: ( IsString a
               , Monoid a ) =>
               a
            -> [(a, a)]
            -> UrlString a

-- | We choose to not provide a @Show@ instance for @UrlString@ to evade the 
-- @String@ demand.
showUrlString :: UrlString a
              -> a
showUrlString (UrlString !t []) = t
showUrlString (UrlString !t ((!k,!v):xs)) =
  t <> "?" <> k <> "=" <> v <>
    foldl (\acc (x,y) -> acc <> "&" <> x <> "=" <> y) "" xs


-- | Lifts a raw target path and a GET parameter pair into a @UrlString@.
(<?>) :: ( IsString a
         , Monoid a ) =>
         a -- ^ Target string
      -> (a, a) -- ^ GET Parameter
      -> UrlString a
(<?>) !t !kv = UrlString t [kv]

infixl 9 <?>

-- | Adds another GET parameter pair to a @UrlString@.
(<&>) :: ( IsString a
         , Monoid a ) =>
         UrlString a -- ^ Old Url
      -> (a, a) -- ^ Additional GET Parameter
      -> UrlString a
(<&>) (UrlString !t !p) !kv = UrlString t $ p ++ [kv]

infixl 8 <&>


-- | Render the Url String as relative
expandRelative :: ( IsString a
                  , Monoid a ) =>
                  UrlString a
               -> a
expandRelative = showUrlString

-- | Render the Url String as grounded
expandGrounded :: ( IsString a
                  , Monoid a ) =>
                  UrlString a
               -> a
expandGrounded !x = "/" <> showUrlString x

-- | Render the Url String as absolute - getting the root from a @MonadReader@ 
-- context. The @Monoid@ instance will be decided monomorphically, therefore a 
-- type signature will be needed when ran.
expandAbsolute :: ( MonadReader a m
                  , IsString a
                  , Monoid a ) =>
                  UrlString a
               -> m a
expandAbsolute !x = do
  host <- ask
  return $ host <> "/" <> showUrlString x

-- | Render the Url String as absolute, but with your own configuration type.
--
-- > data SiteConfig = SiteConfig { host :: T.Text
-- >                              , cdnHost :: T.Text
-- >                              }
-- >   deriving (Show, Eq)
-- >
-- > foo :: HtmlT (Reader SiteConfig) ()
-- > foo = do
-- >   url <- lift $ expandAbsoluteWith ("foo.php" <?> ("bar","baz")) host
-- >   script_ [src_ url] ""
-- >
-- > bar :: LT.Text 
-- > bar = (runReader (runTextT foo)) $
-- >   SiteConfig "example.com" "cdn.example.com"
expandAbsoluteWith :: ( MonadReader a m
                      , IsString a
                      , Monoid a ) =>
                      UrlString a
                   -> (a -> a)
                   -> m a
expandAbsoluteWith !x f = do
  root <- liftM f ask
  return $ root <> "/" <> showUrlString x

-- | Rendering mode transformer. This isn't an instance of @UrlReader@ - to use, 
-- simple @lift@ as many levels as you need:
-- 
-- > foo :: Monad m => HtmlT (RelativeUrlT m) ()
-- > foo = do
-- >   path <- lift $ url $ "foo.php" <?> ("bar","baz")
-- >   script_ [src_ path] ""
--
-- When rendering @foo@, simply use the Transformer's @run@ function to convert 
-- it to a reader:
--
-- > bar :: ( Monad m, IsString a, Monoid a ) => m a
-- > bar = (runRelativeUrlT (renderTextT foo)) "example.com"
newtype RelativeUrlT h m a = RelativeUrlT { runRelativeUrlT :: h -> m a }
  deriving Functor

instance Applicative f => Applicative (RelativeUrlT h f) where
  (<*>) f x = RelativeUrlT $ \a ->
    (<*>) (runRelativeUrlT f a) (runRelativeUrlT x a)

instance Monad m => Monad (RelativeUrlT h m) where
  return x = RelativeUrlT $ \_ -> return x
  m >>= f = RelativeUrlT $ \a ->
    runRelativeUrlT m a >>= (\x -> runRelativeUrlT (f x) a)

instance MonadTrans (RelativeUrlT h) where
  lift m = RelativeUrlT (const m)

instance ( Monad m
         , IsString a ) => MonadReader a (RelativeUrlT a m) where
  ask = return ""

instance MonadIO m => MonadIO (RelativeUrlT a m) where
  liftIO = lift . liftIO

-- | Concrete Monad for automatically coercing HtmlT's to use a mode of Url 
-- rendering (relative, grounded, or absolute).
--
-- > foo :: HtmlT RelativeUrl ()
-- > foo = do
-- >   path <- lift $ url $ "foo.php" <?> ("bar","baz")
-- >   script_ [src_ path] ""
--
-- when running the monad reader, use the @runUrlReader@ member function of the
-- @UrlReader@ typeclass:
--
-- > bar :: ( IsString a, Monoid a ) => a
-- > bar = (runUrlReader (renderTextT foo)) "example.com"
--
-- To change the deployment sheme, simply coerce the environment monad in @foo@.
newtype RelativeUrl h a = RelativeUrl { runRelativeUrl :: h -> a }
  deriving Functor

instance Applicative (RelativeUrl h) where
  (<*>) f x = RelativeUrl $ \a ->
    runRelativeUrl f a (runRelativeUrl x a)

instance Monad (RelativeUrl h) where
  return x = RelativeUrl $ const x
  m >>= f = RelativeUrl $ \a ->
    (\y -> runRelativeUrl (f y) a) (runRelativeUrl m a)

instance IsString a => MonadReader a (RelativeUrl a) where
  ask = return ""

newtype GroundedUrlT h m a = GroundedUrlT { runGroundedUrlT :: h -> m a }

instance Functor f => Functor (GroundedUrlT h f) where
  fmap f x = GroundedUrlT $ \a ->
    fmap f (runGroundedUrlT x a)

instance Applicative f => Applicative (GroundedUrlT h f) where
  (<*>) f x = GroundedUrlT $ \a ->
    (<*>) (runGroundedUrlT f a) (runGroundedUrlT x a)

instance Monad m => Monad (GroundedUrlT h m) where
  return x = GroundedUrlT $ \_ -> return x
  m >>= f = GroundedUrlT $ \a ->
    runGroundedUrlT m a >>= (\x -> runGroundedUrlT (f x) a)

instance MonadTrans (GroundedUrlT h) where
  lift m = GroundedUrlT (const m)

instance ( Monad m
         , IsString a ) => MonadReader a (GroundedUrlT a m) where
  ask = return "/"

instance MonadIO m => MonadIO (GroundedUrlT a m) where
  liftIO = lift . liftIO

newtype GroundedUrl h a = GroundedUrl { runGroundedUrl :: h -> a }

instance Functor (GroundedUrl h) where
  fmap f x = GroundedUrl $ \a -> f $ runGroundedUrl x a

instance Applicative (GroundedUrl h) where
  (<*>) f x = GroundedUrl $ \a ->
    runGroundedUrl f a (runGroundedUrl x a)

instance Monad (GroundedUrl h) where
  return x = GroundedUrl $ const x
  m >>= f = GroundedUrl $ \a ->
    (\y -> runGroundedUrl (f y) a) (runGroundedUrl m a)

instance IsString a => MonadReader a (GroundedUrl a) where
  ask = return "/" 
  
newtype AbsoluteUrlT h m a = AbsoluteUrlT { runAbsoluteUrlT :: h -> m a }

instance Functor f => Functor (AbsoluteUrlT h f) where
  fmap f x = AbsoluteUrlT $ \a ->
    fmap f (runAbsoluteUrlT x a)

instance Applicative f => Applicative (AbsoluteUrlT h f) where
  (<*>) f x = AbsoluteUrlT $ \a ->
    (<*>) (runAbsoluteUrlT f a) (runAbsoluteUrlT x a)

instance Monad m => Monad (AbsoluteUrlT h m) where
  return x = AbsoluteUrlT $ const $ return x
  m >>= f = AbsoluteUrlT $ \a ->
    runAbsoluteUrlT m a >>= (\x -> runAbsoluteUrlT (f x) a)

instance MonadTrans (AbsoluteUrlT h) where
  lift m = AbsoluteUrlT (const m)

instance ( Monad m
         , IsString a ) => MonadReader a (AbsoluteUrlT a m) where
  ask = AbsoluteUrlT return

instance MonadIO m => MonadIO (AbsoluteUrlT a m) where
  liftIO = lift . liftIO

newtype AbsoluteUrl h a = AbsoluteUrl { runAbsoluteUrl :: h -> a }

instance Functor (AbsoluteUrl h) where
  fmap f x = AbsoluteUrl $ \a -> f $ runAbsoluteUrl x a

instance Applicative (AbsoluteUrl h) where
  (<*>) f x = AbsoluteUrl $ \a ->
    runAbsoluteUrl f a (runAbsoluteUrl x a)

instance Monad (AbsoluteUrl h) where
  return x = AbsoluteUrl $ const x
  m >>= f = AbsoluteUrl $ \a ->
    (\y -> runAbsoluteUrl (f y) a) (runAbsoluteUrl m a)

instance IsString a => MonadReader a (AbsoluteUrl a) where
  ask = AbsoluteUrl id