content: {-# LANGUAGE CPP, OverloadedStrings, NamedFieldPuns, ScopedTypeVariables #-}
  #ifdef CLANG_INLINER
  {-# LANGUAGE TemplateHaskell #-}
  #endif
  
  module Caide.Commands.Init(
        initialize
  ) where
  
  import Control.Exception (try, displayException)
  import Control.Monad (forM_, unless, when)
  import Control.Monad.State (execStateT)
  import Codec.Archive.Zip (extractFilesFromArchive, toArchive, ZipOption(..))
  import qualified Data.ByteString as BS
  import Data.ByteString.Lazy (fromStrict)
  import Data.Char (isSpace, toLower)
  import Data.List (dropWhileEnd, isInfixOf, isPrefixOf)
  import Data.Function ((&))
  import Data.Maybe (fromMaybe, isNothing, listToMaybe, mapMaybe)
  import qualified Data.Text as T
  import System.Environment (lookupEnv)
  import System.Info (arch, os)
  import System.IO.Error (isDoesNotExistError)
  import System.Process (readProcessWithExitCode)
  
  import qualified Filesystem as FS
  import qualified Filesystem.Path.CurrentOS as FS
  import Filesystem.Path.CurrentOS (encodeString, (</>))
  import Filesystem.Util (pathToText, writeTextFile)
  
  import qualified Data.ConfigFile as CF
  import Data.FileEmbed (embedFile)
  
  import Caide.Configuration (writeConfigFile, putProp)
  import Caide.Logger (logInfo, logWarn)
  import qualified Caide.Paths as Paths
  import Caide.Templates (templates)
  import Paths_CaideExt (resourcesZipFilePath) -- autogenerated by Setup.hs
  
  
  getSystemCompilerInfo :: IO SystemCompilerInfo
  getSystemCompilerInfo = do
      vsCompilerVersion <- if "mingw" `isPrefixOf` os
          then do
              clResult <- try $ readProcessWithExitCode "cl" ["/?"] ""
              case clResult of
                  Left ex -> do
                      unless (isDoesNotExistError ex) $
                          logWarn $ T.pack $ "Failed to run cl.exe: " <> displayException ex
                      pure Nothing
                  Right (_exitCode, _stdOut, stdErr) -> do
                      let res = parseClOutput stdErr
                      when (isNothing res) $
                          logWarn "Failed to find compiler version in `cl.exe /?'"
                      pure res
          else pure Nothing
  
      -- Could be g++ or clang++
      gccLikeCompiler <- fromMaybe "c++" <$> lookupEnv "CXX"
      -- TODO: More robust subprocess handling (e.g. timeout)
      -- TODO: Set locale
      gccResult <- try $ readProcessWithExitCode gccLikeCompiler ["-x", "c++", "-E" ,"-v", "-"] ""
      gccIncludeDirectories <- case gccResult of
          Left ex -> do
              unless (isDoesNotExistError ex) $
                  logWarn $ T.pack $ "Failed to run " <> gccLikeCompiler <> ": " <> displayException ex
              pure []
          Right (_exitCode, _stdOut, stdErr) -> pure $ parseGccOutput stdErr
      return $ SystemCompilerInfo { vsCompilerVersion, gccIncludeDirectories }
  
  parseClOutput :: String -> Maybe String
  parseClOutput stderr = stderr & lines & mapMaybe tryParse & listToMaybe
    where
      -- Example output: "Microsoft (R) C/C++ Optimizing Compiler Version 19.44.35217 for x64"
      tryParse line = go $ words $ map toLower line
      go [] = Nothing
      go ("compiler":"version":ver:_) = Just ver
      go (_:rest) = go rest
  
  parseGccOutput :: String -> [String]
  parseGccOutput output = map trim $ filter isDirectory $ takeWhile (not . endOfSearchListLine) $ dropWhile (not . searchStartsHereLine) $ lines output
    where
      trim = dropWhileEnd isSpace . dropWhile isSpace
      isDirectory s = take 1 s == " "
      endOfSearchListLine s = "End of search list." `isInfixOf` s
      searchStartsHereLine s = "search starts here:" `isInfixOf` s
  
  
  initialize :: FS.FilePath -> Bool -> IO ()
  initialize curDir useSystemCppHeaders = do
      compiler <- getSystemCompilerInfo
      writeConfigFile (defaultCaideConf curDir useSystemCppHeaders compiler) $
          curDir </> Paths.caideConfFile
      writeConfigFile defaultCaideState $ curDir </> Paths.caideStateFile
  #ifdef CLANG_INLINER
      unpackResources curDir
  #endif
      FS.createTree $ curDir </> "templates"
      FS.createTree $ curDir </> ".caide" </> "templates"
      forM_ templates $ \(fileName, cont) -> do
          writeTextFile (curDir </> "templates" </> fileName) cont
          writeTextFile (curDir </> ".caide" </> "templates" </> fileName) cont
      logInfo $ T.concat $ ["Initialized caide directory at ", pathToText curDir]
  
  data SystemCompilerInfo = SystemCompilerInfo
      { vsCompilerVersion :: Maybe String -- ^ https://learn.microsoft.com/en-us/cpp/overview/compiler-versions#version-macros
      , gccIncludeDirectories :: [String]
      }
  
  defaultLanguage :: T.Text
  #ifdef CLANG_INLINER
  defaultLanguage = "cpp"
  #else
  defaultLanguage = "simplecpp"
  #endif
  
  defaultCaideConf :: FS.FilePath -> Bool -> SystemCompilerInfo -> CF.ConfigParser
  defaultCaideConf root useSystemHeaders compiler = forceEither $ flip execStateT CF.emptyCP $ do
      putProp "core" "language" defaultLanguage
      putProp "core" "features" T.empty
      putProp "cpp" "keep_macros"
          ("_WIN32,_WIN64,_MSC_VER,__GNUC__,__clang__,__cplusplus,__STDC_VERSION__,__linux,__linux__" :: T.Text)
      putProp "cpp" "max_consequent_empty_lines" (2 :: Int)
      putProp "cpp" "clang_options" $
          clangOptions root useSystemHeaders compiler & map T.pack & T.intercalate ",\n  "
  
  -- Bundled headers
  clangOptions :: FS.FilePath -> Bool -> SystemCompilerInfo -> [String]
  clangOptions root False _ =
      [ "-target"
      , "i386-pc-mingw32"
      , "-nostdinc"
      , "-isystem"
      , encodeString $ root </> "include" </> "c++"
      , "-isystem"
      , encodeString $ root </> "include" </> "c++" </> "i686-w64-mingw32"
      , "-isystem"
      , encodeString $ root </> "include" </> "crt"
      , "-I"
      , encodeString $ root </> "cpplib"
      , "-std=c++17"
      , "-fparse-all-comments"
      , "-DONLINE_JUDGE"
      ]
  
  -- Windows with VS headers
  -- We rely on libclang to determine correct include paths.
  clangOptions root True SystemCompilerInfo{vsCompilerVersion=Just vsVer} =
      [ "-target"
      , "i386-pc-windows-msvc"
      , "-fdiagnostics-format=msvc"
      -- https://github.com/llvm/llvm-project/issues/24605
      , "-fms-compatibility-version=" ++ vsVer
      , "-fparse-all-comments"
      , "-std=c++23"
      , "-D_CRT_SECURE_NO_WARNINGS"
      , "-DONLINE_JUDGE"
      , "-I"
      , encodeString $ root </> "cpplib"
      ]
  
  -- Linux with system headers
  clangOptions root True compiler =
      [ "-target"
      , arch ++ "-" ++ os
      ] ++
      gccHeadersOptions (gccIncludeDirectories compiler) ++
      -- clang builtin headers are still required:
      -- https://clang.llvm.org/docs/LibTooling.html#libtooling-builtin-includes
      [ "-isystem"
      , encodeString $ root </> "include" </> "clang-builtins"
      , "-I"
      , encodeString $ root </> "cpplib"
      , "-fparse-all-comments"
      , "-DONLINE_JUDGE"
      ]
  
  gccHeadersOptions :: [String] -> [String]
  gccHeadersOptions [] = []
  gccHeadersOptions includeDirectories = ["-nostdinc"] ++
      concat [["-isystem", dir] | dir <- includeDirectories]
  
  forceEither :: Either a c -> c
  forceEither = either (error "Left in forceEither") id
  
  defaultCaideState :: CF.ConfigParser
  defaultCaideState = forceEither $ flip execStateT CF.emptyCP $
      putProp "core" "problem" T.empty
  
  
  #ifdef CLANG_INLINER
  -- This zip file is prepared in advance in Setup.hs
  resourcesZipFile :: BS.ByteString
  resourcesZipFile = $(embedFile resourcesZipFilePath)
  
  unpackResources :: FS.FilePath -> IO ()
  unpackResources rootDir = do
      let archive = toArchive $ fromStrict resourcesZipFile
          destination = FS.encodeString rootDir
          options = [OptDestination destination]
      extractFilesFromArchive options archive
  #endif
  