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

use std::io::IsTerminal;
use std::sync::Arc;
use std::time::Duration;

use console_static_text::ConsoleStaticText;
use deno_core::parking_lot::Mutex;
use deno_core::unsync::spawn_blocking;
use deno_runtime::ops::tty::ConsoleSize;
use once_cell::sync::Lazy;

use super::console::new_console_static_text;
use crate::util::console::console_size;

/// Renders text that will be displayed stacked in a
/// static place on the console.
pub trait DrawThreadRenderer: Send + Sync + std::fmt::Debug {
  fn render(&self, data: &ConsoleSize) -> String;
}

/// Draw thread guard. Keep this alive for the duration
/// that you wish the entry to be drawn for. Once it is
/// dropped, then the entry will be removed from the draw
/// thread.
#[derive(Debug)]
pub struct DrawThreadGuard(u16);

impl Drop for DrawThreadGuard {
  fn drop(&mut self) {
    DrawThread::finish_entry(self.0)
  }
}

#[derive(Debug, Clone)]
struct InternalEntry {
  id: u16,
  renderer: Arc<dyn DrawThreadRenderer>,
}

#[derive(Debug)]
struct InternalState {
  // this ensures only one actual draw thread is running
  drawer_id: usize,
  hide_count: usize,
  has_draw_thread: bool,
  next_entry_id: u16,
  entries: Vec<InternalEntry>,
  static_text: ConsoleStaticText,
}

impl InternalState {
  pub fn should_exit_draw_thread(&self, drawer_id: usize) -> bool {
    self.drawer_id != drawer_id || self.entries.is_empty()
  }
}

static INTERNAL_STATE: Lazy<Arc<Mutex<InternalState>>> = Lazy::new(|| {
  Arc::new(Mutex::new(InternalState {
    drawer_id: 0,
    hide_count: 0,
    has_draw_thread: false,
    entries: Vec::new(),
    next_entry_id: 0,
    static_text: new_console_static_text(),
  }))
});

static IS_TTY_WITH_CONSOLE_SIZE: Lazy<bool> = Lazy::new(|| {
  std::io::stderr().is_terminal()
    && console_size()
      .map(|s| s.cols > 0 && s.rows > 0)
      .unwrap_or(false)
});

/// The draw thread is responsible for rendering multiple active
/// `DrawThreadRenderer`s to stderr. It is global because the
/// concept of stderr in the process is also a global concept.
#[derive(Clone, Debug)]
pub struct DrawThread;

impl DrawThread {
  /// Is using a draw thread supported.
  pub fn is_supported() -> bool {
    // don't put the log level in the lazy because the
    // log level may change as the application runs
    log::log_enabled!(log::Level::Info) && *IS_TTY_WITH_CONSOLE_SIZE
  }

  /// Adds a renderer to the draw thread.
  pub fn add_entry(renderer: Arc<dyn DrawThreadRenderer>) -> DrawThreadGuard {
    let internal_state = &*INTERNAL_STATE;
    let mut internal_state = internal_state.lock();
    let id = internal_state.next_entry_id;
    internal_state.entries.push(InternalEntry { id, renderer });

    if internal_state.next_entry_id == u16::MAX {
      internal_state.next_entry_id = 0;
    } else {
      internal_state.next_entry_id += 1;
    }

    Self::maybe_start_draw_thread(&mut internal_state);

    DrawThreadGuard(id)
  }

  /// Hides the draw thread.
  pub fn hide() {
    let internal_state = &*INTERNAL_STATE;
    let mut internal_state = internal_state.lock();
    let is_showing =
      internal_state.has_draw_thread && internal_state.hide_count == 0;
    internal_state.hide_count += 1;

    if is_showing {
      // Clear it on the current thread in order to stop it from
      // showing immediately. Also, don't stop the draw thread here
      // because the calling code might be called from outside a
      // tokio runtime and when it goes to start the thread on the
      // thread pool it might panic.
      internal_state.static_text.eprint_clear();
    }
  }

  /// Shows the draw thread if it was previously hidden.
  pub fn show() {
    let internal_state = &*INTERNAL_STATE;
    let mut internal_state = internal_state.lock();
    if internal_state.hide_count > 0 {
      internal_state.hide_count -= 1;
    }
  }

  fn finish_entry(entry_id: u16) {
    let internal_state = &*INTERNAL_STATE;
    let mut internal_state = internal_state.lock();

    if let Some(index) =
      internal_state.entries.iter().position(|e| e.id == entry_id)
    {
      internal_state.entries.remove(index);

      if internal_state.entries.is_empty() {
        Self::clear_and_stop_draw_thread(&mut internal_state);
      }
    }
  }

  fn clear_and_stop_draw_thread(internal_state: &mut InternalState) {
    if internal_state.has_draw_thread {
      internal_state.static_text.eprint_clear();
      // bump the drawer id to exit the draw thread
      internal_state.drawer_id += 1;
      internal_state.has_draw_thread = false;
    }
  }

  fn maybe_start_draw_thread(internal_state: &mut InternalState) {
    if internal_state.has_draw_thread
      || internal_state.entries.is_empty()
      || !DrawThread::is_supported()
    {
      return;
    }

    internal_state.drawer_id += 1;
    internal_state.has_draw_thread = true;

    let drawer_id = internal_state.drawer_id;
    spawn_blocking(move || {
      let mut previous_size = console_size();
      loop {
        let mut delay_ms = 120;
        {
          // Get the entries to render.
          let maybe_entries = {
            let internal_state = &*INTERNAL_STATE;
            let internal_state = internal_state.lock();
            if internal_state.should_exit_draw_thread(drawer_id) {
              break;
            }
            let should_display = internal_state.hide_count == 0;
            should_display.then(|| internal_state.entries.clone())
          };

          if let Some(entries) = maybe_entries {
            // this should always be set, but have the code handle
            // it not being for some reason
            let size = console_size();

            // Call into the renderers outside the lock to prevent a potential
            // deadlock between our internal state lock and the renderers
            // internal state lock.
            //
            // Example deadlock if this code didn't do this:
            // 1. Other thread - Renderer - acquired internal lock to update state
            // 2. This thread  - Acquired internal state
            // 3. Other thread - Renderer - drops DrawThreadGuard
            // 4. This thread - Calls renderer.render within internal lock,
            //    which attempts to acquire the other thread's Render's internal
            //    lock causing a deadlock
            let mut text = String::new();
            if size != previous_size {
              // means the user is actively resizing the console...
              // wait a little bit until they stop resizing
              previous_size = size;
              delay_ms = 200;
            } else if let Some(size) = size {
              let mut should_new_line_next = false;
              for entry in entries {
                let new_text = entry.renderer.render(&size);
                if should_new_line_next && !new_text.is_empty() {
                  text.push('\n');
                }
                should_new_line_next = !new_text.is_empty();
                text.push_str(&new_text);
              }

              // now reacquire the lock, ensure we should still be drawing, then
              // output the text
              {
                let internal_state = &*INTERNAL_STATE;
                let mut internal_state = internal_state.lock();
                if internal_state.should_exit_draw_thread(drawer_id) {
                  break;
                }
                internal_state.static_text.eprint_with_size(
                  &text,
                  console_static_text::ConsoleSize {
                    cols: Some(size.cols as u16),
                    rows: Some(size.rows as u16),
                  },
                );
              }
            }
          }
        }

        std::thread::sleep(Duration::from_millis(delay_ms));
      }
    });
  }
}
