/*
 * Copyright (C) 2020-2021  Tomasz Kramkowski <tk@the-tk.com>
 * SPDX-License-Identifier: MIT
 */
#include <ctype.h>
#include <errno.h>
#include <inttypes.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

#include "common.h"
#include "ieee754b.h"
#include "pack.h"
#include "trace.h"

struct dest {
	enum pack_type type;
	union ptr {
		#define T(type, sign, c_type, va_type) sign c_type *type;
		ITYPE_MACROS
		#undef T
		float *f;
		double *d;
	} ptr;
	size_t count;
};

static uintmax_t read_val(const unsigned char *buf, size_t size, enum pack_endian e)
{
	uintmax_t val = 0;

	for (size_t i = 0; i < size; i++) {
		val |= (uintmax_t)(buf[i] & 0xff) << (e == PACK_ENDIAN_LITTLE ? i : size - i - 1) * 8;
	}

	return val;
}

static intmax_t minval(size_t s)
{
	switch (s) {
	case 1: return INTMAX_C(-128);
	case 2: return INTMAX_C(-32768);
	case 4: return INTMAX_C(-2147483648);
	default: return -INTMAX_C(9223372036854775807) - 1;
	}
}

static void read_fields(struct dest dest, const void *src_, enum pack_endian endianness)
{
	union {
		uintmax_t unsigned_;
		intmax_t signed_;
		float f;
		double d;
	} val;
	const unsigned char *src = src_;
	size_t s = getsize(dest.type);

	for (size_t i = 0; i < dest.count; i++) {
		val.unsigned_ =
			read_val(&src[s * i], s, endianness);
		tr_debug("val.u: %" PRIuMAX ", at: %" PRIuSIZE,
			 val.unsigned_, s * i);

		if (dest.type == PACK_TYPE_FLOAT) {
			float f = ieee754b32_deserialise(val.unsigned_);
			val.f = f;
			tr_debug("val.f: %f", val.f);
		} else if (dest.type == PACK_TYPE_DOUBLE) {
			double d = ieee754b64_deserialise(val.unsigned_);
			val.d = d;
			tr_debug("val.d: %f", val.d);
		} else if (islower((char)dest.type)) {
			intmax_t vals;
			if (!(val.unsigned_ & (UINTMAX_C(1) << (s * 8 - 1)))) {
				vals = val.unsigned_;
			} else {
				vals = minval(s);
				vals += val.unsigned_ ^ (UINTMAX_C(1) << (s * 8 - 1));
			}
			val.signed_ = vals;
			tr_debug("val.s: %" PRIdMAX, val.signed_);
		}

		switch (dest.type) {
		#define T(type, sign, c_type, va_type) \
			case (char)PACK_TYPE_##type: \
				dest.ptr.type[i] = val.sign##_; \
				break;
		ITYPE_MACROS
		#undef T
		case PACK_TYPE_FLOAT: dest.ptr.f[i] = val.f; break;
		case PACK_TYPE_DOUBLE: dest.ptr.d[i] = val.d; break;
		case PACK_TYPE_PADDING: break;
		case PACK_TYPE_COUNT: return;
		}
	}
}

enum pack_status unpack(const void *buf_, size_t size, const char *fmt, ...)
{
	enum pack_endian endianness = PACK_ENDIAN_BIG;
	const unsigned char *buf = buf_;
	enum pack_status ret = PACK_OK;
	size_t offset = 0;
	va_list ap;

	tr_call("unpack(%p, %" PRIuSIZE ", %s, ...)", (const void *)buf, size, fmt);

	va_start(ap, fmt);

