///                MentOS, The Mentoring Operating system project
/// @file zone_allocator.h
/// @brief Implementation of the Zone Allocator
/// @copyright (c) 2014-2021 This file is distributed under the MIT License.
/// See LICENSE.md for details.

#pragma once

#include "mem/gfp.h"
#include "math.h"
#include "stdint.h"
#include "klib/list_head.h"
#include "sys/bitops.h"
#include "klib/stdatomic.h"
#include "boot.h"
#include "mem/buddysystem.h"
#include "mem/slab.h"

#define page_count(p)        atomic_read(&(p)->count)   ///< Reads the page count.
#define set_page_count(p, v) atomic_set(&(p)->count, v) ///< Sets the page count.
#define page_inc(p)          atomic_inc(&(p)->count)    ///< Increments the counter for the given page.
#define page_dec(p)          atomic_dec(&(p)->count)    ///< Decrements the counter for the given page.

/// @brief Page descriptor. Use as a bitmap to understand
/// the order of the block and if it is free or allocated.
typedef struct page_t {
    /// Array of flags encoding also the zone number to which the page frame belongs.
    unsigned long flags;
    /// Page frame’s reference counter. 0 free, 1 used, 2+ copy on write
    atomic_t count;
    /// Buddy system page definition
    bb_page_t bbpage;
    /// Contains pointers to the slabs doubly linked list of pages.
    list_head slabs;

    // Slab allocator variables
    /// Contains the total number of objects in this page, 0 if not managed by the slub
    unsigned int slab_objcnt;
    ///  Tracks the number of free objects in the current page
    unsigned int slab_objfree;
    /// Holds the first free object (if slab_objfree is > 0)
    list_head slab_freelist;
    /// @brief This union can either contain the pointer to the slab main page
    ///        that handles this page, or the cache that contains it.
    union {
        /// Holds the slab page used to handle this memory region (root page)
        struct page_t *slab_main_page;
        /// Holds the slab cache pointer on the main page
        kmem_cache_t *slab_cache;
    } container;
} page_t;

/// @brief Enumeration for zone_t.
enum zone_type {
    /*
     * ZONE_DMA is used when there are devices that are not able
     * to do DMA to all of addressable memory (ZONE_NORMAL). Then we
     * carve out the portion of memory that is needed for these devices.
     * The range is arch specific.
     *
     * Some examples
     *
     * Architecture		Limit
     * ---------------------------
     * parisc, ia64, sparc	<4G
     * s390			<2G
     * arm			Various
     * alpha		Unlimited or 0-16MB.
     *
     * i386, x86_64 and multiple other arches
     * 			<16M.
     */

    // /// Direct memory access.
    //ZONE_DMA,

    /*
     * Normal addressable memory is in ZONE_NORMAL. DMA operations can be
     * performed on pages in ZONE_NORMAL if the DMA devices support
     * transfers to all addressable memory.
     */
    /// Direct mapping. Used by the kernel.
    ZONE_NORMAL,

    /*
     * A memory area that is only addressable by the kernel through
     * mapping portions into its own address space. This is for example
     * used by i386 to allow the kernel to address the memory beyond
     * 900MB. The kernel will set up special mappings (page
     * table entries on i386) for each page that the kernel needs to
     * access.
     */
    /// Page tables mapping. Used by user processes.
    ZONE_HIGHMEM,

    /// The maximum number of zones.
    __MAX_NR_ZONES
};

/// @brief Data structure to differentiate memory zone.
typedef struct zone_t {
    /// Number of free pages in the zone.
    unsigned long free_pages;
    /// Buddy system managing this zone
    bb_instance_t buddy_system;
    /// Pointer to first page descriptor of the zone.
    page_t *zone_mem_map;
    /// Index of the first page frame of the zone.
    uint32_t zone_start_pfn;
    /// Zone's name.
    char *name;
    /// Zone's size in number of pages.
    unsigned long size;
} zone_t;

/// @brief Data structure to rapresent a memory node.
///        In UMA Architecture there is only one node called
///        contig_page_data.
typedef struct pg_data_t {
    /// Zones of the node.
    zone_t node_zones[__MAX_NR_ZONES];
    /// Number of zones in the node.
    int nr_zones;
    /// Array of pages of the node.
    page_t *node_mem_map;
    /// Physical address of the first page of the node.
    unsigned long node_start_paddr;
    /// Index on global mem_map for node_mem_map.
    unsigned long node_start_mapnr;
    /// Node's size in number of pages.
    unsigned long node_size;
    /// NID.
    int node_id;
    /// Next item in the memory node list.
    struct pg_data_t *node_next;
} pg_data_t;

