module Aihc.Cli.Options
  ( Command (..),
    CompileOptions (..),
    GarbageCollector (..),
    InstallErrorFormat (..),
    InstallOptions (..),
    InstallV2Options (..),
    PrepareRuntimeOptions (..),
    ReplOptions (..),
    parseCommandIO,
    parseCommandPure,
    parserInfo,
  )
where

import Aihc.Native (NativeTarget, parseNativeTarget)
import Control.Applicative (many)
import Options.Applicative qualified as OA

data Command
  = CmdCompile !CompileOptions
  | CmdInstall !InstallOptions
  | CmdInstallV2 !InstallV2Options
  | CmdPrepareRuntime !PrepareRuntimeOptions
  | CmdRepl !ReplOptions
  deriving (Eq, Show)

data CompileOptions = CompileOptions
  { compileSourceFile :: !FilePath,
    compileOutputFile :: !(Maybe FilePath),
    compileKeepCore :: !Bool,
    compileKeepGrin :: !Bool,
    compileKeepAsm :: !Bool,
    compileWholeProgram :: !Bool,
    compileTarget :: !(Maybe NativeTarget),
    compileGarbageCollector :: !GarbageCollector,
    compileStoreRoot :: !(Maybe FilePath),
    compileUseWasmOpt :: !Bool
  }
  deriving (Eq, Show)

data GarbageCollector
  = GcCalloc
  | GcSemispace
  deriving (Eq, Show)

data PrepareRuntimeOptions = PrepareRuntimeOptions
  { prepareRuntimeTarget :: !NativeTarget,
    prepareRuntimeGarbageCollector :: !GarbageCollector,
    prepareRuntimeStoreRoot :: !(Maybe FilePath)
  }
  deriving (Eq, Show)

newtype ReplOptions = ReplOptions
  { replStoreRoot :: Maybe FilePath
  }
  deriving (Eq, Show)

data InstallOptions = InstallOptions
  { installPackageName :: !String,
    installPackageVersion :: !(Maybe String),
    installStoreRoot :: !(Maybe FilePath),
    installOffline :: !Bool,
    installDryRun :: !Bool,
    installKeepCore :: !Bool,
    installKeepGrin :: !Bool,
    installTargets :: ![NativeTarget],
    installFirstErrorModule :: !Bool,
    installErrorFormat :: !InstallErrorFormat
  }
  deriving (Eq, Show)

data InstallV2Options = InstallV2Options
  { installV2PackageDirectory :: !FilePath,
    installV2StoreRoot :: !(Maybe FilePath),
    installV2Verbose :: !Bool
  }
  deriving (Eq, Show)

data InstallErrorFormat
  = InstallErrorsJson
  | InstallErrorsHuman
  deriving (Eq, Show)

parseCommandIO :: IO Command
parseCommandIO = OA.execParser parserInfo

parseCommandPure :: [String] -> Either String Command
parseCommandPure args =
  case OA.execParserPure OA.defaultPrefs parserInfo args of
    OA.Success command -> Right command
    OA.Failure failure ->
      let (message, _) = OA.renderFailure failure "aihc"
       in Left message
    OA.CompletionInvoked _ -> Left "completion invoked"

parserInfo :: OA.ParserInfo Command
parserInfo =
  OA.info
    (commandParser OA.<**> OA.helper)
    ( OA.fullDesc
        <> OA.header "aihc - command-line interface for the aihc compiler"
    )

commandParser :: OA.Parser Command
commandParser =
  OA.subparser
    ( OA.command
        "compile"
        ( OA.info
            (CmdCompile <$> compileOptionsParser OA.<**> OA.helper)
            (OA.progDesc "Compile a Haskell source file to an executable")
        )
        <> OA.command
          "install"
          ( OA.info
              (CmdInstall <$> installOptionsParser OA.<**> OA.helper)
              (OA.progDesc "Compile and install a library package and its dependencies")
          )
        <> OA.command
          "install-v2"
          ( OA.info
              (CmdInstallV2 <$> installV2OptionsParser OA.<**> OA.helper)
              (OA.progDesc "Build and install one local Cabal library")
          )
        <> OA.command
          "prepare-runtime"
          ( OA.info
              (CmdPrepareRuntime <$> prepareRuntimeOptionsParser OA.<**> OA.helper)
              (OA.progDesc "Compile and install a runtime for one backend and garbage collector")
          )
        <> OA.command
          "repl"
          ( OA.info
              (CmdRepl <$> replOptionsParser OA.<**> OA.helper)
              (OA.progDesc "Start the aihc expression REPL")
          )
    )

