{-# LANGUAGE OverloadedStrings, DoAndIfThenElse #-}
-- | This module provides a way in which the Haskell standard input may be
-- forwarded to the IPython frontend and thus allows the notebook to use
-- the standard input.
--
-- This relies on the implementation of file handles in GHC, and is
-- generally unsafe and terrible. However, it is difficult to find another
-- way to do it, as file handles are generally meant to point to streams
-- and files, and not networked communication protocols.
--
-- In order to use this module, it must first be initialized with two
-- things. First of all, in order to know how to communicate with the
-- IPython frontend, it must know the kernel profile used for
-- communication. For this, use @recordKernelProfile@ once the profile is
-- known. Both this and @recordParentHeader@ take a directory name where
-- they can store this data.
--
-- Finally, the module must know what @execute_request@ message is
-- currently being replied to (which will request the input). Thus, every
-- time the language kernel receives an @execute_request@ message, it
-- should inform this module via @recordParentHeader@, so that the module
-- may generate messages with an appropriate parent header set. If this is
-- not done, the IPython frontends will not recognize the target of the
-- communication.
--
-- Finally, in order to activate this module, @fixStdin@ must be called
-- once. It must be passed the same directory name as @recordParentHeader@
-- and @recordKernelProfile@. Note that if this is being used from within
-- the GHC API, @fixStdin@ /must/ be called from within the GHC session
-- not from the host code.
module IHaskell.IPython.Stdin (
  fixStdin,
  recordParentHeader,
  recordKernelProfile
  ) where

import            Control.Concurrent          
import            Control.Applicative         ((<$>))
import            Control.Concurrent.Chan
import            Control.Monad
import            GHC.IO.Handle
import            GHC.IO.Handle.Types
import            System.IO
import            System.Posix.IO
import            System.IO.Unsafe
import qualified  Data.Map                    as Map

import            IHaskell.IPython.Types
import            IHaskell.IPython.ZeroMQ
import            IHaskell.IPython.Message.UUID       as UUID

stdinInterface :: MVar ZeroMQStdin
{-# NOINLINE stdinInterface #-}
stdinInterface = unsafePerformIO newEmptyMVar

-- | Manipulate standard input so that it is sourced from the IPython
-- frontend. This function is build on layers of deep magical hackery, so
-- be careful modifying it.
fixStdin :: String -> IO ()
fixStdin dir = do
  -- Initialize the stdin interface.
  profile <- read <$> readFile (dir ++ "/.kernel-profile")
  interface <- serveStdin profile
  putMVar stdinInterface interface
  void $ forkIO $ stdinOnce dir

stdinOnce :: String -> IO ()
stdinOnce dir = do
  -- Create a pipe using and turn it into handles.
  (readEnd, writeEnd) <- createPipe
  newStdin <- fdToHandle readEnd
  stdinInput <- fdToHandle writeEnd
  hSetBuffering newStdin NoBuffering
  hSetBuffering stdinInput NoBuffering

  -- Store old stdin and swap in new stdin.
  oldStdin <- hDuplicate stdin
  hDuplicateTo newStdin stdin

  loop stdinInput oldStdin newStdin
  where
    loop stdinInput oldStdin newStdin = do
      let FileHandle _ mvar = stdin
      threadDelay $ 150 * 1000
      empty <- isEmptyMVar mvar
      if not empty
      then loop stdinInput oldStdin newStdin
      else do
        line <- getInputLine dir
        hPutStr stdinInput $ line ++ "\n"
        loop stdinInput oldStdin newStdin

-- | Get a line of input from the IPython frontend.
getInputLine :: String -> IO String
getInputLine dir = do
  StdinChannel req rep <- readMVar stdinInterface

  -- Send a request for input.
  uuid <- UUID.random
  parentHeader <- read <$> readFile (dir ++ "/.last-req-header")
  let header = MessageHeader {
      username = username parentHeader,
      identifiers = identifiers parentHeader,
      parentHeader = Just parentHeader,
      messageId = uuid,
      sessionId = sessionId parentHeader,
      metadata = Map.fromList [],
      msgType = InputRequestMessage
    }
  let msg = RequestInput header ""
  writeChan req msg

  -- Get the reply.
  InputReply _ value <- readChan rep
  return value

recordParentHeader :: String -> MessageHeader -> IO ()
recordParentHeader dir header =
  writeFile (dir ++ "/.last-req-header") $ show header

recordKernelProfile :: String -> Profile -> IO ()
recordKernelProfile dir profile =
  writeFile (dir ++ "/.kernel-profile") $ show profile