// Copyright 2022 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include "iree/compiler/Tools/iree_compile_lib.h"

#include <functional>
#include <memory>
#include <string>
#include <type_traits>

#include "iree/compiler/Pipelines/Pipelines.h"
#include "iree/compiler/embedding_api.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/InitLLVM.h"
#include "llvm/Support/raw_ostream.h"

namespace mlir::iree_compiler {

namespace {

enum class OutputFormat {
  none,
  vm_asm,
  vm_bytecode,
  vm_c,
  // Non-user exposed output formats.
  hal_executable,
  precompile,
};

enum class CompileMode {
  // IREE's full compilation pipeline.
  std,
  // Compile from VM IR (currently this does nothing but may do more in the
  // future).
  vm,
  // Translates an MLIR module containing a single hal.executable into a
  // target-specific binary form (such as an ELF file or a flatbuffer containing
  // a SPIR-V blob).
  hal_executable,
  // IREE's precompilation pipeline, which does input preprocessing and
  // pre-fusion global optimizations.
  precompile,
};

struct BytecodeVersionParser : llvm::cl::parser<std::optional<int64_t>> {
  BytecodeVersionParser(llvm::cl::Option &O)
      : llvm::cl::parser<std::optional<int64_t>>(O) {}
  bool parse(llvm::cl::Option &O, StringRef /*argName*/, StringRef arg,
             std::optional<int64_t> &v) {
    long long w;
    if (llvm::getAsSignedInteger(arg, 10, w)) {
      return O.error("Invalid argument '" + arg +
                     "', only integer is supported.");
    }
    v = w;
    return false;
  }
};

} // namespace

} // namespace mlir::iree_compiler

