/*
 * Copyright (c) 2026, Chloe M.
 * Provided under the BSD-3 clause.
 *
 * Description: Platform MMU HAL interface
 * Author:      Chloe M.
 */

#include <hal/mmu.h>
#include <machine/mmu.h>
#include <machine/tlb.h>
#include <mm/pm.h>
#include <mm/vm.h>
#include <ke/bugcheck.h>
#include <ptdef.h>
#include <string.h>

/*
 * Valid paging structure levels
 */
typedef enum {
    PAGEMAP_PML1,
    PAGEMAP_PML2,
    PAGEMAP_PML3,
    PAGEMAP_PML4
} PAGEMAP_LEVEL;

/*
 * Returns true if the given page size is valid
 *
 * @PageSize: Page size to check
 */
ALWAYS_INLINE static inline BOOLEAN
MmuPageSizeValid(MMU_PAGESIZE PageSize)
{
    switch (PageSize) {
    case PAGESIZE_4K:
        return true;
    }

    return false;
}

/*
 * Obtain the pagemap top-level index
 *
 * XXX: In future revisions we should read CR4 and determine
 *      if 57-bit linear addresses are enabled for 5-level
 *      paging.
 */
ALWAYS_INLINE static inline PAGEMAP_LEVEL
MmuTopLevel(VOID)
{
    return PAGEMAP_PML4;
}

/*
 * Obtain PTE flags from MI page protection
 * flags
 *
 * @Prot:  Protection flags to convert
 */
static USIZE
MmuProtToPte(MM_PROT Prot)
{
    USIZE PteFlags = PTE_P | PTE_NX;

    if (ISSET(Prot, PAGE_WRITE))
        PteFlags |= PTE_RW;
    if (ISSET(Prot, PAGE_EXEC))
        PteFlags &= ~PTE_NX;
    if (ISSET(Prot, PAGE_USER))
        PteFlags |= PTE_US;

    return PteFlags;
}

/*
 * Extract a pagemap index from a virtual address for a given
 * level.
 *
 * @VirtualBase:  Virtual base address
 * @Level:        Pagemap level to get index of
 *
 * XXX: Caller should ensure the page alignment of @VirtualBase
 */
static inline USHORT
MmuPageMapIndex(UPTR VirtualBase, PAGEMAP_LEVEL Level)
{
    switch (Level) {
    case PAGEMAP_PML1:
        return (VirtualBase >> 12) & 0x1FF;
    case PAGEMAP_PML2:
        return (VirtualBase >> 21) & 0x1FF;
    case PAGEMAP_PML3:
        return (VirtualBase >> 30) & 0x1FF;
    case PAGEMAP_PML4:
        return (VirtualBase >> 39) & 0x1FF;
    }

    /* This should not happen */
    KeBugCheck(
        BUGCHECK_IO_ERROR,
        "bad pagemap level in MmuPageMapIndex()\n"
    );
}

/*
 * Extract a pagemap at a specific level
 *
 * @Vas:            Virtual address space to translate from
 * @VirtualBase:    Virtual base address
 * @Level:          Level to extract
 * @Alloc:          If true, allocate new level
 *
 * Returns the pogemap level base on success, otherwise NULL
 * on failure.
 *
 * XXX: Caller should ensure the page alignment of @VirtualBase
 */
static UPTR *
MmuPageMapExtract(MMU_VAS *Vas, UPTR VirtualBase, PAGEMAP_LEVEL Level, BOOLEAN Alloc)
{
    UPTR *PageMap;
    PAGEMAP_LEVEL CurrentLevel;
    MM_PFN Pfn;
    UPTR PhysicalBase;
    USIZE Index;

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

    /* Get the top-level */
    CurrentLevel = MmuTopLevel();
    PageMap = PMA_TO_VMA((Vas->Cr3 & PTE_ADDR_MASK));

    /* Walk through each level */
    while (CurrentLevel > Level) {
        Index = MmuPageMapIndex(VirtualBase, CurrentLevel);

        /* Is this entry already present? */
        if (ISSET(PageMap[Index], PTE_P)) {
            PageMap = PMA_TO_VMA((PageMap[Index] & PTE_ADDR_MASK));
            --CurrentLevel;
            continue;
        }

        if (!Alloc) {
            return NULL;
        }

        /*
         * Now we allocate a level for this entry
         *
         * XXX: We need to consider how we'll clean up when unmapping pages
         *      as the destruction of a virtual address space will end up
         *      leaking memory if this point is reached.
         */
        Pfn = MmRequestFrame();
        if (Pfn == PFN_ERROR) {
            return NULL;
        }

        PhysicalBase = PFN_TO_ADDRESS(Pfn);
        PageMap[Index] = PhysicalBase | (PTE_P | PTE_RW | PTE_US);
        PageMap = PMA_TO_VMA(PhysicalBase);
        RtlMemSet(PageMap, 0, PAGESIZE);
        --CurrentLevel;
    }

    return PageMap;
}

