/*
 * FIXME(antoyo): implement equality in libgccjit based on https://zpz.github.io/blog/overloading-equality-operator-in-cpp-class-hierarchy/ (for type equality?)
 * For Thin LTO, this might be helpful:
// cspell:disable-next-line
 * In gcc 4.6 -fwhopr was removed and became default with -flto. The non-whopr path can still be executed via -flto-partition=none.
 * Or the new incremental LTO (https://www.phoronix.com/news/GCC-Incremental-LTO-Patches)?
 *
 * Maybe some missing optimizations enabled by rustc's LTO is in there: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
// cspell:disable-next-line
 * Like -fipa-icf (should be already enabled) and maybe -fdevirtualize-at-ltrans.
 * FIXME: disable debug info always being emitted. Perhaps this slows down things?
 *
 * FIXME(antoyo): remove the patches.
 */

#![feature(rustc_private)]
#![recursion_limit = "256"]
#![warn(rust_2018_idioms)]
#![warn(unused_lifetimes)]
#![deny(clippy::pattern_type_mismatch)]
#![expect(clippy::uninlined_format_args)]
#![allow(clippy::collapsible_match)]

// The rustc crates we need
extern crate rustc_abi;
extern crate rustc_apfloat;
extern crate rustc_ast;
extern crate rustc_codegen_ssa;
extern crate rustc_data_structures;
extern crate rustc_errors;
extern crate rustc_fs_util;
extern crate rustc_hir;
extern crate rustc_index;
#[cfg(feature = "master")]
extern crate rustc_interface;
extern crate rustc_log;
extern crate rustc_macros;
extern crate rustc_middle;
extern crate rustc_session;
extern crate rustc_span;
extern crate rustc_symbol_mangling;
extern crate rustc_target;
extern crate rustc_type_ir;

// This prevents duplicating functions and statics that are already part of the host rustc process.
#[expect(unused_extern_crates)]
extern crate rustc_driver;

mod abi;
mod allocator;
mod asm;
mod attributes;
mod back;
mod base;
mod builder;
mod callee;
mod common;
mod consts;
mod context;
mod coverageinfo;
mod debuginfo;
mod declare;
mod errors;
mod gcc_util;
mod int;
mod intrinsic;
mod mono_item;
mod type_;
mod type_of;

use std::any::Any;
use std::ffi::CString;
use std::fmt::Debug;
use std::fs;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};

use gccjit::{CType, Context, OptimizationLevel};
#[cfg(feature = "master")]
use gccjit::{TargetInfo, Version};
use rustc_ast::expand::allocator::AllocatorMethod;
use rustc_codegen_ssa::back::lto::ThinModule;
use rustc_codegen_ssa::back::write::{
    CodegenContext, FatLtoInput, ModuleConfig, SharedEmitter, TargetMachineFactoryFn, ThinLtoInput,
};
use rustc_codegen_ssa::base::codegen_crate;
use rustc_codegen_ssa::target_features::cfg_target_feature;
use rustc_codegen_ssa::traits::{CodegenBackend, ExtraBackendMethods, WriteBackendMethods};
use rustc_codegen_ssa::{CompiledModule, CompiledModules, CrateInfo, ModuleCodegen, TargetConfig};
use rustc_data_structures::fx::FxIndexMap;
use rustc_data_structures::profiling::SelfProfilerRef;
use rustc_data_structures::sync::IntoDynSyncSend;
use rustc_errors::{DiagCtxt, DiagCtxtHandle};
use rustc_middle::dep_graph::{WorkProduct, WorkProductId};
use rustc_middle::ty::TyCtxt;
use rustc_middle::util::Providers;
use rustc_session::Session;
use rustc_session::config::{OptLevel, OutputFilenames};
use rustc_span::Symbol;
use rustc_target::spec::{Arch, RelocModel};
use tempfile::TempDir;

use crate::back::lto::ModuleBuffer;
use crate::gcc_util::{target_cpu, to_gcc_features};

pub struct PrintOnPanic<F: Fn() -> String>(pub F);

impl<F: Fn() -> String> Drop for PrintOnPanic<F> {
    fn drop(&mut self) {
        if ::std::thread::panicking() {
            println!("{}", (self.0)());
        }
    }
}

#[cfg(not(feature = "master"))]
#[derive(Debug)]
pub struct TargetInfo {
    supports_128bit_integers: AtomicBool,
}

#[cfg(not(feature = "master"))]
impl TargetInfo {
    fn cpu_supports(&self, _feature: &str) -> bool {
        false
    }

    fn supports_target_dependent_type(&self, typ: CType) -> bool {
        match typ {
            CType::UInt128t | CType::Int128t => {
                if self.supports_128bit_integers.load(Ordering::SeqCst) {
                    return true;
                }
            }
            _ => (),
        }
        false
    }
}

