-----------------------------------------------------------------------------
-- |
-- Module      :  ForSyDe.Deep.Backend.VHDL.Modelsim
-- Copyright   :  (c) ES Group, KTH/ICT/ES 2007-2013
-- License     :  BSD-style (see the file LICENSE)
-- 
-- Maintainer  :  forsyde-dev@ict.kth.se
-- Stability   :  experimental
-- Portability :  portable
--
-- Functions to process the VHDL compilation results with Modelsim.
-----------------------------------------------------------------------------
module ForSyDe.Deep.Backend.VHDL.Modelsim (compileResultsModelsim,
                                      executeTestBenchModelsim) where

import ForSyDe.Deep.Backend.VHDL.Traverse.VHDLM
import ForSyDe.Deep.Backend.VHDL.TestBench

import ForSyDe.Deep.System.SysDef
import ForSyDe.Deep.OSharing
import ForSyDe.Deep.ForSyDeErr
import ForSyDe.Deep.Config (getDataDir)

import Data.List (intersperse)
import System.Directory (setCurrentDirectory)
import Control.Monad (liftM, when)
import Control.Monad.State (gets)
import System.Directory (findExecutable, getTemporaryDirectory)
import System.Process (runProcess, waitForProcess)
import System.Exit (ExitCode(..))
import System.IO 
import System.FilePath ((</>))
import qualified Language.Haskell.TH as TH (Exp)

-- | Generate a testbench and execute it with Modelsim
--   (Note: the initial and final CWD will be / )
executeTestBenchModelsim :: Maybe Int -- ^ Number of cycles to simulate          
                         -> [[TH.Exp]] -- ^ input stimuli, each signal value
                                       --   is expressed as a template haskell
                                       --   expression 
                         -> VHDLM [[String]] -- ^ results, each signal value
                                             --   is expressed as a string
executeTestBenchModelsim mCycles stimuli= do
  cycles <- writeVHDLTestBench mCycles stimuli
  sysid <- gets (sid.globalSysDef.global)
  -- change to sysid/vhdl/
  liftIO $ setCurrentDirectory (sysid </> "vhdl")
  -- compile the testbench with modelsim
  run_vcom ["-93", "-quiet", "-nologo", "-work", "work", 
            "test" </> (sysid ++ "_tb.vhd")]           
  -- execute the testbench and capture the results
  tmpdir <- liftIO getTemporaryDirectory 
  (file, handle) <- liftIO $ openTempFile tmpdir "tb_out.txt"
  -- we close the temporal file to avoid opening problems with vsim on windows
  liftIO $ hClose handle 
  run_vsim ["-c", "-std_output", file, "-quiet", 
            "-do", "run " ++ show (cycles*10) ++ " ns; exit",
            "work." ++ sysid ++ "_tb"]
  handle2 <- liftIO $ openFile file ReadMode
  flatOut <- liftIO $ hGetContents handle2
  -- go back to the original directory
  liftIO $ setCurrentDirectory (".." </> "..")
  parseTestBenchOut flatOut

-- | Compile the generated VHDL code with Modelsim
--   (Note: the initial and final CWD will be /systemName/vhdl )
compileResultsModelsim :: VHDLM ()
compileResultsModelsim = do
 -- Check if modelsim is installed
 installed <- liftIO $ isModelsimInstalled
 when (not installed) (throwFError ModelsimFailed) 
 -- get the name of the vhdl files to compile
 sys <- gets (globalSysDef.global)
 let sysId = sid sys
     syslib = sysId ++ "_lib"
     libFile = syslib </> (syslib ++ ".vhd")
     workFiles = ("work" </> (sysId ++ ".vhd")) :
          map (("work"</>).(++".vhd").sid.readURef.unPrimSysDef)
              (subSys sys)
 -- get the modelsim directory  of ForSyDe's vhdl library
 -- (generated during installation)
 dataPath <- liftIO $ getDataDir

 -- map the directory to the logical name forsyde
 let modelsimForSyDe = dataPath </> "lib" </> "modelsim"
 run_vmap ["forsyde", modelsimForSyDe]
 
 -- compile the library of current model
 let modelsimLib = syslib </> "modelsim"
 run_vlib [modelsimLib]
 run_vcom ["-93", "-quiet", "-nologo", "-work", modelsimLib, libFile]
 -- map the directory of the library to its logical name
 run_vmap [syslib, modelsimLib]
 
 -- compile the work files
 -- NOTE: Since vcom doesn't figure out
 --       the compilation order (according to compoment dependencies),
 --       we first compile the entities and then the architectures.
 --       Another option would be keeping a dependency tree in SysDef
 --       and execute vcom using a depth search (currently we keep
 --       all the subsystems in a flatenned list) 
 let modelsimWork = "work" </> "modelsim"
 run_vlib [modelsimWork]
 run_vcom ("-93" : "-quiet" : "-nologo" : "-work" : modelsimWork : 
           "-just" : "e" : workFiles)
 run_vcom ("-93" : "-quiet" : "-nologo" : "-work" : modelsimWork : 
           "-just" : "a" : workFiles)
 -- map the directory work library to its logical name
 run_vmap ["work", modelsimWork]
 



-- | Run vlib
run_vlib :: [String] -- ^ arguments
         -> VHDLM ()
run_vlib = runModelsimCommand "vlib"

-- | Run vmap
run_vmap :: [String] -- ^ arguments
         -> VHDLM ()
run_vmap = runModelsimCommand "vmap"


-- | Run vmap
run_vcom :: [String] -- ^ arguments
         -> VHDLM ()
run_vcom = runModelsimCommand "vcom"

-- | Run vsim
run_vsim :: [String] -- ^ arguments
         -> VHDLM ()
run_vsim = runModelsimCommand "vsim"

-- | run a ModelSim command
runModelsimCommand :: String -- ^ Command to execute 
                   -> [String] -- ^ Command arguments
                   -> VHDLM ()
runModelsimCommand command args = do
  success <- liftIO $ runWait msg command args
  when (not success) (throwFError ModelsimFailed)
 where msg = "Running: " ++ command ++ " " ++ (concat $ intersperse " " args)


-- | Run a process, previously announcing a message and waiting for it
--   to finnish its execution.
runWait :: String -- ^ message to show
        -> FilePath -- ^ command to execute 
        -> [String] -- ^ command arguments
        -> IO Bool -- ^ Did the execution end succesfully?
runWait msg proc args = do
           putStrLn msg 
           h <- runProcess proc args Nothing Nothing Nothing Nothing Nothing
           code <- waitForProcess h
           return $ code == ExitSuccess 


-- Look for modelsim executables
isModelsimInstalled :: IO Bool
isModelsimInstalled =  executablePresent "vlib" <&&> 
                       executablePresent "vmap" <&&> 
                       executablePresent "vcom" <&&>
                       executablePresent "vsim"
 where executablePresent = (liftM (maybe False (\_-> True))) .findExecutable

-- | short-circuit and for monads
(<&&>) :: Monad m => m Bool -> m Bool -> m Bool
x <&&> y = do p <- x
              if p then y else return False