VOID
HalMmuReadVas(MMU_VAS *Result)
{
    if (Result == NULL) {
        return;
    }

    ASMV(
        "mov %%cr3, %0"
        : "=r" (Result->Cr3)
        :
        : "memory"
    );
}

VOID
HalMmuWriteVas(const MMU_VAS *Vas)
{
    if (Vas == NULL) {
        return;
    }

    ASMV(
        "mov %0, %%cr3"
        :
        : "r" (Vas->Cr3)
        : "memory"
    );
}

PT_STATUS
HalMmuMapPage(
    MMU_VAS *Vas, UPTR VirtualBase,
    UPTR PhysicalBase, MM_PROT Prot,
    MMU_PAGESIZE PageSize)
{
    UPTR *PageTable;
    USIZE PageTableIndex;
    USIZE PteFlags;

    if (Vas == NULL) {
        return STATUS_INVALID_PARAM;
    }

    if (!MmuPageSizeValid(PageSize)) {
        return STATUS_INVALID_PARAM;
    }

    PageTable = MmuPageMapExtract(Vas, VirtualBase, PAGEMAP_PML1, true);
    if (PageTable == NULL) {
        return STATUS_NO_MEMORY;
    }

    PageTableIndex = MmuPageMapIndex(VirtualBase, PAGEMAP_PML1);
    PteFlags = MmuProtToPte(Prot);

    /* Map the memory and flush the TLB entry */
    PageTable[PageTableIndex] = PhysicalBase | PteFlags;
    MdTlbFlushSingle(VirtualBase);
    return STATUS_SUCCESS;
}

PT_STATUS
HalMmuUnmapPage(MMU_VAS *Vas, UPTR VirtualBase, MMU_PAGESIZE PageSize)
{
    UPTR *PageTable;
    USIZE PageTableIndex;

    if (Vas == NULL) {
        return STATUS_INVALID_PARAM;
    }

    if (!MmuPageSizeValid(PageSize)) {
        return STATUS_INVALID_PARAM;
    }

    PageTable = MmuPageMapExtract(Vas, VirtualBase, PAGEMAP_PML1, false);
    if (PageTable == NULL) {
        return STATUS_NOT_FOUND;
    }

    PageTableIndex = MmuPageMapIndex(VirtualBase, PAGEMAP_PML1);

    /* Unmap and flush the TLB */
    PageTable[PageTableIndex] = 0;
    MdTlbFlushSingle(VirtualBase);
    return STATUS_SUCCESS;
}

PT_STATUS
HalMmuForkVas(const MMU_VAS *Source, MMU_VAS *Result)
{
    const UPTR *Src;
    UPTR *Dest;
    UPTR DestPhysBase;
    MM_PFN DestPfn;

    if (Source == NULL || Result == NULL) {
        return STATUS_INVALID_PARAM;
    }

    DestPfn = MmRequestFrame();
    if (DestPfn == PFN_ERROR) {
        return STATUS_NO_MEMORY;
    }

    DestPhysBase = PFN_TO_ADDRESS(DestPfn);
    Src = PMA_TO_VMA((Source->Cr3 & PTE_ADDR_MASK));
    Dest = PMA_TO_VMA(DestPhysBase);

    /* Begin copy */
    for (USIZE Idx = 0; Idx < 512; ++Idx) {
        if (Idx < 256) {
            Dest[Idx] = 0;
        } else {
            Dest[Idx] = Src[Idx];
        }
    }

    Result->Cr3 = DestPhysBase;
    return STATUS_SUCCESS;
}