#[derive(Clone)]
pub struct LockedTargetInfo {
    info: Arc<Mutex<IntoDynSyncSend<Option<TargetInfo>>>>,
}

impl Debug for LockedTargetInfo {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.info.lock().expect("lock").fmt(formatter)
    }
}

impl LockedTargetInfo {
    fn cpu_supports(&self, feature: &str) -> bool {
        self.info
            .lock()
            .expect("lock")
            .as_ref()
            .expect("target info not initialized")
            .cpu_supports(feature)
    }

    fn supports_target_dependent_type(&self, typ: CType) -> bool {
        self.info
            .lock()
            .expect("lock")
            .as_ref()
            .expect("target info not initialized")
            .supports_target_dependent_type(typ)
    }
}

#[derive(Clone)]
pub struct GccCodegenBackend {
    target_info: LockedTargetInfo,
    lto_supported: Arc<AtomicBool>,
}

fn load_libgccjit_if_needed(libgccjit_target_lib_file: &Path) {
    if gccjit::is_loaded() {
        // Do not load a libgccjit second time.
        return;
    }

    let path = libgccjit_target_lib_file.to_str().expect("libgccjit path");

    let string = CString::new(path).expect("string to libgccjit path");

    if let Err(error) = gccjit::load(&string) {
        panic!("Cannot load libgccjit.so: {}", error);
    }
}

impl CodegenBackend for GccCodegenBackend {
    fn name(&self) -> &'static str {
        "gcc"
    }

    fn init(&self, sess: &Session) {
        fn file_path(sysroot_path: &Path, sess: &Session) -> PathBuf {
            let rustlib_path =
                rustc_target::relative_target_rustlib_path(sysroot_path, &sess.host.llvm_target);
            sysroot_path
                .join(rustlib_path)
                .join("codegen-backends")
                .join("lib")
                .join(sess.target.llvm_target.as_ref())
                .join("libgccjit.so")
        }

        // We use all_paths() instead of only path() in case the path specified by --sysroot is
        // invalid.
        // This is the case for instance in Rust for Linux where they specify --sysroot=/dev/null.
        for path in sess.opts.sysroot.all_paths() {
            let libgccjit_target_lib_file = file_path(path, sess);
            if let Ok(true) = fs::exists(&libgccjit_target_lib_file) {
                load_libgccjit_if_needed(&libgccjit_target_lib_file);
                break;
            }
        }

        if !gccjit::is_loaded() {
            let mut paths = vec![];
            for path in sess.opts.sysroot.all_paths() {
                let libgccjit_target_lib_file = file_path(path, sess);
                paths.push(libgccjit_target_lib_file);
            }

            panic!("Could not load libgccjit.so. Attempted paths: {:#?}", paths);
        }

        #[cfg(feature = "master")]
        {
            gccjit::set_lang_name(c"GNU Rust");

            let target_cpu = target_cpu(sess);

            // Get the second TargetInfo with the correct CPU features by setting the arch.
            let context = Context::default();
            if target_cpu != "generic" {
                context.add_command_line_option(format!("-march={}", target_cpu));
            }

            *self.target_info.info.lock().expect("lock") =
                IntoDynSyncSend(Some(context.get_target_info()));
        }

        #[cfg(feature = "master")]
        {
            let lto_supported = gccjit::is_lto_supported();
            self.lto_supported.store(lto_supported, Ordering::SeqCst);

            gccjit::set_global_personality_function_name(b"rust_eh_personality\0");
        }

        #[cfg(not(feature = "master"))]
        {
            let temp_dir = TempDir::new().expect("cannot create temporary directory");
            let temp_file = temp_dir.keep().join("result.asm");
            let check_context = Context::default();
            check_context.set_print_errors_to_stderr(false);
            let _int128_ty = check_context.new_c_type(CType::UInt128t);
            // NOTE: we cannot just call compile() as this would require other files than libgccjit.so.
            check_context.compile_to_file(
                gccjit::OutputKind::Assembler,
                temp_file.to_str().expect("path to str"),
            );
            self.target_info
                .info
                .lock()
                .expect("lock")
                .0
                .as_ref()
                .expect("target info not initialized")
                .supports_128bit_integers
                .store(check_context.get_last_error() == Ok(None), Ordering::SeqCst);
        }
    }

    fn thin_lto_supported(&self) -> bool {
        false
    }

    fn provide(&self, providers: &mut Providers) {
        providers.queries.global_backend_features =
            |tcx, ()| gcc_util::global_gcc_features(tcx.sess)
    }

    fn target_cpu(&self, sess: &Session) -> String {
        target_cpu(sess).to_owned()
    }

    fn codegen_crate(&self, tcx: TyCtxt<'_>, crate_info: &CrateInfo) -> Box<dyn Any> {
        Box::new(codegen_crate(self.clone(), tcx, crate_info))
    }

    fn join_codegen(
        &self,
        ongoing_codegen: Box<dyn Any>,
        sess: &Session,
        _outputs: &OutputFilenames,
    ) -> (CompiledModules, FxIndexMap<WorkProductId, WorkProduct>) {
        ongoing_codegen
            .downcast::<rustc_codegen_ssa::back::write::OngoingCodegen<GccCodegenBackend>>()
            .expect("Expected GccCodegenBackend's OngoingCodegen, found Box<Any>")
            .join(sess)
    }

    fn target_config(&self, sess: &Session) -> TargetConfig {
        target_config(sess, &self.target_info)
    }
}

