// Copyright 2018-2026 the Deno authors. MIT license.

use std::fs;
use std::io::prelude::*;
use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::thread;
use std::time::SystemTime;

use chrono::DateTime;
use chrono::Utc;
use deno_core::parking_lot::Mutex;

static LSP_DEBUG_FLAG: AtomicBool = AtomicBool::new(false);
static LSP_LOG_LEVEL: AtomicUsize = AtomicUsize::new(log::Level::Info as usize);
#[allow(dead_code, reason = "part of LSP logging infrastructure")]
static LSP_WARN_LEVEL: AtomicUsize =
  AtomicUsize::new(log::Level::Warn as usize);
static LOG_FILE: LogFile = LogFile {
  enabled: AtomicBool::new(true),
  buffer: Mutex::new(String::new()),
};

pub struct LogFile {
  enabled: AtomicBool,
  buffer: Mutex<String>,
}

impl LogFile {
  pub fn write_line(&self, s: &str) {
    if LOG_FILE.enabled.load(Ordering::Relaxed) {
      let mut buffer = self.buffer.lock();
      buffer.push_str(s);
      buffer.push('\n');
    }
  }

  fn commit(&self, path: &Path) {
    let unbuffered = {
      let mut buffer = self.buffer.lock();
      if buffer.is_empty() {
        return;
      }
      // We clone here rather than take so the buffer can retain its capacity.
      let unbuffered = buffer.clone();
      buffer.clear();
      unbuffered
    };
    if let Ok(file) = fs::OpenOptions::new().append(true).open(path) {
      write!(&file, "{}", unbuffered).ok();
    }
  }
}

pub fn init_log_file(enabled: bool, cwd: &Path) {
  let prepare_path = || {
    if !enabled {
      return None;
    }
    let now = SystemTime::now();
    let now: DateTime<Utc> = now.into();
    let now = now.to_rfc3339().replace(':', "_");
    let path = cwd.join(format!(".deno_lsp/log_{}.txt", now));
    fs::create_dir_all(path.parent()?).ok()?;
    fs::write(&path, "").ok()?;
    Some(path)
  };
  let Some(path) = prepare_path() else {
    LOG_FILE.enabled.store(false, Ordering::Relaxed);
    LOG_FILE.buffer.lock().clear();
    return;
  };
  thread::spawn(move || {
    loop {
      LOG_FILE.commit(&path);
      thread::sleep(std::time::Duration::from_secs(1));
    }
  });
}

pub fn write_line_to_log_file(s: &str) {
  LOG_FILE.write_line(s);
}

pub fn set_lsp_debug_flag(value: bool) {
  LSP_DEBUG_FLAG.store(value, Ordering::SeqCst)
}

pub fn lsp_debug_enabled() -> bool {
  LSP_DEBUG_FLAG.load(Ordering::SeqCst)
}

/// Change the lsp to log at the provided level.
#[allow(dead_code, reason = "part of LSP logging infrastructure")]
pub fn set_lsp_log_level(level: log::Level) {
  LSP_LOG_LEVEL.store(level as usize, Ordering::SeqCst)
}

pub fn lsp_log_level() -> log::Level {
  let level = LSP_LOG_LEVEL.load(Ordering::SeqCst);
  // SAFETY: level is only ever stored from valid log::Level values
  unsafe { std::mem::transmute(level) }
}

/// Change the lsp to warn at the provided level.
#[allow(dead_code, reason = "part of LSP logging infrastructure")]
pub fn set_lsp_warn_level(level: log::Level) {
  LSP_WARN_LEVEL.store(level as usize, Ordering::SeqCst)
}

pub fn lsp_warn_level() -> log::Level {
  let level = LSP_LOG_LEVEL.load(Ordering::SeqCst);
  // SAFETY: level is only ever stored from valid log::Level values
  unsafe { std::mem::transmute(level) }
}

/// Use this macro to do "info" logs in the lsp code. This allows
/// for downgrading these logs to another log level in the REPL.
macro_rules! lsp_log {
  ($($arg:tt)+) => (
    let lsp_log_level = $crate::lsp::logging::lsp_log_level();
    if lsp_log_level == log::Level::Debug {
      $crate::lsp::logging::lsp_debug!($($arg)+)
    } else {
      let s = std::format!($($arg)+);
      $crate::lsp::logging::write_line_to_log_file(&s);
      log::log!(lsp_log_level, "{}", s)
    }
  )
}

/// Use this macro to do "warn" logs in the lsp code. This allows
/// for downgrading these logs to another log level in the REPL.
macro_rules! lsp_warn {
  ($($arg:tt)+) => (
    {
      let lsp_log_level = $crate::lsp::logging::lsp_warn_level();
      if lsp_log_level == log::Level::Debug {
        $crate::lsp::logging::lsp_debug!($($arg)+)
      } else {
        let s = std::format!($($arg)+);
        $crate::lsp::logging::write_line_to_log_file(&s);
        log::log!(lsp_log_level, "{}", s)
      }
    }
  )
}

macro_rules! lsp_debug {
  ($($arg:tt)+) => (
    {
      let s = std::format!($($arg)+);
      $crate::lsp::logging::write_line_to_log_file(&s);
      if $crate::lsp::logging::lsp_debug_enabled() {
        log::debug!("{}", s)
      }
    }
  )
}

macro_rules! lsp_tracing_info_span {
  ($($arg:tt)*) => {{
    #[cfg(feature = "lsp-tracing")]
    {
      ::tracing::info_span!($($arg)*)
    }
    #[cfg(not(feature = "lsp-tracing"))]
    {
      $crate::lsp::trace::Span {}
    }
  }};
}

macro_rules! lsp_tracing_info {
    ($($arg:tt)*) => {
      #[cfg(feature = "lsp-tracing")]
      {
        ::tracing::info!($($arg)*);
      }
      #[cfg(not(feature = "lsp-tracing"))]
      {}
    };
}

pub(super) use lsp_debug;
pub(super) use lsp_log;
pub(super) use lsp_tracing_info;
pub(super) use lsp_tracing_info_span;
pub(super) use lsp_warn;
