use std::collections::HashSet;
use std::env;
use std::sync::Arc;
use std::time::Instant;

use gccjit::{CType, Context, FunctionType, GlobalKind};
use rustc_codegen_ssa::ModuleCodegen;
use rustc_codegen_ssa::base::maybe_create_entry_wrapper;
use rustc_codegen_ssa::mono_item::MonoItemExt;
use rustc_codegen_ssa::traits::DebugInfoCodegenMethods;
use rustc_hir::attrs::{AttributeKind, Linkage};
use rustc_hir::find_attr;
use rustc_middle::dep_graph;
#[cfg(feature = "master")]
use rustc_middle::mono::Visibility;
use rustc_middle::ty::TyCtxt;
use rustc_session::config::DebugInfo;
use rustc_span::Symbol;
#[cfg(feature = "master")]
use rustc_target::spec::SymbolVisibility;
use rustc_target::spec::{Arch, RelocModel};

use crate::builder::Builder;
use crate::context::CodegenCx;
use crate::{GccContext, LockedTargetInfo, LtoMode, SyncContext, gcc_util, new_context};

#[cfg(feature = "master")]
pub fn visibility_to_gcc(visibility: Visibility) -> gccjit::Visibility {
    match visibility {
        Visibility::Default => gccjit::Visibility::Default,
        Visibility::Hidden => gccjit::Visibility::Hidden,
        Visibility::Protected => gccjit::Visibility::Protected,
    }
}

#[cfg(feature = "master")]
pub fn symbol_visibility_to_gcc(visibility: SymbolVisibility) -> gccjit::Visibility {
    match visibility {
        SymbolVisibility::Hidden => gccjit::Visibility::Hidden,
        SymbolVisibility::Protected => gccjit::Visibility::Protected,
        SymbolVisibility::Interposable => gccjit::Visibility::Default,
    }
}

pub fn global_linkage_to_gcc(linkage: Linkage) -> GlobalKind {
    match linkage {
        Linkage::External => GlobalKind::Imported,
        Linkage::AvailableExternally => GlobalKind::Imported,
        Linkage::LinkOnceAny => unimplemented!(),
        Linkage::LinkOnceODR => unimplemented!(),
        Linkage::WeakAny => unimplemented!(),
        Linkage::WeakODR => unimplemented!(),
        Linkage::Internal => GlobalKind::Internal,
        Linkage::ExternalWeak => GlobalKind::Imported, // FIXME(antoyo): should be weak linkage.
        Linkage::Common => unimplemented!(),
    }
}

pub fn linkage_to_gcc(linkage: Linkage) -> FunctionType {
    match linkage {
        Linkage::External => FunctionType::Exported,
        // FIXME(antoyo): set the attribute externally_visible.
        Linkage::AvailableExternally => FunctionType::Extern,
        Linkage::LinkOnceAny => unimplemented!(),
        Linkage::LinkOnceODR => unimplemented!(),
        Linkage::WeakAny => FunctionType::Exported, // FIXME(antoyo): should be similar to linkonce.
        Linkage::WeakODR => unimplemented!(),
        Linkage::Internal => FunctionType::Internal,
        Linkage::ExternalWeak => unimplemented!(),
        Linkage::Common => unimplemented!(),
    }
}