fn new_context<'gcc, 'tcx>(tcx: TyCtxt<'tcx>) -> Context<'gcc> {
    let context = Context::default();
    if matches!(tcx.sess.target.arch, Arch::X86 | Arch::X86_64) {
        context.add_command_line_option("-masm=intel");
    }
    #[cfg(feature = "master")]
    {
        context.set_special_chars_allowed_in_func_names("$.*");
        let version = Version::get();
        let version = format!("{}.{}.{}", version.major, version.minor, version.patch);
        context.set_output_ident(&format!(
            "rustc version {} with libgccjit {}",
            rustc_interface::util::rustc_version_str().unwrap_or("unknown version"),
            version,
        ));
    }
    // FIXME(antoyo): check if this should only be added when using -Cforce-unwind-tables=n.
    context.add_command_line_option("-fno-asynchronous-unwind-tables");
    context
}

impl ExtraBackendMethods for GccCodegenBackend {
    fn supports_parallel(&self) -> bool {
        false
    }

    fn codegen_allocator(
        &self,
        tcx: TyCtxt<'_>,
        module_name: &str,
        methods: &[AllocatorMethod],
    ) -> Self::Module {
        let lto_supported = self.lto_supported.load(Ordering::SeqCst);
        let mut mods = GccContext {
            context: Arc::new(SyncContext::new(new_context(tcx))),
            relocation_model: tcx.sess.relocation_model(),
            lto_mode: LtoMode::None,
            lto_supported,
            temp_dir: None,
        };

        unsafe {
            allocator::codegen(tcx, &mut mods, module_name, methods);
        }
        mods
    }

    fn compile_codegen_unit(
        &self,
        tcx: TyCtxt<'_>,
        cgu_name: Symbol,
    ) -> (ModuleCodegen<Self::Module>, u64) {
        base::compile_codegen_unit(
            tcx,
            cgu_name,
            self.target_info.clone(),
            self.lto_supported.load(Ordering::SeqCst),
        )
    }
}

#[derive(Clone, Copy, PartialEq)]
pub enum LtoMode {
    None,
    Thin,
    Fat,
}

pub struct GccContext {
    context: Arc<SyncContext>,
    /// This field is needed in order to be able to set the flag -fPIC when necessary when doing
    /// LTO.
    relocation_model: RelocModel,
    lto_mode: LtoMode,
    lto_supported: bool,
    // Temporary directory used by LTO. We keep it here so that it's not removed before linking.
    temp_dir: Option<TempDir>,
}

struct SyncContext {
    context: Context<'static>,
}

impl SyncContext {
    fn new(context: Context<'static>) -> Self {
        Self { context }
    }
}

impl Deref for SyncContext {
    type Target = Context<'static>;

    fn deref(&self) -> &Self::Target {
        &self.context
    }
}

unsafe impl Send for SyncContext {}
// FIXME(antoyo): that shouldn't be Sync. Parallel compilation is currently disabled with "CodegenBackend::supports_parallel()".
unsafe impl Sync for SyncContext {}

impl WriteBackendMethods for GccCodegenBackend {
    type Module = GccContext;
    type TargetMachine = ();
    type ModuleBuffer = ModuleBuffer;
    type ThinData = ();

    fn target_machine_factory(
        &self,
        _sess: &Session,
        _opt_level: OptLevel,
        _features: &[String],
    ) -> TargetMachineFactoryFn<Self> {
        // FIXME(antoyo): set opt level.
        Arc::new(|_, _| ())
    }

    fn optimize_and_codegen_fat_lto(
        cgcx: &CodegenContext,
        prof: &SelfProfilerRef,
        shared_emitter: &SharedEmitter,
        _tm_factory: TargetMachineFactoryFn<Self>,
        // FIXME(bjorn3): Limit LTO exports to these symbols
        _exported_symbols_for_lto: &[String],
        each_linked_rlib_for_lto: &[PathBuf],
        modules: Vec<FatLtoInput<Self>>,
    ) -> CompiledModule {
        back::lto::run_fat(cgcx, prof, shared_emitter, each_linked_rlib_for_lto, modules)
    }

    fn run_thin_lto(
        _cgcx: &CodegenContext,
        _prof: &SelfProfilerRef,
        _dcx: DiagCtxtHandle<'_>,
        // FIXME(bjorn3): Limit LTO exports to these symbols
        _exported_symbols_for_lto: &[String],
        _each_linked_rlib_for_lto: &[PathBuf],
        _modules: Vec<ThinLtoInput<Self>>,
    ) -> (Vec<ThinModule<Self>>, Vec<WorkProduct>) {
        unreachable!()
    }

    fn optimize(
        _cgcx: &CodegenContext,
        _prof: &SelfProfilerRef,
        _shared_emitter: &SharedEmitter,
        module: &mut ModuleCodegen<Self::Module>,
        config: &ModuleConfig,
    ) {
        module.module_llvm.context.set_optimization_level(to_gcc_opt_level(config.opt_level));
    }

    fn optimize_and_codegen_thin(
        _cgcx: &CodegenContext,
        _prof: &SelfProfilerRef,
        _shared_emitter: &SharedEmitter,
        _tm_factory: TargetMachineFactoryFn<Self>,
        _thin: ThinModule<Self>,
    ) -> CompiledModule {
        unreachable!()
    }

    fn codegen(
        cgcx: &CodegenContext,
        prof: &SelfProfilerRef,
        shared_emitter: &SharedEmitter,
        module: ModuleCodegen<Self::Module>,
        config: &ModuleConfig,
    ) -> CompiledModule {
        let dcx = DiagCtxt::new(Box::new(shared_emitter.clone()));
        let dcx = dcx.handle();
        back::write::codegen(cgcx, prof, dcx, module, config)
    }

    fn serialize_module(_module: Self::Module, _is_thin: bool) -> Self::ModuleBuffer {
        unimplemented!();
    }
}

/// This is the entrypoint for a hot plugged rustc_codegen_gccjit
#[unsafe(no_mangle)]
pub fn __rustc_codegen_backend() -> Box<dyn CodegenBackend> {
    #[cfg(feature = "master")]
    let info = {
        // Check whether the target supports 128-bit integers, and sized floating point types (like
        // Float16).
        Arc::new(Mutex::new(IntoDynSyncSend(None)))
    };
    #[cfg(not(feature = "master"))]
    let info = Arc::new(Mutex::new(IntoDynSyncSend(Some(TargetInfo {
        supports_128bit_integers: AtomicBool::new(false),
    }))));

    Box::new(GccCodegenBackend {
        lto_supported: Arc::new(AtomicBool::new(false)),
        target_info: LockedTargetInfo { info },
    })
}

fn to_gcc_opt_level(optlevel: Option<OptLevel>) -> OptimizationLevel {
    match optlevel {
        None => OptimizationLevel::None,
        Some(level) => match level {
            OptLevel::No => OptimizationLevel::None,
            OptLevel::Less => OptimizationLevel::Limited,
            OptLevel::More => OptimizationLevel::Standard,
            OptLevel::Aggressive => OptimizationLevel::Aggressive,
            OptLevel::Size | OptLevel::SizeMin => OptimizationLevel::Limited,
        },
    }
}

/// Returns the features that should be set in `cfg(target_feature)`.
fn target_config(sess: &Session, target_info: &LockedTargetInfo) -> TargetConfig {
    let (unstable_target_features, target_features) = cfg_target_feature(
        sess,
        |feature| to_gcc_features(sess, feature),
        |feature| {
            // FIXME: we disable Neon for now since we don't support the LLVM intrinsics for it.
            if feature == "neon" {
                return false;
            }
            target_info.cpu_supports(feature)
            // cSpell:disable
            /*
              adx, aes, avx, avx2, avx512bf16, avx512bitalg, avx512bw, avx512cd, avx512dq, avx512er, avx512f, avx512fp16, avx512ifma,
              avx512pf, avx512vbmi, avx512vbmi2, avx512vl, avx512vnni, avx512vp2intersect, avx512vpopcntdq,
              bmi1, bmi2, cmpxchg16b, ermsb, f16c, fma, fxsr, gfni, lzcnt, movbe, pclmulqdq, popcnt, rdrand, rdseed, rtm,
              sha, sse, sse2, sse3, sse4.1, sse4.2, sse4a, ssse3, tbm, vaes, vpclmulqdq, xsave, xsavec, xsaveopt, xsaves
            */
            // cSpell:enable
        },
    );

    let has_reliable_f16 = target_info.supports_target_dependent_type(CType::Float16);
    let has_reliable_f128 = target_info.supports_target_dependent_type(CType::Float128);

    TargetConfig {
        target_features,
        unstable_target_features,
        // There are no known bugs with GCC support for f16 or f128
        has_reliable_f16,
        has_reliable_f16_math: has_reliable_f16,
        has_reliable_f128,
        has_reliable_f128_math: has_reliable_f128,
    }
}
