// For hacking the C API
#include <llvm/IR/PassManager.h>
#include <llvm-c/Core.h>

#include "c3_llvm.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/Object/Archive.h"
#include "llvm/Object/ArchiveWriter.h"
#include "llvm/Object/IRObjectFile.h"
#include "llvm/Object/SymbolicFile.h"
#include "llvm/Target/TargetMachine.h"
#include "lld/Common/CommonLinkerContext.h"
#include "llvm/IR/GlobalValue.h"
#include "llvm-c/Transforms/PassBuilder.h"
#include "llvm-c/TargetMachine.h"
#include "llvm-c/Target.h"
#include "llvm/IR/Verifier.h"
#include "llvm/IR/Module.h"
#include "llvm/Transforms/Scalar/LowerMatrixIntrinsics.h"
#include "llvm/IR/Function.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/StandardInstrumentations.h"
#include "llvm/Support/CBindingWrapping.h"
#include "llvm/Transforms/Instrumentation/MemorySanitizer.h"
#include "llvm/Transforms/Instrumentation/AddressSanitizer.h"
#include "llvm/Transforms/Instrumentation/ThreadSanitizer.h"
#include "llvm/Transforms/Instrumentation/HWAddressSanitizer.h"
#include "llvm/Transforms/Scalar/EarlyCSE.h"
#include "llvm/Transforms/Scalar/GVN.h"
#include "llvm/Transforms/Scalar/JumpThreading.h"
#include "llvm/Transforms/InstCombine/InstCombine.h"
#include "llvm/Analysis/GlobalsModRef.h"
static_assert(LLVM_VERSION_MAJOR >= 19, "Unsupported LLVM version, 19+ is needed.");

#define LINK_SIG \
bool link(llvm::ArrayRef<const char *> args, llvm::raw_ostream &stdoutOS, \
		  llvm::raw_ostream &stderrOS, bool exitEarly, bool disableOutput);
#define CALL_ARGS arg_vector, output, output_err, false, false

namespace lld {

	namespace coff {
		LINK_SIG
	}

	namespace mingw {
		LINK_SIG
	}

	namespace elf {
		LINK_SIG
	}

	namespace mach_o {
		LINK_SIG
	}

	namespace macho {
		LINK_SIG
	}

	namespace wasm {
		LINK_SIG
	}
}

typedef enum
{
	ELF,
	WASM,
	MACHO,
	COFF,
	MINGW
} ObjFormat;

static bool llvm_link(ObjFormat format, const char **args, int arg_count, const char** error_string)
{
	std::vector<const char*> arg_vector = std::vector<const char *>();
	switch (format)
	{
		case ELF:
			arg_vector.push_back("ld.lld");
			break;
		case WASM:
			arg_vector.push_back("wasm-ld");
			break;
		case MACHO:
			arg_vector.push_back("ld64.lld");
			break;
		case COFF:
			arg_vector.push_back("lld-link");
			break;
		case MINGW:
			arg_vector.push_back("ld");
			break;
	}
	for (int i = 0; i < arg_count; i++) arg_vector.push_back(strdup(args[i]));
	std::string output_string {};
	std::string output_err_string {};

	llvm::raw_ostream &output = llvm::outs();
	llvm::raw_ostream &output_err = llvm::errs();
	bool success;
	switch (format)
	{
		case ELF:
			success = lld::elf::link(CALL_ARGS);
			break;
#ifndef __OpenBSD__
		case MACHO:
			success = lld::macho::link(CALL_ARGS);
			break;
#else
#warning "the maintainers of LLVM on this platform has deliberately excluded targets, which makes it impossible to offer the standard cross compilation available in the normal compiler"
#endif
		case WASM:
			success = lld::wasm::link(CALL_ARGS);
			break;
		case COFF:
			success = lld::coff::link(CALL_ARGS);
			break;
		case MINGW:
			printf("Mingw not enabled");
			exit(1);
		default:
			printf("Unsupported linker");
			exit(-1);
	}
	if (success)
	{
		lld::CommonLinkerContext::destroy();
	}
	//*error_string = strdup(output_err_string.c_str());
	return success;
}

