-- |
-- This module provides a Monad for an internal state and IO commands.
-- The state consists of two parts, the user state and the system state
-- user state ist a type parameter, the system state is a list
-- name-value pair. If the user state is not needed, the type parameter
-- can be instantiated with @()@.
--
-- Furthermore there are types for Xml filter working on this monad
-- and functions for manipulating the state components
-- and for lifting i\/o commands and XmlFilter to monad filters.
--
-- Error reporting functions are also located in this module.

module Text.XML.HXT.DOM.XmlState
    ( module Text.XML.HXT.DOM.XmlState
    )
where

import Text.XML.HXT.DOM.XmlTree

import qualified Control.Monad.MonadStateIO as MonadStateIO

import System.IO
import Data.Maybe

-- ------------------------------------------------------------

-- |
-- The internal system state consists of a list of name-value pairs
-- of type @(String, XmlTrees)@, so arbitrary lists of trees can be stored.
-- For options, often only strings are used as values, so a set of access
-- functions with string values is available
-- The error handling method can be controlled by an error handler filter,
-- the default filter issues the errors on stderr

data SysState			= SysState { sysStateAttrs		:: ! SysStateAttrs
					   , sysStateErrorHandler	:: ! (XmlStateFilter ())
					   }

type SysStateAttrs		= AssocList String XmlTrees

-- |
-- The State has a system and a user part
-- the user state is a type parameter

data XmlState state		= XmlState { sysState	:: ! SysState
					   , userState	:: ! state
					   }

-- |
-- The monad type for commands. It is an instance of "StateIO" from the
-- general module "Control.Monad.MonadStateIO".

type XState state res		= MonadStateIO.StateIO (XmlState state) res

-- |
-- The "XmlFilter" type for filters working on a state

type XmlStateFilter state	= XmlTree -> XState state XmlTrees

-- ------------------------------------------------------------
--
-- user defined state
-- access functions

-- |
-- change the user state
--
--    * 1.parameter fct :  the user state change function
--
--    - returns : the new state

changeState	:: (state -> state) -> XState state state
changeState f
    = do
      ns <- MonadStateIO.changeState f'
      return (userState ns)
      where
      f' s = s { userState = f (userState s) }

-- |
-- set the user state.
--
--    * 1.parameter s :  the new state
--
--    - returns : the new state

setState	:: state -> XState state state
setState s
    = changeState ( \_ -> s )

-- |
-- read the user state
--
--    - returns : the current state

getState	:: XState state state
getState
    = changeState id

-- ------------------------------------------------------------

-- |
-- change the system part of the state.
--
-- see also : 'changeState'

changeSysState	:: (SysState -> SysState) -> XState state SysState
changeSysState f
    = do
      ns <- MonadStateIO.changeState f'
      return (sysState ns)
      where
      f' s = s { sysState = f (sysState s) }

-- |
-- set the system part of the state.
--
-- see also : 'setState'

setSysState		:: SysState -> XState state SysState
setSysState s
    = changeSysState ( \_ -> s )

-- |
-- read the system part of the state.
--
-- see also : 'getState'

getSysState		:: XState state SysState
getSysState
    = changeSysState id

-- |
-- the initial system state
--
-- an empty list of attribute value pairs

initialSysState		:: SysState
initialSysState
    = SysState { sysStateAttrs        = []
	       , sysStateErrorHandler = errorMsgToStderr
	       }

-- |
-- change the attributes in the system state

changeSysStateAttrs		:: (SysStateAttrs -> SysStateAttrs) -> (SysState -> SysState)
changeSysStateAttrs cf sstate
    = sstate { sysStateAttrs = cf (sysStateAttrs sstate) }


-- |
-- set the error message handler

setSysErrorHandler		:: XmlStateFilter () -> XState state ()
setSysErrorHandler ehf
    = changeSysState (\ s -> s { sysStateErrorHandler = ehf })
      >> return ()

-- |
-- get the error handler

getSysErrorHandler		:: XState state (XmlStateFilter ())
getSysErrorHandler
    = do
      s <- getSysState
      return (sysStateErrorHandler s)

-- ------------------------------------------------------------

-- |
-- set or change a single system parameter.
--
--    * 1.parameter name :  the name of the parameter
--
--    - 2.parameter value :  the list of associated trees
--
--    - returns : nothing
--
-- see also : 'setSysParam', 'setSysParamInt'

