------------------------------------------------------------------
-- |
-- Module      :  Language.JSMW.Monad
-- Copyright   :  (c) Dmitry Golubovsky, 2009
-- License     :  BSD-style
-- 
-- Maintainer  :  golubovsky@gmail.com
-- Stability   :  experimental
-- Portability :  portable
-- 
--
--
-- A special monad: the core of the Writer.
------------------------------------------------------------------

module Language.JSMW.Monad (
 -- * The Monad itself
  JSMW 
 ,runJSMW
 ,runJSMWWith
 ,getBlock
 ,currDocBody
 -- * Compilation control
 ,once
 ,mkNewVar
 ,writeStmt
 -- * Create Javascript values, Monadic versions
 ,stringM
 ,numberM
 ,boolM
 -- * Layout control
 ,ECRF
 ,passive
 ,nest
 ,container
 -- * Element references
 -- $ref
 ,ref
 ,ref2ecrf
 ,inside
 -- * Inline style and other decorations
 ,CSSDeco (..)
 ,setStyle
 -- * Event handling
 ,OnHandler
 ,setHandler
 ,ask
  )where

import Control.Monad
import Control.Monad.State
import Control.Monad.RWS
import BrownPLT.JavaScript
import Data.DOM
import Data.DOM.Dom
import Data.DOM.Node
import Data.DOM.Html2
import Data.DOM.Events
import Data.DOM.HTMLBodyElement
import Data.DOM.CSSStyleDeclaration

-- | A type of the writer: based on the 'RWS' Monad. The Reader part holds an expression
-- to reference the curent HTML container element. The Writer part is the list of Javascript
-- statements being formed. Container may be any DOM Element, but not a Text node 
-- or anything else.

type JSMW e a = RWS (Expression e) [Statement ()] Int a

-- | Run the code writer (raw way, returns both state and log). Container will be
-- initialized into the body of the current HTML document. Same as
-- @ 'runJSMWWith' 'currDocBody' @

runJSMW :: Int                                    -- ^ Initial state (usually 0)
        -> JSMW THTMLBodyElement (Expression a)   -- ^ The JSMW expression to process
        -> (Expression a, Int, [Statement ()])    -- ^ Result: (final expression, 
                                                  --   final state,
                                                  --   produced statements),

runJSMW st q = runJSMWWith currDocBody st q

-- | Run the code writer (raw way, returns both state and log) with explicitly
-- specified container.

runJSMWWith :: (CElement e)
            => (Expression e)                         -- ^ container
            -> Int                                    -- ^ Initial state (usually 0)
            -> JSMW e (Expression a)                  -- ^ The JSMW expression to process
            -> (Expression a, Int, [Statement ()])    -- ^ Result: (final expression, 
                                                      --   final state,
                                                      --   produced statements),
runJSMWWith e st q = runRWS q e st

-- | Body of the current document: use it to start the toplevel instance
-- of the Writer as a container for 'runJSMWWith'.

currDocBody :: Expression THTMLBodyElement

currDocBody = VarRef THTMLBodyElement (Id THTMLBodyElement "window.document.body")

-- | Obtain a block statement from the result of 'runJSWM'. The last expression
-- forms a \'return\' statement, so the resulting block may be used as a function\'s body.

getBlock :: (Expression a, Int, [Statement ()]) -> Statement ()

getBlock (fin, _, stmts) = BlockStmt () (stmts ++ [ReturnStmt () (Just (fin /\ ()))])

-- | Type of a function creating HTML elements, e. g. 'mkButton', 'mkDiv'

type ECRF e n = Expression THTMLDocument -> JSMW e (Expression n)

-- | Insert a passive element into the current container. 

passive :: (CNode n, CElement e) 
        => ECRF e n                     -- ^ function that creates a HTML element
        -> JSMW e (Expression ())       -- ^ 'passive' does not return a value

passive crf = do
  cntr <- ask
  doc <- get'ownerDocument cntr
  e <- once =<< crf doc
  once =<< addChild e cntr
  return $ NullLit ()


