-----------------------------------------------------------------------------
{-# LANGUAGE CPP               #-}
{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE TemplateHaskell   #-}
{-# LANGUAGE OverloadedStrings #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  Miso.Reload
-- Copyright   :  (C) 2016-2026 David M. Johnson
-- License     :  BSD3-style (see the file LICENSE)
-- Maintainer  :  David M. Johnson <code@dmj.io>
-- Stability   :  experimental
-- Portability :  non-portable
--
-- Support for live reloading of miso applications.
--
-- = Live Reload
--
-- This module exposes two functions meant to be used during interactive
-- development with GHC WASM browser mode, 'live' and 'reload'.
--
-- == Reload
--
-- Use 'reload' if you'd like to redraw the page on each file change, resetting
-- the working application state.
--
-- @
-- main :: IO ()
-- main = 'reload' 'defaultEvents' app
-- @
--
-- == Live
--
-- Use 'live' if you'd like to persist the working application state (all 'Component' 'model')
-- between GHCi reloads. This only works if you do not alter the 'model' schema (e.g. add, remove, change a field's type).
--
-- @
-- main :: IO ()
-- main = 'live' 'defaultEvents' app
-- @
--
-- See the [miso-sampler](https://github.com/haskell-miso/miso-sampler) for example usage.
--
----------------------------------------------------------------------------
module Miso.Reload
  ( -- ** Functions
    reload
  , live
  ) where
-----------------------------------------------------------------------------
import           Control.Concurrent
import           Control.Monad
-----------------------------------------------------------------------------
import           Miso.DSL ((!), jsg, setField)
import qualified Miso.FFI.Internal as FFI
import           Miso.Types (Component(..), Events, App)
import           Miso.String (MisoString)
import           Miso.Runtime (componentModel, initComponent, topLevelComponentId, Hydrate(..))
import           Miso.Runtime.Internal (components, schedulerThread)
-----------------------------------------------------------------------------
import           Miso.Lens
-----------------------------------------------------------------------------
import qualified Data.IntMap.Strict as IM
import           Data.IORef
import           Foreign hiding (void)
import           Foreign.C.Types
-----------------------------------------------------------------------------
foreign import ccall unsafe "miso_x_store"
  x_store :: StablePtr a -> IO ()
-----------------------------------------------------------------------------
foreign import ccall unsafe "miso_x_get"
  x_get :: IO (StablePtr a)
-----------------------------------------------------------------------------
foreign import ccall unsafe "miso_x_exists"
  x_exists :: IO CInt
-----------------------------------------------------------------------------
foreign import ccall unsafe "miso_x_clear"
  x_clear :: IO ()
-----------------------------------------------------------------------------
#define MISO_JS_PATH "js/miso.js"
-----------------------------------------------------------------------------
-- | Clears the \<body\> and \<head\> on each 'reload'.
--
-- Meant to be used with WASM browser mode.
--
-- @
-- main :: IO ()
-- main = 'reload' 'defaultEvents' app
-- @
--
-- N.B. This also resets the internal 'component' state. This means all currently
-- mounted components become unmounted and t'ComponentId' are reset to their
-- original form factory.
--
-- If you'd like to preserve application state between calls to GHCi `:r`, see 'live'.
--
-- @since 1.9.0.0
reload
  :: Eq model
  => Events
  -> App model action
  -> IO ()
reload :: forall model action.
Eq model =>
Events -> App model action -> IO ()
reload Events
events App model action
vcomp = do
  exists <- IO CInt
x_exists
  when (exists == 1) $ do
    (_, oldSchedulerRef) <- deRefStablePtr =<< x_get
    killThread =<< readIORef oldSchedulerRef
    x_clear
  clearPage
  void (initComponent events Draw False vcomp)
  x_store =<< newStablePtr (components, schedulerThread)
-----------------------------------------------------------------------------
-- | Live reloading. Persists all t'Component' `model` between successive GHCi reloads.
--
-- This means application state should persist between GHCi reloads
--
-- Schema changes to 'model' are currently unsupported. If you're
-- changing fields in 'model' (adding, removing, changing a field's type), this
-- will more than likely segfault. If you change the 'view' or 'update' functions
-- it will be fine.
--
-- Use 'reload' if you're changing the 'model' frequently and 'live'
-- if you're adjusting the 'view' / 'update' function logic.
--
-- @
-- main :: IO ()
-- main = 'live' 'defaultEvents' app
-- @
--
-- @since 1.9.0.0
live
  :: Eq model
  => Events
  -> App model action
  -> IO ()
live :: forall model action.
Eq model =>
Events -> App model action -> IO ()
live Events
events App model action
vcomp = do
  exists <- IO CInt
x_exists
  if exists == 1
    then do
      -- clearBody (only clear the body)
      clearBody

      -- Deref old state, update new state, set pointer in C heap.
      (oldComponentsRef, oldSchedulerRef) <- deRefStablePtr =<< x_get
      killThread =<< readIORef oldSchedulerRef

      _oldState <- readIORef oldComponentsRef
      let oldModel = (IntMap (ComponentState (ZonkAny 0) (ZonkAny 1) model (ZonkAny 2))
_oldState IntMap (ComponentState (ZonkAny 0) (ZonkAny 1) model (ZonkAny 2))
-> Key -> ComponentState (ZonkAny 0) (ZonkAny 1) model (ZonkAny 2)
forall a. IntMap a -> Key -> a
IM.! Key
topLevelComponentId) ComponentState (ZonkAny 0) (ZonkAny 1) model (ZonkAny 2)
-> Lens
     (ComponentState (ZonkAny 0) (ZonkAny 1) model (ZonkAny 2)) model
-> model
forall record field. record -> Lens record field -> field
^. Lens
  (ComponentState (ZonkAny 0) (ZonkAny 1) model (ZonkAny 2)) model
forall parent props model action.
Lens (ComponentState parent props model action) model
componentModel
          initialVComp = App model action
vcomp { model = oldModel }

      -- Overwrite new components state with old components state
      atomicWriteIORef components _oldState

      -- Perform initial draw, this will fetch the model from the old component state
      -- and overwrite the old state with the new state for everything else.
      initComponent events Draw True initialVComp

      -- Don't forget to flush (native mobile needs this too)
      FFI.flush

      -- Clear and set static ptr to use new state (new CAF state)
      x_clear
      x_store =<< newStablePtr (components, schedulerThread)
    else do
      -- This means it is initial load, just store the pointer.
      void (initComponent events Draw False vcomp)
      x_store =<< newStablePtr (components, schedulerThread)
-----------------------------------------------------------------------------
clearPage, clearBody, clearHead :: IO ()
clearPage :: IO ()
clearPage = IO ()
clearBody IO () -> IO () -> IO ()
forall a b. IO a -> IO b -> IO b
forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> IO ()
clearHead
clearBody :: IO ()
clearBody = do
  body_ <- MisoString -> IO JSVal
jsg MisoString
"document" IO JSVal -> MisoString -> IO JSVal
forall o. ToObject o => o -> MisoString -> IO JSVal
! (MisoString
"body" :: MisoString)
  setField body_ "innerHTML" ("" :: MisoString)
clearHead :: IO ()
clearHead = do
  head_ <- MisoString -> IO JSVal
jsg MisoString
"document" IO JSVal -> MisoString -> IO JSVal
forall o. ToObject o => o -> MisoString -> IO JSVal
! (MisoString
"head" :: MisoString)
  setField head_ "innerHTML" ("" :: MisoString)
-----------------------------------------------------------------------------