{-# LANGUAGE GADTs #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE LambdaCase #-}

-- | JavaScript interpreter
--
-- See Note [The JS interpreter]
--
module GHC.Runtime.Interpreter.JS
  ( spawnJSInterp
  , jsLinkRts
  , jsLinkInterp
  , jsLinkObject
  , jsLinkObjects
  , jsLoadFile
  , jsRunServer
  -- * Reexported for convenience
  , mkExportedModFuns
  )
where

import GHC.Prelude
import GHC.Runtime.Interpreter.Types
import GHC.Runtime.Interpreter.Process
import GHC.Runtime.Utils
import GHCi.Message

import GHC.StgToJS.Linker.Types
import GHC.StgToJS.Linker.Linker
import GHC.StgToJS.Types
import GHC.StgToJS.Object

import GHC.Unit.Env
import GHC.Unit.Types
import GHC.Unit.State

import GHC.Utils.Logger
import GHC.Utils.TmpFs
import GHC.Utils.Panic
import GHC.Utils.Error (logInfo)
import GHC.Utils.Outputable (text)
import GHC.Data.FastString
import GHC.Types.Unique.FM

import Control.Concurrent
import Control.Monad

import System.Process
import System.IO
import System.FilePath

import Data.IORef
import qualified Data.Set    as Set
import qualified Data.ByteString as B

import Foreign.C.String


-- Note [The JS interpreter]
-- ~~~~~~~~~~~~~~~~~~~~~~~~~
--
-- The JS interpreter works as follows:
--
-- ghc-interp.js is a simple JS script used to bootstrap the external
-- interpreter server (iserv) that is written in Haskell. This script waits for
-- commands on stdin:
--
--      LOAD foo.js
--
--        load a JS file in the current JS environment
--
--      RUN_SERVER ghci_unit_id
--
--        execute h$main(h$ghci_unit_idZCGHCiziServerzidefaultServer),
--        the entry point of the interpreter server
--
-- On the GHC side, when we need the interpreter we do the following:
--
-- 1. spawn nodejs with $topdir/ghc-interp.js script
-- 2. link the JS rts and send a LOAD command to load it
-- 3. link iserv (i.e. use GHCi.Server.defaultServer as root) and LOAD it
-- 4. send a RUN_SERVER command to execute the JS iserv
--
-- From this moment on, everything happens as with the native iserv, using a
-- pipe for communication, with the following differences:
--
--  - the JS iserv only supports the LoadObj linking command which has been
--  repurposed to load a JS source file. The JS iserv doesn't deal with
--  libraries (.a) and with object files (.o). The linker state is maintained on
--  the GHC side and GHC only sends the appropriate chunks of JS code to link.
--
--  - the JS iserv doesn't support ByteCode (i.e. it doesn't support CreateBCOs
--  messages). JS iserv clients should use the usual JS compilation pipeline and
--  send JS code instead. See GHC.Driver.Main.hscCompileCoreExpr for an example.
--
-- GHC keeps track of JS blocks (JS unit of linking corresponding to top-level
-- binding groups) that have already been linked by the JS interpreter. It only
-- links new ones when necessary.
--
-- Note that the JS interpreter isn't subject to staging issues: we can use it
-- in a Stage1 GHC.
--

---------------------------------------------------------
-- Running node
---------------------------------------------------------

-- | Start NodeJS interactively with "ghc-interp.js" script loaded in
startTHRunnerProcess :: FilePath -> NodeJsSettings -> IO (Handle,InterpProcess)
startTHRunnerProcess interp_js settings = do
  interp_in <- newIORef undefined

  let createProc cp = do
          let cp' = cp
                      { std_in  = CreatePipe
                      , std_out = Inherit
                      , std_err = Inherit
                      }
          (mb_in, _mb_out, _mb_err, hdl) <- createProcess cp'
          -- we can't directly return stdin for the process given the current
          -- implementation of runWithPipes. So we just use an IORef for this...
          case mb_in of
            Nothing -> panic "startTHRunnerProcess: expected stdin for interpreter"
            Just i  -> writeIORef interp_in i
          return hdl

  (hdl, rh, wh) <- runWithPipes createProc (nodeProgram settings)
                                           [interp_js]
                                           (nodeExtraArgs settings)
  std_in <- readIORef interp_in

  lo_ref <- newIORef Nothing
  let pipe = Pipe { pipeRead = rh, pipeWrite = wh, pipeLeftovers = lo_ref }
  let proc = InterpProcess
              { interpHandle = hdl
              , interpPipe   = pipe
              }
  pure (std_in, proc)