-- | Nest an element inside another element via monadic composition.
-- Example usage: 
--
--  @'ask' >>= 'nest' 'mkButton' >>= 'nest' ('mkText' $ 'string' \"Foo\")@
--
-- inserts a button with text \"Foo\" into the current container.
--
-- The type system makes sure that only an instance of a DOM Element
-- can nest other elements, e. g. 
--
-- @ ... mkText (string \"Foo\") >>= nest mkDiv@
--
-- would not typecheck.
--
-- Example: a text, a newline, and two buttons: @'ask'@ retrieves the current
-- container.
--
-- @
-- q = do
--   passive (mkText $ string \"Hello\")
--   passive mkBr
--   ask >>= nest mkButton >>= nest (mkText $ string \"Foo\")
--   ask >>= nest mkButton >>= nest (mkText $ string \"Bar\")
-- @

nest :: (CNode n, CElement e, CElement p) 
     => ECRF e n                    -- ^ function that creates a HTML element to nest
     -> Expression p                -- ^ parent element (implicit if '>>=' is used)
     -> JSMW e (Expression n)       -- ^ nested element is returned (per 'addChild')

nest crf par = do
  doc <- once =<< get'ownerDocument par
  c <- once =<< crf doc
  once =<< addChild c par

-- | Specify a new container that in nested into the current one. As long as the container
-- is active, all subsequently defined elements will be inserted into it. 
--
-- Example: a Button with two text labels separated with a newline:
--
-- @
--   mkButton \`container\` (do
--     passive (mkText $ string \"Hello\") 
--     passive mkBr
--     passive (mkText $ string \"GoodBye\"))
-- @
--
-- Everything defined within a @do@ expression is inserted into the button
-- which is the new container.

container :: (CElement n, CElement e) 
          => ECRF e n                -- ^ function that creates a new container
          -> JSMW n (Expression x)   -- ^ whatever goes into that container
          -> JSMW e (Expression ())  -- ^ 'container' does not return a value

container crf cnt = do
  curc <- once =<< ask
  doc <- once =<< get'ownerDocument curc
  newc <- once =<< crf doc
  once =<< addChild newc curc
  carg <- mkNewVar
  st <- get
  let et = exprType newc
      (finx, fins, stms) = runJSMWWith (VarRef et (Id et carg)) st cnt
      blk = getBlock (finx, fins, stms)
      fun = ParenExpr () (FuncExpr () [Id () carg] blk)
      call = CallExpr () fun [newc /\ ()]
  writeStmt (ExprStmt () call)
  put fins
  return $ NullLit ()

-- $ref Sometimes it may be necessary to create an element, but \"engage\" it later.
-- Element references help achieve this. References also can be useful when 
-- several elements have to interact with each other: elements created
-- with 'nest' or 'container' are accessible only from the \"inside\" code.
-- To enable interaction, a reference to an element has to be made known
-- to another element's event handler.
--
-- A rather contrived example below shows how to create an input element, and insert it
-- into a button later.
--
-- @
-- import qualified Data.DOM.HTMLInputElement as I
-- ...
-- inp <- 'ref' 'I.mkInput' -- create a reference
-- inp \`inside\` ('setStyle' [\"border-color\" := \"green\"]) -- do something with the reference
-- ...
-- inpr <- 'ref2ecrf' inp -- simulate an element creation function
-- mkButton \`container\` (inpr \`container\` (ask >>= 'I.set'value' ('string' \"foo\")))
-- @

-- | Create an element for future use, and return a reference to it.
-- The element may be inserted into a container different from one it was
-- created with (when 'ref' was called). But it should be used within the same
-- document it was created inside.

ref :: (CNode e, CElement n) => ECRF e n -> JSMW e (Expression n)

ref crf = do
  cntr <- ask
  doc <- get'ownerDocument cntr
  e <- once =<< crf doc
  return e
  
-- | Turn an element reference into element creation function. It can be useful
-- when an element created earlier has to be used as a container, or a passive element,
-- or nested. The type signature of 'ref2ecrf' reflects the fact that the element
-- was created when one container was current, but may be used with another container.

ref2ecrf :: (CElement e1, CElement e2, CNode n) => Expression n -> JSMW e1 (ECRF e2 n)

ref2ecrf n = return $ \d -> return n

-- | Essentially same as 'container' except that a reference to an element
-- has to be supplied rather than an element creation function. Another
-- difference from 'container': element referenced is not added as a child to
-- the current container.

inside    :: (CElement n, CElement e) 
          => Expression n            -- ^ reference to an element              
          -> JSMW n (Expression x)   -- ^ whatever goes into that element
          -> JSMW e (Expression ())  -- ^ 'inside' does not return a value

inside    e cnt = do
  newc <- once =<< return e
  carg <- mkNewVar
  st <- get
  let et = exprType newc
      (finx, fins, stms) = runJSMWWith (VarRef et (Id et carg)) st cnt
      blk = getBlock (finx, fins, stms)
      fun = ParenExpr () (FuncExpr () [Id () carg] blk)
      call = CallExpr () fun [newc /\ ()]
  writeStmt (ExprStmt () call)
  put fins
  return $ NullLit ()


-- | Data type for building inline style assignment expressions.

data CSSDeco = String := String
 
-- | An action to use within a container to update its inline style.
-- 'setStyle' called with an empty list does not change the inline
-- style. Note that style settings are compile-time only.
--
-- Example: a DIV element with style settings applied and a text:
--
-- @
--   mkDiv \`container\` (do
--     setStyle [\"display\" := \"inline\"
--              ,\"float\" := \"right\"
--              ,\"width\" := \"45%\"
--              ,\"text-align\" := \"center\"
--              ,\"background-color\" := \"green\"
--              ,\"color\" := \"white\"
--              ,\"font-weight\" := \"bold\"]
--     passive (mkText $ string \"Styled\"))
-- @


setStyle :: (CHTMLElement e) => [CSSDeco] -> JSMW e (Expression ())

setStyle csp = do
  istd <- once =<< (ask >>= inlineStyleDecl)
  mapM (\(p := v) -> once =<< setProperty (string p) (string v) (string "") istd) csp 
  return $ NullLit ()

-- | A type for a on-style event handler. It represents a function which
-- takes an event and returns a boolean.

type OnHandler e c = Expression e -> JSMW c (Expression Bool)


-- | Set a on-style (e. g. onclick) event handler on the current container.
--
-- Example: a button with a click handler which shows the X coordinate of the click.
--
-- @
--  mkButton \`container\` (do
--    passive (mkText $ string \"Click Me\")
--    setHandler \"click\" clickHandler)
--  ...
--  clickHandler :: OnHandler TMouseEvent THTMLButtonElement
--  clickHandler e = do
--    getm'clientX e >>= toString >>= alert
--    return true
-- @
--
-- A handler function has one argument which gets the reference to the event caught.
-- The handler function also may implicitly address the container it was set on by
-- calling 'ask' or 'passive'. For example, calling @passive (mkText $ string \"x\")@
-- within a handler will result in a text node being added to the container.
--
-- Also note that the 'OnHandler' type may be parameterized by the type of containers
-- it can be set on. In the example above, the handler may only be set on buttons.
--
-- The MSIE-specific code to obtain event from the static attribute of the current
-- window is inserted in the beginning of the handler automatically.

setHandler :: (CHTMLElement c, CEvent e) => String -> OnHandler e c -> JSMW c (Expression ())

setHandler s x = do
  ctr <- once =<< ask
  earg <- mkNewVar
  st <- get
  let et = undefined :: e
      prop = "on" ++ s
      evar = VarRef et (Id et earg)
      (finx, fins, stms) = runJSMWWith ctr st (x evar)
      msievent = IfSingleStmt () (PrefixExpr () PrefixLNot (evar /\ ()))
                   (BlockStmt () [ExprStmt () 
                                   (AssignExpr () OpAssign (evar /\ ()) 
                                                           (VarRef () (Id () "window.event")))])
      blk = getBlock (finx, fins, msievent : stms)
      fun = FuncExpr () [Id () earg] blk
      seth = ExprStmt () $ AssignExpr () OpAssign (DotRef () (ctr /\ ()) (Id () prop)) (fun /\ ())
  writeStmt seth
  put fins
  return (NullLit ())

-- | Create a unique variable name. This function increments the internal state of the
-- monad and produces a string consisting of the letter \'v\' and a unique number.

mkNewVar :: JSMW e String

mkNewVar = do
  modify (+ 1)
  n <- get
  return ('v' : show n)
  
-- | Write out a statement. This function utilizes the Writer part of the monad, and
-- adds the Javascript statement provided to the Writer's log.

writeStmt :: Statement () -> JSMW e ()

writeStmt = tell . (: [])


-- | The JSMW code consists of monadic smart constructors forming Javascript
-- method calls. These constructors are inlined each time they are referenced.
-- The "once" combinator causes a variable assignment statement to be formed
-- with the variable assigned to the expression returned. All future references
-- will be to the variable rather than to the expression. Since the expression
-- will be evaluated when assigned to the variable, referencing the variable
-- will reference the result, and possible effect will not be repeated.

once :: Expression a -> JSMW e (Expression a)

once e@(VarRef _ _) = return e

once e = do
  nv <- mkNewVar
  let et = exprType e
      eo = e /\ ()
      stm = VarDeclStmt () [VarDecl () (Id () nv) (Just eo)]
      vr = VarRef et (Id et nv)
  writeStmt stm
  return vr

-- | Create a Javascript string literal out of a string, monadic version.

stringM :: String -> JSMW e (Expression String)

stringM s = once =<< (return $ string s)

-- | Create a Javascript numeric literal out of a numeric value, monadic version.

numberM :: (Integral n) => n -> JSMW e (Expression Double)

numberM = return . number

-- | Create a Javascript boolean literal out of a Boolean, monadic version.

boolM :: Bool -> JSMW e (Expression Bool)

boolM = return . bool