pub fn compile_codegen_unit(
    tcx: TyCtxt<'_>,
    cgu_name: Symbol,
    target_info: LockedTargetInfo,
    lto_supported: bool,
) -> (ModuleCodegen<GccContext>, u64) {
    let prof_timer = tcx.prof.generic_activity("codegen_module");
    let start_time = Instant::now();

    let dep_node = tcx.codegen_unit(cgu_name).codegen_dep_node(tcx);
    let (module, _) = tcx.dep_graph.with_task(
        dep_node,
        tcx,
        || module_codegen(tcx, cgu_name, target_info, lto_supported),
        Some(dep_graph::hash_result),
    );
    let time_to_codegen = start_time.elapsed();
    drop(prof_timer);

    // We assume that the cost to run GCC on a CGU is proportional to
    // the time we needed for codegenning it.
    let cost = time_to_codegen.as_secs() * 1_000_000_000 + time_to_codegen.subsec_nanos() as u64;

    fn module_codegen(
        tcx: TyCtxt<'_>,
        cgu_name: Symbol,
        target_info: LockedTargetInfo,
        lto_supported: bool,
    ) -> ModuleCodegen<GccContext> {
        let cgu = tcx.codegen_unit(cgu_name);
        // Instantiate monomorphizations without filling out definitions yet...
        let context = new_context(tcx);

        if tcx.sess.panic_strategy().unwinds() {
            context.add_command_line_option("-fexceptions");
            context.add_driver_option("-fexceptions");
        }

        let disabled_features: HashSet<_> = tcx
            .sess
            .opts
            .cg
            .target_feature
            .split(',')
            .filter(|feature| feature.starts_with('-'))
            .map(|string| &string[1..])
            .collect();

        if !disabled_features.contains("avx") && tcx.sess.target.arch == Arch::X86_64 {
            // NOTE: we always enable AVX because the equivalent of llvm.x86.sse2.cmp.pd in GCC for
            // SSE2 is multiple builtins, so we use the AVX __builtin_ia32_cmppd instead.
            // FIXME(antoyo): use the proper builtins for llvm.x86.sse2.cmp.pd and similar.
            context.add_command_line_option("-mavx");
        }

        for arg in &tcx.sess.opts.cg.llvm_args {
            context.add_command_line_option(arg);
        }
        // NOTE: This is needed to compile the file src/intrinsic/archs.rs during a bootstrap of rustc.
        context.add_command_line_option("-fno-var-tracking-assignments");
        // NOTE: an optimization (https://github.com/rust-lang/rustc_codegen_gcc/issues/53).
        context.add_command_line_option("-fno-semantic-interposition");
        // NOTE: Rust relies on LLVM not doing TBAA (https://github.com/rust-lang/unsafe-code-guidelines/issues/292).
        context.add_command_line_option("-fno-strict-aliasing");
        // NOTE: Rust relies on LLVM doing wrapping on overflow.
        context.add_command_line_option("-fwrapv");

        // NOTE: We need to honor the `#![no_builtins]` attribute to prevent GCC from
        // replacing code patterns (like loops) with calls to builtins (like memset).
        // The `-fno-tree-loop-distribute-patterns` flag disables the loop distribution pass
        // that transforms loops into calls to library functions (memset, memcpy, etc.).
        // See GCC handling for more details:
        // https://github.com/rust-lang/gcc/blob/efdd0a7290c22f5438d7c5380105d353ee3e8518/gcc/c-family/c-opts.cc#L953
        let crate_attrs = tcx.hir_attrs(rustc_hir::CRATE_HIR_ID);
        if find_attr!(crate_attrs, AttributeKind::NoBuiltins) {
            context.add_command_line_option("-fno-tree-loop-distribute-patterns");
        }

        if let Some(model) = tcx.sess.code_model() {
            use rustc_target::spec::CodeModel;

            context.add_command_line_option(match model {
                CodeModel::Tiny => "-mcmodel=tiny",
                CodeModel::Small => "-mcmodel=small",
                CodeModel::Kernel => "-mcmodel=kernel",
                CodeModel::Medium => "-mcmodel=medium",
                CodeModel::Large => "-mcmodel=large",
            });
        }

        add_pic_option(&context, tcx.sess.relocation_model());

        let target_cpu = gcc_util::target_cpu(tcx.sess);
        if target_cpu != "generic" {
            context.add_command_line_option(format!("-march={}", target_cpu));
        }

        if tcx
            .sess
            .opts
            .unstable_opts
            .function_sections
            .unwrap_or(tcx.sess.target.function_sections)
        {
            context.add_command_line_option("-ffunction-sections");
            context.add_command_line_option("-fdata-sections");
        }

        if env::var("CG_GCCJIT_DUMP_RTL").as_deref() == Ok("1") {
            context.add_command_line_option("-fdump-rtl-vregs");
        }
        if env::var("CG_GCCJIT_DUMP_RTL_ALL").as_deref() == Ok("1") {
            context.add_command_line_option("-fdump-rtl-all");
        }
        if env::var("CG_GCCJIT_DUMP_TREE_ALL").as_deref() == Ok("1") {
            context.add_command_line_option("-fdump-tree-all-eh");
        }
        if env::var("CG_GCCJIT_DUMP_IPA_ALL").as_deref() == Ok("1") {
            context.add_command_line_option("-fdump-ipa-all-eh");
        }
        if env::var("CG_GCCJIT_DUMP_CODE").as_deref() == Ok("1") {
            context.set_dump_code_on_compile(true);
        }
        if env::var("CG_GCCJIT_DUMP_GIMPLE").as_deref() == Ok("1") {
            context.set_dump_initial_gimple(true);
        }
        if env::var("CG_GCCJIT_DUMP_EVERYTHING").as_deref() == Ok("1") {
            context.set_dump_everything(true);
        }
        if env::var("CG_GCCJIT_KEEP_INTERMEDIATES").as_deref() == Ok("1") {
            context.set_keep_intermediates(true);
        }
        if env::var("CG_GCCJIT_VERBOSE").as_deref() == Ok("1") {
            context.add_driver_option("-v");
        }

        // NOTE: The codegen generates unreachable blocks.
        context.set_allow_unreachable_blocks(true);

        {
            // FIXME: to make it less error-prone (calling get_target_info() will add the flag
            // -fsyntax-only), forbid the compilation when get_target_info() is called on a
            // context.
            let f16_type_supported = target_info.supports_target_dependent_type(CType::Float16);
            let f32_type_supported = target_info.supports_target_dependent_type(CType::Float32);
            let f64_type_supported = target_info.supports_target_dependent_type(CType::Float64);
            let f128_type_supported = target_info.supports_target_dependent_type(CType::Float128);
            let u128_type_supported = target_info.supports_target_dependent_type(CType::UInt128t);
            // FIXME: improve this to avoid passing that many arguments.
            let mut cx = CodegenCx::new(
                &context,
                cgu,
                tcx,
                u128_type_supported,
                f16_type_supported,
                f32_type_supported,
                f64_type_supported,
                f128_type_supported,
            );

            let mono_items = cgu.items_in_deterministic_order(tcx);
            for &(mono_item, data) in &mono_items {
                mono_item.predefine::<Builder<'_, '_, '_>>(
                    &mut cx,
                    cgu_name.as_str(),
                    data.linkage,
                    data.visibility,
                );
            }

            // ... and now that we have everything pre-defined, fill out those definitions.
            for &(mono_item, item_data) in &mono_items {
                mono_item.define::<Builder<'_, '_, '_>>(&mut cx, cgu_name.as_str(), item_data);
            }

            // If this codegen unit contains the main function, also create the
            // wrapper here
            maybe_create_entry_wrapper::<Builder<'_, '_, '_>>(&cx, cx.codegen_unit);

            // Finalize debuginfo
            if cx.sess().opts.debuginfo != DebugInfo::None {
                cx.debuginfo_finalize();
            }
        }

        ModuleCodegen::new_regular(
            cgu_name.to_string(),
            GccContext {
                context: Arc::new(SyncContext::new(context)),
                relocation_model: tcx.sess.relocation_model(),
                lto_supported,
                lto_mode: LtoMode::None,
                temp_dir: None,
            },
        )
    }

    (module, cost)
}

pub fn add_pic_option<'gcc>(context: &Context<'gcc>, relocation_model: RelocModel) {
    match relocation_model {
        rustc_target::spec::RelocModel::Static => {
            context.add_command_line_option("-fno-pie");
            context.add_driver_option("-fno-pie");
        }
        rustc_target::spec::RelocModel::Pic => {
            context.add_command_line_option("-fPIC");
            // NOTE: we use both add_command_line_option and add_driver_option because the usage in
            // this module (compile_codegen_unit) requires add_command_line_option while the usage
            // in the back::write module (codegen) requires add_driver_option.
            context.add_driver_option("-fPIC");
        }
        rustc_target::spec::RelocModel::Pie => {
            context.add_command_line_option("-fPIE");
            context.add_driver_option("-fPIE");
        }
        model => eprintln!("Unsupported relocation model: {:?}", model),
    }
}
