/*
	libloc - A library to determine the location of someone on the Internet

	Copyright (C) 2017 IPFire Development Team <info@ipfire.org>

	This library is free software; you can redistribute it and/or
	modify it under the terms of the GNU Lesser General Public
	License as published by the Free Software Foundation; either
	version 2.1 of the License, or (at your option) any later version.

	This library is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
	Lesser General Public License for more details.
*/

#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

#include <libloc/libloc.h>
#include <libloc/format.h>
#include <libloc/private.h>
#include <libloc/stringpool.h>

enum loc_stringpool_mode {
	STRINGPOOL_DEFAULT,
	STRINGPOOL_MMAP,
};

struct loc_stringpool {
	struct loc_ctx* ctx;
	int refcount;

	enum loc_stringpool_mode mode;

	char* data;
	ssize_t length;

	char* pos;
};

static off_t loc_stringpool_get_offset(struct loc_stringpool* pool, const char* pos) {
	if (pos < pool->data)
		return -EFAULT;

	if (pos > (pool->data + pool->length))
		return -EFAULT;

	return pos - pool->data;
}

static char* __loc_stringpool_get(struct loc_stringpool* pool, off_t offset) {
	if (offset < 0 || offset >= pool->length)
		return NULL;

	return pool->data + offset;
}

static int loc_stringpool_grow(struct loc_stringpool* pool, size_t length) {
	DEBUG(pool->ctx, "Growing string pool to %zu bytes\n", length);

	// Save pos pointer
	off_t pos = loc_stringpool_get_offset(pool, pool->pos);

	// Reallocate data section
	pool->data = realloc(pool->data, length);
	if (!pool->data)
		return -ENOMEM;

	pool->length = length;

	// Restore pos
	pool->pos = __loc_stringpool_get(pool, pos);

	return 0;
}

static off_t loc_stringpool_append(struct loc_stringpool* pool, const char* string) {
	if (!string)
		return -EINVAL;

	DEBUG(pool->ctx, "Appending '%s' to string pool at %p\n", string, pool);

	// Make sure we have enough space
	int r = loc_stringpool_grow(pool, pool->length + strlen(string) + 1);
	if (r) {
		errno = r;
		return -1;
	}

	off_t offset = loc_stringpool_get_offset(pool, pool->pos);

	// Copy string byte by byte
	while (*string)
		*pool->pos++ = *string++;

	// Terminate the string
	*pool->pos++ = '\0';

	return offset;
}

static void loc_stringpool_free(struct loc_stringpool* pool) {
	DEBUG(pool->ctx, "Releasing string pool %p\n", pool);
	int r;

	switch (pool->mode) {
		case STRINGPOOL_DEFAULT:
			if (pool->data)
				free(pool->data);
			break;

		case STRINGPOOL_MMAP:
			if (pool->data) {
				r = munmap(pool->data, pool->length);
				if (r)
					ERROR(pool->ctx, "Could not unmap data at %p: %s\n",
						pool->data, strerror(errno));
			}
			break;
	}

	loc_unref(pool->ctx);
	free(pool);
}

LOC_EXPORT int loc_stringpool_new(struct loc_ctx* ctx, struct loc_stringpool** pool) {
	struct loc_stringpool* p = calloc(1, sizeof(*p));
	if (!p)
		return 1;

	p->ctx = loc_ref(ctx);
	p->refcount = 1;

	// Save mode
	p->mode = STRINGPOOL_DEFAULT;

	*pool = p;

	return 0;
}

static int loc_stringpool_mmap(struct loc_stringpool* pool, FILE* f, size_t length, off_t offset) {
	if (pool->mode != STRINGPOOL_MMAP)
		return -EINVAL;

	DEBUG(pool->ctx, "Reading string pool starting from %jd (%zu bytes)\n", (intmax_t)offset, length);

	// Map file content into memory
	pool->data = pool->pos = mmap(NULL, length, PROT_READ,
		MAP_PRIVATE, fileno(f), offset);

	// Store size of section
	pool->length = length;

	if (pool->data == MAP_FAILED)
		return 1;

	return 0;
}

LOC_EXPORT int loc_stringpool_open(struct loc_ctx* ctx, struct loc_stringpool** pool,
		FILE* f, size_t length, off_t offset) {
	struct loc_stringpool* p = NULL;

	// Allocate a new stringpool
	int r = loc_stringpool_new(ctx, &p);
	if (r)
		return r;

	// Change mode to mmap
	p->mode = STRINGPOOL_MMAP;

	// Map data into memory
	if (length > 0) {
		r = loc_stringpool_mmap(p, f, length, offset);
		if (r) {
			loc_stringpool_free(p);
			return r;
		}
	}

	*pool = p;
	return 0;
}

LOC_EXPORT struct loc_stringpool* loc_stringpool_ref(struct loc_stringpool* pool) {
	pool->refcount++;

	return pool;
}

LOC_EXPORT struct loc_stringpool* loc_stringpool_unref(struct loc_stringpool* pool) {
	if (--pool->refcount > 0)
		return NULL;

	loc_stringpool_free(pool);

	return NULL;
}

static off_t loc_stringpool_get_next_offset(struct loc_stringpool* pool, off_t offset) {
	const char* string = loc_stringpool_get(pool, offset);
	if (!string)
		return offset;

	return offset + strlen(string) + 1;
}

LOC_EXPORT const char* loc_stringpool_get(struct loc_stringpool* pool, off_t offset) {
	return __loc_stringpool_get(pool, offset);
}

LOC_EXPORT size_t loc_stringpool_get_size(struct loc_stringpool* pool) {
	return loc_stringpool_get_offset(pool, pool->pos);
}

static off_t loc_stringpool_find(struct loc_stringpool* pool, const char* s) {
	if (!s || !*s)
		return -EINVAL;

	off_t offset = 0;
	while (offset < pool->length) {
		const char* string = loc_stringpool_get(pool, offset);
		if (!string)
			break;

		int r = strcmp(s, string);
		if (r == 0)
			return offset;

		offset = loc_stringpool_get_next_offset(pool, offset);
	}

	return -ENOENT;
}

LOC_EXPORT off_t loc_stringpool_add(struct loc_stringpool* pool, const char* string) {
	off_t offset = loc_stringpool_find(pool, string);
	if (offset >= 0) {
		DEBUG(pool->ctx, "Found '%s' at position %jd\n", string, (intmax_t)offset);
		return offset;
	}

	return loc_stringpool_append(pool, string);
}

LOC_EXPORT void loc_stringpool_dump(struct loc_stringpool* pool) {
	off_t offset = 0;

	while (offset < pool->length) {
		const char* string = loc_stringpool_get(pool, offset);
		if (!string)
			break;

		printf("%jd (%zu): %s\n", (intmax_t)offset, strlen(string), string);

		offset = loc_stringpool_get_next_offset(pool, offset);
	}
}

LOC_EXPORT size_t loc_stringpool_write(struct loc_stringpool* pool, FILE* f) {
	size_t size = loc_stringpool_get_size(pool);

	return fwrite(pool->data, sizeof(*pool->data), size, f);
}