{- Created by Peter Tanski on 10 January 2007 -}

module UpdateMSTools where

import System.IO    ( IO, putStrLn, readFile, stdout )
import System.Environment ()    -- Instances only (will use getEnvironment)
import System.Directory ( findExecutable, doesFileExist )
import System.Exit  ( exitFailure )
import Control.Exception()    -- Instances only
import Distribution.Program()    -- TODO: make a "tools" package for VC tools
import Distribution.Simple.Utils ( findFile )
import Data.Maybe        ( Maybe(..), isJust, fromJust, catMaybes )
import Data.List as List ( (++), filter, map, lookup, break, tail, head )
import Data.Map as Map   ( Map, findWithDefault, empty, lookup,
			   insert, union, mapWithKey, fromList )
import Text.ParserCombinators.Parsec ( noneOf, oneOf, sepBy1, Parser, 
				       many, parse, (<?>) )
import Text.ParserCombinators.Parsec.Error ( ParseError, messageString,
					     errorMessages )
import Text.Printf ( hPrintf )
import Text.Regex  ( mkRegex, matchRegex, subRegex, splitRegex, Regex )

-- convenient type for storing the environment variables
-- IO [(String,String)] is returned from System.Environment.getEnvironment

type Environment = [(String,String)]
type VsVarsMap   = Map String String

-- TODO: create a separate library/module to capture "tools" package programs
-- using Distribution libraries
-- If use Distribution, may sponge off configure step to find everything...


-- Check SysEnv for %VSnnCOMNTOOLS% and use that as start path to search
-- for correct vsvars32.bat.
-- option: find vcvars32.bat in VC directory and run it
getEnvValRegex :: String -> Environment -> IO (Either String (String,String))
getEnvValRegex str env = 
    case regx_val env of
	     ((vr,vs):_) -> return $ Right (vr,vs)
	     []    -> return $ Left $ "regex " ++ str ++ " not found"

    where
    vs_regx :: Regex
    vs_regx = mkRegex str

    matchVS :: Regex -> (String,String) -> Bool
    matchVS regx (vr,vl) = isJust $! matchRegex regx vr

    regx_val :: Environment -> [(String,String)]
    regx_val _env = List.filter (matchVS vs_regx) _env

-- for a given variable value (String), find the variable in the Storage
-- (a List or Map), separate the variable's value into a list of strings,
-- split at the directory separator token (';', on Windows)
-- and return the [String] or a list of errors.
getValFromVars :: (String -> a -> Maybe String) -> String -> a 
		  -> IO (Either [String] [String])
getValFromVars lkup str stor =
    case lkup str stor of
         Nothing  -> return $ Left [str ++ " not found"]
	 Just val -> return $ Right (splitDirs str val)

getEnvVals :: String -> Environment -> IO (Either [String] [String])
getEnvVals var env = getValFromVars List.lookup var env

getVsVarVals :: String -> VsVarsMap -> IO (Either [String] [String])
getVsVarVals var vs_vars = getValFromVars Map.lookup var vs_vars

splitDirs :: String -> String -> [String]
splitDirs var dir_lst = 
	case parse elements [] dir_lst of
	     Left errs -> 
		 (("error parsing " ++ var):(printParseErrors errs))
	     Right dirs -> dirs
	where
	pathSeparator :: String
	pathSeparator = ";" -- separator is ';' on Windows 

        valElement :: Parser String
        valElement = many (noneOf pathSeparator) 

        -- TODO: redo the crappy error here
        elements :: Parser [String]
        elements = do {  elems <- valElement `sepBy1` (oneOf pathSeparator)
		   ; return elems } <?> "environment variable value"

        printParseErrors :: ParseError -> [String]
        printParseErrors errs = List.map messageString (errorMessages errs)

{-
   Open vsvars32.bat and grab the environment settings with regexes.
   The hardest part here is expanding any variables in vsvars32.bat
   before storing the entire Map (key==variable name) (val==variable value).
   For example, suppose vsvars32.bat contained a definition of the PATH
   Visual Studio Tools/DOS environment variable and the definition of 
   PATH referred to the variable %FrameworkSDKDir%,
   like this:
      @SET FrameworkSDKDir=C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0
      
      @set PATH=...;%FrameworkSDKDir%;...
   the regexVars function, below, calls expandAllVars to find the variables
   and expand them in the definition, so in the Map (shown as a list of 
   (String,String)==(key,value) pairs), the result would be:
   ("PATH","...;C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0;...")
-} 
readVarsFromVsVars :: Environment -> IO VsVarsMap
readVarsFromVsVars env = 
    do {  either_lst <- getEnvValRegex "VS[0-9]*COMNTOOLS" env
	; (vst,vs_dir) <- handleNoVarFound either_lst
        ; vsv_bat <- return (vs_dir ++ "/vsvars32.bat") -- need \
        ; vsv_exist <- doesFileExist vsv_bat
        ; if vsv_exist then
	  do { vsv_str <- readFile vsv_bat
	     ; regexVars (vsv_str,(Map.insert vst vs_dir Map.empty))
	     }
         else
          do { hPrintf stdout "%s %s\n " vsv_bat " not found."
	     ; putStrLn "You may need to repair your Visual Studio installation"
	     ; exitFailure
	     }
       }
    where 
    handleNoVarFound :: Either String (String,String) -> IO (String,String)
    handleNoVarFound (Right vs_pair) = return vs_pair
    handleNoVarFound (Left  err_s)   = do putStrLn err_s >> exitFailure


