{-# LANGUAGE LambdaCase     #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes     #-}
{-# LANGUAGE Safe           #-}
{-# LANGUAGE ViewPatterns   #-}

-- | Communication with SMT solvers or theorem provers.
--
-- A solver is a running process defined by a 'Backend'.
module Copilot.Theorem.Prover.SMTIO
  ( Solver
  , startNewSolver, stop
  , assume, entailed, declVars
  ) where

import Copilot.Theorem.IL
import Copilot.Theorem.Prover.Backend

import System.IO
import System.Process
import Control.Monad
import Control.Monad.Trans
import Control.Monad.Trans.Maybe
import Data.Maybe
import Data.Set ((\\), fromList, Set, union, empty, elems)

-- | A connection with a running SMT solver or theorem prover.
data Solver a = Solver
  { solverName :: String
  , inh        :: Handle
  , outh       :: Handle
  , process    :: ProcessHandle
  , debugMode  :: Bool
  , vars       :: Set VarDescr
  , model      :: Set Expr
  , backend    :: Backend a
  }

-- | Output a debugging message if debugging is enabled for the solver.
debug :: Bool -> Solver a -> String -> IO ()
debug printName s str = when (debugMode s) $
  putStrLn $ (if printName then "<" ++ solverName s ++ ">  " else "") ++ str

send :: Show a => Solver a -> a -> IO ()
send _ (show -> "") = return ()
send s (show -> a) = do
    hPutStr (inh s) $ a ++ "\n"
    debug True s a
    hFlush $ inh s

receive :: Solver a -> IO SatResult
receive s = fromJust <$> runMaybeT (msum $ repeat line)
  where
    line :: MaybeT IO SatResult
    line = do
      eof <- liftIO $ hIsEOF $ outh s
      if eof
        then liftIO (debug True s "[received: EOF]") >> return Unknown
        else do
          ln <- liftIO $ hGetLine $ outh s
          liftIO $ debug True s $ "[received: " ++ ln ++ "]"
          MaybeT $ return $ (interpret $ backend s) ln

-- | Create a new solver implemented by the backend specified.
--
-- The error handle from the backend handle is immediately closed/discarded,
-- and the logic initialized as specifiied by the backend options.
startNewSolver :: SmtFormat a => String -> Bool -> Backend a -> IO (Solver a)
startNewSolver name dbgMode b = do
  (i, o, e, p) <- runInteractiveProcess (cmd b) (cmdOpts b) Nothing Nothing
  hClose e
  let s = Solver name i o p dbgMode empty empty b
  send s $ setLogic $ logic b
  return s

-- | Stop a solver, closing all communication handles and terminating the
-- process.
stop :: Solver a -> IO ()
stop s = do
  hClose $ inh s
  hClose $ outh s
  terminateProcess $ process s

-- | Register the given expressions as assumptions or axioms with the solver.
assume :: SmtFormat a => Solver a -> [Expr] -> IO (Solver a)
assume s@(Solver { model }) cs = do
  let newAxioms = elems $ fromList cs \\ model
  assume' s newAxioms
  return s { model = model `union` fromList newAxioms }

assume' :: SmtFormat a => Solver a -> [Expr] -> IO ()
assume' s cs = forM_ cs (send s . assert . bsimpl)

-- | Check if a series of expressions are entailed by the axioms or assumptions
-- already registered with the solver.
entailed :: SmtFormat a => Solver a -> [Expr] -> IO SatResult
entailed s cs = do
  when (incremental $ backend s) $ send s push
  case cs of
      []  -> putStrLn "Warning: no proposition to prove." >> assume' s [ConstB True]
      _   -> assume' s [foldl1 (Op2 Bool Or) (map (Op1 Bool Not) cs)]
  send s checkSat
  (inputTerminator $ backend s) (inh s)

  when (incremental $ backend s) $ send s pop
  receive s

-- | Register the given variables with the solver.
declVars :: SmtFormat a => Solver a -> [VarDescr] -> IO (Solver a)
declVars s@(Solver { vars }) decls = do
  let newVars = elems $ fromList decls \\ vars
  forM_ newVars $ \(VarDescr {varName, varType, args}) ->
    send s $ declFun varName varType args
  return s { vars = vars `union` fromList newVars }
