{-# LANGUAGE OverloadedStrings #-}

-- | Module that communicates with GHCid, splits the commands in the code block,
-- gather the results and emits the trasnformed JSON based Pandoc AST.


module CodeBlockExecutor
    (
      applyFilterToBlock,
      runCodeBlock,
      processResults
    ) where


--import GHC.Generics
import Text.Pandoc

import Language.Haskell.Ghcid
import Control.Applicative
import Control.Exception
import Data.String
import Data.List as L
import Data.Map.Strict as M
import Data.Maybe

import qualified Data.Text as T

removeAll:: T.Text -> T.Text -> T.Text
removeAll pat str = if (T.replace pat "" str) == str then str
  else removeAll pat (T.replace pat "" str)

-- | Determines if the command encountered is an `interactive` command.
-- This is one of the conditions that determines whether output needs to be captured.
isInteractive :: T.Text -> Bool
isInteractive cmd = T.isPrefixOf ">>" cmd

-- | This function takes care of placing back the newline characters
-- that may been stripped out before sending the commands to GHCid
updateSuffixForInteractiveCmd :: Data.String.IsString p => T.Text -> p
updateSuffixForInteractiveCmd cmd = if isInteractive cmd then
  if T.last cmd == '\n' then "" else "\n"
  else "\n\n"

-- | Intercalate the commands and respective results. Typically, only errors
-- encountered while running definitions, and outut of interactive commands (
-- i.e commands prefixed with `>>`) are captured. All empty strins are dropped.
intercalateCmdAndResults :: T.Text -> T.Text -> T.Text
intercalateCmdAndResults cmd result =
  T.concat [cmd, updateSuffixForInteractiveCmd cmd, result, trailResult result] where
  trailResult r = if r /= "" then "\n" else ""

-- | Post-processing function that interleaves command and results
processResults :: [T.Text] -- ^ List of commands that were executed
               -> [T.Text] -- ^ List of results for the executed commands
               -> String -- ^ New string that will form the bodyof the modified Code Block.
processResults cmds results =
  let cmd_result = getZipList $ intercalateCmdAndResults <$> ZipList cmds <*> ZipList results
  in
    (T.unpack . T.concat) $ cmd_result

-- | Apply the filter block only if the language attribute
-- is set to `haskell` and `code-filter` property is *not* set to "Off"
--
-- Example:
-- ``` {.haskell code-filter="Off"} ``` will turn off any kind of transformation.
--
-- By default the filer is "On"
applyFilterToBlock:: Block -- ^ The 'Block' yielded by toJSONFilter in "Text.Pandoc.JSON"
                  -> IO Block -- ^ The newly minted 'Block'
applyFilterToBlock c@(CodeBlock (_, classes, key_values) _) = let
  attrs = M.fromList key_values
  haskell_in_class = L.find (== "haskell") classes
  code_filter_flag = maybe "On" id (M.lookup ("code-filter") attrs)
  in
    if code_filter_flag == "On" && isJust haskell_in_class then runCodeBlock c
    else (return c)
applyFilterToBlock b = return b

-- | Run the commands in the 'Block' in one single GHCid session.
runCodeBlock:: Block -- ^ 'Block' to execute. Only 'CodeBlock' is executed.
            -> IO Block -- ^ transformed code block
runCodeBlock (CodeBlock attr str) = bracket startGhciProcess' stopGhci runCommands
  where
    startGhciProcess' = do
      (ghci_handle, _) <- startGhci "stack ghci" (Just ".") (\_ _ -> return ())
      return ghci_handle
    runCommands g = do
      let cmds = L.filter (\s -> s /= "") $ T.splitOn "\n\n" $ T.pack str
      results <- mapM (runCmd g) cmds
      let results''' = processResults cmds results
      return (CodeBlock attr results''')
runCodeBlock b = return b

-- | Executes a command in GHCIid.
runCmd :: Ghci -- ^ Handle to the GHCi process through the GHCid interface
       -> T.Text -- ^ Statement to execute
       -> IO T.Text -- ^ Result of the executed statement
runCmd g cmd = do
  let executeStatement = exec g
      cmd_ = T.concat [":{\n", T.replace ">>" "" cmd, "\n:}\n"]
  result <- executeStatement . T.unpack $ cmd_
  -- we send this PROBE here since GHCi has its own mind on how it prefixes output based on its native needs. By sending the probe we can guess what is the latest prompt and then discard it while processing thye output.
  probe <- exec g ":{\nshow (\"PANDOC_FILTER_PROBE_PROMPT_INTERNAL\"::String)\n:}\n"
  let current_prompt = preparePrompt probe
        where
          preparePrompt probe' =
            let prompt = T.replace " \"\\\"PANDOC_FILTER_PROBE_PROMPT_INTERNAL\\\"\"\n" "" (T.pack . unlines $ probe')
            in
              T.concat [T.takeWhile (/='|') prompt, "|"]
  --putStrLn $ show . unlines $ probe
  --putStrLn $ show current_prompt
      result' = T.stripStart $ removeAll current_prompt (T.pack . unlines $ result)
  --putStrLn $ show result'
  return $ result'