#include "sentry_unix_pageallocator.h"
#include "sentry_core.h"
#include "sentry_unix_spinlock.h"

#include <sys/mman.h>
#include <unistd.h>

// some macos versions do not have this yet
#if !defined(MAP_ANONYMOUS) && defined(SENTRY_PLATFORM_DARWIN)
#    define MAP_ANONYMOUS MAP_ANON
#endif

#define ALIGN 8

struct page_header;
struct page_header {
    struct page_header *next;
    size_t num_pages;
};

struct page_allocator_s {
    size_t page_size;
    struct page_header *last_page;
    char *current_page;
    size_t page_offset;
    size_t pages_allocated;
};

static struct page_allocator_s g_page_allocator_backing = { 0 };
static struct page_allocator_s *g_alloc = NULL;
static sentry_spinlock_t g_lock = SENTRY__SPINLOCK_INIT;

bool
sentry__page_allocator_enabled(void)
{
    return !!g_alloc;
}

void
sentry__page_allocator_enable(void)
{
    sentry__spinlock_lock(&g_lock);
    if (!g_alloc) {
        g_alloc = &g_page_allocator_backing;
        g_alloc->page_size = getpagesize();
        g_alloc->last_page = NULL;
        g_alloc->current_page = NULL;
        g_alloc->page_offset = 0;
        g_alloc->pages_allocated = 0;
    }
    sentry__spinlock_unlock(&g_lock);
}

static void *
get_pages(size_t num_pages)
{
    void *rv = mmap(NULL, g_alloc->page_size * num_pages,
        PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (rv == MAP_FAILED) {
        return NULL;
    }

#if defined(__has_feature)
#    if __has_feature(memory_sanitizer)
    __msan_unpoison(rv, g_alloc->page_size * num_pages);
#    endif
#endif

    struct page_header *header = (struct page_header *)rv;
    header->next = g_alloc->last_page;
    header->num_pages = num_pages;
    g_alloc->last_page = header;

    g_alloc->pages_allocated += num_pages;

    return rv;
}

void *
sentry__page_allocator_alloc(size_t size)
{
    if (!size) {
        return NULL;
    }

    // make sure the requested size is correctly aligned
    size_t diff = size % ALIGN;
    size = size + ALIGN - diff;

    char *rv = NULL;

    sentry__spinlock_lock(&g_lock);

    // current page is large enough
    if (g_alloc->current_page
        && g_alloc->page_size - g_alloc->page_offset >= size) {
        rv = g_alloc->current_page + g_alloc->page_offset;
        g_alloc->page_offset += size;
        if (g_alloc->page_offset == g_alloc->page_size) {
            g_alloc->page_offset = 0;
            g_alloc->current_page = NULL;
        }
    } else {
        // allocate new pages

        size_t requested_size = size + sizeof(struct page_header);
        size_t pages
            = (requested_size + g_alloc->page_size - 1) / g_alloc->page_size;
        size_t actual_size = g_alloc->page_size * pages;

        rv = get_pages(pages);

        if (rv) {
            size_t diff = actual_size - requested_size;
            g_alloc->page_offset
                = (g_alloc->page_size - diff) % g_alloc->page_size;
            g_alloc->current_page = g_alloc->page_offset
                ? rv + g_alloc->page_size * (pages - 1)
                : NULL;
            rv += sizeof(struct page_header);
        }
    }

    sentry__spinlock_unlock(&g_lock);
    return rv;
}

#ifdef SENTRY_UNITTEST
void
sentry__page_allocator_disable(void)
{
    if (!g_alloc) {
        return;
    }
    struct page_header *next;
    for (struct page_header *cur = g_alloc->last_page; cur; cur = next) {
        next = cur->next;
        munmap(cur, cur->num_pages * g_alloc->page_size);
    }
    g_alloc = NULL;
}
#endif
