/**
 * \file asn1gen.c
 * \brief ASN.1 generator.
 * \author Dmytro Podgornyi (d.podgornyi@samsung.com)
 * \version 0.1
 * \date Created May 28, 2013
 * \par In Samsung Ukraine R&D Center (SURC) under a contract between
 * \par LLC "Samsung Electronics Ukraine Company" (Kiev, Ukraine) and
 * \par "Samsung Elecrtronics Co", Ltd (Seoul, Republic of Korea)
 * \par Copyright: (c) Samsung Electronics Co, Ltd 2012. All rights reserved.
 **/

#include <stdint.h>
#include <string.h>

#include "CommLayerData.h"
#include "asn1.h"
#include "bn/bn_wrapper.h"
#include "log.h"

#include "asn1gen.h"

static uint32_t asn1_get_seq_bytes(const asn1_gen_t *seq);
static int32_t asn1_gen_seq_helper(uint8_t *out, uint32_t out_len, const asn1_gen_t *seq);

static uint32_t asn1_get_val_bytes(const asn1_gen_t *field)
{
	void *value = field->value;
	asn1_type_t type = field->type;
	asn1_object_t *obj_val = NULL;
	asn1_string_t *str_val = NULL;
	BIGNUM *bn_val = NULL;
	unsigned long long_val = 0;
	uint32_t result = 0;
	int i = 0;

	switch (type)
	{
		case ASN1_TYPE_BOOLEAN:
			result = 1;
			break;
		case ASN1_TYPE_LONG:
			long_val = (unsigned long)value;
			result = 0;
			do {
				++result;
				i = long_val & 0x80;
				long_val >>= 8;
			} while (long_val > 0);
			if (i) {
				++result;
			}
			break;

		case ASN1_TYPE_BIGNUM:
			bn_val = (BIGNUM*)value;
			result = BN_num_bytes(bn_val);
			if (bn_val->top > 0) {
				long_val = bn_val->d[bn_val->top - 1];
				if (long_val != ((long_val << 1) >> 1)) {
					++result;
				}
			}
			break;
		case ASN1_TYPE_BITSTRING:
			str_val = (asn1_string_t*)value;
			result = str_val->size + 1;
			break;
		case ASN1_TYPE_OCTETSTRING:
			str_val = (asn1_string_t*)value;
			result = str_val->size;
			break;
		case ASN1_TYPE_NULL:
			result = 0;
			break;
		case ASN1_TYPE_OBJECT:
			obj_val = (asn1_object_t*)value;
			result = 0;
			for (i = 1; i < obj_val->size; i++) {
				long_val = obj_val->content[i];
				if (i == 1) {
					long_val += obj_val->content[0] * 40;
				}
				do {
					++result;
					long_val >>= 7;
				} while (long_val > 0);
			}
			break;
		case ASN1_TYPE_UTCTIME:
		case ASN1_TYPE_UTF8STRING:
		case ASN1_TYPE_IA5STRING:
		case ASN1_TYPE_PRINTABLE_STRING:
			result = strlen((char*)value);
			break;
		case ASN1_TYPE_SET:
		case ASN1_TYPE_EXPL_0:
		case ASN1_TYPE_IMPL_0:
		case ASN1_TYPE_EXPL_1:
		case ASN1_TYPE_EXPL_3:
		case ASN1_TYPE_SEQUENCE:
			/* size of the entry of sequence */
			result = asn1_get_seq_bytes((const asn1_gen_t*)value);
			break;
		case ASN1_TYPE_RAW:
			str_val = (asn1_string_t*)value;
			result = str_val->size;
			break;
		default:
			LOGE("asn1_get_val_bytes: unknown type: %d", type);
			result = 0;
			break;
	}

	return result;
}

/* return value 0 means error */
static uint32_t asn1_get_len_bytes(uint32_t len)
{
	if (len < 128) {
		return 1;
	}
	if (len < 256) {
		return 2;
	}
	if (len < 65536) {
		return 3;
	}
	if (len < 0x1000000) {
		return 4;
	}

	LOGD("asn1_get_len_bytes: too big length for ASN.1");
	return 0;
}

uint32_t asn1_get_field_bytes(const asn1_gen_t *field)
{
	uint32_t len = 0;

	len = asn1_get_val_bytes(field);

	if (field->type == ASN1_TYPE_RAW) {
		return len;
	} else {
		/* tag + length + value */
		return (1 + asn1_get_len_bytes(len) + len);
	}
}

