#include <stddef.h>
#include <stdlib.h>
#include "sysdep.h"
#include "constants.h"
#include "Python.h"
#include <limits.h>
#include <string.h>

#ifdef __cplusplus
extern "C" {
#endif

typedef struct erlpack_buffer {
    char *buf;
    size_t length;
    size_t allocated_size;
} erlpack_buffer;

static inline int erlpack_buffer_write(erlpack_buffer* pk, const char *bytes, size_t l)
{
    char* buf = pk->buf;
    size_t allocated_size = pk->allocated_size;
    size_t length = pk->length;

    if (length + l > allocated_size) {
        // Grow buffer 2x to avoid excessive re-allocations.
        allocated_size = (length + l) * 2;
        buf = (char*)realloc(buf, allocated_size);

        if (!buf)
            return -1;
    }

    memcpy(buf + length, bytes, l);
    length += l;

    pk->buf = buf;
    pk->allocated_size = allocated_size;
    pk->length = length;
    return 0;
}

#define erlpack_append(pk, buf, len) return erlpack_buffer_write(pk, (const char*)buf, len)


static inline int erlpack_append_version(erlpack_buffer *b) {
    static unsigned char buf[1] = {FORMAT_VERSION};
    erlpack_append(b, buf, 1);
}

static inline int erlpack_append_nil(erlpack_buffer* b) {
    unsigned char buf[1 + 2 + 3];
    buf[0] = ATOM_EXT;
    _erlpack_store16(&buf[1], 3);
    buf[3] = 'n';
    buf[4] = 'i';
    buf[5] = 'l';
    erlpack_append(b, buf, 6);
}
static inline int erlpack_append_false(erlpack_buffer* b) {
    unsigned char buf[1 + 2 + 5];
    buf[0] = ATOM_EXT;
    _erlpack_store16(&buf[1], 5);
    buf[3] = 'f';
    buf[4] = 'a';
    buf[5] = 'l';
    buf[6] = 's';
    buf[7] = 'e';
    erlpack_append(b, buf, 1 + 2 + 5);
}

static inline int erlpack_append_true(erlpack_buffer* b) {
    unsigned char buf[1 + 2 + 4];
    buf[0] = ATOM_EXT;
    _erlpack_store16(&buf[1], 4);
    buf[3] = 't';
    buf[4] = 'r';
    buf[5] = 'u';
    buf[6] = 'e';
    erlpack_append(b, buf, 1 + 2 + 4);
}

static inline int erlpack_append_small_integer(erlpack_buffer *b, unsigned char d) {
    unsigned char buf[2] = {SMALL_INTEGER_EXT, d};
    erlpack_append(b, buf, 2);
}

static inline int erlpack_append_integer(erlpack_buffer *b, int32_t d) {
    unsigned char buf[5];
    buf[0] = INTEGER_EXT;
    _erlpack_store32(buf + 1, d);
    erlpack_append(b, buf, 5);
}

static inline int erlpack_append_unsigned_long_long(erlpack_buffer *b, unsigned long long d) {
    unsigned char buf[1 + 2 + sizeof(unsigned long long)];
    buf[0] = SMALL_BIG_EXT;

    unsigned char bytes_enc = 0;
    while (d > 0) {
        buf[3 + bytes_enc] = d & 0xFF;
        d >>= 8;
        bytes_enc++;
    }
    buf[1] = bytes_enc;
    buf[2] = 0;

    erlpack_append(b, buf, 1 + 2 + bytes_enc);
}

static inline int erlpack_append_long_long(erlpack_buffer *b, long long d) {
    unsigned char buf[1 + 2 + sizeof(unsigned long long)];
    buf[0] = SMALL_BIG_EXT;
    buf[2] = d < 0 ? 1 : 0;
    unsigned long long ull = d < 0 ? -d : d;
    unsigned char bytes_enc = 0;
    while (ull > 0) {
        buf[3 + bytes_enc] = ull & 0xFF;
        ull >>= 8;
        bytes_enc++;
    }
    buf[1] = bytes_enc;
    erlpack_append(b, buf, 1 + 2 + bytes_enc);
}

static inline int erlpack_append_double(erlpack_buffer *b, double f) {
    unsigned char buf[1 + 31] = {0};
    buf[0] = FLOAT_EXT;
    snprintf((char *)(buf + 1), 31, "%.20e", f);
    erlpack_append(b, buf, 1 + 31);
}

static inline int erlpack_append_atom(erlpack_buffer *b, PyObject *o) {
    unsigned char buf[3];
    buf[0] = ATOM_EXT;

    size_t size = PyString_Size(o);
    if (size > 0xFFFF) {
        return 1;
    }

    _erlpack_store16(buf + 1, size);

    int ret = erlpack_buffer_write(b, (const char*)buf, 3);
    if (ret < 0)
        return ret;

    erlpack_append(b, PyString_AsString(o), size);
}

static inline int erlpack_append_binary(erlpack_buffer *b, PyObject *o) {
    unsigned char buf[5];
    buf[0] = BINARY_EXT;

    size_t size = PyString_Size(o);
    _erlpack_store32(buf + 1, size);

    int ret = erlpack_buffer_write(b, (const char*)buf, 5);
    if (ret < 0)
        return ret;

    erlpack_append(b, PyString_AsString(o), size);
}

static inline int erlpack_append_string(erlpack_buffer *b, PyObject *o) {
    unsigned char buf[3];
    buf[0] = STRING_EXT;

    size_t size = PyString_Size(o);
    _erlpack_store16(buf + 1, size);

    int ret = erlpack_buffer_write(b, (const char*)buf, 3);
    if (ret < 0)
        return ret;

    erlpack_append(b, PyString_AsString(o), size);
}

static inline int erlpack_append_tuple_header(erlpack_buffer *b, size_t size) {
    if (size < 256) {
        unsigned char buf[2];
        buf[0] = SMALL_TUPLE_EXT;
        buf[1] = (unsigned char) size;
        erlpack_append(b, buf, 2);
    } else {
        unsigned char buf[5];
        buf[0] = LARGE_TUPLE_EXT;
        _erlpack_store32(buf + 1, size);
        erlpack_append(b, buf, 5);
    }
}

static inline int erlpack_append_nil_ext(erlpack_buffer *b) {
    static unsigned char buf[1] = {NIL_EXT};
    erlpack_append(b, buf, 1);
}

static inline int erlpack_append_list_header(erlpack_buffer *b, size_t size) {
    unsigned char buf[5];
    buf[0] = LIST_EXT;
    _erlpack_store32(buf + 1, size);
    erlpack_append(b, buf, 5);
}

static inline int erlpack_append_map_header(erlpack_buffer *b, size_t size) {
    unsigned char buf[5];
    buf[0] = MAP_EXT;
    _erlpack_store32(buf + 1, size);
    erlpack_append(b, buf, 5);
}


#ifdef __cplusplus
}

#endif