{-# LANGUAGE LambdaCase #-}

module Haskell.Debug.Adapter.GHCi where

import Control.Monad.IO.Class
import Control.Lens
import qualified Data.ByteString as B
import Control.Concurrent.MVar
import Control.Monad.State.Lazy
import Control.Monad.Except
import GHC.IO.Encoding
import Distribution.System
import qualified System.Process as S
import qualified System.IO as S
import qualified System.Environment as S
import qualified Data.Map as M
import qualified Data.List as L
import qualified Data.String.Utils as U

import Haskell.Debug.Adapter.Type
import qualified Haskell.Debug.Adapter.Utility as U
import Haskell.Debug.Adapter.Constant

---------------------------------------------------------------------------------
-- |
--  run ghci.
--
startGHCi :: String
          -> [String]
          -> FilePath
          -> M.Map String String
          -> AppContext ()
startGHCi cmd opts cwd envs =
  U.liftIOE (startGHCiIO cmd opts cwd envs) >>= liftEither >>= updateGHCi

  where
    updateGHCi proc = do
      mvar <- view ghciProcAppStores <$> get
      --_ <- liftIO $ takeMVar mvar
      liftIO $ putMVar mvar proc


-- |
--
startGHCiIO :: String
            -> [String]
            -> FilePath
            -> M.Map String String
            -> IO (Either ErrMsg GHCiProc)
startGHCiIO cmd opts cwd envs = do

  (fromPhoityneHandle, toGHCiHandle) <- S.createPipe
  (fromGHCiHandle, toPhoityneHandle) <- S.createPipe

  osEnc <- getReadHandleEncoding

  let bufMode = S.NoBuffering
  --let bufMode = S.BlockBuffering $ Just 1024

  S.hSetBuffering toPhoityneHandle bufMode
  S.hSetEncoding toPhoityneHandle osEnc
  S.hSetNewlineMode toPhoityneHandle $ S.NewlineMode S.CRLF S.LF
  --S.hSetBinaryMode toPhoityneHandle True

  S.hSetBuffering fromPhoityneHandle bufMode
  S.hSetEncoding fromPhoityneHandle  S.utf8
  S.hSetNewlineMode fromPhoityneHandle $ S.NewlineMode S.LF S.LF
  --S.hSetBinaryMode fromPhoityneHandle True

  S.hSetBuffering toGHCiHandle bufMode
  S.hSetEncoding toGHCiHandle S.utf8
  S.hSetNewlineMode toGHCiHandle $ S.NewlineMode S.LF S.LF
  --S.hSetBinaryMode toGHCiHandle True

  S.hSetBuffering fromGHCiHandle bufMode
  S.hSetEncoding fromGHCiHandle osEnc
  S.hSetNewlineMode fromGHCiHandle $ S.NewlineMode S.CRLF S.LF
  --S.hSetBinaryMode fromGHCiHandle True

  runEnvs <- getRunEnv

  ghciGHCi <- S.runProcess cmd opts (Just cwd) runEnvs (Just fromPhoityneHandle) (Just toPhoityneHandle) (Just toPhoityneHandle)

  return . Right $ GHCiProc toGHCiHandle fromGHCiHandle fromGHCiHandle ghciGHCi

  where
    -- |
    -- 
    getReadHandleEncoding :: IO TextEncoding
    getReadHandleEncoding = if
      | Windows == buildOS -> mkTextEncoding "CP932//TRANSLIT"
      | otherwise -> mkTextEncoding "UTF-8//TRANSLIT"

    -- |
    -- 
    getRunEnv
      | null envs = return Nothing
      | otherwise = do
          curEnvs <- S.getEnvironment
          return $ Just $ M.toList envs ++ curEnvs


-- |
--
type ExpectCallBack = Bool -> [String] -> [String] -> AppContext ()

-- |
--   expect prompt or eof
-- 
expectEOF :: ExpectCallBack -> AppContext [String]
expectEOF func = expectH' True func

-- |
--   expect prompt. eof throwError.
--
expectH :: ExpectCallBack -> AppContext [String]
expectH func = expectH' False func

-- |
--
expectH' :: Bool -> ExpectCallBack -> AppContext [String]
expectH' tilEOF func = do
  pmpt <- view ghciPmptAppStores <$> get
  mvar <- view ghciProcAppStores <$> get
  proc <- liftIO $ readMVar mvar
  let hdl = proc^.rHdlGHCiProc
      plen = length pmpt

  go tilEOF plen hdl []

  where
    go False plen hdl acc = U.readLine hdl >>= go' plen hdl acc
    go True plen hdl acc = liftIO (S.hIsEOF hdl) >>= \case
      False -> U.readLine hdl >>= go' plen hdl acc
      True  -> return acc

    go' plen hdl acc b = do
      let newL = U.rstrip b
      if L.isSuffixOf _DAP_CMD_END2 newL
        then goEnd plen hdl acc
        else cont plen hdl acc newL

    cont plen hdl acc newL = do
      let newAcc = acc ++ [newL]
      func False newAcc [newL]
      go tilEOF plen hdl newAcc

    goEnd plen hdl acc = do
      b <- liftIO $ B.hGet hdl plen
      let l = U.bs2str b
          newAcc = acc ++ [l]

      func True newAcc [l]
      return newAcc


-- |
--
expect :: String -> ExpectCallBack -> AppContext ()
expect key func = do
  mvar <- view ghciProcAppStores <$> get
  proc <- liftIO $ readMVar mvar
  let hdl = proc^.rHdlGHCiProc

  xs <- go key hdl ""
  let strs = map U.rstrip $ lines xs

  func True strs strs

  where
    go kb hdl acc = U.readChar hdl
      >>= go' kb hdl acc

    go' kb hdl acc b = do
      let newAcc = acc ++ b
      if L.isSuffixOf kb newAcc
        then return newAcc
        else go kb hdl newAcc


-- |
--  write to ghci.
--
command :: String -> AppContext ()
command cmd = do
  mver <- view ghciProcAppStores <$> get
  proc <- liftIO $ readMVar mver
  let hdl = proc^.wHdLGHCiProc

  U.liftIOE (Right <$> S.hPutStrLn hdl cmd) >>= liftEither


-- |
--  write to ghci.
--
stdoutCallBk :: Bool -> [String] -> [String] -> AppContext ()
stdoutCallBk _ _ ([]) = return ()
stdoutCallBk True  _ (x:[]) = U.sendStdoutEvent x
stdoutCallBk False _ (x:[]) = U.sendStdoutEventLF x
stdoutCallBk True  _ xs = do
  mapM_ U.sendStdoutEventLF $ init xs
  U.sendStdoutEvent $ last xs
stdoutCallBk False _ xs = mapM_ U.sendStdoutEventLF xs


-- |
--
cmdAndOut :: String -> AppContext ()
cmdAndOut cmd = do
  pout cmd
  command cmd
  where
    pout s
      | L.isPrefixOf ":dap-" s = U.sendStdoutEventLF $ (takeWhile ((/=) ' ') s) ++ " ..."
      | otherwise = U.sendStdoutEventLF s


-- |
--  write to ghci.
--
funcCallBk :: (Bool -> String -> AppContext ()) -> Bool -> [String] -> [String] -> AppContext ()
funcCallBk _ _ _ ([]) = return ()
funcCallBk f b _ (x:[]) = f b x
funcCallBk f True  _ xs = do
  mapM_ (f False) $ init xs
  f True $ last xs
funcCallBk f False _ xs = mapM_ (f False) xs