/*
 * Copyright (c) 2026, Chloe M.
 * Provided under the BSD-3 clause.
 *
 * Description: Physical memory management
 * Author:      Chloe M.
 */

#include <mm/pm.h>
#include <mm/vm.h>
#include <ke/bpal.h>
#include <ke/spinlock.h>
#include <ex/trace.h>
#include <string.h>
#include <units.h>

#define DTRACE(Fmt, ...) \
    TRACE("[ PM ]: " Fmt, ##__VA_ARGS__)

/* Safe macro to get region string */
#define PM_REGION(TYPE)                 \
    ((TYPE) >= NELEM(PmRegionTable))    \
        ? "bad type"                    \
        : PmRegionTable[(TYPE)]

/*
 * Table of known memory regions
 */
static const CHAR *PmRegionTable[] = {
    [MEMORY_USABLE]         = "available",
    [MEMORY_RESERVED]       = "reserved",
    [MEMORY_ACPI_RECLAIM]   = "acpi reclaimable",
    [MEMORY_ACPI_NVS]       = "acpi nv-sleep",
    [MEMORY_BAD]            = "bad",
    [MEMORY_BOOTLOADER]     = "bootloader",
    [MEMORY_KERNEL]         = "system",
    [MEMORY_FRAMEBUFFER]    = "vram",
    [MEMORY_ACPI_TABLES]    = "acpi tables"
};

/*
 * A page frame descriptor is an entry within the physical memory
 * freelist.
 *
 * @Next:  Next PFD in freelist
 * @Prev:  Previous PFD in freelist
 */
typedef struct _MM_PFD {
    struct _MM_PFD *Next;
    struct _MM_PFD *Prev;
} MM_PFD;

/*
 * A page frame descriptor list holds many page frame
 * descriptors and marks the start and end of a physical
 * memory freelist.
 *
 * @First:  First entry in freelist
 * @Last:   Last entry in freelist
 * @Lock:   Protects this list
 *
 * TODO: Add a lock to this structure
 */
typedef struct {
    MM_PFD *First;
    MM_PFD *Last;
    SPINLOCK Lock;
} MM_PFD_LIST;

/* Standard usable memory list */
static MM_PFD_LIST AvlList;

/* Memory statistics */
static USIZE UsableMemory;

/*
 * Used to convert bytes into human readable units
 */
static inline VOID
PmZoneSize(const CHAR *TypeName, USIZE Length)
{
    if (TypeName == NULL) {
        return;
    }

    if (Length >= UNIT_GIB) {
        DTRACE("%s : %d GiB\n", TypeName, Length / UNIT_GIB);
    } else if (Length >= UNIT_MIB) {
        DTRACE("%s : %d MiB\n", TypeName, Length / UNIT_MIB);
    } else if (Length >= UNIT_KIB) {
        DTRACE("%s : %d KiB\n", TypeName, Length / UNIT_KIB);
    } else {
        DTRACE("%s : %d bytes\n", TypeName, Length);
    }
}

/*
 * Appends a page frame descriptor to a page frame descriptor
 * list.
 *
 * @List: Page frame descriptor list to append to
 * @Pfd:  Page frame descriptor to append
 */
static VOID
PmPfdListAppend(MM_PFD_LIST *List, MM_PFD *Pfd)
{
    MM_PFD *LastPfd;

    if (List == NULL || Pfd == NULL) {
        return;
    }

    Pfd->Next = NULL;
    Pfd->Prev = NULL;

    if (List->First == NULL || List->Last == NULL) {
        List->First = Pfd;
        List->Last = Pfd;
    } else {
        LastPfd = List->Last;
        LastPfd->Next = Pfd;
        Pfd->Prev = LastPfd;
        List->Last = Pfd;
    }
}

/*
 * Pop a page frame descriptor from the beginning of a page
 * frame descriptor list.
 *
 * @List: Page frame descriptor list to pop from
 *
 * Returns NULL on failure
 */
static MM_PFD *
PmPfdListPop(MM_PFD_LIST *List)
{
    MM_PFD *FirstPfd;

    if (List == NULL) {
        return NULL;
    }

    FirstPfd = List->First;
    if (FirstPfd == NULL) {
        return NULL;
    }

    List->First = FirstPfd->Next;
    return FirstPfd;
}

/*
 * Probe physical memory for usable regions and list
 * installed memory.
 */
static VOID
PmProbe(VOID)
{
    PT_STATUS Status;
    BPAL_HANDLE BpalHandle;
    MEMMAP_ENTRY Entry;
    UPTR Start, End, Off;
    MM_PFD *PfdBase, *PfdCurrent;
    USIZE Index, PfdCount = 0;
    USIZE ZoneCount = 0;

    /*
     * We obtain memory map entries from the BPAL layer as it is provided
     * by the bootloader which got it from firmware.
     */
    KeBpalGetHandle(&BpalHandle);

    /*
     * Iterate each memory entry, we use two terms here:
     *
     * Zones: A zone is a contigious region of frames and consists
     *        of many page frame descriptors.
     *
     * PFDs:  A page frame descriptor represents a single frame within
     *        a zone.
     */
    for (Index = 0;; ++Index) {
        Status = BpalHandle.MemEntryIdx(Index, &Entry);

        /* Are we out of entries to fetch? */
        if (PT_ERROR(Status)) {
            break;
        }

        Start = Entry.Base;
        End = Start + Entry.Length;
        DTRACE(
            "%p - %p : %s\n",
            Start,
            End,
            PM_REGION(Entry.Type)
        );

        /* Usable entries from this point only */
        if (Entry.Type != MEMORY_USABLE) {
            continue;
        }

        /* Append this zone */
        PfdBase = (MM_PFD *)PMA_TO_VMA(Entry.Base);
        for (Off = 0; Off < Entry.Length; Off += PAGESIZE) {
            PfdCurrent = PTR_OFFSET(PfdBase, Off);
            PmPfdListAppend(&AvlList, PfdCurrent);
            ++PfdCount;
        }

        ++ZoneCount;
        UsableMemory += Entry.Length;
    }

    /* Log some statistics */
    DTRACE("%d pfds, %d zones\n", PfdCount, ZoneCount);
    PmZoneSize("usable", UsableMemory);
}

MM_PFN
MmRequestFrame(VOID)
{
    MM_PFD *Pfd;
    UPTR PhysicalBase;

    KeSpinLockAcquire(&AvlList.Lock, true);
    Pfd = PmPfdListPop(&AvlList);
    KeSpinLockRelease(&AvlList.Lock);

    if (Pfd == NULL) {
        return PFN_ERROR;
    }

    RtlMemSet(Pfd, 0, PAGESIZE);
    PhysicalBase = VMA_TO_PMA(Pfd);
    return ADDRESS_TO_PFN(PhysicalBase);
}

VOID
MmReclaimFrame(MM_PFN Pfn)
{
    MM_PFD *Pfd;
    UPTR PhysicalBase;

    if (Pfn == PFN_ERROR) {
        return;
    }

    KeSpinLockAcquire(&AvlList.Lock, true);
    PhysicalBase = PFN_TO_ADDRESS(Pfn);
    Pfd = (VOID *)PMA_TO_VMA(PhysicalBase);
    PmPfdListAppend(&AvlList, Pfd);
    KeSpinLockRelease(&AvlList.Lock);
}

VOID
MmPmInit(VOID)
{
    KeSpinLockInit(&AvlList.Lock, "pmm.avllock");
    DTRACE("probing physical memory...\n");
    PmProbe();
}
