-- | Using Isabelle to check theory files.
--
-- Requirements:
--
--   1. A working installation of Isabelle2009-1 (http://isabelle.in.tum.de/)
--   2. The \'isabelle\' command must be on the PATH.
--
module System.Isabelle (
  checkTheoryFile
) where

import Data.Maybe
import Data.List

import System.FilePath
import System.Directory
import System.Process
import System.IO

import Control.Concurrent
import Control.Monad
import Control.Exception

------------------------------------------------------------------------------
-- Utilities: TODO MOVE INTO GENERAL FILE
------------------------------------------------------------------------------

-- | Create and use a temporary file ensuring that it is closed if the
-- computation fails.
--
-- See 'openTempFile' for more information on the parameters.
withTempFile :: FilePath -> String -> ((FilePath, Handle) -> IO a) -> IO (a, FilePath)
withTempFile dir nameTemplate io = 
  bracket (openTempFile dir nameTemplate) (hClose . snd) 
          (\args@(file,_) -> do{ x <- io args; return (x, file) })

-- | Like 'withTempFile' buth without returning the results of the inner
-- computation.
withTempFile_ :: FilePath -> String -> ((FilePath, Handle) -> IO a) -> IO FilePath
withTempFile_ dir nameTemplate io = snd `liftM` withTempFile dir nameTemplate io

-- | Waits the given amount of microseconds before trying to terminate the
-- process using 'terminateProcess'.
waitForProcessTimeout :: Int -> ProcessHandle -> IO Bool
waitForProcessTimeout maxTime hProc
  | maxTime <= 0 = do _<- waitForProcess hProc; return False
  | otherwise    = do
      timeout <- newMVar False
      _ <- forkIO $ do
        threadDelay maxTime
        modifyMVar_ timeout (return . const True)
        terminateProcess hProc
      _ <- waitForProcess hProc
      readMVar timeout
    

------------------------------------------------------------------------------
-- Isabelle commands
------------------------------------------------------------------------------

-- | Use Isabelle to check the correctness of a theory file.
checkTheoryFile :: FilePath            -- ^ Path to 'isabelle' binary.
                -> Maybe Int           -- ^ Number of parallel thread to use while checking.
                -> Int                 -- ^ Maximal available time in micro-seconds
                -> String              -- ^ Logic Image to use
                -> FilePath            -- ^ Path to file to be checked
                -> IO (IO String, Maybe String)
                   -- ^ If the check went through, log-file content only, otherwise
                   -- also an error message.
checkTheoryFile isabelleTool threads maxTime logic thyFile = do
  let thyName     = takeBaseName thyFile
      rootName    = "ROOT-" ++ thyName ++ ".ML"
      sessionName = "scyther-proof-" ++ thyName
  -- create ROOT.ML file
  tmpDir <- getTemporaryDirectory
  rootFile <- withTempFile_ tmpDir rootName 
    (\(_, hRootFile) -> 
      hPutStrLn hRootFile $ "use_thy \"" ++ dropExtension thyFile ++ "\""
    )
  -- call "isabelle usedir"
  finally 
    (do let cmd = isabelleTool ++ 
                     " usedir -f " ++ rootFile ++ 
                     " -s " ++ sessionName ++ 
                     " -M " ++ maybe "0" show threads ++
                        " " ++ logic ++ " ."
        (_,   _,    hErr, hProc) <- runInteractiveCommand cmd
        isaOutVar <- newEmptyMVar
        _ <-forkIO $ redirect hErr isaOutVar
        timeout <- waitForProcessTimeout maxTime hProc
        isaOut <- takeMVar isaOutVar
        if timeout
          then do return (return $ isaOut ++ "Interrupted due to timeout!"
                         , Just $ "timeout: " ++ 
                              show ((fromIntegral maxTime / 10E6)::Double) ++ "s")
          else do return $ parseIsaOutput sessionName isaOut
    )
    (do removeFile rootFile)
  where
  -- | Redirect the contents of a the given handle into the message variable.
  redirect hIn mvar = hGetContents hIn >>= putMVar mvar

  -- | extract the log file name from the isabelle output
  logFileName str = fromMaybe "no log file" $ do
    let logIndicator = "(see also "
    line <- find (logIndicator `isPrefixOf`) (lines str) 
    init `liftM` stripPrefix logIndicator line
  
  -- | Extract the error message from the isabelle output
  errMsg str = case break ("***" `isPrefixOf`) (lines str) of
    (_,[]) -> str
    (_,first:ls) -> case break ("***" `isPrefixOf`) ls of
      (_,[]) -> unlines (first : ls)
      (ls',(fin:_)) -> unlines (first : ls'++[fin])

  -- | Parse the Isabelle output.
  parseIsaOutput sessionName str
    | ("Finished "++logic++"-"++sessionName) `isInfixOf` str = (return str, Nothing)
    | otherwise = (readFile (logFileName str), Just (errMsg str))