{-# LANGUAGE OverloadedStrings, FlexibleContexts, NamedFieldPuns #-}

module React.Render
    ( render
    , cancelRender
    ) where

import Control.Applicative
import Control.Monad
import Control.Monad.IO.Class
import Data.Functor.Identity
import Data.IORef
import Data.List
import Data.Maybe
import Data.Monoid
import Data.String

import Haste hiding (fromString)
import Haste.Foreign
import Haste.JSON
import Haste.Prim

import React.Anim
import React.Attrs
import React.Class
import React.Elements
import React.Events
import React.Imports
import React.Interpret
import React.Local
import React.Types


doRender :: Elem -> Double -> ReactClass ty -> IO ()
doRender elem time ReactClass{ classRender,
                               classTransition,
                               transitionRef,
                               runningAnimRef,
                               animRef,
                               stateRef } = do

    transitions <- readIORef transitionRef
    runningAnims <- readIORef runningAnimRef
    prevState <- readIORef stateRef
    prevAnim <- readIORef animRef

    let (newState, newAnims) =
            mapAccumL classTransition prevState transitions

        newAnims' = concat newAnims
        newRunningAnims = map (\conf -> RunningAnim conf time) newAnims'

        (runningAnims', endingAnims) = partition
            (\(RunningAnim AnimConfig{duration} beganAt) ->
                beganAt + duration > time)
            (runningAnims <> newRunningAnims)

        endingAnims' = zip endingAnims [1..]
        runningAnims'' = zip runningAnims' (map (lerp time) runningAnims')
        newAnim = stepRunningAnims prevAnim (endingAnims' ++ runningAnims'')

        -- TODO should this run before or after rendering?
        -- TODO expose a way to cancel / pass False in that case
        endAnimTrans = mapMaybe
            (\anim -> onComplete (config anim) True)
            endingAnims

    foreignNode <- runIdentity $
        interpret (classRender newState) newAnim (updateCb transitionRef)
    js_render foreignNode elem

    writeIORef stateRef newState
    writeIORef animRef newAnim
    writeIORef runningAnimRef runningAnims'
    writeIORef transitionRef endAnimTrans


updateCb :: IORef [signal] -> signal -> IO ()
updateCb ref update = modifyIORef ref (update:)


render :: Elem
       -> ReactClass ty
       -> IO RenderHandle
render elem cls@ReactClass{transitionRef, runningAnimRef} = do
    let renderCb time = do
            transitions <- readIORef transitionRef
            runningAnims <- readIORef runningAnimRef

            -- only rerender when dirty
            when (length transitions + length runningAnims > 0) $
                doRender elem time cls

            js_raf $ toPtr renderCb
            return ()

    doRender elem 0 cls
    js_raf $ toPtr renderCb


cancelRender :: RenderHandle -> IO ()
cancelRender = js_cancelRaf