// SPDX-License-Identifier: MPL-2.0

use alloc::{sync::Arc, vec::Vec};

use aster_console::{
    AnyConsoleDevice, ConsoleCallback, ConsoleSetFontError,
    font::BitmapFont,
    mode::{ConsoleMode, KeyboardMode},
};
use ostd::{
    mm::{HasSize, VmReader},
    sync::{LocalIrqDisabled, SpinLock},
};
use spin::Once;

use crate::{
    FRAMEBUFFER, FrameBuffer, Pixel,
    ansi_escape::{EscapeFsm, EscapeOp},
};

/// A text console rendered onto the framebuffer.
pub struct FramebufferConsole {
    callbacks: SpinLock<ConsoleCallbacks, LocalIrqDisabled>,
    inner: SpinLock<(ConsoleState, EscapeFsm), LocalIrqDisabled>,
}

pub const CONSOLE_NAME: &str = "Framebuffer-Console";

pub static FRAMEBUFFER_CONSOLE: Once<Arc<FramebufferConsole>> = Once::new();

pub(crate) fn init() {
    let Some(fb) = FRAMEBUFFER.get() else {
        log::warn!("Framebuffer not initialized");
        return;
    };

    FRAMEBUFFER_CONSOLE.call_once(|| Arc::new(FramebufferConsole::new(fb.clone())));
}

impl AnyConsoleDevice for FramebufferConsole {
    fn send(&self, buf: &[u8]) {
        let mut inner = self.inner.lock();
        let (state, esc_fsm) = &mut *inner;

        for byte in buf {
            if esc_fsm.eat(*byte, state) {
                // The character is part of an ANSI escape sequence.
                continue;
            }

            if *byte == 0 {
                // The character is a NUL character.
                continue;
            }

            state.send_char(*byte);
        }
    }

    fn register_callback(&self, callback: &'static ConsoleCallback) {
        self.callbacks.lock().callbacks.push(callback);
    }

    fn set_font(&self, font: BitmapFont) -> Result<(), ConsoleSetFontError> {
        self.inner.lock().0.set_font(font)
    }

    fn set_mode(&self, mode: ConsoleMode) -> bool {
        self.inner.lock().0.set_mode(mode);
        true
    }

    fn mode(&self) -> Option<ConsoleMode> {
        Some(self.inner.lock().0.mode())
    }

    fn set_keyboard_mode(&self, mode: KeyboardMode) -> bool {
        match mode {
            KeyboardMode::Xlate => self.callbacks.lock().is_input_enabled = true,
            KeyboardMode::Off => self.callbacks.lock().is_input_enabled = false,
            _ => return false,
        }
        true
    }

    fn keyboard_mode(&self) -> Option<KeyboardMode> {
        if self.callbacks.lock().is_input_enabled {
            Some(KeyboardMode::Xlate)
        } else {
            Some(KeyboardMode::Off)
        }
    }
}

impl FramebufferConsole {
    /// Creates a new framebuffer console.
    pub(self) fn new(framebuffer: Arc<FrameBuffer>) -> Self {
        let callbacks = ConsoleCallbacks {
            callbacks: Vec::new(),
            is_input_enabled: true,
        };

        let state = ConsoleState {
            x_pos: 0,
            y_pos: 0,
            fg_color: Pixel::WHITE,
            bg_color: Pixel::BLACK,
            font: BitmapFont::new_basic8x8(),
            is_output_enabled: true,

            bytes: alloc::vec![0u8; framebuffer.io_mem().size()],
            backend: framebuffer,
        };

        let esc_fsm = EscapeFsm::new();

        Self {
            callbacks: SpinLock::new(callbacks),
            inner: SpinLock::new((state, esc_fsm)),
        }
    }

    /// Triggers the registered input callbacks with the given data.
    pub(crate) fn trigger_input_callbacks(&self, bytes: &[u8]) {
        let callbacks = self.callbacks.lock();
        if !callbacks.is_input_enabled {
            return;
        }

        let reader = VmReader::from(bytes);
        for callback in callbacks.callbacks.iter() {
            callback(reader.clone());
        }
    }
}

impl core::fmt::Debug for FramebufferConsole {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("FramebufferConsole").finish_non_exhaustive()
    }
}

struct ConsoleCallbacks {
    callbacks: Vec<&'static ConsoleCallback>,
    /// Whether the input characters will be handled by the callbacks.
    is_input_enabled: bool,
}

#[derive(Debug)]
struct ConsoleState {
    x_pos: usize,
    y_pos: usize,
    fg_color: Pixel,
    bg_color: Pixel,
    font: BitmapFont,
    /// Whether the output characters will be drawn in the framebuffer.
    is_output_enabled: bool,

