/* -*- coding: utf-8 -*-
 * ----------------------------------------------------------------------
 * Copyright © 2011-2014, RedJack, LLC.
 * All rights reserved.
 *
 * Please see the COPYING file in this distribution for license details.
 * ----------------------------------------------------------------------
 */

#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "libcork/core/allocator.h"
#include "libcork/core/attributes.h"
#include "libcork/core/error.h"
#include "libcork/core/types.h"
#include "libcork/os/process.h"


/*-----------------------------------------------------------------------
 * Allocator interface
 */

struct cork_alloc_priv {
    struct cork_alloc  public;
    struct cork_alloc_priv  *next;
};

static void *
cork_alloc__default_calloc(const struct cork_alloc *alloc,
                           size_t count, size_t size)
{
    void  *result = cork_alloc_xcalloc(alloc, count, size);
    if (CORK_UNLIKELY(result == NULL)) {
        abort();
    }
    return result;
}

static void *
cork_alloc__default_malloc(const struct cork_alloc *alloc, size_t size)
{
    void  *result = cork_alloc_xmalloc(alloc, size);
    if (CORK_UNLIKELY(result == NULL)) {
        abort();
    }
    return result;
}

static void *
cork_alloc__default_realloc(const struct cork_alloc *alloc, void *ptr,
                            size_t old_size, size_t new_size)
{
    void  *result = cork_alloc_xrealloc(alloc, ptr, old_size, new_size);
    if (CORK_UNLIKELY(result == NULL)) {
        abort();
    }
    return result;
}

static void *
cork_alloc__default_xcalloc(const struct cork_alloc *alloc,
                            size_t count, size_t size)
{
    void  *result;
    assert(count < (SIZE_MAX / size));
    result = cork_alloc_xmalloc(alloc, count * size);
    if (result != NULL) {
        memset(result, 0, count * size);
    }
    return result;
}

static void *
cork_alloc__default_xmalloc(const struct cork_alloc *alloc, size_t size)
{
    cork_abort("%s isn't defined", "cork_alloc:xmalloc");
}

static void *
cork_alloc__default_xrealloc(const struct cork_alloc *alloc, void *ptr,
                             size_t old_size, size_t new_size)
{
    void  *result = cork_alloc_xmalloc(alloc, new_size);
    if (CORK_LIKELY(result != NULL) && ptr != NULL) {
        size_t  min_size = (new_size < old_size)? new_size: old_size;
        memcpy(result, ptr, min_size);
        cork_alloc_free(alloc, ptr, old_size);
    }
    return result;
}

static void
cork_alloc__default_free(const struct cork_alloc *alloc, void *ptr, size_t size)
{
    cork_abort("%s isn't defined", "cork_alloc:free");
}

static bool  cleanup_registered = false;
static struct cork_alloc_priv  *all_allocs = NULL;

static void
cork_alloc_free_alloc(struct cork_alloc_priv *alloc)
{
    cork_free_user_data(&alloc->public);
    cork_alloc_delete(alloc->public.parent, struct cork_alloc_priv, alloc);
}

static void
cork_alloc_free_all(void)
{
    struct cork_alloc_priv  *curr;
    struct cork_alloc_priv  *next;
    for (curr = all_allocs; curr != NULL; curr = next) {
        next = curr->next;
        cork_alloc_free_alloc(curr);
    }
}

static void
cork_alloc_register_cleanup(void)
{
    if (CORK_UNLIKELY(!cleanup_registered)) {
        /* We don't use cork_cleanup because that requires the allocators to
         * have already been set up!  (atexit calls its functions in reverse
         * order, and this one will be registered before cork_cleanup's, which
         * makes it safe for cork_cleanup functions to still use the allocator,
         * since the allocator atexit function will be called last.) */
        atexit(cork_alloc_free_all);
        cleanup_registered = true;
    }
}