-- | Spawn a JS interpreter
--
-- Run NodeJS with "ghc-interp.js" loaded in. Then load GHCi.Server and its deps
-- (including the rts) and run GHCi.Server.defaultServer.
spawnJSInterp :: JSInterpConfig -> IO (ExtInterpInstance JSInterpExtra)
spawnJSInterp cfg = do
  let logger= jsInterpLogger cfg
  when (logVerbAtLeast logger 2) $
    logInfo logger (text "Spawning JS interpreter")

  let tmpfs        = jsInterpTmpFs cfg
      tmp_dir      = jsInterpTmpDir cfg
      logger       = jsInterpLogger cfg
      codegen_cfg  = jsInterpCodegenCfg cfg
      unit_env     = jsInterpUnitEnv cfg
      finder_opts  = jsInterpFinderOpts cfg
      finder_cache = jsInterpFinderCache cfg

  (std_in, proc) <- startTHRunnerProcess (jsInterpScript cfg) (jsInterpNodeConfig cfg)

  js_state <- newMVar (JSState
                { jsLinkState     = emptyLinkPlan
                , jsServerStarted = False
                })

  -- get the unit-id of the ghci package. We need this to load the
  -- interpreter code.
  ghci_unit_id <- case lookupPackageName (ue_units unit_env) (PackageName (fsLit "ghci")) of
    Nothing -> cmdLineErrorIO "JS interpreter: couldn't find \"ghci\" package"
    Just i  -> pure i

  let extra = JSInterpExtra
        { instStdIn        = std_in
        , instJSState      = js_state
        , instFinderCache  = finder_cache
        , instFinderOpts   = finder_opts
        , instGhciUnitId   = ghci_unit_id
        }

  pending_frees <- newMVar []
  lookup_cache  <- newMVar emptyUFM
  let inst = ExtInterpInstance
        { instProcess           = proc
        , instPendingFrees      = pending_frees
        , instLookupSymbolCache = lookup_cache
        , instExtra             = extra
        }

  -- link rts and its deps
  jsLinkRts logger tmpfs tmp_dir codegen_cfg unit_env inst

  -- link interpreter and its deps
  jsLinkInterp logger tmpfs tmp_dir codegen_cfg unit_env inst

  -- run interpreter main loop
  jsRunServer inst

  pure inst



---------------------------------------------------------
-- Interpreter commands
---------------------------------------------------------

-- | Link JS RTS
jsLinkRts :: Logger -> TmpFs -> TempDir -> StgToJSConfig -> UnitEnv -> ExtInterpInstance JSInterpExtra -> IO ()
jsLinkRts logger tmpfs tmp_dir cfg unit_env inst = do
  let link_cfg = JSLinkConfig
        { lcNoStats         = True  -- we don't need the stats
        , lcNoRts           = False -- we need the RTS
        , lcCombineAll      = False -- we don't need the combined all.js, we'll link each part independently below
        , lcForeignRefs     = False -- we don't need foreign references
        , lcNoJSExecutables = True  -- we don't need executables
        , lcNoHsMain        = True  -- nor HsMain
        }

  -- link the RTS and its dependencies (things it uses from `base`, etc.)
  let link_spec = LinkSpec
        { lks_unit_ids        = [rtsUnitId, baseUnitId, primUnitId]
        , lks_obj_files       = mempty
        , lks_obj_root_filter = const False
        , lks_extra_roots     = mempty
        , lks_extra_js        = mempty
        }

  let finder_opts  = instFinderOpts (instExtra inst)
      finder_cache = instFinderCache (instExtra inst)

  link_plan <- computeLinkDependencies cfg unit_env link_spec finder_opts finder_cache
  jsLinkPlan logger tmpfs tmp_dir link_cfg cfg inst link_plan

