/* framebuffer.c — Linear framebuffer driver for KHY OS
 *
 * Parses Multiboot2 framebuffer tag to obtain the linear framebuffer
 * address, then provides pixel-level drawing and text rendering.
 *
 * The embedded 8x16 font covers ASCII 0x20-0x7E (95 printable characters).
 * Font data is a subset of the classic VGA/PC BIOS font.
 * @pattern Adapter
 */

#include "framebuffer.h"
#include "serial.h"
#include "string.h"
#include "vmm.h"
#include "cjk_font16.h"

/* ── Multiboot2 Tag Parsing ──────────────────────────────────── */

#define MB2_TAG_END         0
#define MB2_TAG_FRAMEBUFFER 8

/* Hard cap on Multiboot2 tag-walk iterations (malformed-block backstop). */
#define FB_TAG_WALK_MAX     4096

struct mb2_tag_header {
    uint32_t type;
    uint32_t size;
} __attribute__((packed));

struct mb2_tag_framebuffer {
    uint32_t type;
    uint32_t size;
    uint64_t framebuffer_addr;
    uint32_t framebuffer_pitch;
    uint32_t framebuffer_width;
    uint32_t framebuffer_height;
    uint8_t  framebuffer_bpp;
    uint8_t  framebuffer_type; /* 0=indexed, 1=RGB, 2=EGA text */
    uint16_t reserved;
} __attribute__((packed));

/* ── State ───────────────────────────────────────────────────── */

static struct fb_info fb;

/* ── 8x16 Bitmap Font (ASCII 0x20-0x7E) ─────────────────────── */

/* Each character is 16 bytes (16 rows of 8 pixels packed as bits).
 * Bit 7 = leftmost pixel. */
