{-# LANGUAGE GADTs, FlexibleInstances, MultiParamTypeClasses, 
             FlexibleContexts, OverlappingInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
-- | DOM events and utilities for the Haste reactive library.
module Haste.Reactive.DOM (clicked,valueOf,valueAt,ElemProp,elemProp) where
import FRP.Fursuit
import Haste
import qualified Data.Map as M
import System.IO.Unsafe (unsafePerformIO)
import Data.IORef

{-# NOINLINE eventHandlers #-}
-- | Contains a list of all installed event handlers.
eventHandlers :: JSType a => IORef (M.Map (ElemID, Event IO e) (Signal a))
eventHandlers = unsafePerformIO $ newIORef M.empty

-- | Represents a property of a DOM object.
data ElemProp where
  D :: ElemID -> PropID -> ElemProp

-- | Create a 'DOMObject' from a string describing the object. For example,
--   domObj "myobject.value" corresponds to the value attribute of the object
--   with the ID "myobject".
elemProp :: String -> ElemProp
elemProp str =
  case span (/= '.') str of
    ([], _)     -> error "elemProp: No object ID given!"
    (_, [])     -> error "elemProp: No object attribute given!"
    (obj, attr) -> D obj (tail attr)

unlessExists :: JSType a => ElemID -> Event IO e -> IO (Signal a) -> Signal a
unlessExists eid evt create = new $ do
  handlers <- readIORef eventHandlers
  case M.lookup (eid, evt) handlers of
    Just s -> return s
    _      -> do
      sig <- create
      writeIORef eventHandlers (M.insert (eid, evt) sig handlers)
      return sig


-- | An event that gets raised whenever the element with the specified ID is
--   clicked.
clicked :: ElemID -> Signal ()
clicked eid = unlessExists eid OnClick clickedIO
  where
    clickedIO = withElem eid $ \e -> do
      (p,s) <- pipe ()
      _ <- setCallback e OnClick (\_ _ -> write p ())
      return s

-- | The value property of the given element, updated whenever an onchange
--   event is raised.
valueOf :: JSType a  => ElemID -> Signal a
valueOf e = e `valueAt` OnChange

-- | The value property of the given element, triggered on a custom event.
valueAt :: (JSType a, Callback e) => ElemID -> Event IO e -> Signal a
valueAt eid evt = filterMapS fromString $ unlessExists eid evt valueAtIO
  where
    valueAtIO = withElem eid $ \e -> do
      str <- getProp e "value"
      (src, sig) <- pipe str
      success <- setCallback e evt $ constCallback $ do
        getProp e "value" >>= write src

      if (not success) 
        then error $ "Browser doesn't support sane event handlers!"
        else return sig

-- | Like show, but strips enclosing quotes.
toStr :: Show a => a -> String
toStr x =
  case show x of
    ('"':xs) -> init xs
    xs       -> xs

instance Show a => Sink ElemProp a where
  (D obj attr) << val = withElem obj $ \e -> sink (setProp e attr . toStr) val

-- | Replace the sink element's list of child nodes whenever a new list of
--   nodes comes down the pipe.
instance Sink Elem [Elem] where
  e << val = sink (setChildren e) val

-- | Same as the instance for [Elem].
instance Sink Elem [IO Elem] where
  e << val = sink (\children -> sequence children >>= setChildren e) val

-- | Set the sink element's innerHTML property whenever a new string comes down
--   the pipe.
instance Sink Elem String where
  e << val = sink (setProp e "innerHTML") val