extern page_t *mem_map;
extern pg_data_t *contig_page_data;

/// @brief Find the nearest block's order of size greater than the
///        amount of byte.
/// @param base_addr The start address, used to handle extra page
///                  calculation in case of not page aligned addresses.
/// @param amount    The amount of byte which we want to calculate order.
/// @return The block's order greater and nearest than amount.
uint32_t find_nearest_order_greater(uint32_t base_addr, uint32_t amount);

/// @brief Physical memory manager initialization.
/// @param boot_info Information coming from the booloader.
/// @return Outcome of the operation.
int pmmngr_init(boot_info_t *boot_info);

/// @brief Alloc a single cached page.
/// @param gfp_mask The GetFreePage mask.
/// @return Pointer to the page.
page_t *alloc_page_cached(gfp_t gfp_mask);

/// @brief Free a page allocated with alloc_page_cached.
/// @param page Pointer to the page to free.
void free_page_cached(page_t *page);

/// @brief Find the first free page frame, set it allocated and return the
///        memory address of the page frame.
/// @param gfp_mask GFP_FLAGS to decide the zone allocation.
/// @return Memory address of the first free block.
uint32_t __alloc_page_lowmem(gfp_t gfp_mask);

/// @brief Frees the given page frame address.
/// @param addr The block address.
void free_page_lowmem(uint32_t addr);

/// @brief Find the first free 2^order amount of page frames, set it allocated
///        and return the memory address of the first page frame allocated.
/// @param gfp_mask GFP_FLAGS to decide the zone allocation.
/// @param order    The logarithm of the size of the page frame.
/// @return Memory address of the first free page frame allocated.
uint32_t __alloc_pages_lowmem(gfp_t gfp_mask, uint32_t order);

/// @brief Find the first free 2^order amount of page frames, set it allocated
///        and return the memory address of the first page frame allocated.
/// @param gfp_mask GFP_FLAGS to decide the zone allocation.
/// @param order    The logarithm of the size of the page frame.
/// @return Memory address of the first free page frame allocated.
page_t *_alloc_pages(gfp_t gfp_mask, uint32_t order);

/// @brief Get the start address of the corresponding page.
/// @param page A page structure.
/// @return The address that corresponds to the page.
uint32_t get_lowmem_address_from_page(page_t *page);

/// @brief Get the start physical address of the corresponding page.
/// @param page A page structure
/// @return The physical address that corresponds to the page.
uint32_t get_physical_address_from_page(page_t *page);

/// @brief Get the page from it's physical address.
/// @param phy_addr The physical address
/// @return The page that corresponds to the physical address.
page_t *get_page_from_physical_address(uint32_t phy_addr);

/// @brief Get the page that contains the specified address.
/// @param addr A phisical address.
/// @return The page that corresponds to the address.
page_t *get_lowmem_page_from_address(uint32_t addr);

/// @brief Frees from the given page frame address up to 2^order amount
///        of page frames.
/// @param addr The page frame address.
void free_pages_lowmem(uint32_t addr);

/// @brief Frees from the given page frame address up to 2^order amount
///        of page frames.
/// @param page The page.
void __free_pages(page_t *page);

/// @brief Returns the total space for the given zone.
/// @param gfp_mask GFP_FLAGS to decide the zone.
/// @return Total space of the given zone.
unsigned long get_zone_total_space(gfp_t gfp_mask);

/// @brief Returns the total free space for the given zone.
/// @param gfp_mask GFP_FLAGS to decide the zone.
/// @return Total free space of the given zone.
unsigned long get_zone_free_space(gfp_t gfp_mask);

/// @brief Returns the total cached space for the given zone.
/// @param gfp_mask GFP_FLAGS to decide the zone.
/// @return Total cached space of the given zone.
unsigned long get_zone_cached_space(gfp_t gfp_mask);

/// @brief Checks if the specified address points to a page_t (or field)
///        that belongs to lowmem.
/// @param addr The address to check.
/// @return 1 if it belongs to lowmem, 0 otherwise.
static inline int is_lowmem_page_struct(void *addr)
{
    uint32_t start_lowm_map  = (uint32_t)contig_page_data->node_zones[ZONE_NORMAL].zone_mem_map;
    uint32_t lowmem_map_size = sizeof(page_t) * contig_page_data->node_zones[ZONE_NORMAL].size;
    uint32_t map_index       = (uint32_t)addr - start_lowm_map;
    return map_index < lowmem_map_size;
}