-- | Link JS interpreter
jsLinkInterp :: Logger -> TmpFs -> TempDir -> StgToJSConfig -> UnitEnv -> ExtInterpInstance JSInterpExtra -> IO ()
jsLinkInterp logger tmpfs tmp_dir cfg unit_env inst = do

  let link_cfg = JSLinkConfig
        { lcNoStats         = True  -- we don't need the stats
        , lcNoRts           = True  -- we don't need the RTS
        , lcCombineAll      = False -- we don't need the combined all.js, we'll link each part independently below
        , lcForeignRefs     = False -- we don't need foreign references
        , lcNoJSExecutables = True  -- we don't need executables
        , lcNoHsMain        = True  -- nor HsMain
        }

  let is_root _ = True -- FIXME: we shouldn't consider every function as a root

  let ghci_unit_id = instGhciUnitId (instExtra inst)

  -- compute unit dependencies of ghc_unit_id
  let unit_map = unitInfoMap (ue_units unit_env)
  dep_units <- mayThrowUnitErr $ closeUnitDeps unit_map [(ghci_unit_id,Nothing)]
  let units = dep_units ++ [ghci_unit_id]

  -- indicate that our root function is GHCi.Server.defaultServer
  let root_deps = Set.fromList $ mkExportedFuns ghci_unit_id (fsLit "GHCi.Server") [fsLit "defaultServer"]

  -- link the interpreter and its dependencies
  let link_spec = LinkSpec
        { lks_unit_ids        = units
        , lks_obj_files       = mempty
        , lks_obj_root_filter = is_root
        , lks_extra_roots     = root_deps
        , lks_extra_js        = mempty
        }

  let finder_cache = instFinderCache (instExtra inst)
      finder_opts  = instFinderOpts (instExtra inst)

  link_plan <- computeLinkDependencies cfg unit_env link_spec finder_opts finder_cache

  jsLinkPlan logger tmpfs tmp_dir link_cfg cfg inst link_plan


-- | Link object files
jsLinkObjects :: Logger -> TmpFs -> TempDir -> StgToJSConfig -> UnitEnv -> ExtInterpInstance JSInterpExtra -> [FilePath] -> (ExportedFun -> Bool) -> IO ()
jsLinkObjects logger tmpfs tmp_dir cfg unit_env inst objs is_root = do
  let link_cfg = JSLinkConfig
        { lcNoStats         = True  -- we don't need the stats
        , lcNoRts           = True  -- we don't need the RTS (already linked)
        , lcCombineAll      = False -- we don't need the combined all.js, we'll link each part independently below
        , lcForeignRefs     = False -- we don't need foreign references
        , lcNoJSExecutables = True  -- we don't need executables
        , lcNoHsMain        = True  -- nor HsMain
        }

  let units = preloadUnits (ue_units unit_env)
              ++ [thUnitId] -- don't forget TH which is an implicit dep

  -- compute dependencies
  let link_spec = LinkSpec
        { lks_unit_ids        = units
        , lks_obj_files       = fmap ObjFile objs
        , lks_obj_root_filter = is_root
        , lks_extra_roots     = mempty
        , lks_extra_js        = mempty
        }

  let finder_opts  = instFinderOpts (instExtra inst)
      finder_cache = instFinderCache (instExtra inst)

  link_plan <- computeLinkDependencies cfg unit_env link_spec finder_opts finder_cache

  -- link
  jsLinkPlan logger tmpfs tmp_dir link_cfg cfg inst link_plan



-- | Link an object file using the given functions as roots
jsLinkObject :: Logger -> TmpFs -> TempDir -> StgToJSConfig -> UnitEnv -> ExtInterpInstance JSInterpExtra -> FilePath -> [ExportedFun] -> IO ()
jsLinkObject logger tmpfs tmp_dir cfg unit_env inst obj roots = do
  let is_root f = Set.member f (Set.fromList roots)
  let objs      = [obj]
  jsLinkObjects logger tmpfs tmp_dir cfg unit_env inst objs is_root


