{-# LANGUAGE FlexibleInstances, GeneralizedNewtypeDeriving,
             MultiParamTypeClasses, TypeOperators #-}
-- | The 'CodeBuilder' monad.
module Nirum.CodeBuilder (
    -- * The CodeBuilder monad
    CodeBuilder,
    runBuilder,
    -- * Builder operations
    writeLine,
    nest,
    lookupType,
    -- * Examples
    -- $examples
    ) where

import Control.Applicative (Applicative)
import Control.Monad (Monad)
import qualified Control.Monad.State as ST
import Control.Monad.State (MonadState, State, state, runState)
import Data.Functor (Functor)
import Data.Maybe (fromMaybe)
import Data.Monoid ((<>))
import qualified Data.Text.Lazy.Builder as B
import qualified Text.PrettyPrint as P
import Text.PrettyPrint (($+$))

import Nirum.Constructs.Identifier (Identifier)
import Nirum.Constructs.ModulePath (ModulePath)
import Nirum.Package.Metadata (Package (..), Target (..))
import qualified Nirum.TypeInstance.BoundModule as BoundModule

-- | A code builder monad parameterized by:
--
--     * @t@ - The build target
--     * @s@ - The state
newtype Target t => CodeBuilder t s a = CodeBuilder (State (BuildState t s) a)
    deriving ( Applicative
             , Functor
             , Monad
             )

data Target t => BuildState t s =
    BuildState { output :: P.Doc
               , boundModule :: BoundModule.BoundModule t
               , innerState :: s
               }

instance Target t => MonadState s (CodeBuilder t s) where
    state f = do
        st <- get'
        let (a, s) = f (innerState st)
        put' $ st { innerState = s }
        return a

get' :: Target t => CodeBuilder t s (BuildState t s)
get' = CodeBuilder ST.get

put' :: Target t => BuildState t s -> CodeBuilder t s ()
put' = CodeBuilder . ST.put

modify' :: Target t
        => (BuildState t s -> BuildState t s)
        -> CodeBuilder t s ()
modify' = CodeBuilder . ST.modify

-- | Put the line below the builder output.
writeLine :: Target t
          => P.Doc               -- ^ The line to append
          -> CodeBuilder t s ()
writeLine code = modify' $ \ s -> s { output = output s $+$ code }

-- | Nest (or indent) an output of inner builder computation by a given number
-- of positions.
nest :: Target t
     => Integer            -- ^ indentation size (may also be negative)
     -> CodeBuilder t s a  -- ^ inner builder computation to generate the
                           --   nested document
     -> CodeBuilder t s a
nest n code = do
    st <- get'
    let st' = st { output = P.empty }
    put' st'
    ret <- code
    after <- get'
    modify' $ \ s -> s {
        output = output st $+$ P.nest (fromIntegral n) (output after)
    }
    return ret

-- | Look up the actual type by the name from the context of the builder
-- computation.
lookupType :: Target t
           => Identifier                     -- ^ name of the type to find
           -> CodeBuilder t s BoundModule.TypeLookup
lookupType identifier = do
    m <- fmap boundModule get'
    return $ BoundModule.lookupType identifier m

-- | Execute the builder computation and retrive output.
runBuilder :: Target t
           => Package t
           -> ModulePath
           -> s                  -- ^ initial state
           -> CodeBuilder t s a  -- ^ code builder computation to execute
           -> (a, B.Builder)     -- ^ return value and build result
runBuilder package modPath st (CodeBuilder a) = (ret, rendered)
  where
    mod' = fromMaybe (error "asdf")
                     (BoundModule.resolveBoundModule modPath package)
    initialState = BuildState { output = P.empty
                              , boundModule = mod'
                              , innerState = st
                              }
    (ret, finalState) = runState a initialState
    out' = output finalState
    rendered = P.fullRender P.PageMode 80 1.5 concat' (B.singleton '\n') out'
    concat' (P.Chr c) rest = B.singleton c <> rest
    concat' (P.Str s) rest = B.fromString s <> rest
    concat' (P.PStr s) rest = concat' (P.Str s) rest

{- $examples

> import Text.PrettyPrint (colon, empty, parens, quotes, (<>), (<+>))
>
> hello = do
>     writeLine $ "def" <+> "hello" <> parens empty <> colon
>     nest 4 $ do
>         writeLine $ "print" <> parens (quotes "Hello, world!")
>         writeLine $ "return" <+> "42"

-}