	for (int i = 0; fmt[i] != '\0'; i++) {
		size_t s;
		struct dest dest = { .count = 1 };
		tr_debug("i: %d, fmt[i]: %c", i, fmt[i]);
		if (isdigit(fmt[i])) {
			unsigned long long c;
			char *end;

			errno = 0;
			c = strtoull(&fmt[i], &end, 10);
			if ((c == ULLONG_MAX && errno == ERANGE) || c > SIZE_MAX)
				SET_AND_GOTO(ret, PACK_FMTINVAL, stop);
			dest.count = c;
			i += end - &fmt[i];
		} else if (fmt[i] == PACK_TYPE_COUNT) {
			dest.count = va_arg(ap, size_t);
			i++;
		}
		tr_debug("dest.count: %" PRIuSIZE ", i: %d, fmt[i]: %c", dest.count, i, fmt[i]);
		switch (fmt[i]) {
		case '>': endianness = PACK_ENDIAN_BIG; continue;
		case '<': endianness = PACK_ENDIAN_LITTLE; continue;
		#define T(type, sign, c_type, va_type) \
			case (char)PACK_TYPE_##type: \
				dest.ptr.type = va_arg(ap, sign c_type *); \
				break;
		ITYPE_MACROS
		#undef T
		case PACK_TYPE_FLOAT: dest.ptr.f = va_arg(ap, float *); break;
		case PACK_TYPE_DOUBLE: dest.ptr.d = va_arg(ap, double *); break;
		case PACK_TYPE_PADDING: break;
		default: SET_AND_GOTO(ret, PACK_FMTINVAL, stop);
		}
		dest.type = fmt[i];

		s = getsize(dest.type);
		tr_debug("s: %" PRIuSIZE, s);
		if (s == (size_t)-1) SET_AND_GOTO(ret, PACK_FMTINVAL, stop);

		if (size - offset < s * dest.count)
			SET_AND_GOTO(ret, PACK_TOOSMALL, stop);

		if (dest.type != PACK_TYPE_PADDING)
			read_fields(dest, &buf[offset], endianness);

		offset += s * dest.count;
	}

stop:
	va_end(ap);

	return ret;
}

enum pack_status unpack_struct(const void *buf_, size_t size,
                               const struct pack_args *args, void *dest_)
{
	const unsigned char *buf = buf_;
	char *dest = dest_;
	size_t offset = 0;

	tr_call("unpack_struct(%p, %" PRIuSIZE ", %p, %p)", \
	        (const void *)buf, size, (const void *)args, (const void *)dest);

	tr_debug("args->endian: %s, args->fields: %p, args->num_fields: %" PRIuSIZE,
	         args->endian == PACK_ENDIAN_BIG ? "big" : "little",
	         (const void *)args->fields, args->num_fields);

	for (size_t i = 0; i < args->num_fields; i++) {
		#define FIELD (void *)&dest[f->offset]
		const struct pack_field *f = &args->fields[i];
		struct pack_field indirect;
		struct dest field;
		size_t s;

		if (f->type == PACK_TYPE_COUNT) {
			if (i + 1 >= args->num_fields) return PACK_FMTINVAL;
			indirect = args->fields[++i];
			indirect.count = *(size_t *)FIELD;
			f = &indirect;
		}

		s = getsize(f->type);
		if (s == (size_t)-1) return PACK_FMTINVAL;

		tr_debug("i: %" PRIuSIZE ", f->type: %c, f->count: %" PRIuSIZE ", "
		         "f->offset: %" PRIuSIZE ", s: %" PRIuSIZE,
		         i, (char)f->type, f->count, f->offset, s);

		field.type = f->type;
		switch (field.type) {
		#define T(type, sign, c_type, va_type) \
			case PACK_TYPE_##type: field.ptr.type = FIELD; break;
		ITYPE_MACROS
		#undef T
		case PACK_TYPE_FLOAT: field.ptr.f = FIELD; break;
		case PACK_TYPE_DOUBLE: field.ptr.d = FIELD; break;
		case PACK_TYPE_PADDING: break;
		default: return PACK_FMTINVAL;
		}
		field.count = f->count;

		if (size - offset < s * field.count) return PACK_TOOSMALL;

		if (f->type != PACK_TYPE_PADDING)
			read_fields(field, &buf[offset], args->endian);

		offset += s * field.count;
		#undef FIELD
	}

	return PACK_OK;
}