-- | Link the given link plan
--
-- Perform incremental linking by removing what is already linked from the plan
jsLinkPlan :: Logger -> TmpFs -> TempDir -> JSLinkConfig -> StgToJSConfig -> ExtInterpInstance JSInterpExtra -> LinkPlan -> IO ()
jsLinkPlan logger tmpfs tmp_dir link_cfg cfg inst link_plan = do
  ----------------------------------------------------------------
  -- Get already linked stuff and compute incremental plan
  ----------------------------------------------------------------

  old_plan <- jsLinkState <$> readMVar (instJSState (instExtra inst))

  -- compute new plan discarding what's already linked
  let (diff_plan, total_plan) = incrementLinkPlan old_plan link_plan

  ----------------------------------------------------------------
  -- Generate JS code for the incremental plan
  ----------------------------------------------------------------

  tmp_out <- newTempSubDir logger tmpfs tmp_dir
  void $ jsLink link_cfg cfg logger tmp_out diff_plan

  -- Code has been linked into the following files:
  --  - generated rts from tmp_out/rts.js (depends on link options)
  --  - raw js files from tmp_out/lib.js
  --  - Haskell generated JS from tmp_out/out.js

  -- We need to combine at least rts.js and lib.js for the RTS because they
  -- depend on each other. We might as well combine them all, so that's what we
  -- do.
  let filenames
        | lcNoRts link_cfg = ["lib.js", "out.js"]
        | otherwise        = ["rts.js", "lib.js", "out.js"]
  let files = map (tmp_out </>) filenames
  let all_js = tmp_out </> "all.js"
  let all_files = all_js : files
  withBinaryFile all_js WriteMode $ \h -> do
    let cpy i = B.readFile i >>= B.hPut h
    mapM_ cpy files

  -- add files to clean
  addFilesToClean tmpfs TFL_CurrentModule all_files

  ----------------------------------------------------------------
  -- Link JS code
  ----------------------------------------------------------------

  -- linking JS code depends on the phase we're in:
  -- - during in the initialization phase, we send a LoadFile message to the
  --   JS server;
  -- - once the Haskell server is started, we send a LoadObj message to the
  --   Haskell server.
  server_started <- jsServerStarted <$> readMVar (instJSState (instExtra inst))
  if server_started
    then sendMessageNoResponse inst $ LoadObj all_js
    else jsLoadFile            inst all_js

  ----------------------------------------------------------------
  -- update linker state
  ----------------------------------------------------------------
  modifyMVar_ (instJSState (instExtra inst)) $ \state -> pure state { jsLinkState = total_plan }


-- | Send a command to the JS interpreter
jsSendCommand :: ExtInterpInstance JSInterpExtra -> String -> IO ()
jsSendCommand inst cmd = send_cmd cmd
  where
    extra      = instExtra inst
    handle     = instStdIn extra
    send_cmd s = do
      withCStringLen s \(p,n) -> hPutBuf handle p n
      hFlush handle

-- | Load a JS file in the interpreter
jsLoadFile :: ExtInterpInstance JSInterpExtra -> FilePath -> IO ()
jsLoadFile inst path = jsSendCommand inst ("LOAD " ++ path ++ "\n")

-- | Run JS server
jsRunServer :: ExtInterpInstance JSInterpExtra -> IO ()
jsRunServer inst = do
  let ghci_unit_id = instGhciUnitId (instExtra inst)
  let zghci_unit_id = zString (zEncodeFS (unitIdFS ghci_unit_id))

  -- Run `GHCi.Server.defaultServer`
  jsSendCommand inst ("RUN_SERVER " ++ zghci_unit_id ++ "\n")

  -- indicate that the Haskell server is now started
  modifyMVar_ (instJSState (instExtra inst)) $ \state -> pure state { jsServerStarted = True }