extern "C" {

void llvm_setup(void)
{
	const char *fake_args[] = {"dummy", "--enable-matrix"};
	llvm::cl::ParseCommandLineOptions(2, fake_args);
}

bool llvm_run_passes(LLVMModuleRef m, LLVMTargetMachineRef tm, LLVMPasses *passes) // NOLINT
{

	llvm::TargetMachine *Machine = (llvm::TargetMachine *)(tm);
	llvm::Module *Mod = llvm::unwrap(m);
	llvm::PassInstrumentationCallbacks PIC;
	llvm::PipelineTuningOptions PTO{};
	PTO.LoopUnrolling = passes->opt.unroll_loops;
	PTO.LoopInterleaving = passes->opt.interleave_loops;
	PTO.LoopVectorization = passes->opt.vectorize_loops;
	PTO.SLPVectorization = passes->opt.slp_vectorize;
	PTO.MergeFunctions = passes->opt.merge_functions;
	PTO.CallGraphProfile = true; // We always use integrated ASM
	PTO.UnifiedLTO = false;

	llvm::PassBuilder PB(Machine, PTO, std::nullopt, &PIC);

	llvm::LoopAnalysisManager LAM;
	llvm::FunctionAnalysisManager FAM;
	llvm::CGSCCAnalysisManager CGAM;
	llvm::ModuleAnalysisManager MAM;


	PB.registerLoopAnalyses(LAM);
	PB.registerFunctionAnalyses(FAM);
	PB.registerCGSCCAnalyses(CGAM);
	PB.registerModuleAnalyses(MAM);
	PB.crossRegisterProxies(LAM, FAM, CGAM, MAM);

	llvm::StandardInstrumentations SI(Mod->getContext(), passes->should_debug, passes->should_verify);
	SI.registerCallbacks(PIC, &MAM);

	// Assignment tracking pass is not enabled, but could be added
	// Skipping TargetLibraryAnalysis

	llvm::OptimizationLevel level;
	switch (passes->opt_level)
	{
		case LLVM_O0:
			level = llvm::OptimizationLevel::O0;
			break;
		case LLVM_O1:
			level = llvm::OptimizationLevel::O1;
			break;
		case LLVM_O2:
			level = llvm::OptimizationLevel::O2;
			break;
		case LLVM_O3:
			level = llvm::OptimizationLevel::O3;
			break;
#if LLVM_VERSION_MAJOR >= 23
		case LLVM_Os:
		case LLVM_Oz:
			level = llvm::OptimizationLevel::O2;
			break;
#else
		case LLVM_Os:
			level = llvm::OptimizationLevel::Os;
			break;
		case LLVM_Oz:
			level = llvm::OptimizationLevel::Oz;
			break;
#endif
		default:
			exit(-1);
	}
#if LLVM_VERSION_MAJOR > 19
	llvm::ModulePassManager MPM = PB.buildPerModuleDefaultPipeline(level, llvm::ThinOrFullLTOPhase::None);
#else
	llvm::ModulePassManager MPM = PB.buildPerModuleDefaultPipeline(level, false);
#endif
	if (passes->should_verify)
	{
		MPM.addPass(llvm::VerifierPass());
	}
	if (passes->sanitizer.mem_sanitize)
	{
		llvm::MemorySanitizerOptions options(passes->sanitizer.mem_track_origins,
											 passes->sanitizer.recover,
											 passes->is_kernel,
											 passes->sanitizer.mem_retval);

		MPM.addPass(llvm::MemorySanitizerPass(options));
		if (passes->opt_level != LLVM_O0)
		{
			// Add another function pass like Clang does.
			MPM.addPass(llvm::RequireAnalysisPass<llvm::GlobalsAA, llvm::Module>());
			llvm::FunctionPassManager FPM;
			FPM.addPass(llvm::EarlyCSEPass(true /* Enable mem-ssa. */));
			FPM.addPass(llvm::InstCombinePass());
			FPM.addPass(llvm::JumpThreadingPass());
			FPM.addPass(llvm::GVNPass());
			FPM.addPass(llvm::InstCombinePass());
			MPM.addPass(createModuleToFunctionPassAdaptor(std::move(FPM)));
		}
	}
	if (passes->sanitizer.thread_sanitize)
	{
		MPM.addPass(llvm::ModuleThreadSanitizerPass());
		MPM.addPass(createModuleToFunctionPassAdaptor(llvm::ThreadSanitizerPass()));
	}

	if (passes->sanitizer.address_sanitize)
	{
		llvm::AddressSanitizerOptions Opts;
		bool use_globals_gc = false; // No need to add this yet.
		Opts.CompileKernel = passes->is_kernel;
		Opts.Recover = passes->sanitizer.recover;
		Opts.UseAfterScope = passes->sanitizer.asan_use_after_scope;
		Opts.UseAfterReturn = passes->sanitizer.asan_use_after_return
				? llvm::AsanDetectStackUseAfterReturnMode::Always
				: llvm::AsanDetectStackUseAfterReturnMode::Never;
		bool is_windows = Machine->getTargetTriple().isOSWindows();
		MPM.addPass(llvm::AddressSanitizerPass(Opts,
											   use_globals_gc,
											   !is_windows,
											   passes->sanitizer.asan_use_global_dstor
												? llvm::AsanDtorKind::Global
												: llvm::AsanDtorKind::None));
	}
	if (passes->sanitizer.hwaddress_sanitize)
	{
		MPM.addPass(llvm::HWAddressSanitizerPass({
			passes->is_kernel,
			passes->sanitizer.recover,
			passes->opt_level == LLVM_O0
		}));
	}
	// MPM.addPass(DataFlowSanitizerPass(LangOpts.NoSanitizeFiles));

	MPM.run(*Mod, MAM);
	return true;
}

bool llvm_ar(const char *out_name, const char **args, size_t count, int ArFormat)
{
	llvm::object::Archive::Kind kind;
	switch (ArFormat)
	{
		case AR_BSD:
			kind = llvm::object::Archive::K_BSD;
			break;
		case AR_DARWIN:
			kind = llvm::object::Archive::K_DARWIN;
			break;
		case AR_DARWIN64:
			kind = llvm::object::Archive::K_DARWIN64;
			break;
		case AR_GNU:
			kind = llvm::object::Archive::K_GNU;
			break;
		case AR_GNU64:
			kind = llvm::object::Archive::K_GNU64;
			break;
		case AR_COFF:
			kind = llvm::object::Archive::K_GNU;
			break;
		default:
			assert(false);
	}
	bool is_win = ArFormat == AR_COFF;
	std::vector<llvm::NewArchiveMember> new_members{};
	for (size_t i = 0; i < count; i++)
	{
		auto member = llvm::NewArchiveMember::getFile(std::string(args[i]), false);
		if (!member) return false;
		if (is_win)
		{
			// Needs relative paths.
			const char *rel_name = strrchr(args[i], '/');
			if (rel_name) member->MemberName = rel_name + 1;
		}
		new_members.push_back(std::move(*member));
	}
#if LLVM_VERSION_MAJOR > 17
	return !llvm::writeArchive(std::string(out_name), std::move(new_members), llvm::SymtabWritingMode::NormalSymtab, kind, true, false, nullptr);
#else
	return !llvm::writeArchive(std::string(out_name), std::move(new_members), true, kind, true, false, nullptr);
#endif
}

int llvm_version_major = LLVM_VERSION_MAJOR;

void LLVMSetTargetMachineUseInitArray(LLVMTargetMachineRef ref, bool use_init_array)
{
	auto machine = (llvm::TargetMachine*)ref;
	machine->Options.UseInitArray = use_init_array;
}
void LLVMSetTargetMachineEmulatedTLS(LLVMTargetMachineRef ref, bool emulated_tls)
{
	auto machine = (llvm::TargetMachine*)ref;
	machine->Options.EmulatedTLS = emulated_tls;
}
void LLVMSetDSOLocal(LLVMValueRef Global, bool value)
{
	llvm::unwrap<llvm::GlobalValue>(Global)->setDSOLocal(value);
}

void LLVMSetNoSanitizeAddress(LLVMValueRef Global)
{
	auto global = llvm::unwrap<llvm::GlobalValue>(Global);
	auto data = global->hasSanitizerMetadata() ? global->getSanitizerMetadata() : llvm::GlobalValue::SanitizerMetadata();
	data.NoAddress = 1;
	global->setSanitizerMetadata(data);
}

unsigned LLVMGetModuleInstructionCount(LLVMModuleRef module)
{
	llvm::Module *mod = llvm::unwrap(module);
	return mod->getInstructionCount();
}
unsigned LLVMGetFunctionInstructionCount(LLVMValueRef function)
{
	auto func = llvm::unwrap<llvm::Function>(function);
	return func->getInstructionCount();
}
void LLVMBuilderSetFastMathFlags(LLVMBuilderRef Builder, FastMathOption option)
{
	llvm::FastMathFlags math_flags {};
	switch (option)
	{
		case RELAXED:
			math_flags.setAllowReassoc(true);
			math_flags.setAllowReciprocal(true);
			math_flags.setAllowContract(true);
			break;
		case FAST:
			math_flags.setFast(true);
			break;
		case STRICT:
		default:
			return;
	}
	llvm::unwrap(Builder)->setFastMathFlags(math_flags);
}

bool LLVMHasUseList(LLVMValueRef value)
{
	llvm::Value *val = llvm::unwrap<llvm::Value>(value);
	return !llvm::isa<llvm::ConstantData>(*val);
}

LLVMValueRef LLVMConstBswap(LLVMValueRef ConstantVal)
{
	llvm::Constant *Val = llvm::unwrap<llvm::Constant>(ConstantVal);
	const llvm::APInt &i = Val->getUniqueInteger();
	return llvm::wrap(llvm::Constant::getIntegerValue(Val->getType(), i.byteSwap()));
}

bool LLVMDIIsTemporary(LLVMMetadataRef value)
{
	auto *Node = llvm::dyn_cast_or_null<llvm::MDNode>(llvm::unwrap(value));
	return Node && Node->isTemporary();
}

void LLVMMemCpySetVolatile(LLVMValueRef memcpy, LLVMValueRef val)
{
	llvm::MemCpyInst *inst = llvm::dyn_cast_or_null<llvm::MemCpyInst>(llvm::unwrap(memcpy));
	assert(inst);
	inst->setVolatile(llvm::unwrap<llvm::Constant>(val));
}

bool llvm_link_elf(const char **args, int arg_count, const char** error_string)
{
	return llvm_link(ELF, args, arg_count, error_string);
}

bool llvm_link_macho(const char **args, int arg_count, const char** error_string)
{
	return llvm_link(MACHO, args, arg_count, error_string);
}

bool llvm_link_coff(const char **args, int arg_count, const char** error_string)
{
	return llvm_link(COFF, args, arg_count, error_string);
}

bool llvm_link_wasm(const char **args, int arg_count, const char** error_string)
{
	return llvm_link(WASM, args, arg_count, error_string);
}

bool llvm_link_mingw(const char **args, int arg_count, const char** error_string)
{
	return llvm_link(MINGW, args, arg_count, error_string);
}


}