{-# LANGUAGE CPP, ScopedTypeVariables, FlexibleInstances, BangPatterns #-}
-- | GF interactive mode
module GF.Interactive (mainGFI,mainRunGFI,mainServerGFI) where

import Prelude hiding (putStrLn,print)
import qualified Prelude as P(putStrLn)
import GF.Command.Interpreter(CommandEnv(..),mkCommandEnv,interpretCommandLine)
import GF.Command.Commands(HasPGF(..),pgfCommands)
import GF.Command.CommonCommands(commonCommands,extend)
import GF.Command.SourceCommands
import GF.Command.CommandInfo
import GF.Command.Help(helpCommand)
import GF.Command.Abstract
import GF.Command.Parse(readCommandLine,pCommand,readTransactionCommand)
import GF.Compile.Rename(renameSourceTerm)
import GF.Compile.TypeCheck(inferLType)
import GF.Compile.Compute(stdPredef,normalForm,Globals(..))
import GF.Compile.GeneratePMCFG(pmcfgForm,type2fields)
import GF.Data.Operations (Err(..))
import GF.Data.Utilities(whenM,repeatM)
import GF.Grammar hiding (Ident,isPrefixOf)
import GF.Grammar.Lookup(lookupResDef)
import GF.Infra.UseIO(ioErrorText,putStrLnE)
import GF.Infra.SIO
import GF.Infra.Option
import GF.Infra.CheckM
import qualified System.Console.Haskeline as Haskeline

import PGF2
import PGF2.Transactions hiding (modifyPGF,checkoutPGF,
                                 startTransaction,
                                 commitTransaction,rollbackTransaction,
                                 inTransaction)

import Data.Char
import Data.List(isPrefixOf,sortOn)
import qualified Data.Map as Map
import qualified Data.Sequence as Seq
import qualified Text.ParserCombinators.ReadP as RP
import System.Directory(getAppUserDataDirectory)
import Control.Exception(SomeException,fromException,evaluate,try)
import Control.Monad ((<=<),when,mplus,join)
import Control.Monad.State hiding (void)
import qualified GF.System.Signal as IO(runInterruptibly)
import GF.Command.Messages(welcome)
#ifdef SERVER_MODE
import GF.Server(server)
#endif

type ReadNGF = FilePath -> IO PGF

-- | Run the GF Shell in quiet mode (@gf -run@).
mainRunGFI :: Options -> [FilePath] -> IO ()
mainRunGFI opts files = shell (beQuiet opts) files

beQuiet = addOptions (modifyFlags (\f -> f{optVerbosity=Quiet}))

-- | Run the interactive GF Shell
mainGFI :: Options -> [FilePath] -> IO ()
mainGFI opts files = do
  P.putStrLn welcome
  shell opts files

shell opts files =
  flip evalStateT (emptyGFEnv opts) $
  do mapStateT runSIO $ importInEnv readNGF opts files
     modify $ \ gfenv0 -> gfenv0 {history = [unwords ("i":files)]}
     repeatM (mapStateT runSIO . execute1 readNGF =<< readCommand)


#ifdef SERVER_MODE
-- | Run the GF Server (@gf -server@).
-- The 'Int' argument is the port number for the HTTP service.
mainServerGFI opts0 port files =
  server jobs port root init execute
  where
    root = flag optDocumentRoot opts
    opts = beQuiet opts0
    jobs = join (flag optJobs opts)

    init readNGF = do
      (_, gfenv) <- runSIO (runStateT (importInEnv readNGF opts files) (emptyGFEnv opts))
      return gfenv

    execute readNGF gfenv0 cmd = do
      (continue,gfenv) <- runStateT (execute1 readNGF cmd) gfenv0
      return $ if continue then Just gfenv else Nothing

#else
mainServerGFI opts port files =
  fail "GF has not been compiled with server mode support"
#endif

-- | Read a command
readCommand :: StateT GFEnv IO String
readCommand =
  do opts <- gets startOpts
     case flag optMode opts of
       ModeRun -> lift tryGetLine
       _       -> do gfenv <- get
                     s <- lift (fetchCommand gfenv)
                     put $ gfenv {history = s : history gfenv}
                     return s

timeIt act =
  do t1 <- liftSIO $ getCPUTime
     a <-  act
     t2 <- liftSIO $ getCPUTime
     return (t2-t1,a)

-- | Optionally show how much CPU time was used to run an IO action
optionallyShowCPUTime :: (Monad m,MonadSIO m) => Options -> m a -> m a
optionallyShowCPUTime opts act
  | not (verbAtLeast opts Normal) = act
  | otherwise = do (dt,r) <- timeIt act
                   liftSIO $ putStrLnFlush $ show (dt `div` 1000000000) ++ " msec"
                   return r


type ShellM = StateT GFEnv SIO

-- | Execute a given command line, returning 'True' to continue execution,
-- | 'False' when it is time to quit
execute1 :: ReadNGF -> String -> ShellM Bool
execute1 readNGF s0 =
  do opts <- gets startOpts
     interruptible $ optionallyShowCPUTime opts $
       case pwords s0 of
      -- cc, sd, so, ss and dg are now in GF.Commands.SourceCommands
      -- special commands
         "q" :_   -> quit
         "!" :ws  -> system_command ws
         "eh":ws  -> execute_history ws
         "i" :ws  -> do import_ readNGF ws; continue
         "r" :_   -> do gfenv0 <- get
                        let imports = [(s,ws) | s <- history gfenv0, ("i":ws) <- [pwords s]]
                        case imports of
                          (s,ws):_ -> do
                            putStrLnE $ "repeating latest import: " ++ s
                            import_ readNGF ws
                            continue
                          _ -> do putStrLnE $ "no import in history"
                                  continue
         (w  :ws) | elem w ["c","a","d"] -> do
                        case readTransactionCommand s0 of
                          Just cmd -> do checkout
                                         env <- gets pgfenv
                                         case env of
                                           (_,Just pgf,mb_txnid) -> transactionCommand cmd pgf mb_txnid
                                           _                     -> fail "Import a grammar before using this command"
                          Nothing  -> putStrLnE $ "command not parsed: "++s0
                        continue
                  | w == "t" -> do
                        env <- gets pgfenv
                        case env of
                          (gr,Just pgf,mb_txnid) ->
                              case ws of
                                ["start"]     ->
                                    case mb_txnid of
                                      Just _  -> fail "You have already started a transaction"
                                      Nothing -> do txnid <- lift $ startTransaction pgf
                                                    modify (\gfenv -> gfenv{pgfenv=(gr,Just pgf,Just txnid)})
                                ["commit"]    ->
                                    case mb_txnid of
                                      Just id -> do lift $ commitTransaction id
                                                    modify (\gfenv -> gfenv{pgfenv=(gr,Just pgf,Nothing)})
                                      Nothing -> fail "There is no active transaction"
                                ["rollback"]  ->
                                    case mb_txnid of
                                      Just id -> do lift $ rollbackTransaction id
                                                    modify (\gfenv -> gfenv{pgfenv=(gr,Just pgf,Nothing)})
                                      Nothing -> fail "There is no active transaction"
                                []            -> fail "The transaction command expects start, commit or rollback as an argument"
                                _             -> fail "The only arguments to the transaction command are start, commit and rollback"
                          _                   -> fail "Import a grammar before using this command"
                        continue
                                           
      -- other special commands, working on GFEnv
         "dc":ws  -> define_command ws
         "dt":ws  -> define_tree ws
      -- ordinary commands
         _        -> do env <- gets commandenv
                        checkout
                        interpretCommandLine env s0
                        continue
  where
    continue,stop :: ShellM Bool
    continue = return True
    stop = return False

    checkout = do
      gfenv <- get
      case pgfenv gfenv of
        (gr,Just pgf,Nothing) -> do pgf <- lift $ checkoutPGF pgf
                                    put (gfenv{pgfenv = (gr,Just pgf,Nothing)})
        _                     -> return ()

    interruptible :: ShellM Bool -> ShellM Bool
    interruptible act =
      do gfenv <- get
         mapStateT (
           either (\e -> printException e >> return (True,gfenv)) return
             <=< runInterruptibly) act

  -- Special commands:

    quit = do
      env <- gets pgfenv
      case env of
        (_,_,Just _) -> fail "Commit or rollback the transaction first!"
        _            -> do opts <- gets startOpts
                           when (verbAtLeast opts Normal) $ putStrLnE "See you."
                           stop

    system_command ws = do lift $ restrictedSystem $ unwords ws ; continue

    execute_history [w] =
      do execute . lines =<< lift (restricted (readFile w))
         continue
      where
        execute []           = return ()
        execute (line:lines) = whenM (execute1 readNGF line) (execute lines)

    execute_history _   =
       do putStrLnE "eh command not parsed"
          continue

    define_command (f:ws) =
        case readCommandLine (unwords ws) of
           Just comm ->
             do modify $
                  \ gfenv ->
                    let env = commandenv gfenv
                    in gfenv {
                         commandenv = env {
                           commandmacros = Map.insert f comm (commandmacros env)
                         }
                       }
                continue
           _ -> dc_not_parsed
    define_command _ = dc_not_parsed

    dc_not_parsed = putStrLnE "command definition not parsed" >> continue

    define_tree (f:ws) =
        case readExpr (unwords ws) of
          Just exp ->
           do modify $
                \ gfenv ->
                  let env = commandenv gfenv
                  in gfenv { commandenv = env {
                               expmacros = Map.insert f exp (expmacros env) } }
              continue
          _ -> dt_not_parsed
    define_tree _ = dt_not_parsed

    dt_not_parsed = putStrLnE "value definition not parsed" >> continue

pwords s = case words s of
             w:ws -> getCommandOp w :ws
             ws -> ws

import_ readNGF args =
  do case parseOptions args of
       Ok (opts',files) -> do
         !opts <- gets startOpts    -- use a bang to avoid retaining a reference to the old state,
                                    -- otherwise we leak references to PGF revisions.
         curr_dir <- lift getCurrentDirectory
         lib_dir  <- lift $ getLibraryDirectory (addOptions opts opts')
         importInEnv readNGF (addOptions opts (fixRelativeLibPaths curr_dir lib_dir opts')) files
       Bad err -> putStrLnE $ "Command parse error: " ++ err

transactionCommand :: TransactionCommand -> PGF -> Maybe TxnID -> ShellM ()
transactionCommand (CreateFun opts f ty) pgf mb_txnid = do
  let prob = realToFrac (valFltOpts "prob" (1/0) opts)
  case checkType pgf ty of
    Left msg -> putStrLnE msg
    Right ty -> do lift $ updatePGF pgf mb_txnid (createFunction f ty 0 [] prob >> return ())
                   return ()
transactionCommand (CreateCat opts c ctxt) pgf mb_txnid = do
  let prob = realToFrac (valFltOpts "prob" (1/0) opts)
  case checkContext pgf ctxt of
    Left msg -> putStrLnE msg
    Right ty -> do lift $ updatePGF pgf mb_txnid (createCategory c ctxt prob)
                   return ()
transactionCommand (CreateConcrete opts name) pgf mb_txnid = do
  lift $ updatePGF pgf mb_txnid (createConcrete name (return ()))
  return ()
transactionCommand (CreateLin opts f mb_t is_alter) pgf mb_txnid = do
  sgr <- getGrammar
  mo <- case greatestResource sgr of
          Nothing -> fail "No source grammar in scope"
          Just mo -> return mo
  lang <- optLang pgf opts
  lift $ updatePGF pgf mb_txnid $ do
    mb_ty <- getFunctionType f
    case mb_ty of
      Just ty@(DTyp _ cat _) -> 
         alterConcrete lang $ do
           mb_fields <- getCategoryFields cat
           case mb_fields of
             Just fields -> case runCheck (compileLinTerm sgr mo f mb_t (type2term mo ty)) of
                              Ok ((rules,fields'),_)
                                   | fields == fields' -> do
                                       (if is_alter then alterLin else createLin) f rules
                                       return ()
                                   | otherwise -> fail "The linearization categories in the resource and the compiled grammar does not match"
                              Bad msg      -> fail msg
             Nothing     -> fail ("Category "++cat++" is not in the concrete syntax")
      _ -> fail ("Function "++f++" is not in the abstract syntax")
  where
    type2term mo (DTyp hypos cat _) =
      foldr (\(b,x,ty1) ty2 -> Prod b (identS x) (type2term mo ty1) ty2)
            (Vr (identS cat))
            hypos

    compileLinTerm sgr mo f mb_t ty = do
      let g = Gl sgr (stdPredef g) False
      (t,ty) <- case mb_t of
                  Just t  -> do t <- renameSourceTerm sgr mo (Typed t ty)
                                
                                (t,ty) <- inferLType g t
                                return (t,ty)
                  Nothing -> case lookupResDef sgr (mo,identS f) of
                               Ok t    -> do ty <- renameSourceTerm sgr mo ty
                                             ty <- normalForm g ty
                                             return (t,ty)
                               Bad msg -> fail msg
      let (ctxt,res_ty) = typeFormCnc ty
      rules <- pmcfgForm g t ctxt res_ty
      return (rules,type2fields sgr res_ty)

transactionCommand (CreateLincat opts c mb_t) pgf mb_txnid = do
  sgr <- getGrammar
  mo <- case greatestResource sgr of
          Nothing -> fail "No source grammar in scope"
          Just mo -> return mo
  lang <- optLang pgf opts
  case runCheck (compileLincatTerm sgr mo mb_t) of
    Ok (fields,_)-> do lift $ updatePGF pgf mb_txnid (alterConcrete lang (createLincat c fields [] [] >> return ()))
                       return ()
    Bad msg      -> fail msg
  where
    compileLincatTerm sgr mo mb_t = do
      t <- case mb_t of
             Just t  -> do t <- renameSourceTerm sgr mo t
                           let g = Gl sgr (stdPredef g) False
                           (t,_) <- inferLType g t
                           return t
             Nothing -> case lookupResDef sgr (mo,identS c) of
                          Ok t    -> return t
                          Bad msg -> fail msg
      return (type2fields sgr t)
transactionCommand (DropFun opts f) pgf mb_txnid = do
  lift $ updatePGF pgf mb_txnid (dropFunction f)
  return ()
transactionCommand (DropCat opts c) pgf mb_txnid = do
  lift $ updatePGF pgf mb_txnid (dropCategory c)
  return ()
transactionCommand (DropConcrete opts name) pgf mb_txnid = do
  lift $ updatePGF pgf mb_txnid (dropConcrete name)
  return ()
transactionCommand (DropLin opts f) pgf mb_txnid = do
  lang <- optLang pgf opts
  lift $ updatePGF pgf mb_txnid (alterConcrete lang (dropLin f))
  return ()
transactionCommand (DropLincat opts c) pgf mb_txnid = do
  lang <- optLang pgf opts
  lift $ updatePGF pgf mb_txnid (alterConcrete lang (dropLincat c))
  return ()

updatePGF pgf mb_txnid f = do
  maybe (modifyPGF pgf f >> return ())
                                 (\txnid -> inTransaction txnid f)
                                 mb_txnid

optLang pgf opts =
  case Map.keys langs of
    [lang] -> completeLang (valStrOpts "lang" lang opts)
    _      -> case valStrOpts "lang" "" opts of
                ""   -> fail "Specify a language to change"
                lang -> completeLang lang
  where
    langs = languages pgf

    completeLang la
      | Map.member la  langs = return la
      | Map.member la' langs = return la'
      | otherwise            = fail "Unknown language"
      where
        la' = abstractName pgf ++ la


-- | Commands that work on 'GFEnv'
moreCommands = [
  ("e",  emptyCommandInfo {
     longname = "empty",
     synopsis = "empty the environment (except the command history)",
     exec = \ _ _ ->
            do modify $ \ gfenv -> (emptyGFEnv (startOpts gfenv))
                                     { history=history gfenv }
               opts <- gets startOpts
               importInEnv readNGF opts []
               return void
     }),
  ("ph", emptyCommandInfo {
     longname = "print_history",
     synopsis = "print command history",
     explanation = unlines [
       "Prints the commands issued during the GF session.",
       "The result is readable by the eh command.",
       "The result can be used as a script when starting GF."
       ],
     examples = [
      mkEx "ph | wf -file=foo.gfs  -- save the history into a file"
      ],
     exec = \ _ _ ->
            fmap (fromString . unlines . reverse . drop 1 . history) get
     }),
  ("r",  emptyCommandInfo {
     longname = "reload",
     synopsis = "repeat the latest import command"
     })
  ]


printException e = maybe (print e) (putStrLn . ioErrorText) (fromException e)

fetchCommand :: GFEnv -> IO String
fetchCommand gfenv = do
  path <- getAppUserDataDirectory "gf_history"
  let settings =
        Haskeline.Settings {
          Haskeline.complete = wordCompletion gfenv,
          Haskeline.historyFile = Just path,
          Haskeline.autoAddHistory = True
        }
  res <- IO.runInterruptibly $ Haskeline.runInputT settings (Haskeline.getInputLine (prompt gfenv))
  case res of
    Left  _        -> return ""
    Right Nothing  -> return "q"
    Right (Just s) -> return s

importInEnv :: ReadNGF -> Options -> [FilePath] -> ShellM ()
importInEnv readNGF opts files =
  do (_,pgf0,mb_txnid) <- gets pgfenv
     case (flag optRetainResource opts,mb_txnid) of
       (RetainAll,Nothing)      -> do src <- lift $ importSource opts Nothing files
                                      pgf <- lift $ link opts pgf0 src
                                      modify $ \gfenv -> gfenv{pgfenv = (snd src,Just pgf,Nothing)}
       (RetainSource,mb_txn)    -> do src <- lift $ importSource opts pgf0 files
                                      modify $ \gfenv -> gfenv{pgfenv = (snd src,pgf0,mb_txn)}
       (RetainCompiled,Nothing) -> do pgf <- lift $ importPGF pgf0
                                      src <- lift $ importSource opts Nothing ["prelude/Predef.gfo"]
                                      modify $ \gfenv -> gfenv{pgfenv = (snd src,pgf,Nothing)}
       _ -> fail "You must commit/rollback the transaction before loading a new grammar"
  where
    importPGF pgf0 =
      do let opts' = addOptions (setOptimization OptCSE False) opts
         pgf1 <- importGrammar readNGF pgf0 opts' files
         if (verbAtLeast opts Normal)
           then case pgf1 of
                  Just pgf -> putStrLnFlush $ unwords $ "\nLanguages:" : Map.keys (languages pgf)
                  Nothing  -> return ()
           else return ()
         return pgf1

tryGetLine = do
  res <- try getLine
  case res of
   Left (e :: SomeException) -> return "q"
   Right l -> return l

prompt env = 
  case pgfenv env of
    (_,mb_pgf,mb_tr) -> 
         maybe "" abstractName mb_pgf ++ 
         maybe "" (const " (transaction)") mb_tr ++
         "> "

type CmdEnv = (Grammar,Maybe PGF,Maybe TxnID)

data GFEnv = GFEnv {
    startOpts :: Options,
    pgfenv :: CmdEnv,
    commandenv :: CommandEnv ShellM,
    history    :: [String]
  }

emptyGFEnv opts = GFEnv opts emptyCmdEnv emptyCommandEnv []

emptyCmdEnv = (emptyGrammar,Nothing,Nothing)

emptyCommandEnv = mkCommandEnv allCommands

allCommands =
  extend pgfCommands (helpCommand allCommands:moreCommands)
  `Map.union` sourceCommands
  `Map.union` commonCommands

instance HasGrammar ShellM where
  getGrammar = gets $ \gfenv ->
    case pgfenv gfenv of
      (gr,_,_) -> gr

instance HasPGF ShellM where
  getPGF = gets $ \gfenv ->
    case pgfenv gfenv of
      (_,mb_pgf,_) -> mb_pgf

wordCompletion gfenv (left,right) = do
  case wc_type (reverse left) of
    CmplCmd pref
      -> ret (length pref) [Haskeline.simpleCompletion name | name <- Map.keys (commands cmdEnv), isPrefixOf pref name]
    CmplStr (Just (Command _ opts _)) s0
      -> case pgfenv gfenv of
           (_,Just pgf,_) ->
                       let langs = languages pgf
                           optLang opts = case valStrOpts "lang" "" opts of
                                            ""   -> case Map.minView langs of
                                                      Nothing        -> Nothing
                                                      Just (concr,_) -> Just concr
                                            lang -> mplus (Map.lookup lang                       langs)
                                                          (Map.lookup (abstractName pgf ++ lang) langs)
                           optType opts = let readOpt str = case readType str of
                                                              Just ty -> case checkType pgf ty of
                                                                           Left _   -> Nothing
                                                                           Right ty -> Just ty
                                                              Nothing -> Nothing
                                          in maybeStrOpts "cat" (Just (startCat pgf)) readOpt opts
                           (rprefix,rs) = break isSpace (reverse s0)
                           s            = reverse rs
                           prefix       = reverse rprefix
                       in case (optLang opts, optType opts) of
                            (Just lang,Just cat) -> let compls = [t | ParseOk res <- [complete lang cat s prefix], (t,_,_,_) <- res]
                                                    in ret (length prefix) (map Haskeline.simpleCompletion compls)
                            _                    -> ret 0 []
           _        -> ret 0 []
    CmplOpt (Just (Command n _ _)) pref
      -> case Map.lookup n (commands cmdEnv) of
           Just inf -> do let flg_compls = [Haskeline.Completion ('-':flg++"=") ('-':flg) False | (flg,_) <- flags   inf, isPrefixOf pref flg]
                              opt_compls = [Haskeline.Completion ('-':opt)      ('-':opt) True | (opt,_) <- options inf, isPrefixOf pref opt]
                          ret (length pref+1)
                              (flg_compls++opt_compls)
           Nothing  -> ret (length pref) []
    CmplIdent (Just (Command "i" _ _)) _        -- HACK: file name completion for command i
      -> Haskeline.completeFilename (left,right)
    CmplIdent _ pref
      -> case pgfenv gfenv of
           (_,Just pgf,_) -> ret (length pref) [Haskeline.simpleCompletion name | name <- functionsByPrefix pgf pref]
           _              -> ret (length pref) []
    _ -> ret 0 []
  where
    cmdEnv = commandenv gfenv

    loop ps []     = Just ps
    loop ps (t:ts) = case error "nextState ps (simpleParseInput t)" of
                       Left  es -> Nothing
                       Right ps -> loop ps ts

    ret len xs  = return (drop len left,xs)


data CompletionType
  = CmplCmd                   Ident
  | CmplStr   (Maybe Command) String
  | CmplOpt   (Maybe Command) Ident
  | CmplIdent (Maybe Command) Ident
  deriving Show

wc_type :: String -> CompletionType
wc_type = cmd_name
  where
    cmd_name cs =
      let cs1 = dropWhile isSpace cs
      in go cs1 cs1
      where
        go x []       = CmplCmd x
        go x (c:cs)
          | isIdent c = go x cs
          | otherwise = cmd x cs

    cmd x []       = ret CmplIdent x "" 0
    cmd _ ('|':cs) = cmd_name cs
    cmd _ (';':cs) = cmd_name cs
    cmd x ('"':cs) = str x cs cs
    cmd x ('-':cs) = option x cs cs
    cmd x (c  :cs)
      | isIdent c  = ident x (c:cs) cs
      | otherwise  = cmd x cs

    option x y []       = ret CmplOpt x y 1
    option x y ('=':cs) = optValue x y cs
    option x y (c  :cs)
      | isIdent c       = option x y cs
      | otherwise       = cmd x cs

    optValue x y ('"':cs) = str x y cs
    optValue x y cs       = cmd x cs

    ident x y []     = ret CmplIdent x y 0
    ident x y (c:cs)
      | isIdent c    = ident x y cs
      | otherwise    = cmd x cs

    str x y []          = ret CmplStr x y 1
    str x y ('\"':cs)   = cmd x cs
    str x y ('\\':c:cs) = str x y cs
    str x y (c:cs)      = str x y cs

    ret f x y d = f cmd y
      where
        x1 = take (length x - length y - d) x
        x2 = takeWhile (\c -> isIdent c || isSpace c || c == '-' || c == '=' || c == '"') x1

        cmd = case [x | (x,cs) <- RP.readP_to_S pCommand x2, all isSpace cs] of
                [x] -> Just x
                _   -> Nothing

    isIdent c = c == '_' || c == '\'' || isAlphaNum c
