-- © 2001-2005 Peter Thiemann
module WASH.CGI.BaseCombinators
{-- interface
  (ask		-- WithHTML x CGI a -> CGI ()
  ,tell		-- CGIOutput a => a -> CGI ()
  ,htell 	-- WithHTML x IO () -> CGI ()
  ,run		-- CGI () -> IO ()
  ,runWithHook 	-- ([String] -> CGI ()) -> CGI () -> IO ()
  )
--}
where

import qualified List
import Maybe
import Monad
import System
import IO

import WASH.Utility.Auxiliary
import qualified WASH.Utility.Base64 as Base64
import WASH.CGI.CookieIO
import WASH.CGI.CGIMonad
import WASH.CGI.CGIOutput
import WASH.CGI.CGITypes
import WASH.CGI.EventHandlers
import WASH.CGI.Fields
import WASH.CGI.Frames
import WASH.CGI.LogEntry
import WASH.CGI.CGIHistory
import qualified WASH.CGI.HTMLWrapper as H hiding (map,head)
import WASH.CGI.Images
import WASH.Utility.JavaScript
import qualified WASH.Utility.RFC2279 as RFC2279
import qualified WASH.Utility.RFC2397 as RFC2397
import WASH.CGI.RawCGIInternal hiding (CGIEnv (..), getSessionMode)
import qualified WASH.Utility.URLCoding as URLCoding

import WASH.CGI.CGIConfig


-- |Safe embedding of an 'IO' action into the 'CGI' monad. Intentionally not
-- parameterized óver its monad to avoid its use inside of transactions.
io :: (Read a, Show a) => IO a -> CGI a
io ioa =
  once (unsafe_io ioa)

