// Copyright (c) 2014 Johannes Huning <mail@johanneshuning.com>
// Copyright (c) 2015 Christian Lundgren, Chris de Vries, and Jon Elverkilde
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use, copy,
// modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
// BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Format this file with
//     indent -kr -i8 -sob c_src/hyper_carray.c
// before committing.

#include <stdint.h>
#include <stdio.h>
#include <string.h>

#include "erl_nif.h"

/*
 * Erlang NIF resource type used to 'tag' hyper_carrays.
 */
static ErlNifResourceType *carray_resource;

/*
 * Single resource produced and consumed by these NIFs.
 * Header placed directly in front of the items it points to.
 */
struct hyper_carray {
	/*
	 * Precision = log_2(size). Handy to have it.
	 */
	unsigned int precision;
	/*
	 * Number of items.
	 */
	unsigned int size;
	/*
	 * Array of items each one byte in size.
	 */
	uint8_t *items;
};

#ifdef _WIN32
typedef struct hyper_carray *carray_ptr;
#else
typedef struct hyper_carray *restrict carray_ptr;
#endif

#define HYPER_CARRAY_SIZE sizeof(struct hyper_carray)

/*
 * Attempts to read a hyper_carray from _term into _varname.
 * Returns badarg on failure to do so.
 */
#define HYPER_CARRAY_OR_BADARG(_term, _varname) \
    void* _varname_res = NULL; \
    if (!enif_get_resource(env, _term, carray_resource, &_varname_res)) \
        return enif_make_badarg(env); \
    _varname = _varname_res;

/*
 * Allocate a new hyper_carray for use as an Erlang NIF resource.
 */
static void carray_alloc(unsigned int precision, carray_ptr * arr)
{
	unsigned int nitems = 0x01 << precision;
	size_t header_size = HYPER_CARRAY_SIZE;
	size_t res_size = header_size + nitems;

	void *res = enif_alloc_resource(carray_resource, res_size);
	*arr = res;

	memset(*arr, 0, header_size);
	(*arr)->precision = precision;
	(*arr)->size = nitems;
	(*arr)->items = (uint8_t *) res + header_size;
}

/*
 * Given an hyper_carray and a valid index, set the value at that index to
 * max(current value, given value).
 */
#ifdef _WIN32
static void carray_merge_item(carray_ptr arr,
				     unsigned int index,
				     unsigned int value)
#else
static inline void carray_merge_item(carray_ptr arr,
				     unsigned int index,
				     unsigned int value)
#endif
{
	uint8_t *item = arr->items + index;
	*item = (value > *item) ? value : *item;
}

/*
 * Create a new hyper_carray resource with all items set to 0.
 */
static ERL_NIF_TERM new_hyper_carray(ErlNifEnv * env, int argc,
				     const ERL_NIF_TERM argv[])
{
	unsigned int precision = 0;
	if (!enif_get_uint(env, argv[0], &precision))
		return enif_make_badarg(env);

	carray_ptr arr = NULL;
	carray_alloc(precision, &arr);
	memset(arr->items, 0, arr->size);

	ERL_NIF_TERM erl_res = enif_make_resource(env, arr);
	enif_release_resource(arr);
	return erl_res;
}

/*
 * NIF variant of carray_merge_item (see above).
 */
static ERL_NIF_TERM set(ErlNifEnv * env, int argc,
			const ERL_NIF_TERM argv[])
{
	carray_ptr arr = NULL;
	HYPER_CARRAY_OR_BADARG(argv[2], arr);

	unsigned int index = 0;
	unsigned int new_value = 0;
	if (!enif_get_uint(env, argv[0], &index)
	    || !enif_get_uint(env, argv[1], &new_value))
		return enif_make_badarg(env);

	// Validate bounds
	if (index > arr->size - 1)
		return enif_make_badarg(env);

	carray_merge_item(arr, index, new_value);

	return argv[2];
}

void dtor(ErlNifEnv * env, void *obj);

/*
 * Given a list of at least 1 hyper_carrays [A,B,...], merge into a single new
 * hyper_carray N. Where the i-ths item N[i] is max(A[i], B[i], ...).
 * A, B, and so on are assumed to be _different_ hyper_carrays.
 */
static ERL_NIF_TERM max_merge(ErlNifEnv * env, int argc,
			      const ERL_NIF_TERM argv[])
{
	unsigned int narrays = 0;
	ERL_NIF_TERM head;
	ERL_NIF_TERM tail;

	if (!enif_get_list_length(env, argv[0], &narrays)
	    || !enif_get_list_cell(env, argv[0], &head, &tail))
		return enif_make_badarg(env);

	if (narrays < 1)
		return enif_make_badarg(env);

	carray_ptr first = NULL;
	HYPER_CARRAY_OR_BADARG(head, first);
	const unsigned int nitems = first->size;

	carray_ptr acc = NULL;
	carray_alloc(first->precision, &acc);
	memcpy(acc->items, first->items, acc->size);

	// Merge arrays
	for (int i = 1; i < narrays; ++i) {
		carray_ptr curr = NULL;

		if (!enif_get_list_cell(env, tail, &head, &tail)
		    || !enif_get_resource(env, head, carray_resource,
					  (void *) &curr))
			goto dealloc_and_badarg;

		// Require uniform precision.
		if (curr->precision != acc->precision)
			goto dealloc_and_badarg;

		for (uint8_t * accitem = acc->items, *item = curr->items,
		     *enditem = curr->items + nitems;
		     item != enditem; ++item, ++accitem) {
			*accitem = (*item > *accitem) ? *item : *accitem;
		}

		continue;

	      dealloc_and_badarg:
		dtor(env, acc);
		return enif_make_badarg(env);
	}

	ERL_NIF_TERM erl_res = enif_make_resource(env, acc);
	enif_release_resource(acc);
	return erl_res;
}