setSysParamTree		:: String -> XmlTrees -> XState state ()
setSysParamTree name val
    = changeSysState (changeSysStateAttrs (addEntry name val))
      >> return ()

-- |
-- set or change a single system parameter of type string.
--
--    * 1.parameter name :  the name of the parameter
--
--    - 2.parameter value :  the (new) string value
--
--    - returns : nothing
--
-- see also : 'setSysParamTree', setSysParamInt

setSysParam		:: String -> String -> XState state ()
setSysParam name val
    = setSysParamTree name (xtext val)

-- |
-- set or change a single integer type system parameter
--
-- see also : 'setSysParam'

setSysParamInt		:: String -> Int -> XState state ()
setSysParamInt name val	= setSysParam name (show val)

-- |
-- add (or change) all attributes of the document root to the system state
--     - returns : this

setSystemParams	:: XmlStateFilter state
setSystemParams t
    = changeSysState (changeSysStateAttrs (addEntries (toTreel . getAttrl $ t)))
      >> thisM t

-- ------------------------------------------------------------

-- |
-- read a system parameter
--
--    * 1.parameter name :  the name of the parameter
--
--    - returns : the list of tres associated with the key, or the empty list for unknown parameters

getSysParamTree		:: String -> XState state XmlTrees
getSysParamTree name
    = do
      s <- getSysState
      return (lookup1 name (sysStateAttrs s))

-- |
-- read a system string parameter
--
--    * 1.parameter name :  the name of the parameter
--
--    - returns : the value, or the empty string for unknown parameters

getSysParam		:: String -> XState state String
getSysParam name
    = do
      ts <- getSysParamTree name
      return (xshow ts)

-- |
-- read a system parameter or return a default value
--
--    * 1.parameter name :  the name of the parameter
--
--    - 2.parameter default :  the default value
--
--    - returns : the value if found, else the default

getSysParamWithDefault	:: String -> String -> XState state String
getSysParamWithDefault name def
    = do
      val <- getSysParam name
      return ( if null val
	       then def
	       else val
	     )

-- |
-- read an integer system parameter
--
--    * 1.parameter name : 
--
--    - 2.parameter default : 
--
-- see also : 'getSysParamWithDefault'

getSysParamInt	:: String -> Int -> XState state Int
getSysParamInt var def
    = do
      val <- getSysParamWithDefault var (show def)
      return (read val)

-- ------------------------------------------------------------

-- |
-- exec a XState command with initial state.
--
--    * 1.parameter initalState :  the inital user state
--
--    - 2.parameter cmd :  the command
--
--    - returns : the i\/o command with result and user state

run0		:: XmlState state -> XState state res -> IO (res, XmlState state)
run0 initialState (MonadStateIO.STIO cmd)
    = do
      (res, finalState) <- cmd initialState
      return (res, finalState)

-- |
-- exec a XState command with initial user state.
-- ignore final user state.
-- like run0, but ignore the resulting user state

run		:: state -> XState state res -> IO res
run initialUserState cmd
    = do
      (res, _finalState) <- run0 (XmlState initialSysState initialUserState) cmd
      return res

-- |
-- exec a XState command in th IO monad.
-- like run with the empty state ().

run'		:: XState () res -> IO res
run'		= run ()

-- ------------------------------------------------------------

-- |
-- run a command in a new user state.
-- chain the system state part,
-- init new system state with the current one, run the command and
-- update the old system state with the resulting new system state
--
--    * 1.parameter initialUserState :  the initial user state
--
--    - 2.parameter cmd :  the command
--
--    - returns : the result of executing cmd and the final state

chain'		:: state1 -> XState state1 res -> XState state0 (res, state1)
chain' initialUserState1 cmd1
    = do
      sysState0 <- getSysState
      (res, finalState1) <- io $ run0 (XmlState sysState0 initialUserState1) cmd1
      _ <- setSysState (sysState finalState1)
      return (res, (userState finalState1))

-- |
-- like chain' but forget the final user state
--
--    * 1.parameter initialUserState :  the initial user state
--
--    - 2.parameter cmd :  the command
--
--    - returns : only the result of executing cmd

