//! Framebuffer console driver

use alloc::sync::Arc;
use libk_util::sync::IrqSafeSpinlock;
use yggdrasil_abi::error::Error;

use crate::debug::DebugSink;

use super::{
    access::KernelFramebufferAccess,
    console::{self, Attributes, ConsoleBuffer, ConsoleState, DisplayConsole},
    font::PcScreenFont,
    Color, DisplayDevice,
};

struct Inner {
    display: Arc<dyn DisplayDevice>,
    font: PcScreenFont<'static>,
    char_width: u32,
    char_height: u32,
    width: u32,
    height: u32,
    cursor_row: u32,
    cursor_col: u32,
    boot_framebuffer: bool,
}

struct DrawGlyph {
    sx: u32,
    sy: u32,
    c: u8,
    fg: Color,
    bg: Color,
    bytes_per_line: usize,
}

/// Framebuffer console device wrapper
pub struct FramebufferConsole {
    inner: IrqSafeSpinlock<Inner>,
    state: IrqSafeSpinlock<ConsoleState>,
}

impl DebugSink for FramebufferConsole {
    fn putc(&self, c: u8) -> Result<(), Error> {
        self.write_char(c);
        Ok(())
    }

    fn supports_control_sequences(&self) -> bool {
        true
    }
}

impl DisplayConsole for FramebufferConsole {
    fn state(&self) -> &IrqSafeSpinlock<ConsoleState> {
        &self.state
    }

    fn flush(&self, state: &mut ConsoleState) {
        let mut inner = self.inner.lock();
        let Ok(mut framebuffer) = KernelFramebufferAccess::acquire(inner.display.clone()) else {
            return;
        };
        let font = inner.font;
        let cw = inner.char_width;
        let ch = inner.char_height;

        let bytes_per_line = (font.width() as usize).div_ceil(8);

        let mut iter = state.buffer.flush_rows();

        let old_cursor_col = inner.cursor_col;
        let old_cursor_row = inner.cursor_row;
        // New cursor
        let cursor_col = state.cursor_col;
        let cursor_row = state.cursor_row;

        framebuffer.fill_rect(
            old_cursor_col * cw,
            old_cursor_row * ch,
            cw,
            ch,
            state.bg_color.as_rgba(false),
        );

        while let Some((row_idx, row)) = iter.next_dirty() {
            if row_idx >= inner.height {
                break;
            }

            for (col_idx, &chr) in row.iter().take(inner.width as _).enumerate() {
                let glyph = chr.character();
                let (fg, bg, attr) = chr.attributes();

                let mut fg = fg.as_rgba(attr.contains(Attributes::BOLD));
                let mut bg = bg.as_rgba(false);

                if row_idx == cursor_row && col_idx == cursor_col as usize {
                    core::mem::swap(&mut fg, &mut bg);
                }

                let glyph = DrawGlyph {
                    sx: (col_idx as u32) * cw,
                    sy: row_idx * ch,
                    c: glyph,
                    fg,
                    bg,
                    bytes_per_line,
                };
                draw_glyph(&mut framebuffer, font, glyph);
            }
        }

        // Place cursor
        framebuffer.fill_rect(
            cursor_col * cw,
            cursor_row * ch,
            cw,
            ch,
            state.fg_color.as_rgba(false),
        );
        inner.display.synchronize().ok();

        inner.cursor_col = cursor_col;
        inner.cursor_row = cursor_row;
    }
}

impl FramebufferConsole {
    /// Constructs an instance of console from its framebuffer reference.
    pub fn from_framebuffer(
        display: Arc<dyn DisplayDevice>,
        font: Option<PcScreenFont<'static>>,
        boot: bool,
    ) -> Result<Self, Error> {
        let mode = display
            .active_display_mode()
            .ok_or(Error::InvalidOperation)?;

        let font = font.unwrap_or_default();
        let char_width = font.width();
        let char_height = font.height();

        let buffer = ConsoleBuffer::new(mode.height / char_height)?;

        let inner = Inner {
            display,
            font,
            width: mode.width / char_width,
            height: mode.height / char_height,
            char_width,
            char_height,
            cursor_row: 0,
            cursor_col: 0,
            boot_framebuffer: boot,
        };

        Ok(Self {
            inner: IrqSafeSpinlock::new(inner),
            state: IrqSafeSpinlock::new(ConsoleState::new(buffer)),
        })
    }

    /// Changes the display used for this console
    pub fn set_display(&self, display: Arc<dyn DisplayDevice>) -> Result<(), Error> {
        let mode = display
            .active_display_mode()
            .ok_or(Error::InvalidOperation)?;

        {
            let mut old_inner = self.inner.lock();
            let mut old_state = self.state.lock();

            let char_width = old_inner.char_width;
            let char_height = old_inner.char_height;

            let buffer = ConsoleBuffer::new(mode.height / char_height)?;

            let inner = Inner {
                display: display.clone(),
                font: old_inner.font,
                width: mode.width / char_width,
                height: mode.height / char_height,
                char_width,
                char_height,
                cursor_row: 0,
                cursor_col: 0,
                boot_framebuffer: false,
            };

            *old_inner = inner;
            *old_state = ConsoleState::new(buffer);
        }

        // Clear the display
        if let Ok(mut framebuffer) = KernelFramebufferAccess::acquire(display) {
            framebuffer.fill_rect(
                0,
                0,
                mode.width,
                mode.height,
                console::DEFAULT_BG_COLOR.as_rgba(false),
            );
        }

        Ok(())
    }

    /// Returns `true` if the console is using a boot-time framebuffer
    pub fn uses_boot_framebuffer(&self) -> bool {
        self.inner.lock().boot_framebuffer
    }
}

fn draw_glyph(
    framebuffer: &mut KernelFramebufferAccess,
    font: PcScreenFont<'static>,
    g: DrawGlyph,
) {
    let mut c = g.c as u32;
    if c >= font.len() {
        c = b'?' as u32;
    }

    let f = framebuffer.format();
    let fg = g.fg.to_pixel_u32(f);
    let bg = g.bg.to_pixel_u32(f);

    let mut glyph = font.raw_glyph_data(c);

    let mut y = 0;

    while y < font.height() {
        let mut mask = 1 << (font.width() - 1);
        let mut x = 0;

        while x < font.width() {
            let v = if glyph[0] & mask != 0 { fg } else { bg };
            framebuffer.set_pixel(g.sx + x, g.sy + y, v);
            mask >>= 1;
            x += 1;
        }

        glyph = &glyph[g.bytes_per_line..];
        y += 1;
    }
}