    bytes: Vec<u8>,
    backend: Arc<FrameBuffer>,
}

impl ConsoleState {
    /// Sends a single character to be drawn on the framebuffer.
    pub(self) fn send_char(&mut self, ch: u8) {
        if ch == b'\n' {
            self.newline();
            return;
        } else if ch == b'\r' {
            self.carriage_return();
            return;
        } else if ch == b'\x08' {
            self.backspace();
            return;
        }

        if self.x_pos > self.backend.width() - self.font.width() {
            self.newline();
        }

        self.draw_char(ch);

        self.x_pos += self.font.width();
    }

    fn newline(&mut self) {
        self.y_pos += self.font.height();
        self.x_pos = 0;

        if self.y_pos > self.backend.height() - self.font.height() {
            self.shift_lines_up();
        }
    }

    fn shift_lines_up(&mut self) {
        let offset = self.backend.calc_offset(0, self.font.height()).as_usize();
        self.bytes.copy_within(offset.., 0);
        self.bytes[self.backend.io_mem().size() - offset..].fill(0);

        if self.is_output_enabled {
            self.backend.write_bytes_at(0, &self.bytes).unwrap();
        }

        self.y_pos -= self.font.height();
    }

    fn carriage_return(&mut self) {
        self.x_pos = 0;
    }

    fn backspace(&mut self) {
        if self.x_pos < self.font.width() {
            // TODO: What should we do if we're at the beginning of the line?
            return;
        }

        self.x_pos -= self.font.width();
        self.draw_char(b' ');
    }

    fn draw_char(&mut self, ch: u8) {
        let Some(font_ch) = self.font.char(ch) else {
            return;
        };

        let fg_pixel = self.backend.render_pixel(self.fg_color);
        let bg_pixel = self.backend.render_pixel(self.bg_color);

        let pixel_size = fg_pixel.nbytes();

        let mut offset = self.backend.calc_offset(self.x_pos, self.y_pos);

        for row in font_ch.rows() {
            let off_st = offset.as_usize();
            let off_ed = off_st + pixel_size * self.font.width();
            let render_buf = &mut self.bytes[off_st..off_ed];

            // Write pixels to the console buffer.
            let chunks = render_buf.chunks_exact_mut(pixel_size);
            for (chunk, is_fg) in chunks.zip(row.bits()) {
                let pixel = if is_fg { fg_pixel } else { bg_pixel };
                chunk.copy_from_slice(pixel.as_slice());
            }

            // Write pixels to the framebuffer.
            if self.is_output_enabled {
                self.backend.write_bytes_at(off_st, render_buf).unwrap();
            }

            offset.y_add(1);
        }
    }

    /// Sets the font for the framebuffer console.
    pub(self) fn set_font(&mut self, font: BitmapFont) -> Result<(), ConsoleSetFontError> {
        // Note that the font height cannot exceed the half the height of the framebuffer.
        // Otherwise, `shift_lines_up` will underflow `x_pos`.
        if font.width() > self.backend.width() || font.height() > self.backend.height() / 2 {
            return Err(ConsoleSetFontError::InvalidFont);
        }

        self.font = font;

        if self.y_pos > self.backend.height() - self.font.height() {
            self.shift_lines_up();
        }

        Ok(())
    }

    /// Sets the console mode (text or graphics).
    pub(self) fn set_mode(&mut self, mode: ConsoleMode) {
        if mode == ConsoleMode::Graphics {
            self.is_output_enabled = false;
            return;
        }

        if self.is_output_enabled {
            return;
        }

        // We're switching from the graphics mode back to the text mode. The characters need to be
        // redrawn in the framebuffer.
        self.is_output_enabled = true;
        self.backend.write_bytes_at(0, &self.bytes).unwrap();
    }

    /// Gets the current console mode.
    pub(self) fn mode(&self) -> ConsoleMode {
        if self.is_output_enabled {
            ConsoleMode::Text
        } else {
            ConsoleMode::Graphics
        }
    }
}

impl EscapeOp for ConsoleState {
    fn set_cursor(&mut self, x: usize, y: usize) {
        let max_x = self.backend.width() / self.font.width() - 1;
        let max_y = self.backend.height() / self.font.height() - 1;

        // Note that if the Y (or X) position is too large, the cursor will move to the last line
        // (or the line end).
        self.x_pos = x.min(max_x) * self.font.width();
        self.y_pos = y.min(max_y) * self.font.height();
    }

    fn set_fg_color(&mut self, val: Pixel) {
        self.fg_color = val;
    }

    fn set_bg_color(&mut self, val: Pixel) {
        self.bg_color = val;
    }
}