chain		:: state1 -> XState state1 res -> XState state0 res
chain initialUserState1 cmd1
    = do
      (res, _) <- chain' initialUserState1 cmd1
      return res

-- ------------------------------------------------------------
--
-- lift functions

-- |
-- lift a XmlFilter to a XmlStateFilter filter
-- issue all error nodes as error messages
-- and remove the error nodes from the result
--
--    * 1.parameter f :  the filter
--
--    - returns : the filter running in the state monad
--
--		  all errors are filtered from the result and issued on stderr

liftF		:: XmlFilter -> XmlStateFilter state
liftF f		= liftMf f .>> issueError

-- |
-- lift an I\/O command
--
--    * 1.parameter cmd :  the i\/o command
--
--    - returns : the i\/o command lifted to the XML state monad

io		:: IO a -> XState state a
io		= MonadStateIO.io

-- ------------------------------------------------------------
--

-- |
-- set the trace level.
--
-- convention:
--
-- 0: no trace output (default)
--
-- 1: trace important computation steps, e.g. accessing a document
--
-- 2: trace small computation steps
--
-- 3: output an intermediate result XmlTree in XML source format
--
-- 4: output an intermediate result XmlTree in tree representation
--
--    * 1.parameter level :  the trace level
--
--    - returns : nothing

setTraceLevel	:: Int -> XState state ()
setTraceLevel l	= setSysParamInt a_trace l

-- |
-- get the current trace level.
--
--    - returns : the current trace level

getTraceLevel	:: XState state Int
getTraceLevel	= getSysParamInt a_trace 0

-- |
-- trace output for arbitray commands.
--
--    * 1.parameter level :  the trace level,
--			  for which the command will be execuded
--			  if level \<= current trace level
--
--    - 2.parameter cmd :  the command to be executed
--
--    - returns : nothing

traceCmd	:: Int -> XState state a -> XState state ()
traceCmd level cmd
    = do
      trcLevel <- getTraceLevel
      if level <= trcLevel
        then do
	     _ <- cmd
	     return ()
        else return ()

-- |
-- trace output function for simple text.
--
--    * 1.parameter level :  like in traceCmd
--
--    - 2.parameter str :  the test
--
--    - returns : nothing

trace		:: Int -> String -> XState state ()
trace level str
    = traceCmd level
      $ do
	io $ hPutStrLn stderr ("-- (" ++ show level ++ ") " ++ str)

-- |
-- trace output of the user part of the program state.
--
--    * 1.parameter level :  like in traceCmd
--
--    - 2.parameter showFct :  the toString function
--
--    - returns : nothing

traceState	:: Int -> (state -> String) -> XState state ()
traceState level fct
    = traceCmd level
      $ do
	s <- getState
	io $ hPutStrLn stderr (fct s)

-- ------------------------------------------------------------
--
-- error functions

-- |
-- filter to reset the state attribute 'a_status'
--    - returns : this

clearStatus	:: XmlStateFilter state
clearStatus t
    = do
      setSysParamInt a_status c_ok
      thisM t

-- |
-- report an error message.
--
--    - returns : if the input tree n represents an error, @res = []@
--		  and the error is processed by the errror handler filter (default: error is issued on stderr)
--		  else @res = [n]@
--
-- see also : 'issueErr'

issueError	:: XmlStateFilter state
issueError
    = (setErrorMsgLevel .>> errorMsgHandler .>> noneM)
      `whenM`
      isXError

errorMsgHandler	:: XmlStateFilter state
errorMsgHandler
    = performAction
      ( \ t -> chain' () ( do
			   ehf <- getSysErrorHandler
			   _ <- ehf t
			   return ()
			 )
      )

-- |
-- set the error level in system state

setErrorMsgLevel	:: XmlStateFilter state
setErrorMsgLevel
    = performAction
      ( \ (NTree (XError level _str) _cs) ->
	do
	errLevel <- getSysParamInt a_status 0
	setSysParamInt a_status (max errLevel level)
      )

-- |
-- default error handler for writing errors to stderr

errorMsgToStderr	:: XmlStateFilter state
errorMsgToStderr
    = performAction
      ( \ (NTree (XError level str) _cs) ->
	io $ hPutStrLn stderr ("\n" ++ errClass level ++ ": " ++ str)
      )

