{-# LANGUAGE NamedFieldPuns, OverloadedStrings, DataKinds,
    ExistentialQuantification, ConstraintKinds #-}
module React.Class
    ( ReactClass(..)
    , ClassConfig(..)
    , ClassCtx
    , PropRequired(..)
    , PropType(..)
    , createClass
    , smartClass
    , dumbClass
    ) where


import Control.Applicative
import Control.Monad
import qualified Data.Aeson as Aeson
import qualified Data.HashMap.Strict as H
import Data.IORef
import Data.Maybe
import Data.Text (Text)
import System.IO.Unsafe

import React.GHCJS
import React.Imports
import React.Interpret
import React.PropTypes
import React.Registry
import React.Types


-- TODO think about whether this could be a monoid or something
data ClassConfig props state insig exsig ctx = ClassConfig
    { renderFn :: props -> state -> ReactNode insig
    , initialState :: state
    , name :: JSString
    , transition :: (state, insig) -> (state, Maybe exsig)
    , startupSignals :: [insig]

    -- lifecycle methods!
    -- TODO(joel) - Lifcycle monad!

--     , componentWillMount :: props -> state -> IO state
--     , componentDidMount
--     , componentWillReceiveProps
--     , shouldComponentUpdate
--     , componentWillUpdate
--     , componentDidUpdate
--     , componentWillUnmount

    -- TODO(joel) - this is static for now - should it be dynamic? and what
    -- does that dynamic implementation have access to?
    , childContext :: Maybe (H.HashMap Text ctx)
    }


dumbClass :: ClassConfig props () sig sig JSString
dumbClass = ClassConfig
    { name = "Anonymous Stateless Class"
    , renderFn = \_ _ -> "give this class a `render`!"
    , initialState = ()
    , transition = \(state, sig) -> (state, Just sig)
    , startupSignals = []
    , childContext = Nothing
    }


smartClass :: ClassConfig props state insig exsig JSString
smartClass = ClassConfig
    { name = "Anonymous Stateful Class"
    , renderFn = \_ _ -> "give this class a `render`!"
    , initialState = error "must define `initialState`!"
    , transition = error "must define `transition`!"
    , startupSignals = []
    , childContext = Nothing
    }


willMount :: ClassRegistry props state insig exsig -> state -> JSRef Int -> IO ()
willMount registry state idRef = do
    -- initialize state in registry
    Just componentId <- fromJSRef idRef
    setState registry state componentId


willUnmount :: ClassRegistry props state insig exsig -> JSRef Int -> IO ()
willUnmount registry idRef = do
    -- remove state from registry
    Just componentId <- fromJSRef idRef
    deallocRegistry registry componentId


type ClassCtx a = (ToJSRef a, PropTypable a)


-- TODO(joel) why not just pass in the class config?
render :: ClassRegistry props state insig exsig
       -> (props -> state -> ReactNode insig)
       -> JSRef (Int, JSAny)
       -> JSAny
       -> IO ()
render registry renderFn inRefs returnObj = do
    -- The fundamental tension here is that this render function is
    -- defined for the class, but each invocation needs access to a
    -- specific instance. How do we get a handle to that specific
    -- instance? This will give us the props and state (and
    -- handler?)

    Just (componentId, thisObj) <- fromJSRef inRefs
    RegistryStuff thisProps thisState thisHandler <-
        lookupRegistry registry componentId

    let rendered = renderFn thisProps thisState

        -- We need to forceUpdate here because we have access to `this`. But
        -- the registered handler does most of the work.
        handler sig = do
            js_forceUpdate thisObj
            thisHandler sig
    ret <- reactNodeToJSAny handler componentId rendered
    setProp ("value" :: JSString) ret returnObj


createClass :: ClassCtx ctx
            => ClassConfig props state insig exsig ctx
            -> ReactClass props state insig exsig ctx
createClass ClassConfig{renderFn,
                        initialState,
                        name,
                        transition,
                        startupSignals,
                        childContext} =

    -- TODO(joel) - verify this use of unsafePerformIO is, well, safe
    let classRegistry = unsafePerformIO $ ClassRegistry
            <$> newIORef H.empty
            <*> newIORef 0

        foreignObj = do
            obj <- newObj

            setProp ("displayName" :: JSString) name obj

            renderCb <- syncCallback2 NeverRetain True
                (render classRegistry renderFn)
            setProp ("render" :: JSString) renderCb obj

            when (isJust childContext) $ do
                let childContext' = fromJust childContext

                ctxObj <- newObj
                ctxTypeObj <- newObj
                forM_ (H.toList childContext') $ \(k, v) -> do
                    ref <- toJSRef v
                    setProp k ref ctxObj

                    let ty = toJsPropType (propType v)
                    setProp k ty ctxTypeObj

                setProp' "childContext" ctxObj obj
                setProp' "childContextTypes" ctxTypeObj obj

            willMountCb <- syncCallback1 NeverRetain True
                (willMount classRegistry initialState)
            setProp ("componentWillMount" :: JSString) willMountCb obj

            willUnmountCb <- syncCallback1 NeverRetain True
                (willUnmount classRegistry)
            setProp ("componentWillUnmount" :: JSString) willUnmountCb obj

            return obj

        foreignClass = unsafePerformIO $ js_createClass <$> foreignObj

    in ReactClass foreignClass transition classRegistry