use std::env::{self, VarError};
use std::str::FromStr;
use std::sync::{Mutex, OnceLock};

use rustc_log::tracing;
use rustc_middle::ty::TyCtxt;
use rustc_session::{CtfeBacktrace, EarlyDiagCtxt};

/// The tracing layer from `tracing-chrome` starts a thread in the background that saves data to
/// file and closes the file when stopped. If the thread is not stopped properly, the file will be
/// missing end terminators (`]` for JSON arrays) and other data may also not be flushed. Therefore
/// we need to keep a guard that, when [Drop]ped, will send a signal to stop the thread. Make sure
/// to manually drop this guard using [deinit_loggers], if you are exiting the program with
/// [std::process::exit]!
#[must_use]
struct TracingGuard {
    #[cfg(feature = "tracing")]
    _chrome: super::tracing_chrome::FlushGuard,
    _no_construct: (),
}

// This ensures TracingGuard is always a drop-type, even when the `_chrome` field is disabled.
impl Drop for TracingGuard {
    fn drop(&mut self) {}
}

fn rustc_logger_config() -> rustc_log::LoggerConfig {
    // Start with the usual env vars.
    let mut cfg = rustc_log::LoggerConfig::from_env("RUSTC_LOG");

    // Overwrite if MIRI_LOG is set.
    if let Ok(var) = env::var("MIRI_LOG") {
        // MIRI_LOG serves as default for RUSTC_LOG, if that is not set.
        if matches!(cfg.filter, Err(VarError::NotPresent)) {
            // We try to be a bit clever here: if `MIRI_LOG` is just a single level
            // used for everything, we only apply it to the parts of rustc that are
            // CTFE-related. Otherwise, we use it verbatim for `RUSTC_LOG`.
            // This way, if you set `MIRI_LOG=trace`, you get only the right parts of
            // rustc traced, but you can also do `MIRI_LOG=miri=trace,rustc_const_eval::interpret=debug`.
            if tracing::Level::from_str(&var).is_ok() {
                cfg.filter = Ok(format!(
                    "rustc_middle::mir::interpret={var},rustc_const_eval::interpret={var},miri={var}"
                ));
            } else {
                cfg.filter = Ok(var);
            }
        }
    }

    cfg
}

/// The global logger can only be set once per process, so track whether that already happened and
/// keep a [TracingGuard] so it can be [Drop]ped later using [deinit_loggers].
static LOGGER_INITED: OnceLock<Mutex<Option<TracingGuard>>> = OnceLock::new();

fn init_logger_once(early_dcx: &EarlyDiagCtxt) {
    // If the logger is not yet initialized, initialize it.
    LOGGER_INITED.get_or_init(|| {
        let guard = if env::var_os("MIRI_TRACING").is_some() {
            #[cfg(not(feature = "tracing"))]
            {
                crate::fatal_error!(
                    "Cannot enable MIRI_TRACING since Miri was not built with the \"tracing\" feature"
                );
            }

            #[cfg(feature = "tracing")]
            {
                let (chrome_layer, chrome_guard) =
                    super::tracing_chrome::ChromeLayerBuilder::new().include_args(true).build();
                rustc_driver::init_logger_with_additional_layer(
                    early_dcx,
                    rustc_logger_config(),
                    || {
                        rustc_log::tracing_subscriber::layer::SubscriberExt::with(
                            rustc_log::tracing_subscriber::Registry::default(),
                            chrome_layer,
                        )
                    },
                );

                Some(TracingGuard { _chrome: chrome_guard, _no_construct: () })
            }
        } else {
            // initialize the logger without any tracing enabled
            rustc_driver::init_logger(early_dcx, rustc_logger_config());
            None
        };
        Mutex::new(guard)
    });
}

pub fn init_early_loggers(early_dcx: &EarlyDiagCtxt) {
    // We only initialize `rustc` if the env var is set (so the user asked for it).
    // If it is not set, we avoid initializing now so that we can initialize later with our custom
    // settings, and *not* log anything for what happens before `miri` starts interpreting.
    if env::var_os("RUSTC_LOG").is_some() {
        init_logger_once(early_dcx);
    }
}

pub fn init_late_loggers(early_dcx: &EarlyDiagCtxt, tcx: TyCtxt<'_>) {
    // If the logger is not yet initialized, initialize it.
    init_logger_once(early_dcx);

    // If `MIRI_BACKTRACE` is set and `RUSTC_CTFE_BACKTRACE` is not, set `RUSTC_CTFE_BACKTRACE`.
    // Do this late, so we ideally only apply this to Miri's errors.
    if let Some(val) = env::var_os("MIRI_BACKTRACE") {
        let ctfe_backtrace = match &*val.to_string_lossy() {
            "immediate" => CtfeBacktrace::Immediate,
            "0" => CtfeBacktrace::Disabled,
            _ => CtfeBacktrace::Capture,
        };
        *tcx.sess.ctfe_backtrace.borrow_mut() = ctfe_backtrace;
    }
}

/// Must be called before the program terminates to ensure the trace file is closed correctly. Not
/// doing so will result in invalid trace files. Also see [TracingGuard].
pub fn deinit_loggers() {
    if let Some(guard) = LOGGER_INITED.get()
        && let Ok(mut guard) = guard.lock()
    {
        std::mem::drop(guard.take());
    }
}