static uint32_t asn1_get_seq_bytes(const asn1_gen_t *seq)
{
	uint32_t result;

	result = 0;
	while (seq->type != ASN1_TYPE_END) {
		result += asn1_get_field_bytes(seq);
		++seq;
	}

	return result;
}

static void asn1_set_len(uint8_t *out, uint32_t len)
{
	/* buffer overflow is checked in asn1_gen_field() */

	if (len < 128) {
		*out = len;
	} else if (len < 256) {
		*out++ = 0x81;
		*out = len;
	} else if (len < 65536) {
		*out++ = 0x82;
		*out++ = len >> 8;
		*out = len & 0xff;
	} else {
		*out++ = 0x83;
		*out++ = (len >> 16) & 0xff;
		*out++ = (len >> 8) & 0xff;
		*out = len & 0xff;
	}
}

static unsigned long asn1_set_object_recurse(uint8_t *out, unsigned long value)
{
	int i;

	if (value < 128) {
		*out = value | 0x80;
		return 1;
	}

	i = asn1_set_object_recurse(out, value >> 7);
	out[i] = (value & 0x7f) | 0x80;
	return i + 1;
}

static void asn1_set_object(uint8_t *out, const asn1_object_t *obj)
{
	int i = 0;
	unsigned long tmp = 0;

	if (obj->size > 1) {
		tmp = obj->content[0] * 40 + obj->content[1];
		out += asn1_set_object_recurse(out, tmp);
		*(out - 1) &= 0x7f;
	}
	for (i = 2; i < obj->size; i++) {
		out += asn1_set_object_recurse(out, obj->content[i]);
		*(out - 1) &= 0x7f;
	}
}

static unsigned long asn1_set_long_recurse(uint8_t *out, unsigned long value)
{
	int i = 0;

	if (value < 0x80) {
		*out = value;
		return 1;
	}

	i = asn1_set_long_recurse(out, value >> 8);
	out[i] = value & 0xff;
	return i + 1;
}

int32_t asn1_gen_field(uint8_t *out, uint32_t out_len, const asn1_gen_t *field)
{
	void *value = field->value;
	asn1_type_t type = field->type;
	BIGNUM *bn_val = NULL;
	unsigned long long_val = 0;
	uint32_t len = 0;
	uint32_t len_len = 0;
	int32_t result = PLATFORM_INTERNAL_ERROR;

	len = asn1_get_field_bytes(field);
	if (out_len < len) {
		LOGE("asn1_gen_field: not enough space to write ASN.1");
		return WRONG_DATA;
	}

	switch (type)
	{
		case ASN1_TYPE_BOOLEAN:
			*out = ASN1_TAG_BOOLEAN;
			break;
		case ASN1_TYPE_LONG:
		case ASN1_TYPE_BIGNUM:
			*out = ASN1_TAG_INTEGER;
			break;
		case ASN1_TYPE_BITSTRING:
			*out = ASN1_TAG_BITSTRING;
			break;
		case ASN1_TYPE_OCTETSTRING:
			*out = ASN1_TAG_OCTETSTRING;
			break;
		case ASN1_TYPE_NULL:
			*out = ASN1_TAG_NULL;
			break;
		case ASN1_TYPE_OBJECT:
			*out = ASN1_TAG_OID;
			break;
		case ASN1_TYPE_UTF8STRING:
			*out = ASN1_TAG_UTF8STRING;
			break;
		case ASN1_TYPE_IA5STRING:
			*out = ASN1_TAG_IA5STRING;
			break;
		case ASN1_TYPE_PRINTABLE_STRING:
			*out = ASN1_TAG_PRINTABLESTRING;
			break;
		case ASN1_TYPE_UTCTIME:
			*out = ASN1_TAG_UTCTIME;
			break;
		case ASN1_TYPE_SEQUENCE:
			*out = ASN1_TAG_SEQUENCE | 0x20;
			break;
		case ASN1_TYPE_SET:
			*out = ASN1_TAG_SET | 0x20;
			break;
		case ASN1_TYPE_EXPL_0:
			*out = 0xA0;
			break;
		case ASN1_TYPE_IMPL_0:
			*out = 0x80;
			break;
		case ASN1_TYPE_EXPL_1:
			*out = 0xA1;
			break;
		case ASN1_TYPE_EXPL_3:
			*out = 0xA3;
			break;
		case ASN1_TYPE_RAW:
			/* do nothing */
			break;
		default:
			LOGD("asn1_gen_field: unknown type: %d", type);
			return WRONG_DATA;
	}

	if (type != ASN1_TYPE_RAW) {
		++out;
		--out_len;

		len = asn1_get_val_bytes(field);
		asn1_set_len(out, len);
		len_len = asn1_get_len_bytes(len);
		out += len_len;
		out_len -= len_len;
	}

	result = NO_ERROR;

	switch (type)
	{
		case ASN1_TYPE_BOOLEAN:
			if (value == (void*)0) {
				*out = 0;
			} else {
				*out = 0xff;
			}
			break;
		case ASN1_TYPE_LONG:
			(void)asn1_set_long_recurse(out, (unsigned long)value);
			break;
		case ASN1_TYPE_BIGNUM:
			bn_val = (BIGNUM*)value;
			if (bn_val->top > 0) {
				long_val = bn_val->d[bn_val->top - 1];
				if (long_val != ((long_val << 1) >> 1)) {
					*out = 0;
					++out;
					--len;
				}
			}
			if (BN_bn2bin(bn_val, out) != len) {
				LOGE("asn1_gen_field: wrote incomplete BIGNUM");
			}
			break;
		case ASN1_TYPE_BITSTRING:
			*out = ((asn1_string_t*)value)->unused;
			memcpy(out + 1, ((asn1_string_t*)value)->data, ((asn1_string_t*)value)->size);
			break;
		case ASN1_TYPE_OCTETSTRING:
			memcpy(out, ((asn1_string_t*)value)->data, ((asn1_string_t*)value)->size);
			break;
		case ASN1_TYPE_NULL:
			break;
		case ASN1_TYPE_OBJECT:
			asn1_set_object(out, (asn1_object_t*)value);
			break;
		case ASN1_TYPE_UTCTIME:
		case ASN1_TYPE_UTF8STRING:
		case ASN1_TYPE_IA5STRING:
		case ASN1_TYPE_PRINTABLE_STRING:
			strncpy((char*)out, (char*)value, strlen((char*)value));
			break;
		case ASN1_TYPE_SET:
		case ASN1_TYPE_EXPL_0:
		case ASN1_TYPE_IMPL_0:
		case ASN1_TYPE_EXPL_1:
		case ASN1_TYPE_EXPL_3:
		case ASN1_TYPE_SEQUENCE:
			result = asn1_gen_seq_helper(out, out_len, (asn1_gen_t*)value);
			break;
		case ASN1_TYPE_RAW:
			memcpy(out, ((asn1_string_t*)value)->data, ((asn1_string_t*)value)->size);
			break;
		default:
			LOGD("asn1_gen_field: unknown type: %d", type);
			return WRONG_DATA;
	}

	if (result < 0) {
		return result;
	}
	return asn1_get_field_bytes(field);
}

