/**
 * \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 "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 int32_t extra_byte_required(const char* long_val)
{
    return long_val ? long_val[0] & 0x80 : 0;
}

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;
    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_RAW:
        str_val = (asn1_string_t*)value;
        result = str_val->size;
        if (extra_byte_required(str_val->data))
                ++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 = 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;
    uint32_t len = 0;
    uint32_t len_len = 0;
    int32_t result = NOT_ERROR;

    len = asn1_get_field_bytes(field);
    if (out_len < len) {
        LOGE("asn1_gen_field: not enough space to write ASN.1");
        return BUFFER_OVERFLOW;
    }

    switch (type) {
    case ASN1_TYPE_BOOLEAN:
        *out = ASN1_TAG_BOOLEAN;
        break;
    case ASN1_TYPE_LONG:
    case ASN1_TYPE_BIGNUM_RAW:
        *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;
    }

    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_RAW:
        if (extra_byte_required(((asn1_string_t*)value)->data)) {
                *out = 0;
                ++out;
                --len;
        }
        memcpy(out, ((asn1_string_t*)value)->data,
                    ((asn1_string_t*)value)->size);
        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;
    case ASN1_TYPE_END:
        /* Do nothing, will be deleted */
        break;
    }

    return result;
}

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 = NOT_ERROR;

    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 != NOT_ERROR) {
            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 = NOT_ERROR;
    asn1_gen_t seq_stub = ASN1GEN_INIT;

    seq_stub.type = ASN1_TYPE_SEQUENCE;
    seq_stub.value = (void*)seq;

    if (!out || !seq || !out_len) 
    {
        LOGE("asn1_gen_sequence: Bad input data!");
        return WRONG_DATA;
    }

    result = asn1_gen_field(out, *out_len, &seq_stub);
    if (result != NOT_ERROR) {
        LOGD("asn1_gen_sequence: error while generation of ASN.1");
        *out_len = 0;
    }
    else
    {
        *out_len = asn1_get_field_bytes(&seq_stub);
    }
    return result;
}

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) != NOT_ERROR) {
                return WRONG_DATA;
            }
            break;

        default:
            break;
        }
        ++seq;
    }

    return NOT_ERROR;
}