struct cork_alloc *
cork_alloc_new_alloc(const struct cork_alloc *parent)
{
    struct cork_alloc_priv  *alloc =
        cork_alloc_new(parent, struct cork_alloc_priv);
    alloc->public.parent = parent;
    alloc->public.user_data = NULL;
    alloc->public.free_user_data = NULL;
    alloc->public.calloc = cork_alloc__default_calloc;
    alloc->public.malloc = cork_alloc__default_malloc;
    alloc->public.realloc = cork_alloc__default_realloc;
    alloc->public.xcalloc = cork_alloc__default_xcalloc;
    alloc->public.xmalloc = cork_alloc__default_xmalloc;
    alloc->public.xrealloc = cork_alloc__default_xrealloc;
    alloc->public.free = cork_alloc__default_free;

    cork_alloc_register_cleanup();
    alloc->next = all_allocs;
    all_allocs = alloc;

    return &alloc->public;
}


void
cork_alloc_set_user_data(struct cork_alloc *alloc,
                         void *user_data, cork_free_f free_user_data)
{
    cork_free_user_data(alloc);
    alloc->user_data = user_data;
    alloc->free_user_data = free_user_data;
}

void
cork_alloc_set_calloc(struct cork_alloc *alloc, cork_alloc_calloc_f calloc)
{
    alloc->calloc = calloc;
}

void
cork_alloc_set_malloc(struct cork_alloc *alloc, cork_alloc_malloc_f malloc)
{
    alloc->malloc = malloc;
}

void
cork_alloc_set_realloc(struct cork_alloc *alloc, cork_alloc_realloc_f realloc)
{
    alloc->realloc = realloc;
}

void
cork_alloc_set_xcalloc(struct cork_alloc *alloc, cork_alloc_calloc_f xcalloc)
{
    alloc->xcalloc = xcalloc;
}

void
cork_alloc_set_xmalloc(struct cork_alloc *alloc, cork_alloc_malloc_f xmalloc)
{
    alloc->xmalloc = xmalloc;
}

void
cork_alloc_set_xrealloc(struct cork_alloc *alloc,
                        cork_alloc_realloc_f xrealloc)
{
    alloc->xrealloc = xrealloc;
}

void
cork_alloc_set_free(struct cork_alloc *alloc, cork_alloc_free_f free)
{
    alloc->free = free;
}


/*-----------------------------------------------------------------------
 * Allocating strings
 */

static inline const char *
strndup_internal(const struct cork_alloc *alloc,
                 const char *str, size_t len)
{
    char  *dest;
    size_t  allocated_size = len + sizeof(size_t) + 1;
    size_t  *new_str = cork_alloc_malloc(alloc, allocated_size);
    *new_str = allocated_size;
    dest = (char *) (void *) (new_str + 1);
    memcpy(dest, str, len);
    dest[len] = '\0';
    return dest;
}

const char *
cork_alloc_strdup(const struct cork_alloc *alloc, const char *str)
{
    return strndup_internal(alloc, str, strlen(str));
}

const char *
cork_alloc_strndup(const struct cork_alloc *alloc,
                   const char *str, size_t size)
{
    return strndup_internal(alloc, str, size);
}

static inline const char *
xstrndup_internal(const struct cork_alloc *alloc,
                  const char *str, size_t len)
{
    size_t  allocated_size = len + sizeof(size_t) + 1;
    size_t  *new_str = cork_alloc_xmalloc(alloc, allocated_size);
    if (CORK_UNLIKELY(new_str == NULL)) {
        return NULL;
    } else {
        char  *dest;
        *new_str = allocated_size;
        dest = (char *) (void *) (new_str + 1);
        memcpy(dest, str, len);
        dest[len] = '\0';
        return dest;
    }
}

const char *
cork_alloc_xstrdup(const struct cork_alloc *alloc, const char *str)
{
    return xstrndup_internal(alloc, str, strlen(str));
}

const char *
cork_alloc_xstrndup(const struct cork_alloc *alloc,
                    const char *str, size_t size)
{
    return xstrndup_internal(alloc, str, size);
}