-- |Runs a 'CGI' state transformer forever. Its specification is just
--
-- > feedback f x = f x >>= feedback f
-- 
-- However, 'feedback' is more efficient because it avoids the 
-- buildup of long interaction logs by cutting back every time just before 'f' 
-- is invoked. Unfortunately, it's useless due to typing restrictions.
feedback :: (CGIMonad cgi, Read a, Show a) => (a -> cgi a) -> a -> cgi a
feedback f x = 
  wrapCGI (\ cgistate ->
  case inparm cgistate of
    -- if we've got a value in the log,
    -- ignore the parameter x and just use the logged value
    PAR_MARK _ : PAR_RESULT code : rest ->
      case reads code of
	(result, ""):_ ->
	  return (result
	         ,cgistate { inparm = rest })
	_ ->
	  reportError "Result unreadable"
	  	(H.text_S "Cannot read " >> H.text (show code)) cgistate
    [] ->
      let stid = stateID cgistate 
	  newparm = PAR_MARK stid
	  newvalue = PAR_RESULT (show x)
	  in
      do (nextx, cgistate') <- 
	   unwrapCGI (f x) cgistate { outparm = newvalue : newparm : outparm cgistate
				, stateID = nextstid (nextstid stid newparm) newvalue }
	 let (oldstid, outparm') = popToMark $ outparm cgistate'
	 unwrapCGI (feedback f nextx)
	       cgistate { inparm =  inparm cgistate'
			, outparm = outparm'
			, stateID = oldstid
			, cookieMap = cookieMap cgistate' 
			, cookiesToSend = cookiesToSend cgistate' }
    _ -> 
      reportError "Out of sync" H.empty cgistate)

-- |Control operator for the 'CGI' monad. Its specification is
--
-- > callWithCurrentHistory (\backto x -> action x >>= backto) x
-- > ==
-- > action x >>= callWithCurrentHistory (\backto x -> action x >>= backto)
-- 
-- However, 'callWithCurrentHistory' is more efficient because it avoids the 
-- buildup of long interaction logs by cutting back every time just before
-- 'action' gets executed. 
callWithCurrentHistory :: (CGIMonad cgi, Read a, Show a) =>
			  ((a -> cgi ()) -> a -> cgi ()) -> a -> cgi ()
callWithCurrentHistory g x = 
  wrapCGI (\ cgistate ->
  let
    prompt stid x =
      wrapCGI (\ cgistate' ->
	let outparm' = popTo stid (outparm cgistate') 
	in
	unwrapCGI (callWithCurrentHistory g x) 
	      cgistate { inparm =  inparm cgistate'
		       , outparm = outparm'
		       , stateID = stid
		       , cookieMap = cookieMap cgistate' 
		       , cookiesToSend = cookiesToSend cgistate' } )
    popTo stid [] = error "popTo did not find its mark"
    popTo stid (PAR_MARK stid' : rest) | stid' == stid = rest
    popTo stid (_ : rest) = popTo stid rest
  in
  case inparm cgistate of
    -- if we've got a value in the log,
    -- ignore the parameter x and just use the logged value
    PAR_MARK stid : PAR_RESULT code : rest ->
      case reads code of
	(result, ""):_ ->
	  unwrapCGI (g (prompt stid) result) cgistate { inparm = rest }
	_ ->
	  reportError "Result unreadable"
	  	(H.text_S "Cannot read " >> H.text (show code)) cgistate
    [] ->
      let stid = stateID cgistate 
	  newmark = PAR_MARK stid
	  newvalue = PAR_RESULT (show x)
	  in
      unwrapCGI (g (prompt stid) x)
	cgistate { outparm = newvalue : newmark : outparm cgistate
		 , stateID = nextstid (nextstid stid newmark) newvalue }
    _ -> 
      reportError "Out of sync" H.empty cgistate)

-- |Brackets a 'CGI' action so that only its result is visible. Improves
-- efficiency by not executing the bracketed action after it has been performed
-- once. Use this for avoiding the inefficient buildup of long interaction logs.
once :: (CGIMonad cgi, Read a, Show a) => cgi a -> cgi a
once cgi = 
  wrapCGI (\ cgistate ->
  case inparm cgistate of
    PAR_RESULT code : rest ->
      case reads code of
	(result, ""):_ ->
	  return (result
	         ,cgistate { inparm = rest })
	_ ->
	  reportError "Result unreadable"
	  	(H.text_S "Cannot read " >> H.text (show code)) cgistate
    [] ->
      let stid = stateID cgistate 
	  newparm = PAR_MARK stid in
      unwrapCGI cgi cgistate { outparm = newparm : outparm cgistate
			 , stateID = nextstid stid newparm }
      >>= finish cgistate
    PAR_MARK _ : rest -> 
      unwrapCGI cgi cgistate { inparm = rest } >>= finish cgistate
    _ -> 
      reportError "Out of sync" H.empty cgistate)
  where finish cgistate (v, cgistate') =
 	  let (oldstid, outparm') = popToMark $ outparm cgistate'
	      newparm = PAR_RESULT (show v)
	  in
	  return (v
	      	 ,cgistate { inparm = inparm cgistate'
			   , outparm = newparm : outparm'
			   , stateID = nextstid oldstid newparm
			   , cookieMap = cookieMap cgistate' 
			   , cookiesToSend = cookiesToSend cgistate' })

-- |Repeats a 'CGI' action without saving its state so that the size of the
-- interaction log remains constant.
forever :: (CGIMonad cgi) => cgi () -> cgi ()
forever cgi = 
  wrapCGI (\ cgistate ->
  case inparm cgistate of
    [] ->
      let stid = stateID cgistate 
	  newparm = PAR_MARK stid in
      unwrapCGI cgi cgistate { outparm = newparm : outparm cgistate
			 , stateID = nextstid stid newparm }
      >>= 
      const (reportError "Black hole" H.empty cgistate)
    PAR_MARK _ : rest -> 
      unwrapCGI cgi cgistate { inparm = rest } >>= finish (mcount cgistate)
    _ -> 
      reportError "Out of sync" H.empty cgistate)
  where finish previousMcount (v, cgistate') =
	  let (oldstid, outparm') = popToMark $ outparm cgistate'
	      newparm_ignored = PAR_RESULT (show v)
	      newparm = PAR_MARK oldstid
	  in
	  unwrapCGI cgi 
	    cgistate' { inparm = []
		      , outparm = newparm : outparm'
		      , stateID = nextstid oldstid newparm
		      , mcount = previousMcount }

-- |Unsafe variant of 'once':  returns the computed value only the first time
-- and returns a default value in all later invocations.
-- [deprecated]
onceAndThen :: (CGIMonad cgi, Read a, Show a) => a -> cgi a -> cgi a
onceAndThen a cgi = 
  wrapCGI (\ cgistate ->
  case inparm cgistate of
    PAR_IGNORED : rest ->
      return (a
	     ,cgistate { inparm = rest })
    [] ->
      unwrapCGI cgi cgistate { outparm = PAR_MARK (stateID cgistate) : outparm cgistate }
      >>= finish
    PAR_MARK _ : rest -> 
      unwrapCGI cgi cgistate { inparm = rest } >>= finish
    _ -> 
      reportError "Out of sync" H.empty cgistate)
  where popToMark [] = []
	popToMark (PAR_MARK _: rest) = rest
	popToMark (_: rest) = popToMark rest
	finish (v, cgistate') =
 	  let out = outparm cgistate' in
	  return (v
	      	 ,cgistate' { outparm = PAR_IGNORED : popToMark out })

-- internal helper function
popToMark [] = error "popToMark did not find MARK"
popToMark (PAR_MARK v: rest) = (v, rest)
popToMark (_: rest) = popToMark rest

-- |Directly lifts the 'IO' monad into the 'CGI' monad. This is generally unsafe
-- and should be avoided. Use 'io' instead.
unsafe_io :: IO a -> CGI a
unsafe_io = lift

-- |Takes a monadic value that constructs a HTML page and delivers this
-- page to the browser. This page may contain forms and input widgets.
ask :: (CGIMonad cgi) => H.WithHTML x cgi a -> cgi ()
ask ma =
  do sessionMode <- getSessionMode
     case sessionMode of
       LogOnly -> 
	 askResumptive ma
       _ ->
	 askContinuously ma

-- |Implementation of `ask` for the pure logged variant.
askResumptive :: (CGIMonad cgi) => H.WithHTML x cgi a -> cgi ()
askResumptive ma =
  do wrapCGI (\cgistate -> 
       return ((), cgistate { pageInfo = initialPageInfo cgistate }))
     setAction tell
     elem <- H.build_document ma 
     wrapCGI $
       \cgistate -> let
	 pi = pageInfo cgistate
	 atable = actionTable pi
	 mbnds = bindings pi
	 msubmitter = mbnds >>= assocParm subVar
	 maction = msubmitter >>= \x -> lookup x atable
	 nextState = nextCGIState cgistate
	 defsubmission = liftM snd $ listToMaybe (reverse atable)
	 defaction = maybe (rawTell) 
			   (const (maybe (tellError "Unspecified action")
					 id
					 defsubmission))
			   mbnds
	 go = (maybe defaction id maction) elem nextState
	 oldgo = (nextaction pi elem) nextState
       in
	 -- appendDebugFile "/tmp/ask" (show (List.map fst atable, msubmitter)) >>
	 go

-- |Experimental implementation of continuous version.
askContinuously :: (CGIMonad cgi) => H.WithHTML x cgi a -> cgi ()
askContinuously ma =
  fromCGIstate id >>= \initialState -> 
  let
    scriptName = cgiScriptName (cgiInfo initialState)
    parentID = (scriptName, stateID initialState)
    askLoop = do 
      wrapCGI (\cgistate -> 
	   return ((), cgistate { pageInfo = initialPageInfo cgistate })) 
      setAction tellContinuously			    -- is this still required?
      elem <- H.build_document ma 
      wrapCGI $
       \cgistate -> let
	 tellAction x = \ cst ->
	   do rawTellContinuously x cst
	      hClose (cgiHandle (cgiInfo cgistate))
	      (decoded_parameters, hout) <- readParameters parentID
	      -- newparm :: CGIParameters
	      let newparm = PAR_VALUES $ dropSpecialParameters decoded_parameters
		  newState = initialState { inparm = [newparm]
					  , outparm = newparm : outparm initialState
					  , cgiInfo = (cgiInfo initialState) { cgiHandle = hout}
					  }
	      unwrapCGI askLoop newState
		    
	 pi = pageInfo cgistate
	 atable = actionTable pi
	 mbnds = bindings pi
	 msubmitter = mbnds >>= assocParm subVar
	 maction = msubmitter >>= \x -> lookup x atable
	 nextState = nextCGIState cgistate
	 defsubmission = liftM snd $ listToMaybe (reverse atable)
	 nextparam = head (inparm cgistate)
	 myID = (scriptName, nextstid (snd parentID) nextparam)
	 boost maction =
	   maction >>= \ cgiact -> 
	   return (\ elem -> 
		     unwrapCGI (lift (createEntry myID (Just parentID) nextparam Nothing)
				 >> wrapCGI (cgiact elem)))
	 defaction = maybe tellAction
			   (const (maybe (tellError "Unspecified action")
					 id
					 (boost defsubmission)))
			   mbnds
       in 
	 (maybe defaction id (boost maction)) elem nextState
  in askLoop


-- |Like 'ask', but passes the constructed HTML page to the @elementAction@
-- parameter. This function may send the page via Email or store it into a
-- file. Anyone loading this page in a browser can resume the interaction.
askOffline :: (CGIMonad cgi) => H.WithHTML x cgi a -> (H.Element -> IO ()) -> cgi ()
askOffline ma elementAction =
  do wrapCGI (\cgistate -> return ((), cgistate { pageInfo = initialPageInfo cgistate }))
     setAction tell
     elem <- H.build_document ma 
     wrapCGI $
       \cgistate ->
       case bindings (pageInfo cgistate) of
         Nothing ->
	   elementAction elem >> return ((), cgistate)
	 Just _ ->
	   (nextaction (pageInfo cgistate) elem) (nextCGIState cgistate)

-- |Turns a 'CGI' action into an 'IO' action. Used to turn the main 'CGI' action
-- into the @main@ function of the program. Typical use:
-- 
-- > main = run mainCGI
run :: CGI () -> IO ()
run =
  runWithOptions []

-- |Turns a 'CGI' action into an 'IO' action. Used to turn the main 'CGI' action
-- into the @main@ function of the program. Takes additional low-level
-- options. Typical use: 
-- 
-- > main = runWithOptions [] mainCGI
runWithOptions :: CGIOptions -> CGI () -> IO ()
runWithOptions options =
  runInternal options (fallbackTranslator Nothing)

-- |Variant of 'run' where an additional argument @cgigen@ specifies an action
-- taken when the script is invoked with a non-empty query string as in
-- @script-name?query-string@ 
runWithHook :: CGIOptions -> ([String] -> CGI ()) -> CGI () -> IO ()
runWithHook options cgigen =
  runInternal options (fallbackTranslator (Just cgigen))

runInternal options cgigen cgiProg =
  start options $ makeServletInternal cgigen cgiProg

-- |Transform a CGI action into a servlet suitable for running from Marlow's web
-- server.
makeServlet :: CGI () -> CGIProgram
makeServlet cgiProg = 
  makeServletInternal (fallbackTranslator Nothing) cgiProg

-- |Like 'makeServlet' with additional CGI generator as in 'runWithHook'.
makeServletWithHook :: ([String] -> CGI ()) -> CGI () -> CGIProgram
makeServletWithHook cgigen cgiProg =
  makeServletInternal (fallbackTranslator $ Just cgigen) cgiProg

makeServletInternal cgigen (CGI cgi) = \ info decoded_parameters ->
  let maybecgiparm = assocParm "=CGI=parm=" decoded_parameters in
  let maybecgistid = assocParm "=CGI=stid=" decoded_parameters in
  let maybejsenabl = assocParm "js_enabled" decoded_parameters in
  let clean_parameters = dropSpecialParameters decoded_parameters in
  let no_parameters = null decoded_parameters in
  do oldparm <- case maybecgiparm of
       Just cgiparm ->
         liftM read $ liftM RFC2279.decode $ decode $ Base64.decode' $ cgiparm
       Nothing -> 
         return []
     key <- generateKey
     let newparm 
          | no_parameters = []
	  | otherwise = (PAR_VALUES $ clean_parameters)
	  		: oldparm
	 oldstid
	  | null decoded_parameters || isNothing maybecgistid = initialStateID
	  | otherwise = Base64.decode' $ fromJust maybecgistid
	 cgistate = CGIState { inparm = reverse newparm
		             , outparm = newparm
			     , stateID = oldstid
			     , cgiInfo = info
			     , mcount = 0
			     , jsEnabled = liftM read maybejsenabl
			     , pageInfo = (initialPageInfo cgistate) {inFrame = 0}
			     , encoder = makeEncoder key
			     , cookieMap = map decodeCookie (cgiCookies info)
			     , cookiesToSend = []
			     }
	 args = cgiArgs info
	 scriptName = cgiScriptName info
	 sessionMode = cgiSessionMode info
     -- writeDebugFile "/tmp/CGIOLDPARM" (show oldparm)
     -- writeDebugFile "/tmp/CGINEWPARM" (show newparm)
     
     -- set b==False if replaying is required
     b <- case sessionMode of
       LogOnly ->
	 return False
       _ ->
	 if no_parameters then
	   do createEntry (scriptName, initialStateID) Nothing PAR_IGNORED Nothing
	      return False
	 else
           notify (scriptName, oldstid) clean_parameters (cgiHandle info)
	   -- if b is True at this point, we have found a thread to handle
	   -- this request. However, we cannot terminate at this time because
	   -- exiting will close the output handle for the other thread!
     unless b $ do
       if null args || null (head args)
	 then cgi cgistate >> return ()
	 else unCGI (cgigen $ args) cgistate >> return ()
       exitWith ExitSuccess

-- ======================================================================
-- output routines

-- |Terminates script by sending its argument to the browser.
tell :: (CGIMonad cgi, CGIOutput a) => a -> cgi ()
tell a =
  wrapCGI (rawTell a)

-- rawTell :: (CGIOutput a) => a -> CGIAction ()
rawTell a cgistate =
  -- appendFile "/tmp/CGIFRAME" ("tell/enter\n") >>
  let frameno = inFrame $ pageInfo cgistate in
  -- appendFile "/tmp/CGIFRAME" ("tell #" ++ show frameno ++ "\n") >>
  if frameno == 0 then
  do putCookies cgistate
     cgiPut (cgiHandle $ cgiInfo cgistate) a
     exitWith ExitSuccess
  else
  do let fname = frameFullPath (outparm cgistate) frameno
     h <- openFile fname WriteMode
     cgiPut h a
     hClose h
     return ((), cgistate)

-- |(experimental: continuous version) send argument to the browser.
tellContinuously :: (CGIMonad cgi, CGIOutput a) => a -> cgi ()
tellContinuously a =
  wrapCGI (rawTellContinuously a)

rawTellContinuously :: (CGIOutput a) => a -> CGIAction ()
rawTellContinuously a =
  (\cgistate -> 
  let h = cgiHandle $ cgiInfo cgistate in
  do putCookies cgistate
     cgiPut h a
     -- hFlush h
     return ((), cgistate))


-- |Terminate script by sending a HTML page constructed by monadic argument. 
htell :: (CGIMonad cgi) => H.WithHTML x IO () -> cgi a
htell hma =
  wrapCGI (\cgistate ->
  do putCookies cgistate
     itell (cgiHandle $ cgiInfo cgistate) hma)
     -- never reached

rawHtell hma = 
  \cgistate ->
  do putCookies cgistate
     itell (cgiHandle $ cgiInfo cgistate) hma

-- 
tellError :: String -> H.Element -> CGIAction a
tellError str elems = 
  rawHtell message 
  where message = 
	  H.standardPage str (backLink H.empty)

-- 
reportError :: String -> H.WithHTML x IO () -> CGIState -> IO (a, CGIState)
reportError ttl elems cgistate =
  unCGI (htell message) cgistate
  -- never reached
  where message = 
	  H.standardPage ttl (elems >> backLink H.empty)

-- |Link to previous page in browser's history. Uses JavaScript.
backLink :: Monad m => H.HTMLCons x y m ()
backLink attrs =
  hlink (URL "javascript:back()") (H.text_S "Try again..." >> attrs)

-- |Plain Hyperlink from an URL string.
hlink :: Monad m => URL -> H.HTMLCons x y m ()
hlink url subs = 
  H.a_T (H.attr_SD "href" (unURL url) >> subs)

-- 
fallbackTranslator mCgigen =
  docTranslator [nothing, question] $
  frameTranslator $
  nextTranslator mCgigen

-- 
frameTranslator :: (CGIMonad cgi) => ([String] -> cgi ()) -> [String] -> cgi ()
frameTranslator next (name@('F':'R':'A':'M':'E':':':_):_) =
  tell (ResponseFileReference (frameDir ++ name))
frameTranslator next strs =
  next strs

-- |A /translator/ is a function @[String] -> CGI ()@. It takes the query string
-- of the URL (of type @[String]@) and translates it into a CGI
-- action. @docTranslator docs next@ 
-- takes a list of 'FreeForm' documents and a next translator. It tries to
-- select a document by its 'ffName' and falls through to the
-- @next@ translator if no document matches.
docTranslator :: [FreeForm] -> ([String] -> CGI ()) -> [String] -> CGI ()
docTranslator docs next [name] =
  let f (doc : rest) =
        if name == ffName doc then tell doc else f rest
      f [] = next [name]
  in  f docs
docTranslator docs next strs =
  next strs

-- |Terminates a sequence of translators.
lastTranslator :: [String] -> CGI ()
lastTranslator =
  nextTranslator Nothing

nextTranslator Nothing _ =
  tell (Status 404 "Not Found" Nothing)
nextTranslator (Just cgigen) strs =
  cgigen strs

-- | Internal: name for the submission variable and form field
subVar :: String
subVar = "WASHsub"