static int32_t asn1_gen_seq_helper(uint8_t *out, uint32_t out_len, const asn1_gen_t *seq)
{
	uint32_t len = 0;
	int32_t result = 0;

	while (seq->type != ASN1_TYPE_END) {
		len = asn1_get_field_bytes(seq);
		if (out_len < len) {
			LOGD("asn1_gen_seq_helper: not enough space to write SEQUENCE");
			return WRONG_DATA;
		}

		result = asn1_gen_field(out, out_len, seq);
		if (result < 0) {
			break;
		}
		out += len;
		out_len -= len;
		++seq;
	}

	return result;
}

int32_t asn1_gen_sequence(uint8_t *out, uint32_t out_len, const asn1_gen_t *seq)
{
	int32_t result = 0;
	asn1_gen_t seq_stub = ASN1GEN_INIT;

	seq_stub.type = ASN1_TYPE_SEQUENCE;
	seq_stub.value = (void*)seq;

	if (!out || !seq) {
		return WRONG_DATA;
	}

	result = asn1_gen_field(out, out_len, &seq_stub);
	if (result < 0) {
		LOGD("asn1_gen_sequence: error while generation of ASN.1");
		return result;
	}

	return asn1_get_field_bytes(&seq_stub);
}

int32_t asn1_validate(const asn1_gen_t *seq)
{
	while (seq->type != ASN1_TYPE_END) {
		if (seq->value == NULL && seq->type != ASN1_TYPE_NULL && seq->type != ASN1_TYPE_LONG) {
			return WRONG_DATA;
		}

		switch (seq->type)
		{
			case ASN1_TYPE_SET:
			case ASN1_TYPE_EXPL_0:
			case ASN1_TYPE_IMPL_0:
			case ASN1_TYPE_EXPL_1:
			case ASN1_TYPE_EXPL_3:
			case ASN1_TYPE_SEQUENCE:
				if (asn1_validate((asn1_gen_t*)seq->value) != NO_ERROR) {
					return WRONG_DATA;
				}
				break;
			default:
				break;
		}
		++seq;
	}

	return NO_ERROR;
}