int mlir::iree_compiler::runIreecMain(int argc, char **argv) {
  static llvm::cl::OptionCategory mainOptions("IREE Main Options");

  // General command line flags.
  llvm::cl::opt<std::string> inputFilename(
      llvm::cl::Positional, llvm::cl::desc("<input file or '-' for stdin>"),
      llvm::cl::Required, llvm::cl::cat(mainOptions));

  llvm::cl::opt<std::string> outputFilename(
      "o", llvm::cl::desc("Output filename"), llvm::cl::value_desc("filename"),
      llvm::cl::init("-"), llvm::cl::cat(mainOptions));

  // The output format flag is the primary control for what we do with the
  // in-memory compiled form.
  llvm::cl::opt<OutputFormat> outputFormat(
      "output-format", llvm::cl::desc("Format of compiled output"),
      llvm::cl::values(
          clEnumValN(OutputFormat::vm_bytecode, "vm-bytecode",
                     "IREE VM Bytecode (default)"),
#ifdef IREE_HAVE_C_OUTPUT_FORMAT
          clEnumValN(OutputFormat::vm_c, "vm-c", "C source module"),
#endif // IREE_HAVE_C_OUTPUT_FORMAT
          clEnumValN(OutputFormat::vm_asm, "vm-asm", "IREE VM MLIR Assembly")),
      llvm::cl::init(OutputFormat::vm_bytecode), llvm::cl::cat(mainOptions));

  llvm::cl::opt<CompileMode> compileMode(
      "compile-mode", llvm::cl::desc("IREE compilation mode"),
      llvm::cl::values(
          clEnumValN(CompileMode::std, "std", "Standard compilation"),
          clEnumValN(CompileMode::vm, "vm", "Compile from VM IR"),
          clEnumValN(
              CompileMode::hal_executable, "hal-executable",
              "Compile an MLIR module containing a single hal.executable into "
              "a target-specific binary form (such as an ELF file or a "
              "flatbuffer containing a SPIR-V blob)"),
          clEnumValN(CompileMode::precompile, "precompile",
                     "Precompilation pipeline which does input conversion and "
                     "global optimizations.")),
      llvm::cl::init(CompileMode::std), llvm::cl::cat(mainOptions));

  // Debugging/diagnostics.
  llvm::cl::opt<bool> verifyIR(
      "verify",
      llvm::cl::desc("Verifies the IR for correctness throughout compilation."),
      llvm::cl::init(true));

  llvm::cl::opt<IREEVMPipelinePhase> compileFrom(
      "compile-from",
      llvm::cl::desc("Compilation phase to resume from, starting with the "
                     "following phase."),
      llvm::cl::init(IREEVMPipelinePhase::Start));
  SmallVector<std::string> compileFromPhases;
  enumerateIREEVMPipelinePhases(
      [&](IREEVMPipelinePhase phase, StringRef name, StringRef desc) {
        compileFrom.getParser().addLiteralOption(name, phase, desc);
        compileFromPhases.push_back(name.str());
      });

  llvm::cl::opt<IREEVMPipelinePhase> compileTo(
      "compile-to",
      llvm::cl::desc(
          "Compilation phase to run up until before emitting output."),
      llvm::cl::init(IREEVMPipelinePhase::End));
  SmallVector<std::string> compileToPhases;
  enumerateIREEVMPipelinePhases(
      [&](IREEVMPipelinePhase phase, StringRef name, StringRef desc) {
        compileTo.getParser().addLiteralOption(name, phase, desc);
        compileToPhases.push_back(name.str());
      });

  llvm::cl::opt<std::string> dumpCompilationPhasesTo(
      "dump-compilation-phases-to",
      llvm::cl::desc("Dumps IR at the end of each compilation phase to the "
                     "given directory."));

  llvm::cl::opt<bool> emitMLIRBytecode(
      "emit-mlir-bytecode",
      llvm::cl::desc(
          "Emit bytecode when generating compile-to or VM MLIR output."),
      llvm::cl::init(false));
  llvm::cl::opt<std::optional<int64_t>, /*ExternalStorage=*/false,
                mlir::iree_compiler::BytecodeVersionParser>
      emitMLIRBytecodeVersion(
          "emit-mlir-bytecode-version",
          llvm::cl::desc("Use specified bytecode version when "
                         "generating compile-to or VM MLIR output."),
          llvm::cl::init(std::nullopt));

  // Remark options.
  llvm::cl::opt<std::string> remarksFilter(
      "remarks-filter",
      llvm::cl::desc("Emit all remarks whose category matches the given regex "
                     "pattern. If empty, no remarks are emitted."),
      llvm::cl::init(""));

  llvm::cl::opt<std::string> remarksOutputFile(
      "remarks-output-file",
      llvm::cl::desc("Output file for remarks in YAML format."),
      llvm::cl::init("mlir-remarks.yaml"));

  // Misc options.
  llvm::cl::opt<bool> splitInputFile(
      "split-input-file",
      llvm::cl::desc("Split the input file into pieces and "
                     "process each chunk independently."),
      llvm::cl::init(false));
  llvm::cl::opt<bool> listHalTargets(
      "iree-hal-list-target-backends",
      llvm::cl::desc(
          "Lists all registered target backends for executable compilation."),
      llvm::cl::init(false), llvm::cl::ValueDisallowed,
      llvm::cl::callback([&](const bool &) {
        llvm::outs() << "Registered target backends:\n";
        ireeCompilerEnumerateRegisteredHALTargetBackends(
            [](const char *backend, void *userData) {
              llvm::outs() << "  " << backend << "\n";
            },
            nullptr);
        exit(0);
      }));
  llvm::cl::opt<bool> listPlugins(
      "iree-list-plugins", llvm::cl::desc("Lists all loaded plugins."),
      llvm::cl::init(false), llvm::cl::ValueDisallowed,
      llvm::cl::callback([&](const bool &) {
        llvm::outs() << "Loaded plugins:\n";
        ireeCompilerEnumeratePlugins(
            [](const char *pluginName, void *userData) {
              llvm::outs() << "  " << pluginName << "\n";
            },
            nullptr);
        exit(0);
      }));

  ireeCompilerGlobalInitialize();
  ireeCompilerGetProcessCLArgs(&argc, const_cast<const char ***>(&argv));
  ireeCompilerSetupGlobalCL(argc, const_cast<const char **>(argv),
                            "IREE compilation driver\n",
                            /*installSignalHandlers=*/true);

  // Stash our globals in an RAII instance.
  struct MainState {
    iree_compiler_session_t *session = ireeCompilerSessionCreate();
    iree_compiler_source_t *source = nullptr;
    iree_compiler_output_t *output = nullptr;
    SmallVector<iree_compiler_source_t *> splitSources;

    ~MainState() {
      for (auto *splitSource : splitSources) {
        ireeCompilerSourceDestroy(splitSource);
      }
      ireeCompilerOutputDestroy(output);
      ireeCompilerSourceDestroy(source);
      ireeCompilerSessionDestroy(session);
      ireeCompilerGlobalShutdown();
    }
    void handleError(iree_compiler_error_t *error) {
      const char *msg = ireeCompilerErrorGetMessage(error);
      llvm::errs() << "error opening input file: " << msg << "\n";
      ireeCompilerErrorDestroy(error);
    }
  };
  MainState s;

  // Open input and output files.
  if (auto error = ireeCompilerSourceOpenFile(s.session, inputFilename.c_str(),
                                              &s.source)) {
    s.handleError(error);
    return 1;
  }
  if (auto error =
          ireeCompilerOutputOpenFile(outputFilename.c_str(), &s.output)) {
    s.handleError(error);
    return 1;
  }

  auto processBuffer = [&](iree_compiler_source_t *source) -> bool {
    // Stash per-invocation state in an RAII instance.
    struct InvState {
      InvState(MainState &s) { inv = ireeCompilerInvocationCreate(s.session); }
      ~InvState() { ireeCompilerInvocationDestroy(inv); }
      iree_compiler_invocation_t *inv;
    };
    InvState r(s);

    ireeCompilerInvocationEnableConsoleDiagnostics(r.inv);
    ireeCompilerInvocationSetCompileFromPhase(
        r.inv,
        compileFromPhases[static_cast<int>(compileFrom.getValue())].c_str());
    ireeCompilerInvocationSetCompileToPhase(
        r.inv, compileToPhases[static_cast<int>(compileTo.getValue())].c_str());
    ireeCompilerInvocationSetDumpCompilationPhasesTo(
        r.inv, dumpCompilationPhasesTo.c_str());
    ireeCompilerInvocationSetVerifyIR(r.inv, verifyIR);

    // Setup remarks.
    if (!remarksFilter.empty()) {
      ireeCompilerInvocationSetupRemarks(r.inv, remarksFilter.c_str(),
                                         remarksOutputFile.c_str());
    }

    if (!ireeCompilerInvocationParseSource(r.inv, source)) {
      return false;
    }

    // Switch on compileMode to choose a pipeline to run.
    switch (compileMode) {
    case CompileMode::std:
      if (!ireeCompilerInvocationPipeline(r.inv, IREE_COMPILER_PIPELINE_STD)) {
        return false;
      }
      break;
    case CompileMode::vm:
      if (!ireeCompilerInvocationPipeline(r.inv, IREE_COMPILER_PIPELINE_VM)) {
        return false;
      }
      break;
    case CompileMode::hal_executable: {
      // Compiling a HAL executable, it is only valid to output in that form.
      outputFormat = OutputFormat::hal_executable;
      if (!ireeCompilerInvocationPipeline(
              r.inv, IREE_COMPILER_PIPELINE_HAL_EXECUTABLE)) {
        return false;
      }
      break;
    }
    case CompileMode::precompile: {
      outputFormat = OutputFormat::precompile;
      if (!ireeCompilerInvocationPipeline(r.inv,
                                          IREE_COMPILER_PIPELINE_PRECOMPILE)) {
        return false;
      }
      break;
    }
    default:
      llvm::errs() << "INTERNAL ERROR: unknown compile mode\n";
      return false;
    }

    auto outputMLIR = [&](iree_compiler_invocation_t *inv,
                          iree_compiler_output_t *output) {
      if (emitMLIRBytecode) {
        return ireeCompilerInvocationOutputIRBytecode(
            inv, output, emitMLIRBytecodeVersion.value_or(-1));
      } else {
        return ireeCompilerInvocationOutputIR(inv, output);
      }
    };

    // Ending early and just emitting IR.
    if (compileTo != IREEVMPipelinePhase::End) {
      if (auto error = outputMLIR(r.inv, s.output)) {
        s.handleError(error);
        return false;
      }
      return true;
    }

    // Switch based on output format.
    iree_compiler_error_t *outputError = nullptr;
    switch (outputFormat) {
    case OutputFormat::vm_asm:
      outputError = outputMLIR(r.inv, s.output);
      break;
    case OutputFormat::vm_bytecode:
      outputError = ireeCompilerInvocationOutputVMBytecode(r.inv, s.output);
      break;
#ifdef IREE_HAVE_C_OUTPUT_FORMAT
    case OutputFormat::vm_c:
      outputError = ireeCompilerInvocationOutputVMCSource(r.inv, s.output);
      break;
#endif // IREE_HAVE_C_OUTPUT_FORMAT
    case OutputFormat::hal_executable: {
      outputError = ireeCompilerInvocationOutputHALExecutable(r.inv, s.output);
      break;
    }
    case OutputFormat::precompile: {
      outputError = outputMLIR(r.inv, s.output);
      break;
    }
    default:
      llvm::errs() << "INTERNAL ERROR: Unknown output format\n";
      return false;
    }

    if (outputError) {
      s.handleError(outputError);
      return false;
    }
    return true;
  };

  // Process buffers, either via splitting or all at once.
  if (splitInputFile) {
    if (auto error = ireeCompilerSourceSplit(
            s.source,
            [](iree_compiler_source_t *source, void *userData) {
              MainState *userState = static_cast<MainState *>(userData);
              userState->splitSources.push_back(source);
            },
            static_cast<void *>(&s))) {
      s.handleError(error);
      return 1;
    }

    bool hadFailure = false;
    for (auto *splitSource : s.splitSources) {
      if (!processBuffer(splitSource)) {
        hadFailure = true;
      }
    }
    if (hadFailure) {
      return 1;
    }
  } else {
    if (!processBuffer(s.source)) {
      return 1;
    }
  }

  ireeCompilerOutputKeep(s.output);
  return 0;
}