compileOptionsParser :: OA.Parser CompileOptions
compileOptionsParser =
  CompileOptions
    <$> OA.strArgument
      ( OA.metavar "SOURCE"
          <> OA.help "Haskell source file containing the main binding"
      )
    <*> OA.optional
      ( OA.strOption
          ( OA.short 'o'
              <> OA.long "output"
              <> OA.metavar "FILE"
              <> OA.help "Write the executable to FILE (default: SOURCE without its extension)"
          )
      )
    <*> OA.switch
      ( OA.long "keep-core"
          <> OA.help "Keep the generated System FC core as OUTPUT.core"
      )
    <*> OA.switch
      ( OA.long "keep-grin"
          <> OA.help "Keep the generated GRIN as OUTPUT.grin"
      )
    <*> OA.switch
      ( OA.long "keep-asm"
          <> OA.help "Keep generated backend source as OUTPUT.s or OUTPUT.ll"
      )
    <*> OA.switch
      ( OA.long "whole-program"
          <> OA.help "After incremental module compilation, merge Core units for whole-program DCE and code generation"
      )
    <*> OA.optional
      ( OA.option
          (OA.eitherReader parseNativeTarget)
          ( OA.long "target"
              <> OA.metavar "TARGET"
              <> OA.help "Target: apple-arm64, linux-amd64, llvm, or wasm32-wasip3 (default: host)"
          )
      )
    <*> OA.option
      (OA.eitherReader parseGarbageCollector)
      ( OA.long "gc"
          <> OA.metavar "calloc|semispace"
          <> OA.value GcCalloc
          <> OA.showDefaultWith (const "calloc")
          <> OA.help "Select the garbage collector compiled into the executable"
      )
    <*> OA.optional
      ( OA.strOption
          ( OA.long "store"
              <> OA.metavar "DIR"
              <> OA.help "Read installed runtimes and core libraries from DIR"
          )
      )
    <*> OA.switch
      ( OA.long "use-wasm-opt"
          <> OA.help "Optimize the linked core WebAssembly module with wasm-opt when available"
      )

parseGarbageCollector :: String -> Either String GarbageCollector
parseGarbageCollector value =
  case value of
    "calloc" -> Right GcCalloc
    "semispace" -> Right GcSemispace
    _ -> Left "expected calloc or semispace"

prepareRuntimeOptionsParser :: OA.Parser PrepareRuntimeOptions
prepareRuntimeOptionsParser =
  PrepareRuntimeOptions
    <$> nativeTargetOption
    <*> garbageCollectorOption
    <*> storeRootOption "Install the prepared runtime into DIR"

nativeTargetOption :: OA.Parser NativeTarget
nativeTargetOption =
  OA.option
    (OA.eitherReader parseNativeTarget)
    ( OA.long "target"
        <> OA.metavar "TARGET"
        <> OA.help "Target: apple-arm64, linux-amd64, llvm, or wasm32-wasip3"
    )

garbageCollectorOption :: OA.Parser GarbageCollector
garbageCollectorOption =
  OA.option
    (OA.eitherReader parseGarbageCollector)
    ( OA.long "gc"
        <> OA.metavar "calloc|semispace"
        <> OA.value GcCalloc
        <> OA.showDefaultWith (const "calloc")
        <> OA.help "Select the garbage collector"
    )

storeRootOption :: String -> OA.Parser (Maybe FilePath)
storeRootOption description =
  OA.optional
    ( OA.strOption
        ( OA.long "store"
            <> OA.metavar "DIR"
            <> OA.help description
        )
    )

replOptionsParser :: OA.Parser ReplOptions
replOptionsParser =
  ReplOptions
    <$> OA.optional
      ( OA.strOption
          ( OA.long "store"
              <> OA.metavar "DIR"
              <> OA.help "Override the aihc store root"
          )
      )

installOptionsParser :: OA.Parser InstallOptions
installOptionsParser =
  InstallOptions
    <$> OA.strArgument
      ( OA.metavar "PACKAGE"
          <> OA.help "Hackage package name or local package directory"
      )
    <*> OA.optional
      ( OA.strOption
          ( OA.long "version"
              <> OA.metavar "VERSION"
              <> OA.help "Exact Hackage package version"
          )
      )
    <*> OA.optional
      ( OA.strOption
          ( OA.long "store"
              <> OA.metavar "DIR"
              <> OA.help "Override the aihc store root"
          )
      )
    <*> OA.switch
      ( OA.long "offline"
          <> OA.help "Use only cached package data"
      )
    <*> OA.switch
      ( OA.long "dry-run"
          <> OA.help "Plan the install without writing store artifacts or package cache files"
      )
    <*> OA.switch
      ( OA.long "keep-core"
          <> OA.help "Keep the generated System FC core in the library cache"
      )
    <*> OA.switch
      ( OA.long "keep-grin"
          <> OA.help "Keep the generated GRIN in the library cache"
      )
    <*> many nativeTargetOption
    <*> OA.switch
      ( OA.long "first-error-module"
          <> OA.help "Only print diagnostics from the module or file that produced the first install error"
      )
    <*> errorFormatParser

installV2OptionsParser :: OA.Parser InstallV2Options
installV2OptionsParser =
  InstallV2Options
    <$> OA.strArgument
      ( OA.metavar "DIRECTORY"
          <> OA.help "Local Cabal package directory"
      )
    <*> storeRootOption "Override the aihc store root"
    <*> OA.switch
      ( OA.long "verbose"
          <> OA.short 'v'
          <> OA.help "Print each installation step"
      )

errorFormatParser :: OA.Parser InstallErrorFormat
errorFormatParser =
  flagFromSwitch
    <$> OA.switch
      ( OA.long "json-errors"
          <> OA.help "Print install interface diagnostics as JSON instead of human-readable text"
      )
  where
    flagFromSwitch True = InstallErrorsJson
    flagFromSwitch False = InstallErrorsHuman
