//! Editor console: captures `log` records into a ring buffer the Console panel
//! renders.
//!
//! The engine and modules already speak through the `log` crate — in particular
//! the scripting layer routes script `print`/`debug` and "script paused: …"
//! errors to `target: "oxide_script"` (see `oxide-script`). This module installs
//! a logger that mirrors every record into an in-memory ring buffer *and* still
//! forwards it to `env_logger` for the terminal, so the editor's Console panel
//! can show script output and errors without the engine knowing about the editor.
//!
//! The buffer is a process global (the `log` facade allows only one logger, set
//! once at startup), reached by the panel through [`log_buffer`] — so wiring it
//! in touches neither `Shell::new` nor its many test call sites.

use std::collections::VecDeque;
use std::sync::{Arc, Mutex, OnceLock};

use log::{Level, Log, Metadata, Record};

/// How many recent log lines the console keeps. Older lines are dropped.
const CAPACITY: usize = 2000;

/// One captured log record, flattened to what the panel renders.
#[derive(Debug, Clone)]
pub struct LogLine {
    /// Severity, used to colour the line.
    pub level: Level,
    /// The record's target (e.g. `oxide_script`), shown dimmed before the text.
    pub target: String,
    /// The formatted message.
    pub message: String,
}

/// A bounded ring buffer of the most recent [`LogLine`]s.
#[derive(Default)]
pub struct LogBuffer {
    lines: VecDeque<LogLine>,
}

impl LogBuffer {
    /// Appends a line, evicting the oldest if at capacity.
    fn push(&mut self, line: LogLine) {
        if self.lines.len() == CAPACITY {
            self.lines.pop_front();
        }
        self.lines.push_back(line);
    }

    /// Iterates the buffered lines, oldest first.
    pub fn iter(&self) -> impl Iterator<Item = &LogLine> {
        self.lines.iter()
    }

    /// The number of buffered lines.
    pub fn len(&self) -> usize {
        self.lines.len()
    }

    /// Whether the buffer is empty.
    pub fn is_empty(&self) -> bool {
        self.lines.is_empty()
    }

    /// Drops all buffered lines (the panel's Clear button).
    pub fn clear(&mut self) {
        self.lines.clear();
    }
}

/// The process-wide capture buffer, set by [`init`].
static LOG_BUFFER: OnceLock<Arc<Mutex<LogBuffer>>> = OnceLock::new();

/// The shared capture buffer, if logging has been initialised.
pub fn log_buffer() -> Option<&'static Arc<Mutex<LogBuffer>>> {
    LOG_BUFFER.get()
}

/// Appends a line to the console from outside the `log` stream — used by the
/// command terminal to echo commands and stream a process's output into the
/// same panel. No-op if logging is not initialised.
pub fn append(level: Level, target: &str, message: impl Into<String>) {
    if let Some(buffer) = LOG_BUFFER.get() {
        if let Ok(mut buffer) = buffer.lock() {
            buffer.push(LogLine {
                level,
                target: target.to_string(),
                message: message.into(),
            });
        }
    }
}

/// A logger that mirrors records into [`LOG_BUFFER`] and forwards them to an
/// inner `env_logger` for the terminal.
struct CaptureLogger {
    inner: env_logger::Logger,
    buffer: Arc<Mutex<LogBuffer>>,
}

impl Log for CaptureLogger {
    fn enabled(&self, metadata: &Metadata) -> bool {
        self.inner.enabled(metadata)
    }

    fn log(&self, record: &Record) {
        // Honour the env filter for both the terminal and the buffer, so
        // RUST_LOG controls the console too.
        if !self.inner.enabled(record.metadata()) {
            return;
        }
        if let Ok(mut buffer) = self.buffer.lock() {
            buffer.push(LogLine {
                level: record.level(),
                target: record.target().to_string(),
                message: record.args().to_string(),
            });
        }
        self.inner.log(record);
    }

    fn flush(&self) {
        self.inner.flush();
    }
}

/// Installs the capturing logger and returns the shared buffer. Mirrors the old
/// `env_logger` setup (honours `RUST_LOG`, default `info`) but also feeds the
/// editor Console. Call once at startup, before any logging.
pub fn init() {
    let inner =
        env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).build();
    let max = inner.filter();
    let buffer = Arc::new(Mutex::new(LogBuffer::default()));
    let _ = LOG_BUFFER.set(buffer.clone());

    if log::set_boxed_logger(Box::new(CaptureLogger { inner, buffer })).is_ok() {
        log::set_max_level(max);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ring_buffer_evicts_oldest_past_capacity() {
        let mut buf = LogBuffer::default();
        for i in 0..(CAPACITY + 10) {
            buf.push(LogLine {
                level: Level::Info,
                target: "t".into(),
                message: format!("line {i}"),
            });
        }
        assert_eq!(buf.len(), CAPACITY);
        // The oldest 10 were evicted, so the first surviving line is "line 10".
        assert_eq!(buf.iter().next().unwrap().message, "line 10");
    }

    #[test]
    fn clear_empties_the_buffer() {
        let mut buf = LogBuffer::default();
        buf.push(LogLine {
            level: Level::Warn,
            target: "t".into(),
            message: "x".into(),
        });
        assert!(!buf.is_empty());
        buf.clear();
        assert!(buf.is_empty());
    }
}