static const uint8_t font8x16[95][16] = {
    /* 0x20 ' ' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
    /* 0x21 '!' */ {0x00,0x00,0x18,0x3C,0x3C,0x3C,0x18,0x18,0x18,0x00,0x18,0x18,0x00,0x00,0x00,0x00},
    /* 0x22 '"' */ {0x00,0x66,0x66,0x66,0x24,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
    /* 0x23 '#' */ {0x00,0x00,0x00,0x6C,0x6C,0xFE,0x6C,0x6C,0x6C,0xFE,0x6C,0x6C,0x00,0x00,0x00,0x00},
    /* 0x24 '$' */ {0x18,0x18,0x7C,0xC6,0xC2,0xC0,0x7C,0x06,0x06,0x86,0xC6,0x7C,0x18,0x18,0x00,0x00},
    /* 0x25 '%' */ {0x00,0x00,0x00,0x00,0xC2,0xC6,0x0C,0x18,0x30,0x60,0xC6,0x86,0x00,0x00,0x00,0x00},
    /* 0x26 '&' */ {0x00,0x00,0x38,0x6C,0x6C,0x38,0x76,0xDC,0xCC,0xCC,0xCC,0x76,0x00,0x00,0x00,0x00},
    /* 0x27 '\''*/ {0x00,0x30,0x30,0x30,0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
    /* 0x28 '(' */ {0x00,0x00,0x0C,0x18,0x30,0x30,0x30,0x30,0x30,0x30,0x18,0x0C,0x00,0x00,0x00,0x00},
    /* 0x29 ')' */ {0x00,0x00,0x30,0x18,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x18,0x30,0x00,0x00,0x00,0x00},
    /* 0x2A '*' */ {0x00,0x00,0x00,0x00,0x00,0x66,0x3C,0xFF,0x3C,0x66,0x00,0x00,0x00,0x00,0x00,0x00},
    /* 0x2B '+' */ {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x7E,0x18,0x18,0x00,0x00,0x00,0x00,0x00,0x00},
    /* 0x2C ',' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x18,0x30,0x00,0x00,0x00},
    /* 0x2D '-' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
    /* 0x2E '.' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x00},
    /* 0x2F '/' */ {0x00,0x00,0x00,0x00,0x02,0x06,0x0C,0x18,0x30,0x60,0xC0,0x80,0x00,0x00,0x00,0x00},
    /* 0x30 '0' */ {0x00,0x00,0x7C,0xC6,0xC6,0xCE,0xDE,0xF6,0xE6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00},
    /* 0x31 '1' */ {0x00,0x00,0x18,0x38,0x78,0x18,0x18,0x18,0x18,0x18,0x18,0x7E,0x00,0x00,0x00,0x00},
    /* 0x32 '2' */ {0x00,0x00,0x7C,0xC6,0x06,0x0C,0x18,0x30,0x60,0xC0,0xC6,0xFE,0x00,0x00,0x00,0x00},
    /* 0x33 '3' */ {0x00,0x00,0x7C,0xC6,0x06,0x06,0x3C,0x06,0x06,0x06,0xC6,0x7C,0x00,0x00,0x00,0x00},
    /* 0x34 '4' */ {0x00,0x00,0x0C,0x1C,0x3C,0x6C,0xCC,0xFE,0x0C,0x0C,0x0C,0x1E,0x00,0x00,0x00,0x00},
    /* 0x35 '5' */ {0x00,0x00,0xFE,0xC0,0xC0,0xC0,0xFC,0x06,0x06,0x06,0xC6,0x7C,0x00,0x00,0x00,0x00},
    /* 0x36 '6' */ {0x00,0x00,0x38,0x60,0xC0,0xC0,0xFC,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00},
    /* 0x37 '7' */ {0x00,0x00,0xFE,0xC6,0x06,0x06,0x0C,0x18,0x30,0x30,0x30,0x30,0x00,0x00,0x00,0x00},
    /* 0x38 '8' */ {0x00,0x00,0x7C,0xC6,0xC6,0xC6,0x7C,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00},
    /* 0x39 '9' */ {0x00,0x00,0x7C,0xC6,0xC6,0xC6,0x7E,0x06,0x06,0x06,0x0C,0x78,0x00,0x00,0x00,0x00},
    /* 0x3A ':' */ {0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x00,0x00},
    /* 0x3B ';' */ {0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x18,0x18,0x30,0x00,0x00,0x00,0x00},
    /* 0x3C '<' */ {0x00,0x00,0x00,0x06,0x0C,0x18,0x30,0x60,0x30,0x18,0x0C,0x06,0x00,0x00,0x00,0x00},
    /* 0x3D '=' */ {0x00,0x00,0x00,0x00,0x00,0x7E,0x00,0x00,0x7E,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
    /* 0x3E '>' */ {0x00,0x00,0x00,0x60,0x30,0x18,0x0C,0x06,0x0C,0x18,0x30,0x60,0x00,0x00,0x00,0x00},
    /* 0x3F '?' */ {0x00,0x00,0x7C,0xC6,0xC6,0x0C,0x18,0x18,0x18,0x00,0x18,0x18,0x00,0x00,0x00,0x00},
    /* 0x40 '@' */ {0x00,0x00,0x00,0x7C,0xC6,0xC6,0xDE,0xDE,0xDE,0xDC,0xC0,0x7C,0x00,0x00,0x00,0x00},
    /* 0x41 'A' */ {0x00,0x00,0x10,0x38,0x6C,0xC6,0xC6,0xFE,0xC6,0xC6,0xC6,0xC6,0x00,0x00,0x00,0x00},
    /* 0x42 'B' */ {0x00,0x00,0xFC,0x66,0x66,0x66,0x7C,0x66,0x66,0x66,0x66,0xFC,0x00,0x00,0x00,0x00},
    /* 0x43 'C' */ {0x00,0x00,0x3C,0x66,0xC2,0xC0,0xC0,0xC0,0xC0,0xC2,0x66,0x3C,0x00,0x00,0x00,0x00},
    /* 0x44 'D' */ {0x00,0x00,0xF8,0x6C,0x66,0x66,0x66,0x66,0x66,0x66,0x6C,0xF8,0x00,0x00,0x00,0x00},
    /* 0x45 'E' */ {0x00,0x00,0xFE,0x66,0x62,0x68,0x78,0x68,0x60,0x62,0x66,0xFE,0x00,0x00,0x00,0x00},
    /* 0x46 'F' */ {0x00,0x00,0xFE,0x66,0x62,0x68,0x78,0x68,0x60,0x60,0x60,0xF0,0x00,0x00,0x00,0x00},
    /* 0x47 'G' */ {0x00,0x00,0x3C,0x66,0xC2,0xC0,0xC0,0xDE,0xC6,0xC6,0x66,0x3A,0x00,0x00,0x00,0x00},
    /* 0x48 'H' */ {0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xFE,0xC6,0xC6,0xC6,0xC6,0xC6,0x00,0x00,0x00,0x00},
    /* 0x49 'I' */ {0x00,0x00,0x3C,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00},
    /* 0x4A 'J' */ {0x00,0x00,0x1E,0x0C,0x0C,0x0C,0x0C,0x0C,0xCC,0xCC,0xCC,0x78,0x00,0x00,0x00,0x00},
    /* 0x4B 'K' */ {0x00,0x00,0xE6,0x66,0x66,0x6C,0x78,0x78,0x6C,0x66,0x66,0xE6,0x00,0x00,0x00,0x00},
    /* 0x4C 'L' */ {0x00,0x00,0xF0,0x60,0x60,0x60,0x60,0x60,0x60,0x62,0x66,0xFE,0x00,0x00,0x00,0x00},
    /* 0x4D 'M' */ {0x00,0x00,0xC6,0xEE,0xFE,0xFE,0xD6,0xC6,0xC6,0xC6,0xC6,0xC6,0x00,0x00,0x00,0x00},
    /* 0x4E 'N' */ {0x00,0x00,0xC6,0xE6,0xF6,0xFE,0xDE,0xCE,0xC6,0xC6,0xC6,0xC6,0x00,0x00,0x00,0x00},
    /* 0x4F 'O' */ {0x00,0x00,0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00},
    /* 0x50 'P' */ {0x00,0x00,0xFC,0x66,0x66,0x66,0x7C,0x60,0x60,0x60,0x60,0xF0,0x00,0x00,0x00,0x00},
    /* 0x51 'Q' */ {0x00,0x00,0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xD6,0xDE,0x7C,0x0C,0x0E,0x00,0x00},
    /* 0x52 'R' */ {0x00,0x00,0xFC,0x66,0x66,0x66,0x7C,0x6C,0x66,0x66,0x66,0xE6,0x00,0x00,0x00,0x00},
    /* 0x53 'S' */ {0x00,0x00,0x7C,0xC6,0xC6,0x60,0x38,0x0C,0x06,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00},
    /* 0x54 'T' */ {0x00,0x00,0xFF,0xDB,0x99,0x18,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00},
    /* 0x55 'U' */ {0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00},
    /* 0x56 'V' */ {0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x6C,0x38,0x10,0x00,0x00,0x00,0x00},
    /* 0x57 'W' */ {0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xD6,0xD6,0xD6,0xFE,0xEE,0x6C,0x00,0x00,0x00,0x00},
    /* 0x58 'X' */ {0x00,0x00,0xC6,0xC6,0x6C,0x7C,0x38,0x38,0x7C,0x6C,0xC6,0xC6,0x00,0x00,0x00,0x00},
    /* 0x59 'Y' */ {0x00,0x00,0xC3,0xC3,0x66,0x3C,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00},
    /* 0x5A 'Z' */ {0x00,0x00,0xFE,0xC6,0x86,0x0C,0x18,0x30,0x60,0xC2,0xC6,0xFE,0x00,0x00,0x00,0x00},
    /* 0x5B '[' */ {0x00,0x00,0x3C,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x3C,0x00,0x00,0x00,0x00},
    /* 0x5C '\\' */{0x00,0x00,0x00,0x80,0xC0,0xE0,0x70,0x38,0x1C,0x0E,0x06,0x02,0x00,0x00,0x00,0x00},
    /* 0x5D ']' */ {0x00,0x00,0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x3C,0x00,0x00,0x00,0x00},
    /* 0x5E '^' */ {0x10,0x38,0x6C,0xC6,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
    /* 0x5F '_' */ {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00},
    /* 0x60 '`' */ {0x30,0x30,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
    /* 0x61 'a' */ {0x00,0x00,0x00,0x00,0x00,0x78,0x0C,0x7C,0xCC,0xCC,0xCC,0x76,0x00,0x00,0x00,0x00},
    /* 0x62 'b' */ {0x00,0x00,0xE0,0x60,0x60,0x78,0x6C,0x66,0x66,0x66,0x66,0x7C,0x00,0x00,0x00,0x00},
    /* 0x63 'c' */ {0x00,0x00,0x00,0x00,0x00,0x7C,0xC6,0xC0,0xC0,0xC0,0xC6,0x7C,0x00,0x00,0x00,0x00},
    /* 0x64 'd' */ {0x00,0x00,0x1C,0x0C,0x0C,0x3C,0x6C,0xCC,0xCC,0xCC,0xCC,0x76,0x00,0x00,0x00,0x00},
    /* 0x65 'e' */ {0x00,0x00,0x00,0x00,0x00,0x7C,0xC6,0xFE,0xC0,0xC0,0xC6,0x7C,0x00,0x00,0x00,0x00},
    /* 0x66 'f' */ {0x00,0x00,0x1C,0x36,0x32,0x30,0x78,0x30,0x30,0x30,0x30,0x78,0x00,0x00,0x00,0x00},
    /* 0x67 'g' */ {0x00,0x00,0x00,0x00,0x00,0x76,0xCC,0xCC,0xCC,0xCC,0x7C,0x0C,0xCC,0x78,0x00,0x00},
    /* 0x68 'h' */ {0x00,0x00,0xE0,0x60,0x60,0x6C,0x76,0x66,0x66,0x66,0x66,0xE6,0x00,0x00,0x00,0x00},
    /* 0x69 'i' */ {0x00,0x00,0x18,0x18,0x00,0x38,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00},
    /* 0x6A 'j' */ {0x00,0x00,0x06,0x06,0x00,0x0E,0x06,0x06,0x06,0x06,0x06,0x06,0x66,0x3C,0x00,0x00},
    /* 0x6B 'k' */ {0x00,0x00,0xE0,0x60,0x60,0x66,0x6C,0x78,0x78,0x6C,0x66,0xE6,0x00,0x00,0x00,0x00},
    /* 0x6C 'l' */ {0x00,0x00,0x38,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00},
    /* 0x6D 'm' */ {0x00,0x00,0x00,0x00,0x00,0xEC,0xFE,0xD6,0xD6,0xD6,0xD6,0xC6,0x00,0x00,0x00,0x00},
    /* 0x6E 'n' */ {0x00,0x00,0x00,0x00,0x00,0xDC,0x66,0x66,0x66,0x66,0x66,0x66,0x00,0x00,0x00,0x00},
    /* 0x6F 'o' */ {0x00,0x00,0x00,0x00,0x00,0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00},
    /* 0x70 'p' */ {0x00,0x00,0x00,0x00,0x00,0xDC,0x66,0x66,0x66,0x66,0x7C,0x60,0x60,0xF0,0x00,0x00},
    /* 0x71 'q' */ {0x00,0x00,0x00,0x00,0x00,0x76,0xCC,0xCC,0xCC,0xCC,0x7C,0x0C,0x0C,0x1E,0x00,0x00},
    /* 0x72 'r' */ {0x00,0x00,0x00,0x00,0x00,0xDC,0x76,0x66,0x60,0x60,0x60,0xF0,0x00,0x00,0x00,0x00},
    /* 0x73 's' */ {0x00,0x00,0x00,0x00,0x00,0x7C,0xC6,0x60,0x38,0x0C,0xC6,0x7C,0x00,0x00,0x00,0x00},
    /* 0x74 't' */ {0x00,0x00,0x10,0x30,0x30,0xFC,0x30,0x30,0x30,0x30,0x36,0x1C,0x00,0x00,0x00,0x00},
    /* 0x75 'u' */ {0x00,0x00,0x00,0x00,0x00,0xCC,0xCC,0xCC,0xCC,0xCC,0xCC,0x76,0x00,0x00,0x00,0x00},
    /* 0x76 'v' */ {0x00,0x00,0x00,0x00,0x00,0xC3,0xC3,0xC3,0xC3,0x66,0x3C,0x18,0x00,0x00,0x00,0x00},
    /* 0x77 'w' */ {0x00,0x00,0x00,0x00,0x00,0xC6,0xC6,0xD6,0xD6,0xD6,0xFE,0x6C,0x00,0x00,0x00,0x00},
    /* 0x78 'x' */ {0x00,0x00,0x00,0x00,0x00,0xC6,0x6C,0x38,0x38,0x38,0x6C,0xC6,0x00,0x00,0x00,0x00},
    /* 0x79 'y' */ {0x00,0x00,0x00,0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xC6,0x7E,0x06,0x0C,0xF8,0x00,0x00},
    /* 0x7A 'z' */ {0x00,0x00,0x00,0x00,0x00,0xFE,0xCC,0x18,0x30,0x60,0xC6,0xFE,0x00,0x00,0x00,0x00},
    /* 0x7B '{' */ {0x00,0x00,0x0E,0x18,0x18,0x18,0x70,0x18,0x18,0x18,0x18,0x0E,0x00,0x00,0x00,0x00},
    /* 0x7C '|' */ {0x00,0x00,0x18,0x18,0x18,0x18,0x00,0x18,0x18,0x18,0x18,0x18,0x00,0x00,0x00,0x00},
    /* 0x7D '}' */ {0x00,0x00,0x70,0x18,0x18,0x18,0x0E,0x18,0x18,0x18,0x18,0x70,0x00,0x00,0x00,0x00},
    /* 0x7E '~' */ {0x00,0x00,0x76,0xDC,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
};

/* ── Initialization ──────────────────────────────────────────── */

void fb_init(uint32_t multiboot_info_addr) {
    memset(&fb, 0, sizeof(fb));

    if (multiboot_info_addr == 0) {
        serial_print("[FB] No multiboot info, framebuffer unavailable\n");
        return;
    }

    /* Parse Multiboot2 tags to find framebuffer */
    const uint8_t *mbi = (const uint8_t *)(uint64_t)multiboot_info_addr;
    uint32_t total_size = *(const uint32_t *)mbi;

    /* [SAFE] The Multiboot2 info block comes from the bootloader and may be
     * malformed or truncated. Without these guards the tag walk can (a) spin
     * forever when a tag reports size 0 — the 8-byte round-up of 0 is 0, so
     * `offset` never advances and the kernel hangs at boot — or (b) stride a
     * tag header past the end of the block and read unmapped memory. Floor the
     * size, bound every header (and the framebuffer tag body) inside the block,
     * reject any tag smaller than its own header, and cap the iteration count.
     * Mirrors the PMM Multiboot2 hardening. */
    if (total_size < 8)
        total_size = 8;

    uint32_t offset = 8; /* Skip total_size + reserved */
    for (uint32_t guard = 0;
         guard < FB_TAG_WALK_MAX &&
         offset + sizeof(struct mb2_tag_header) <= total_size;
         guard++) {
        const struct mb2_tag_header *tag = (const struct mb2_tag_header *)(mbi + offset);
        if (tag->type == MB2_TAG_END)
            break;
        if (tag->size < sizeof(struct mb2_tag_header))
            break; /* a tag must cover its own header (catches the size-0 hang) */

        if (tag->type == MB2_TAG_FRAMEBUFFER &&
            offset + sizeof(struct mb2_tag_framebuffer) <= total_size) {
            const struct mb2_tag_framebuffer *fbtag =
                (const struct mb2_tag_framebuffer *)(mbi + offset);

            /* Only use RGB framebuffer (type 1), not EGA text (type 2) */
            if (fbtag->framebuffer_type == 1 && fbtag->framebuffer_bpp >= 24) {
                fb.addr   = (uint32_t *)(uint64_t)fbtag->framebuffer_addr;
                fb.width  = fbtag->framebuffer_width;
                fb.height = fbtag->framebuffer_height;
                fb.pitch  = fbtag->framebuffer_pitch;
                fb.bpp    = fbtag->framebuffer_bpp;
                fb.available = 1;

                serial_print("[FB] Graphical framebuffer: ");
                serial_print_dec(fb.width);
                serial_print("x");
                serial_print_dec(fb.height);
                serial_print("x");
                serial_print_dec(fb.bpp);
                serial_print(" @ ");
                serial_print_hex((uint64_t)fb.addr);
                serial_print("\n");

                /* The linear framebuffer is a high-physical MMIO region
                 * (typically ~0xFD000000) that the boot-time page tables do
                 * NOT cover — boot.asm only identity-maps the first 1 GB with
                 * 2 MB huge pages. Without an explicit mapping the kernel's
                 * first pixel write page-faults. Identity-map the whole
                 * framebuffer span (pitch * height, page-rounded) into the
                 * kernel address space as PRESENT|WRITABLE 4 KB pages. */
                uint64_t fb_base  = (uint64_t)fb.addr & ~(VMM_PAGE_SIZE - 1);
                uint64_t fb_end   = (uint64_t)fb.addr +
                                    (uint64_t)fb.pitch * fb.height;
                fb_end = (fb_end + VMM_PAGE_SIZE - 1) & ~(VMM_PAGE_SIZE - 1);

                struct vm_space *ks = vmm_kernel_space();
                int map_failed = 0;
                for (uint64_t pa = fb_base; pa < fb_end; pa += VMM_PAGE_SIZE) {
                    if (vmm_map_page(ks, pa, pa,
                                     VMM_FLAG_PRESENT | VMM_FLAG_WRITABLE) != 0) {
                        map_failed = 1;
                        break;
                    }
                }

                if (map_failed) {
                    /* Mapping failed (e.g. out of page-table memory): fall back
                     * to text mode rather than panicking on the first write. */
                    fb.available = 0;
                    serial_print("[FB] ERROR: framebuffer mapping failed, "
                                 "falling back to VGA text mode\n");
                    return;
                }

                serial_print("[FB] Framebuffer region mapped (");
                serial_print_dec((fb_end - fb_base) / 1024);
                serial_print(" KB)\n");
                return;
            }

            /* Text mode framebuffer — don't use for graphics */
            if (fbtag->framebuffer_type == 2) {
                serial_print("[FB] Text-mode framebuffer detected (no graphics)\n");
            }
        }

        /* Advance to next tag (8-byte aligned), with a strict forward-progress
         * check so a rounded stride can never leave offset unchanged. */
        uint32_t next = offset + ((tag->size + 7) & ~7u);
        if (next <= offset)
            break;
        offset = next;
    }

    serial_print("[FB] No graphical framebuffer found, using VGA text mode\n");
}

const struct fb_info *fb_get_info(void) {
    return &fb;
}

int fb_is_available(void) {
    return fb.available;
}

/* ── Drawing Primitives ──────────────────────────────────────── */

void fb_putpixel(int x, int y, uint32_t color) {
    if (!fb.available || x < 0 || y < 0 ||
        (uint32_t)x >= fb.width || (uint32_t)y >= fb.height)
        return;

    /* pitch is in bytes, we write 32-bit pixels */
    uint32_t *row = (uint32_t *)((uint8_t *)fb.addr + (uint32_t)y * fb.pitch);
    row[x] = color;
}

uint32_t fb_getpixel(int x, int y) {
    if (!fb.available || x < 0 || y < 0 ||
        (uint32_t)x >= fb.width || (uint32_t)y >= fb.height)
        return 0;
    uint32_t *row = (uint32_t *)((uint8_t *)fb.addr + (uint32_t)y * fb.pitch);
    return row[x];
}

/* Alpha-composite fg over bg: out = fg*a + bg*(255-a), per 8-bit channel. */
uint32_t fb_blend(uint32_t fg, uint32_t bg, uint8_t a) {
    uint32_t ia = 255u - a;
    uint32_t r = (((fg >> 16) & 0xFF) * a + ((bg >> 16) & 0xFF) * ia) / 255u;
    uint32_t g = (((fg >> 8)  & 0xFF) * a + ((bg >> 8)  & 0xFF) * ia) / 255u;
    uint32_t b = (((fg)       & 0xFF) * a + ((bg)       & 0xFF) * ia) / 255u;
    return (r << 16) | (g << 8) | b;
}

/* Vertical gradient fill: row y interpolates linearly from `top` to `bottom`. */
void fb_fill_vgradient(int x, int y, int w, int h, uint32_t top, uint32_t bottom) {
    if (!fb.available || h <= 0)
        return;
    for (int row = 0; row < h; row++) {
        /* a in [0,255]: 0 at top row, 255 at bottom row. */
        uint8_t a = (h == 1) ? 0 : (uint8_t)((row * 255) / (h - 1));
        fb_fill_rect(x, y + row, w, 1, fb_blend(bottom, top, a));
    }
}

/* Blend `color` over the existing pixels in a rectangle at the given alpha —
 * used for soft drop shadows and translucent overlays. */
void fb_fill_rect_alpha(int x, int y, int w, int h, uint32_t color, uint8_t alpha) {
    if (!fb.available)
        return;
    int x0 = x < 0 ? 0 : x;
    int y0 = y < 0 ? 0 : y;
    int x1 = (x + w > (int)fb.width) ? (int)fb.width : x + w;
    int y1 = (y + h > (int)fb.height) ? (int)fb.height : y + h;
    for (int py = y0; py < y1; py++) {
        uint32_t *row = (uint32_t *)((uint8_t *)fb.addr + (uint32_t)py * fb.pitch);
        for (int px = x0; px < x1; px++) {
            row[px] = fb_blend(color, row[px], alpha);
        }
    }
}

void fb_fill_rect(int x, int y, int w, int h, uint32_t color) {
    if (!fb.available)
        return;

    /* Clip */
    int x0 = x < 0 ? 0 : x;
    int y0 = y < 0 ? 0 : y;
    int x1 = (x + w > (int)fb.width) ? (int)fb.width : x + w;
    int y1 = (y + h > (int)fb.height) ? (int)fb.height : y + h;

    for (int py = y0; py < y1; py++) {
        uint32_t *row = (uint32_t *)((uint8_t *)fb.addr + (uint32_t)py * fb.pitch);
        for (int px = x0; px < x1; px++) {
            row[px] = color;
        }
    }
}

void fb_hline(int x, int y, int w, uint32_t color) {
    fb_fill_rect(x, y, w, 1, color);
}

void fb_vline(int x, int y, int h, uint32_t color) {
    fb_fill_rect(x, y, 1, h, color);
}

void fb_draw_rect(int x, int y, int w, int h, uint32_t color) {
    fb_hline(x, y, w, color);
    fb_hline(x, y + h - 1, w, color);
    fb_vline(x, y, h, color);
    fb_vline(x + w - 1, y, h, color);
}

void fb_clear(uint32_t color) {
    fb_fill_rect(0, 0, (int)fb.width, (int)fb.height, color);
}

/* ── Text Rendering ──────────────────────────────────────────── */

void fb_draw_char(int x, int y, char c, uint32_t fg, uint32_t bg) {
    if (!fb.available)
        return;

    int idx = (int)c - 0x20;
    if (idx < 0 || idx >= 95)
        idx = 0; /* Default to space for unprintable chars */

    const uint8_t *glyph = font8x16[idx];

    for (int row = 0; row < FB_FONT_HEIGHT; row++) {
        uint8_t bits = glyph[row];
        for (int col = 0; col < FB_FONT_WIDTH; col++) {
            int ink = bits & (0x80 >> col);
            if (ink)
                fb_putpixel(x + col, y + row, fg);
            else if (bg != FB_TRANSPARENT)
                fb_putpixel(x + col, y + row, bg);
        }
    }
}

void fb_draw_string(int x, int y, const char *s, uint32_t fg, uint32_t bg) {
    if (!fb.available || !s)
        return;

    int cx = x;
    while (*s) {
        if (*s == '\n') {
            cx = x;
            y += FB_FONT_HEIGHT;
        } else {
            fb_draw_char(cx, y, *s, fg, bg);
            cx += FB_FONT_WIDTH;
        }
        s++;
    }
}

void fb_draw_text_box(int x, int y, int w, int h, const char *s,
                      uint32_t fg, uint32_t bg) {
    if (!fb.available || !s)
        return;

    int cx = x, cy = y;
    int max_x = x + w;
    int max_y = y + h;

    while (*s && cy + FB_FONT_HEIGHT <= max_y) {
        if (*s == '\n') {
            cx = x;
            cy += FB_FONT_HEIGHT;
        } else {
            if (cx + FB_FONT_WIDTH > max_x) {
                cx = x;
                cy += FB_FONT_HEIGHT;
                if (cy + FB_FONT_HEIGHT > max_y)
                    break;
            }
            fb_draw_char(cx, cy, *s, fg, bg);
            cx += FB_FONT_WIDTH;
        }
        s++;
    }
}

void fb_scroll_up(int n, uint32_t bg_color) {
    if (!fb.available || n <= 0)
        return;

    if ((uint32_t)n >= fb.height) {
        fb_clear(bg_color);
        return;
    }

    /* Move rows up */
    uint8_t *dst = (uint8_t *)fb.addr;
    uint8_t *src = (uint8_t *)fb.addr + (uint32_t)n * fb.pitch;
    uint32_t move_bytes = (fb.height - (uint32_t)n) * fb.pitch;
    memcpy(dst, src, move_bytes);

    /* Clear bottom rows */
    fb_fill_rect(0, (int)(fb.height - (uint32_t)n), (int)fb.width, n, bg_color);
}

/* ── UTF-8 / CJK Text Rendering ──────────────────────────────── */

/* Decode one UTF-8 sequence starting at *p. Returns the code point and advances
 * *p past the consumed bytes. Malformed/overlong sequences yield U+FFFD and
 * advance a single byte (resync). */
static uint32_t _utf8_next(const char **p) {
    const unsigned char *s = (const unsigned char *)(*p);
    unsigned char c = s[0];

    if (c < 0x80) {
        *p += 1;
        return c;
    }
    if ((c & 0xE0) == 0xC0) {
        if ((s[1] & 0xC0) == 0x80) {
            *p += 2;
            return ((uint32_t)(c & 0x1F) << 6) | (s[1] & 0x3F);
        }
    } else if ((c & 0xF0) == 0xE0) {
        if ((s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80) {
            *p += 3;
            return ((uint32_t)(c & 0x0F) << 12) |
                   ((uint32_t)(s[1] & 0x3F) << 6) | (s[2] & 0x3F);
        }
    } else if ((c & 0xF8) == 0xF0) {
        if ((s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80 &&
            (s[3] & 0xC0) == 0x80) {
            *p += 4;
            return ((uint32_t)(c & 0x07) << 18) |
                   ((uint32_t)(s[1] & 0x3F) << 12) |
                   ((uint32_t)(s[2] & 0x3F) << 6) | (s[3] & 0x3F);
        }
    }
    *p += 1;       /* invalid lead/continuation — resync one byte */
    return 0xFFFD; /* replacement character */
}

/* Render one 16x16 CJK glyph (or a hollow box placeholder if absent).
 * The glyph is 8-bit alpha coverage; ink is alpha-blended over the background
 * so strokes look smooth. With bg == FB_TRANSPARENT, ink blends over whatever
 * is already on screen (gradients, shadows), otherwise over the solid bg. */
static void _draw_cjk(int x, int y, uint32_t cp, uint32_t fg, uint32_t bg) {
    int gi = cjk_lookup(cp);
    if (gi < 0) {
        /* Placeholder: hollow box so missing chars are visible, not blank. */
        if (bg != FB_TRANSPARENT)
            fb_fill_rect(x, y, FB_CJK_WIDTH, FB_CJK_HEIGHT, bg);
        fb_draw_rect(x + 1, y + 1, FB_CJK_WIDTH - 2, FB_CJK_HEIGHT - 2, fg);
        return;
    }
    const uint8_t *g = cjk_glyph16[gi];
    for (int row = 0; row < CJK_GLYPH_HEIGHT; row++) {
        for (int col = 0; col < CJK_GLYPH_WIDTH; col++) {
            uint8_t a = g[row * CJK_GLYPH_WIDTH + col];
            if (bg == FB_TRANSPARENT) {
                if (a)
                    fb_putpixel(x + col, y + row,
                                fb_blend(fg, fb_getpixel(x + col, y + row), a));
            } else {
                fb_putpixel(x + col, y + row, fb_blend(fg, bg, a));
            }
        }
    }
}

int fb_draw_utf8(int x, int y, const char *s, uint32_t fg, uint32_t bg) {
    if (!fb.available || !s)
        return x;

    int cx = x;
    const char *p = s;
    while (*p) {
        if (*p == '\n') {
            cx = x;
            y += FB_CJK_HEIGHT;
            p++;
            continue;
        }
        uint32_t cp = _utf8_next(&p);
        if (cp < 0x80) {
            /* ASCII: vertically center the 8x16 glyph in the 16px line. */
            fb_draw_char(cx, y, (char)cp, fg, bg);
            cx += FB_FONT_WIDTH;
        } else {
            _draw_cjk(cx, y, cp, fg, bg);
            cx += FB_CJK_WIDTH;
        }
    }
    return cx;
}

int fb_utf8_width(const char *s) {
    if (!s)
        return 0;
    int w = 0;
    const char *p = s;
    while (*p) {
        if (*p == '\n') { p++; continue; }
        uint32_t cp = _utf8_next(&p);
        w += (cp < 0x80) ? FB_FONT_WIDTH : FB_CJK_WIDTH;
    }
    return w;
}
