//! The kernel heap: dynamic allocation for the kernel.
//!
//! Where the frame allocator ([pmm]) hands out fixed 4 KiB physical frames, the
//! heap hands out arbitrary byte-sized blocks from a virtual region, growing on
//! demand by mapping fresh frames into it (architecture.mapPage) — the first real user of
//! the VMM (see docs/paging.md).
//!
//! The algorithm is a first-fit free list: an address-ordered singly linked list
//! of free blocks, split on allocation and coalesced with neighbours on free. It
//! is exposed as a std.mem.Allocator, so the kernel can use std containers.
//!
//! Not yet concurrency-safe: it assumes a single caller and no allocation from
//! interrupt handlers (ours don't). A lock comes with threads/SMP.

const std = @import("std");
const abi = @import("abi");
const architecture = @import("architecture");
const pmm = @import("pmm.zig");

const page_size = abi.page_size;

/// Virtual base of the heap: the start of the higher half, which is unmapped and
/// well clear of the identity-mapped low half. (Canonical on x86_64; an architecture that
/// splits the address space differently would choose its own.)
const heap_base: usize = 0xFFFF_8000_0000_0000;
/// Cap on heap growth for now.
const heap_maximum: usize = 64 * 1024 * 1024;

/// A block header, placed at the start of every block. While the block is free it
/// also links into the free list via `next`.
const Block = extern struct {
    size: usize, // total block size in bytes, including this header; a multiple of 16
    next: ?*Block, // free-list link (only meaningful while free)
};

const header_size = @sizeOf(Block); // 16
const minimum_block = header_size + 16; // smallest block worth splitting off

var free_list: ?*Block = null;
var heap_end: usize = heap_base; // [heap_base, heap_end) is currently mapped

fn alignUp(value: usize, alignment: usize) usize {
    return (value + alignment - 1) & ~(alignment - 1);
}

fn payloadOf(block: *Block) [*]u8 {
    return @ptrFromInt(@intFromPtr(block) + header_size);
}

/// Bring the heap up with an initial mapped region.
pub fn init() void {
    free_list = null;
    heap_end = heap_base;
    _ = grow(page_size);
}

/// Map more pages onto the end of the heap and add them as a free block. Returns
/// false if out of heap virtual space or out of physical frames.
fn grow(minimum_bytes: usize) bool {
    const start = heap_end;
    const bytes = alignUp(minimum_bytes, page_size);
    if (start + bytes > heap_base + heap_maximum) return false;

    var virtual = start;
    while (virtual < start + bytes) : (virtual += page_size) {
        const frame = pmm.alloc() orelse return false;
        architecture.mapPage(virtual, frame, true);
    }
    heap_end = start + bytes;

    const block: *Block = @ptrFromInt(start);
    block.size = bytes;
    insertFree(block); // coalesces with the previous tail block if adjacent
    return true;
}

/// Insert a block into the address-ordered free list, coalescing with the
/// physically adjacent free blocks on either side.
fn insertFree(block: *Block) void {
    var previous: ?*Block = null;
    var current = free_list;
    while (current) |c| : (current = c.next) {
        if (@intFromPtr(c) > @intFromPtr(block)) break;
        previous = c;
    }

    block.next = current;
    if (previous) |p| p.next = block else free_list = block;

    // Merge forward into `current` if they're contiguous.
    if (current) |c| {
        if (@intFromPtr(block) + block.size == @intFromPtr(c)) {
            block.size += c.size;
            block.next = c.next;
        }
    }
    // Merge `previous` forward into `block` if they're contiguous.
    if (previous) |p| {
        if (@intFromPtr(p) + p.size == @intFromPtr(block)) {
            p.size += block.size;
            p.next = block.next;
        }
    }
}

/// Allocate `len` bytes (16-byte aligned), or null if out of memory.
fn rawAlloc(len: usize) ?[*]u8 {
    const need = alignUp(header_size + len, 16);

    var attempts: u32 = 0;
    while (attempts < 2) : (attempts += 1) {
        var previous: ?*Block = null;
        var current = free_list;
        while (current) |block| : ({
            previous = block;
            current = block.next;
        }) {
            if (block.size < need) continue;

            if (block.size >= need + minimum_block) {
                // Split: carve `need` off the front, leave the rest free.
                const rest: *Block = @ptrFromInt(@intFromPtr(block) + need);
                rest.size = block.size - need;
                rest.next = block.next;
                if (previous) |p| p.next = rest else free_list = rest;
                block.size = need;
            } else {
                // Take the whole block.
                if (previous) |p| p.next = block.next else free_list = block.next;
            }
            return payloadOf(block);
        }

        // Nothing fit: grow and try once more.
        if (!grow(need)) return null;
    }
    return null;
}

fn rawFree(ptr: [*]u8) void {
    const block: *Block = @ptrFromInt(@intFromPtr(ptr) - header_size);
    insertFree(block);
}

// --- std.mem.Allocator interface -----------------------------------------

pub fn allocator() std.mem.Allocator {
    return .{ .ptr = undefined, .vtable = &vtable };
}

const vtable = std.mem.Allocator.VTable{
    .alloc = allocImpl,
    .resize = resizeImpl,
    .remap = remapImpl,
    .free = freeImpl,
};

fn allocImpl(_: *anyopaque, len: usize, alignment: std.mem.Alignment, _: usize) ?[*]u8 {
    // Blocks are 16-byte aligned; larger alignments aren't supported yet.
    if (alignment.toByteUnits() > 16) return null;
    return rawAlloc(len);
}

fn resizeImpl(_: *anyopaque, _: []u8, _: std.mem.Alignment, _: usize, _: usize) bool {
    return false; // no in-place resize; the caller reallocates
}

fn remapImpl(_: *anyopaque, _: []u8, _: std.mem.Alignment, _: usize, _: usize) ?[*]u8 {
    return null;
}

fn freeImpl(_: *anyopaque, memory: []u8, _: std.mem.Alignment, _: usize) void {
    rawFree(memory.ptr);
}
