/*
  Copyright (c) 2024, Intel Corporation

  SPDX-License-Identifier: BSD-3-Clause
*/

#include "ispc.h"
#include "llvmutil.h"
#include "opt.h"
#include "opt/ISPCPasses.h"
#include "target_enums.h"
#include "util.h"

#include "llvm/Support/CommandLine.h"
#include <llvm/Support/Signals.h>
#if ISPC_LLVM_VERSION >= ISPC_LLVM_14_0
#include <llvm/MC/TargetRegistry.h>
#else
#include <llvm/Support/TargetRegistry.h>
#endif
#if ISPC_LLVM_VERSION >= ISPC_LLVM_16_0
#include <llvm/IRPrinter/IRPrintingPasses.h>
#else
#include <llvm/IR/IRPrintingPasses.h>
#endif
#include "llvm/IR/LLVMContext.h"
#include "llvm/IRReader/IRReader.h"
#include <llvm/Support/TargetSelect.h>
#include <llvm/Support/ToolOutputFile.h>

using namespace llvm;

static cl::opt<int> Addressing("addressing", cl::desc("Select 32- or 64-bit addressing."), cl::init(32),
                               cl::value_desc("model"));
static cl::opt<std::string> Passes("passes", cl::desc("Passes to run"));
static cl::opt<bool> PrintPasses("print-passes", cl::desc("Print passes"));
static cl::opt<std::string> InputFilename(cl::Positional, cl::desc("<input bitcode file>"), cl::init("-"),
                                          cl::value_desc("filename"));
static cl::opt<std::string> OutputFilename("o", cl::desc("Override output filename"), cl::value_desc("filename"));
static cl::opt<std::string> TargetTarget("target", cl::desc("ISPC target"), cl::init("host"), cl::value_desc("target"));
// TODO: unused for now.
static cl::opt<std::string> TargetArch("arch", cl::desc("ISPC target architecture"), cl::value_desc("arch"));
static cl::opt<std::string> TargetCPU("cpu", cl::desc("ISPC target CPU"), cl::value_desc("cpu"));
// TODO: target-os

static void lSignal(void *) {
    using namespace ispc;
    FATAL("Unhandled signal sent to process; terminating.");
}

static void lPrintPassName(StringRef PassName, raw_ostream &OS) { OS << "  " << PassName << "\n"; }

static void lPrintPasses(raw_ostream &OS) {
    OS << "Module passes:\n";
#define MODULE_PASS(NAME, CREATE_PASS) lPrintPassName(NAME, OS);
#include "opt/ISPCPassRegistry.def"

    OS << "Function passes:\n";
#define FUNCTION_PASS(NAME, CREATE_PASS) lPrintPassName(NAME, OS);
#include "opt/ISPCPassRegistry.def"
}

static bool lAddPass(ispc::DebugModulePassManager &PM, const std::string &PassName) {
    using namespace ispc;
#define MODULE_PASS(NAME, CREATE_PASS)                                                                                 \
    if (PassName == (NAME)) {                                                                                          \
        PM.addModulePass(CREATE_PASS);                                                                                 \
        return true;                                                                                                   \
    }
#define FUNCTION_PASS(NAME, CREATE_PASS)                                                                               \
    if (PassName == (NAME)) {                                                                                          \
        PM.initFunctionPassManager();                                                                                  \
        PM.addFunctionPass(CREATE_PASS);                                                                               \
        PM.commitFunctionToModulePassManager();                                                                        \
        return true;                                                                                                   \
    }
#include "opt/ISPCPassRegistry.def"

    ispc::Error(ispc::SourcePos(), "Unknown pass: %s", PassName.c_str());
    return false;
}

int main(int argc, char **argv) {
    llvm::sys::AddSignalHandler(lSignal, nullptr);
    // initialize available LLVM targets
#ifdef ISPC_X86_ENABLED
    LLVMInitializeX86TargetInfo();
    LLVMInitializeX86Target();
    LLVMInitializeX86AsmPrinter();
    LLVMInitializeX86AsmParser();
    LLVMInitializeX86Disassembler();
    LLVMInitializeX86TargetMC();
#endif

#ifdef ISPC_ARM_ENABLED
    LLVMInitializeARMTargetInfo();
    LLVMInitializeARMTarget();
    LLVMInitializeARMAsmPrinter();
    LLVMInitializeARMAsmParser();
    LLVMInitializeARMDisassembler();
    LLVMInitializeARMTargetMC();

    LLVMInitializeAArch64TargetInfo();
    LLVMInitializeAArch64Target();
    LLVMInitializeAArch64AsmPrinter();
    LLVMInitializeAArch64AsmParser();
    LLVMInitializeAArch64Disassembler();
    LLVMInitializeAArch64TargetMC();
#endif

#ifdef ISPC_WASM_ENABLED
    LLVMInitializeWebAssemblyAsmParser();
    LLVMInitializeWebAssemblyAsmPrinter();
    LLVMInitializeWebAssemblyDisassembler();
    LLVMInitializeWebAssemblyTarget();
    LLVMInitializeWebAssemblyTargetInfo();
    LLVMInitializeWebAssemblyTargetMC();
#endif

    cl::ParseCommandLineOptions(argc, argv);

    if (PrintPasses) {
        lPrintPasses(outs());
        return 0;
    }

    ispc::g = new ispc::Globals;
    LLVMContext *ctx = ispc::g->ctx;

    if (Addressing == 64) {
        ispc::g->opt.force32BitAddressing = false;
    } else if (Addressing == 32) {
        ispc::g->opt.force32BitAddressing = true;
    } else {
        ispc::Error(ispc::SourcePos(), "Invalid addressing model");
        return 1;
    }

    ispc::ISPCTarget target = ispc::ParseISPCTarget(TargetTarget.getValue());

    // TODO: here, we rely on arch and cpu autodetection in ispc::Target constructor.
    ispc::g->target =
        new ispc::Target(ispc::Arch::none, nullptr, target, ispc::PICLevel::NotPIC, ispc::MCModel::Default, false);
    if (!ispc::g->target->isValid()) {
        ispc::Error(ispc::SourcePos(), "Unsupported target\n");
        return 1;
    }

    // If it is true then LLVM Assembly won't be read.
    ctx->setDiscardValueNames(false);

    ispc::InitLLVMUtil(ctx, *ispc::g->target);

    if (Passes.empty()) {
        ispc::Error(ispc::SourcePos(), "No pass specified");
        return 1;
    }

    SMDiagnostic err;
    std::error_code EC;

    auto M = getLazyIRFileModule(InputFilename, err, *ctx);
    if (!M.get()) {
        err.print(argv[0], errs());
        return 1;
    }

    ToolOutputFile Out(OutputFilename, EC, sys::fs::OF_None);
    if (EC) {
        ispc::Error(ispc::SourcePos(), "Error opening output file: %s: %s", OutputFilename.c_str(),
                    EC.message().c_str());
        return 1;
    }

    ispc::DebugModulePassManager PM(*M, 0);

    // TODO: support multiple passes separated by comma.
    if (!lAddPass(PM, Passes)) {
        return 1;
    }
    PM.addModulePass(PrintModulePass(Out.os(), ""));

    PM.run();
    Out.keep();

    return 0;
}
