import Development.Shake
import Development.Shake.Command
import Development.Shake.FilePath
import Development.Shake.Util
import System.IO (readFile)
import System.Directory (findExecutable, getCurrentDirectory, removeDirectoryRecursive)
import Data.Foldable (asum)
import System.Console.GetOpt
import Data.Hashable (hash)
import System.Environment
import System.Process (readProcess)
import Data.Maybe (listToMaybe, fromMaybe)
import Data.Function ((&))

flags = fmap (filter (not . null) . lines) . readFile

findExe :: [String] -> IO (Maybe FilePath)
findExe cmds = asum <$> mapM findExecutable cmds

cdir = "src"
incdir = "include"
builddir = "_build"

data BuildTypes = Default | Test | Bench deriving (Read)
data OptLevels = Release | Debug deriving (Read)
data Options
  = BuildType BuildTypes
  | OptLevel OptLevels
  | DisableCCACHE
  | EmitLLVM
  | BuildFromASM
  | Prefix FilePath
  | Runner String

options :: [OptDescr (Either a Options)]
options =
  [ Option "t" ["build-type"]     (ReqArg (Right . BuildType . read) "default") "[default|test|bench]"
  , Option "o" ["opt-level"]      (ReqArg (Right . OptLevel . read) "g") "[0-3|g]"
  , Option ""  ["disable-ccache"] (NoArg $ Right DisableCCACHE) ""
  , Option ""  ["emit-llvm"]      (NoArg $ Right EmitLLVM) ""
  , Option ""  ["build-from-asm"] (NoArg $ Right BuildFromASM) ""
  , Option "p" ["prefix"]         (ReqArg (Right . Prefix) "/usr/local") ""
  , Option "r" ["runner"]         (ReqArg (Right . Runner) "gdb") ""
  ]

main :: IO ()
main = shakeArgsWith shakeOptions options $ \opts targets -> pure $ Just $ do
  cwd <- liftIO $ getCurrentDirectory
  let proj = takeFileName cwd
  gitBranch <- liftIO $ readProcess "git" ["branch", "--show-current"] ""
  inNixShell <- liftIO $ System.Environment.getEnv "IN_NIX_SHELL"

  cflags <- liftIO $ flags "build/cflags"
  ldflags <- liftIO $ flags "build/ldflags"
  asmflags <- liftIO $ flags "build/asmflags"
  optflags <- liftIO $ flags "build/optflags"
  debugflags <- liftIO $ flags "build/debugflags"
  optldflags <- liftIO $ flags "build/optldflags"

  let typeFlag = [a | BuildType a <- opts] & listToMaybe & fromMaybe Default
  let optFlag = [a | OptLevel a <- opts] & listToMaybe & fromMaybe Debug
  let prefix = [a | Prefix a <- opts] & listToMaybe & fromMaybe (error "")
  let runner = [a | Runner a <- opts]

  compile <- do
      Just cc <- liftIO $ findExe ["clang-21", "clang-20", "clang-19", "clang"]
      ccache <- liftIO $ findExe ["ccache"]
      let p DisableCCACHE = True
          p _             = False
      let disable_ccache = any p opts
      case (disable_ccache, ccache) of
        (False, Just ch) -> return [ch, cc]
        _                -> return [cc]

  let typeFlags Default = []
      typeFlags Test    = ["-DTEST_MODE"]
      typeFlags Bench   = ["-DBENCHMARK_MODE"]
  
  let optFlags Release = optflags
      optFlags Debug   = debugflags

  let optLDFlags Release = optldflags
      optLDFlags Debug   = debugflags
  
  let compileFlags = cflags ++ typeFlags typeFlag ++ optFlags optFlag ++ ["-I" ++ incdir]
  let linkFlags = ldflags ++ optLDFlags optFlag

  let seed = unwords compile ++ unwords compileFlags ++ unwords linkFlags ++ gitBranch ++ inNixShell
  let h = abs $ hash seed

  let outDir = builddir </> show h
  let target = outDir </> proj
  let bin = builddir </> proj

  want targets

  phony "all" $ do
    need [bin]

  bin %> \out -> do
    need [target]
    copyFileChanged target out

  target %> \out -> do
    cs <- getDirectoryFiles cdir ["*.c"]
    let os = [outDir </> c -<.> "o" | c <- map takeFileName cs]
    need os
    cmd_ compile "-o" [out] os linkFlags

  outDir </> "*.o" %> \out -> do
    let c = cdir </> (takeFileName $ out -<.> "c")
    let m = out -<.> "m"
    cmd_ compile "-o" [out] "-c" [c] "-MMD -MF" [m] compileFlags
    neededMakefileDependencies m

  phony "install" $ do
    copyFileChanged bin (prefix </> "bin" </> proj)

  phony "clear" $ do
    liftIO $ removeDirectoryRecursive outDir
  phony "clear-all" $ do
    liftIO $ removeDirectoryRecursive builddir