#include <ruby.h>
#include <ruby/intern.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#include "mmap.h"

VALUE MMAPED_FILE = Qnil;

#define START_POSITION 8
#define INITIAL_SIZE (2 * sizeof(int32_t))

int open_and_extend_file(mm_ipc *i_mm, size_t len) {
    int fd;

    if ((fd = open(i_mm->t->path, i_mm->t->smode)) == -1) {
        rb_raise(rb_eArgError, "Can't open %s", i_mm->t->path);
    }

    if (lseek(fd, len - i_mm->t->len - 1, SEEK_END) == -1) {
        close(fd);
        rb_raise(rb_eIOError, "Can't lseek %lu", len - i_mm->t->len - 1);
    }

    if (write(fd, "\000", 1) != 1) {
        close(fd);
        rb_raise(rb_eIOError, "Can't extend %s", i_mm->t->path);
    }

    return fd;
}

void expand(mm_ipc *i_mm, size_t len) {
    if (len < i_mm->t->len) {
        rb_raise(rb_eArgError, "Can't reduce the size of mmap");
    }

    if (munmap(i_mm->t->addr, i_mm->t->len)) {
        rb_raise(rb_eArgError, "munmap failed");
    }

    int fd = open_and_extend_file(i_mm, len);

    i_mm->t->addr = mmap(0, len, i_mm->t->pmode, i_mm->t->vscope, fd, i_mm->t->offset);

    if (i_mm->t->addr == MAP_FAILED) {
        close(fd);
        rb_raise(rb_eArgError, "mmap failed");
    }

    if (close(fd) == -1){
        rb_raise(rb_eArgError, "Can't close %s", i_mm->t->path);
    }

    if ((i_mm->t->flag & MM_LOCK) && mlock(i_mm->t->addr, len) == -1) {
        rb_raise(rb_eArgError, "mlock(%d)", errno);
    }
    i_mm->t->len = len;
    i_mm->t->real = len;
}

inline uint32_t padding_length(uint32_t key_length) {
    return 8 - (sizeof(uint32_t) + key_length) % 8; // padding | 8 byte aligned
}

void save_entry(mm_ipc *i_mm, uint32_t offset, VALUE key, VALUE value){
    uint32_t key_length = (uint32_t)RSTRING_LEN(key);

    char *pos = (char *)i_mm->t->addr + offset;

    memcpy(pos, &key_length, sizeof(uint32_t));
    pos += sizeof(uint32_t);

    memmove(pos, StringValuePtr(key), key_length);
    pos += key_length;

    memset(pos, ' ', padding_length(key_length));
    pos += padding_length(key_length);

    double val = NUM2DBL(value);
    memcpy(pos, &val, sizeof(double));
}

inline uint32_t load_used(mm_ipc *i_mm) {
    uint32_t used = *((uint32_t *)i_mm->t->addr);

    if (used == 0){
        used = START_POSITION;
    }
    return used;
}

inline void save_used(mm_ipc *i_mm, uint32_t used) {
    *((uint32_t *)i_mm->t->addr) = used;
}

VALUE method_load_used(VALUE self) {
    mm_ipc *i_mm;

    GET_MMAP(self, i_mm, MM_MODIFY);
    return UINT2NUM(load_used(i_mm));
}

VALUE method_save_used(VALUE self, VALUE value) {
	Check_Type(value, T_FIXNUM);
    mm_ipc *i_mm;

    GET_MMAP(self, i_mm, MM_MODIFY);

    if (i_mm->t->len < INITIAL_SIZE) {
        expand(i_mm, INITIAL_SIZE);
    }

    save_used(i_mm, NUM2UINT(value));
    return value;
}

VALUE method_add_entry(VALUE self, VALUE positions, VALUE key, VALUE value) {
    Check_Type(positions, T_HASH);
    Check_Type(key, T_STRING);

    VALUE position = rb_hash_lookup(positions, key);
    if (position != Qnil){
        return position;
    }

    mm_ipc *i_mm;
    GET_MMAP(self, i_mm, MM_MODIFY);

    if (i_mm->t->flag & MM_FROZEN) {
        rb_error_frozen("mmap");
    }

    if (RSTRING_LEN(key) > UINT32_MAX) {
        rb_raise(rb_eArgError, "string length gt %u", UINT32_MAX);
    }

    uint32_t key_length = (uint32_t)RSTRING_LEN(key);
    uint32_t value_offset = sizeof(uint32_t) + key_length + padding_length(key_length);
    uint32_t entry_length = value_offset + sizeof(double);

    uint32_t used = load_used(i_mm);
    while (i_mm->t->len < (used + entry_length)) {
        expand(i_mm, i_mm->t->len * 2);
    }

    save_entry(i_mm, used, key, value);
    save_used(i_mm, used + entry_length);
    return rb_hash_aset(positions, key, INT2NUM(used + value_offset));
}

VALUE method_get_double(VALUE self, VALUE index) {
    mm_ipc *i_mm;
    GET_MMAP(self, i_mm, MM_MODIFY);

    Check_Type(index, T_FIXNUM);
    size_t idx = NUM2UINT(index);

    if ((i_mm->t->real + sizeof(double)) <= idx) {
        rb_raise(rb_eIndexError, "index %ld out of string", idx);
    }

    double tmp;

    memcpy(&tmp, (char *)i_mm->t->addr + idx, sizeof(double));
    return DBL2NUM(tmp);
}

void Init_fast_mmaped_file() {
    MMAPED_FILE = rb_define_module("FastMmapedFile");
    rb_define_method(MMAPED_FILE, "get_double", method_get_double, 1);
    rb_define_method(MMAPED_FILE, "used", method_load_used, 0);
    rb_define_method(MMAPED_FILE, "used=", method_save_used, 1);
    rb_define_method(MMAPED_FILE, "add_entry", method_add_entry, 3);
}