regexVars :: (String,(Map String String)) -> IO VsVarsMap
regexVars (flstr,vstools) = return $ 
	(expandAllVars . (extractVars vstools) . setVars . toLines) flstr
    where
    toLines :: String -> [String]
    toLines = splitRegex winLineSepRgx

setVars :: [String] -> [[String]]
setVars = catMaybes . (List.map (matchRegex setVarRgx))

extractVars :: Map String String -> [[String]] -> Map String String
extractVars vst = (Map.union vst) . fromList . (List.map toVarValP)

toVarValP :: [String] -> (String,String)
toVarValP = stripEq . (break (=='=')) . head

stripEq :: (String,String) -> (String,String)
stripEq (vr,val) = (vr, tail val)

expandAllVars :: Map String String -> Map String String
expandAllVars cur_map = mapWithKey (expandVar cur_map) cur_map

expandVar :: Map String String -> String -> String -> String
expandVar vvmap cur_key val
    | isJust m_expv = expandVar vvmap cur_key subExp_val
    | otherwise = val
    where 
    m_expv = matchRegex varRgx val
    -- bizarre note here: putting subExpandVal in place of 'subExp_val',
    -- above, even when strict, results in uncompleted evaluations
    subExp_val = subExpandVal vvmap cur_key (head (fromJust m_expv)) val

subExpandVal :: Map String String -> String -> String -> String -> String
subExpandVal vvmap cur_key expv val = 
	if expv /= cur_key then
	   subRegex expRgx val (findWithDefault "" expv vvmap)
	else
	   subRegex varExpandRgx val ""
	where
	expRgx = mkRegex ('(':';':'?':'%':expv ++ "%)")

winLineSepRgx :: Regex
winLineSepRgx = mkRegex "\r\n"

setVarRgx :: Regex
setVarRgx = mkRegex "([a-zA-Z0-9]*=[A-Za-z0-9:;. \\%]*$)"

varExpandRgx :: Regex
varExpandRgx = mkRegex "(;?%[a-zA-Z0-9]*%)"

varRgx :: Regex
varRgx = mkRegex ";?%([a-zA-Z0-9]*)%"

{- 
   o.k., now we have the environment variables (for the configuration files)
   and we know where vsvars32.bat is, we may execute vsvars32.bat to set
   the environment and then simply use findExecutable to find the executables.
   OR
   Next, iterate over PATH directories and Distribution.Simple.Utils.findFile 
   for cl.exe, lib.exe, link.exe, yasm.exe (any others?)
   If there is more than one executable in PATH go with the first executable
   found. If the vsvars32.bat file is correct then the first executable found
   is the correct one.  
-}
getProg :: VsVarsMap -> String -> IO String
getProg vs_vars name = 
    do either_lst <- getVsVarVals name vs_vars
       val_lst    <- handleNoVarFound either_lst
       findFile val_lst name
   where
   handleNoVarFound :: Either [String] [String] -> IO [String]
   handleNoVarFound (Right vs_lst) = return vs_lst
   handleNoVarFound (Left  err_s)   = do mapM_ putStrLn err_s >> exitFailure

clProg, libProg, linkProg, yasmProg :: VsVarsMap -> IO String
clProg    vs_vars = getProg vs_vars "cl.exe"
libProg   vs_vars = getProg vs_vars "lib.exe"
linkProg  vs_vars = getProg vs_vars "link.exe"
yasmProg  vs_vars = getProg vs_vars "yasm.exe"

{- 
   TODO: write to file, put in Distribution package.conf, print or export to C
   Now write the environment variables to the command file...
   NOTE: this requires nothing more than placing each option with the 
   same syntax as command line compiler options, with each option beginning
   and ending on the same line.  (You may use either '/' or '-' to prefix.)

   DISCUSSION: since command files may not be specified from the VS development
   environment, the command file should be parsed and stored in, say, the
   CL environment variable.
-}