-- |
-- error message handler for collecting all error messages
-- all messages are stored under attribute 'a_error_log'
-- they can be read with @getSysParamTree a_error_log@ or by
-- applying the filter 'getErrorMsg' to the root node

errorMsgLogging		:: XmlStateFilter state
errorMsgLogging
    = performAction
      ( \ t ->
	do
	errLog <- getSysParamTree a_error_log
	setSysParamTree a_error_log (t : errLog)
      )

errorMsgLoggingAndToStderr      :: XmlStateFilter state
errorMsgLoggingAndToStderr	= errorMsgLogging.>> errorMsgToStderr

-- |
-- the filter for reading all collected error mesages
--
-- result is the list of error messages, the input tree is ignored

getErrorMsg	:: XmlStateFilter state
getErrorMsg _t
    = do
      el <- getSysParamTree a_error_log
      setSysParamTree a_error_log []
      return (reverse el)

-- |
-- error level translation
-- 'c_warn' (1) : warning,
-- 'c_err' (2): error (e.g. parse error, validation error, ...),
-- 'c_fatal' (3) : fatal error (document access error, internal error, ...)

errClass	:: Int -> String
errClass l
    = fromMaybe "fatal error" . lookup l $ msgList
      where
      msgList	= [ (c_ok,	"no error")
		  , (c_warn,	"warning")
		  , (c_err,	"error")
		  , (c_fatal,	"fatal error")
		  ]

-- |
-- short cut for issuing a warning
--
-- see also : 'issueError', 'issueErr'

issueWarn	:: String -> XmlStateFilter state
issueWarn msg	= liftMf (warn msg) .>> issueError

-- |
-- short cut for issuing an error
--
-- see also : 'issueError'

issueErr	:: String -> XmlStateFilter state
issueErr msg	= liftMf (err msg) .>> issueError

-- |
-- short cut for issuing a fatal error
--
-- see also : 'issueError', 'issueErr'

issueFatal	:: String -> XmlStateFilter state
issueFatal msg	= liftMf (fatal msg) .>> issueError


-- ------------------------------------------------------------
--
-- issue an error, add the error to the document root tree
-- and return the tree

addFatal	:: String -> XmlStateFilter state
addFatal msg
    = liftF ( fatal msg
	      +++
	      setStatus c_fatal "accessing documents"
	    )

-- ------------------------------------------------------------

-- |
-- checks the value of the attribute 'a_status' in a document root.
-- if it contains a value greater or equal to 'c_err', an error with error message
-- stored in attribute 'a_module' is issued and the filter acts as the 'noneM' filter
-- else its the 'thisM' filter

checkStatus	:: XmlStateFilter state
checkStatus t
    = if status >= c_err
      then errorMsgHandler (mkXErrorTree c_warn (errClass status ++ "s detected in " ++ msg) [])
	   >> noneM t
	else
           thisM t
    where
    status = intValueOf a_status t
    msg    =    valueOf a_module t

-- |
-- add the error level and the module where the error occured
-- to the attributes of a document root node and remove the children when level is greater or equal to 'c_err'

setStatus	:: Int -> String -> XmlFilter
setStatus level msg
    = ( addAttrInt a_status level
	.>
	addAttr a_module msg
	.>
	( if level >= c_err
	  then replaceChildren []
	  else this
	)
      )
      `when` isRoot

-- |
-- check whether tree is a document root and the status attribute has a value less than 'c_err'

statusOk	:: XmlFilter
statusOk
    = isRoot
      .>
      isOf (\ t -> not (intValueOf a_status t >= c_err))
-- |
-- check whether the error level attribute in the system state
-- is set to error, in this case the children of the document root are
-- removed and error info is added as attributes with 'setStatus'
-- else nothing is changed

checkResult	:: String -> XmlStateFilter state
checkResult msg t
    = do
      level <- getSysParamInt a_status 0
      ( if level <= c_warn
	then thisM
	else liftMf ( setStatus level msg )
        ) t

-- |
-- monadic filter for processing the attribute list of a tag.
-- for other trees this filter acts like 'noneM'
--
-- see also : 'processAttr', 'processAttrl'

processAttrM				:: XmlStateFilter a -> XmlStateFilter a
processAttrM f t
    = do
      res <- f $$< al
      return $ replaceAttrl res t
    where
    al = getAttrl t

-- ------------------------------------------------------------