{- Allows linking haskell programs too big for all the files to fit in a
 - command line.
 -
 - See https://ghc.haskell.org/trac/ghc/ticket/8596
 -
 - Copyright 2013 Joey Hess <id@joeyh.name>
 -
 - Licensed under the GNU GPL version 3 or higher.
 -}

module Main where

import Data.List.Utils
import Text.Parsec
import Text.Parsec.String
import Control.Applicative ((<$>))
import Control.Monad
import System.Directory
import Data.Maybe
import Data.List

import Utility.Monad
import Utility.Process hiding (env)
import Utility.Env

data CmdParams = CmdParams
	{ cmd :: String
	, opts :: String
	, env :: Maybe [(String, String)]
	} deriving (Show)

{- Find where ghc calls gcc to link the executable. -}
parseGhcLink :: Parser CmdParams
parseGhcLink = do
	void $ many prelinkline
	void linkheaderline
	void $ char '"'
	gcccmd <- many1 (noneOf "\"")
	void $ string "\" "
	gccparams <- restOfLine
	return $ CmdParams gcccmd (manglepaths gccparams) Nothing
  where
	linkheaderline = do
		void $ string "*** Linker"
		restOfLine
	prelinkline = do
		void $ notFollowedBy linkheaderline
		restOfLine
	manglepaths = replace "\\" "/"

{- Find where gcc calls collect2. -}
parseGccLink :: Parser CmdParams
parseGccLink = do
	cenv <- collectenv
	void $ try $ char ' '
	path <- manyTill anyChar (try $ string collectcmd)
	void $ char ' '
	collect2params <- restOfLine
	return $ CmdParams (path ++ collectcmd) (escapeDosPaths collect2params) cenv
  where
	collectcmd = "collect2.exe"
	collectgccenv = "COLLECT_GCC"
	collectltoenv = "COLLECT_LTO_WRAPPER"
	pathenv = "COMPILER_PATH"
	libpathenv = "LIBRARY_PATH"
	optenv = "COLLECT_GCC_OPTIONS"
	collectenv = do
		void $ many1 $ do
			notFollowedBy $ string collectgccenv
			restOfLine
		void $ string collectgccenv
		void $ char '='
		g <- restOfLine
		void $ string collectltoenv
		void $ char '='
		lt <- restOfLine
		void $ many1 $ do
			notFollowedBy $ string pathenv
			restOfLine
		void $ string pathenv
		void $ char '='
		p <- restOfLine
		void $ string libpathenv
		void $ char '='
		lp <- restOfLine
		void $ string optenv
		void $ char '='
		o <- restOfLine
		return $ Just [(collectgccenv, g), (collectltoenv, lt), (pathenv, p), (libpathenv, lp), (optenv, o)]

{- Find where collect2 calls ld. -}
parseCollect2 :: Parser CmdParams
parseCollect2 = do
	void $ manyTill restOfLine (try versionline)
	path <- manyTill anyChar (try $ string ldcmd)
	void $ char ' '
	params <- restOfLine
	return $ CmdParams (path ++ ldcmd) (skipHack $ escapeDosPaths params) Nothing
  where
	ldcmd = "ld.exe"
	versionline = do
		void $ string "collect2 version"
		restOfLine

{- For unknown reasons, asking the linker to link this in fails,
 - with error about multiple definitions of a symbol from the library.
 - This is a horrible hack. -}
skipHack :: String -> String
skipHack = replace "dist/build/git-annex/git-annex-tmp/Utility/winprocess.o" ""

{- Input contains something like 
 - c:/program files/haskell platform/foo -LC:/Program Files/Haskell Platform/ -L...
 - and the *right* spaces must be escaped with \
 -
 - Argh.
 -}
escapeDosPaths :: String -> String
escapeDosPaths = replace "Program Files" "Program\\ Files"
	. replace "program files" "program\\ files"
	. replace "Haskell Platform" "Haskell\\ Platform"
	. replace "haskell platform" "haskell\\ platform"
	. replace "Application Data" "Application\\ Data"
	. replace "Documents and Settings" "Documents\\ and\\ Settings"
	. replace "Files (x86)" "Files\\ (x86)"
	. replace "files (x86)" "files\\ (x86)"

restOfLine :: Parser String
restOfLine = newline `after` many (noneOf "\n")

getOutput :: String -> [String] -> Maybe [(String, String)] -> IO (String, Bool)
getOutput c ps environ = do
	putStrLn $ unwords [c, show ps]
	systemenviron <- getEnvironment
	let environ' = fromMaybe [] environ ++ systemenviron
	out@(_, ok) <- processTranscript' c ps (Just environ') Nothing
	putStrLn $ unwords [c, "finished", show ok]
	return out

atFile :: FilePath -> String
atFile f = '@':f

runAtFile :: Parser CmdParams -> String -> FilePath -> [String] -> IO (String, Bool)
runAtFile p s f extraparams = do
	when (null $ opts c) $
		error $ "failed to find any options for " ++ f ++ " in >>>" ++ s ++ "<<<"
	writeFile f (opts c)
	out <- getOutput (cmd c) (atFile f:extraparams) (env c)
	removeFile f
	return out
  where
	c = case parse p "" s of
		Left e -> error $
			(show e) ++ 
			"\n<<<\n" ++ s ++ "\n>>>"
		Right r -> r

main :: IO ()
main = do
	ghcout <- fst <$> getOutput "cabal"
		["build", "--ghc-options=-v -keep-tmp-files"] Nothing
	gccout <- fst <$> runAtFile parseGhcLink ghcout "gcc.opt" ["-v"]
	collect2out <- fst <$> runAtFile parseGccLink gccout "collect2.opt" ["-v"]
	(out, ok) <- runAtFile parseCollect2 collect2out "ld.opt" []
	unless ok $
		error $ "ld failed:\n" ++ out