/*
 * Return the total number of bytes allocated for the given hyper_carray.
 * Includes the header's size.
 */
static ERL_NIF_TERM bytes(ErlNifEnv * env, int argc,
			  const ERL_NIF_TERM argv[])
{
	carray_ptr arr = NULL;
	HYPER_CARRAY_OR_BADARG(argv[0], arr);
	return enif_make_int(env, HYPER_CARRAY_SIZE + arr->size);
}

/*
 * Sum over 2^-X where X is the value of each item in the given hyper_carray.
 */
static ERL_NIF_TERM register_sum(ErlNifEnv * env, int argc,
				 const ERL_NIF_TERM argv[])
{
	carray_ptr arr = NULL;
	HYPER_CARRAY_OR_BADARG(argv[0], arr);

	int currval = 0;
	double sum = 0.0;
	unsigned int size = arr->size;

	for (int i = 0; i < size; ++i) {
		currval = arr->items[i];
		sum += 1.0 / (double) (0x01 << currval);
	}

	return enif_make_double(env, sum);
}

/*
 * Number of items with a 0 as value;
 */
static ERL_NIF_TERM zero_count(ErlNifEnv * env, int argc,
			       const ERL_NIF_TERM argv[])
{
	carray_ptr arr = NULL;
	HYPER_CARRAY_OR_BADARG(argv[0], arr);

	unsigned int nzeros = 0;
	unsigned int size = arr->size;

	for (int i = 0; i < size; ++i) {
		if (arr->items[i] == 0)
			++nzeros;
	}

	return enif_make_int(env, nzeros);
}

/*
 * Encode the given hyper_carray as an Erlang binary.
 */
static ERL_NIF_TERM encode_registers(ErlNifEnv * env, int argc,
				     const ERL_NIF_TERM argv[])
{
	carray_ptr arr = NULL;
	HYPER_CARRAY_OR_BADARG(argv[0], arr);

	size_t nbytes = arr->size;

	ERL_NIF_TERM bin;
	unsigned char *buf = enif_make_new_binary(env, nbytes, &bin);
	memcpy(buf, arr->items, nbytes);

	return bin;
}

/*
 * Decode the given serialized hyper_carray into a new resource.
 */
static ERL_NIF_TERM decode_registers(ErlNifEnv * env, int argc,
				     const ERL_NIF_TERM argv[])
{
	unsigned int precision = 0;
	ErlNifBinary bin;

	if (!enif_get_uint(env, argv[1], &precision)
	    || !enif_inspect_binary(env, argv[0], &bin))
		return enif_make_badarg(env);

	carray_ptr arr = NULL;
	carray_alloc(precision, &arr);
	memcpy(arr->items, bin.data, arr->size);

	ERL_NIF_TERM erl_res = enif_make_resource(env, arr);
	enif_release_resource(arr);

	return erl_res;
}

/*
 * Map of funs to NIFs.
 */
static ErlNifFunc niftable[] = {
	{"new", 1, new_hyper_carray},
	{"set", 3, set},
	{"max_merge", 1, max_merge},
	{"bytes", 1, bytes},
	{"register_sum", 1, register_sum},
	{"zero_count", 1, zero_count},
	{"encode_registers", 1, encode_registers},
	{"decode_registers", 2, decode_registers}
};

/*
 * Destructor for hyper_carray resources.
 */
void dtor(ErlNifEnv * env, void *obj)
{
	enif_release_resource(obj);
}

/*
 * Creates or opens the hyper_carray resource _type_.
 * Registers dtor to be called on garbage collection of hyper_carrays.
 * Please see http://www.erlang.org/doc/man/erl_nif.html.
 */
static int load(ErlNifEnv * env, void **priv_data, ERL_NIF_TERM load_info)
{
	carray_resource =
	    enif_open_resource_type(env, NULL, "hyper_carray", &dtor,
				    ERL_NIF_RT_CREATE |
				    ERL_NIF_RT_TAKEOVER, 0);
	return 0;
}

/*
 * Called when the NIF library is loaded and there is old code of this module
 * with a loaded NIF library.
 */
static int upgrade(ErlNifEnv * env, void **priv, void **old_priv,
		   ERL_NIF_TERM load_info)
{
	*priv = *old_priv;
	return 0;
}

/*
 * Initialize the NIF library.
 */
ERL_NIF_INIT(hyper_carray, niftable, &load, NULL, &upgrade, NULL);