void
cork_alloc_strfree(const struct cork_alloc *alloc, const char *str)
{
    size_t  *base = ((size_t *) str) - 1;
    cork_alloc_free(alloc, base, *base);
}


/*-----------------------------------------------------------------------
 * stdlib allocator
 */

static void *
cork_stdlib_alloc__calloc(const struct cork_alloc *alloc,
                          size_t count, size_t size)
{
    void  *result = calloc(count, size);
    if (CORK_UNLIKELY(result == NULL)) {
        abort();
    }
    return result;
}

static void *
cork_stdlib_alloc__malloc(const struct cork_alloc *alloc, size_t size)
{
    void  *result = malloc(size);
    if (CORK_UNLIKELY(result == NULL)) {
        abort();
    }
    return result;
}

static void *
cork_stdlib_alloc__realloc(const struct cork_alloc *alloc, void *ptr,
                           size_t old_size, size_t new_size)
{
    /* Technically we don't really need to free `ptr` if the reallocation fails,
     * since we'll abort the process immediately after.  But my sense of
     * cleanliness makes me do it anyway. */

#if CORK_HAVE_REALLOCF
    void  *result = reallocf(ptr, new_size);
    if (result == NULL) {
        abort();
    }
    return result;
#else
    void  *result = realloc(ptr, new_size);
    if (result == NULL) {
        free(ptr);
        abort();
    }
    return result;
#endif
}

static void *
cork_stdlib_alloc__xcalloc(const struct cork_alloc *alloc,
                           size_t count, size_t size)
{
    return calloc(count, size);
}

static void *
cork_stdlib_alloc__xmalloc(const struct cork_alloc *alloc, size_t size)
{
    return malloc(size);
}

static void *
cork_stdlib_alloc__xrealloc(const struct cork_alloc *alloc, void *ptr,
                            size_t old_size, size_t new_size)
{
    return realloc(ptr, new_size);
}

static void
cork_stdlib_alloc__free(const struct cork_alloc *alloc, void *ptr, size_t size)
{
    free(ptr);
}


static const struct cork_alloc  default_allocator = {
    NULL,
    NULL,
    NULL,
    cork_stdlib_alloc__calloc,
    cork_stdlib_alloc__malloc,
    cork_stdlib_alloc__realloc,
    cork_stdlib_alloc__xcalloc,
    cork_stdlib_alloc__xmalloc,
    cork_stdlib_alloc__xrealloc,
    cork_stdlib_alloc__free
};


/*-----------------------------------------------------------------------
 * Customizing libcork's allocator
 */

const struct cork_alloc  *cork_allocator = &default_allocator;

void
cork_set_allocator(const struct cork_alloc *alloc)
{
    cork_allocator = alloc;
}


/*-----------------------------------------------------------------------
 * Debugging allocator
 */

static void *
cork_debug_alloc__xmalloc(const struct cork_alloc *alloc, size_t size)
{
    size_t  real_size = size + sizeof(size_t);
    size_t  *base = cork_alloc_xmalloc(alloc->parent, real_size);
    *base = size;
    return base + 1;
}

static void
cork_debug_alloc__free(const struct cork_alloc *alloc, void *ptr,
                       size_t expected_size)
{
    size_t  *base = ((size_t *) ptr) - 1;
    size_t  actual_size = *base;
    size_t  real_size = actual_size + sizeof(size_t);
    if (CORK_UNLIKELY(actual_size != expected_size)) {
        cork_abort
            ("Incorrect size when freeing pointer (got %zu, expected %zu)",
             expected_size, actual_size);
    }
    cork_alloc_free(alloc->parent, base, real_size);
}

struct cork_alloc *
cork_debug_alloc_new(const struct cork_alloc *parent)
{
    struct cork_alloc  *debug = cork_alloc_new_alloc(parent);
    cork_alloc_set_xmalloc(debug, cork_debug_alloc__xmalloc);
    cork_alloc_set_free(debug, cork_debug_alloc__free);
    return debug;
}
