/**
 * \file TLV.c
 * \brief TLV buffer API.
 * \author Dmytro Podgornyi (d.podgornyi@samsung.com)
 * \version 0.1
 * \date Created Nov 04, 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 <stdlib.h>
#include <string.h>
#include "commonConfig.h"
#include "TLV.h"
#include "log.h"

int tlvInit(uint8_t *tlv, size_t tlvLen)
{
    if (tlvLen < TAGLENGTH_FIELD_SIZE) {
        LOGE("Allocated TLV buffer too small.");
        return ERR_INVALID_ARGUMENT;
    }

    memset(tlv, 0, tlvLen);
    tlv[0] = TLV_START;

    return NOT_ERROR;
}

int tlvAdd(uint8_t *tlvBuffer, size_t tlvBufferLen, TlvTag_t tag,
           const void *value, uint16_t valueLen)
{
    uint16_t tlvTotalLen = 0;
    uint32_t pos = 0;

    if (tlvBuffer == NULL || tlvBufferLen < TAGLENGTH_FIELD_SIZE) {
        LOGE("TLV buffer is invalid.(%zu).", tlvBufferLen);
        return ERR_INVALID_ARGUMENT;
    }

    if ((0 == valueLen) || (NULL == value)) {
        LOGE("tlvAdd: Skip zero size or bad pointer to data block for TLV buffer.");
        return NOT_ERROR;
    }

    if (tlvBuffer[pos++] != TLV_START) {
        LOGE("TLV start byte is wrong.");
        return ERR_INVALID_ARGUMENT;
    }

    tlvTotalLen = GET_UINT16(tlvBuffer, pos);
    pos += LENGTH_FIELD_SIZE;
    pos += tlvTotalLen;

    // Header(buffer's tag and length) + dataLen + new item's length.
    if (pos + TAGLENGTH_FIELD_SIZE + valueLen > (uint32_t)tlvBufferLen) {
        LOGE("tlvAdd: TLV len is smaller that data requested to add.");
        return ERR_BUFFER_OVERFLOW;
    }

    // Add Tlv data.
    tlvBuffer[pos++] = tag;
    SET_UINT16_FROM_U16(tlvBuffer, pos, valueLen);
    pos += LENGTH_FIELD_SIZE;
    memcpy(tlvBuffer + pos, value, valueLen);
    pos += valueLen;

    SET_UINT16_FROM_U16(tlvBuffer, 1, pos - TAGLENGTH_FIELD_SIZE);

    return NOT_ERROR;
}

int tlvSize(const uint8_t *tlvBuffer, size_t tlvBufferLen)
{
    uint16_t tlvTotalLen = 0;

    if (tlvBuffer == NULL || tlvBufferLen < TAGLENGTH_FIELD_SIZE) {
        LOGE("TLV buffer is invalid.(%zu).", tlvBufferLen);
        return ERR_INVALID_ARGUMENT;
    }

    if (tlvBuffer[0] != TLV_START) {
        LOGE("TLV start byte is wrong.");
        return ERR_INVALID_ARGUMENT;
    }

    tlvTotalLen = GET_UINT16(tlvBuffer, 1);
    tlvTotalLen += TAGLENGTH_FIELD_SIZE;

    if (tlvTotalLen > (uint16_t)tlvBufferLen) {
        return ERR_BUFFER_OVERFLOW;
    }

    return (int)(tlvTotalLen);
}

int tlvGet(const uint8_t *tlvBuffer, size_t tlvBufferLen, TlvTag_t tag,
           uint16_t *length, uint8_t **value)
{
    uint32_t tlvTotalLen = 0, pos = 0;
    uint16_t tlvLen = 0;

    if (tlvBuffer == NULL || tlvBufferLen < (TAGLENGTH_FIELD_SIZE * 2)) {
        LOGE("TLV buffer is invalid.(%zu).", tlvBufferLen);
        return ERR_INVALID_ARGUMENT;
    }

    if(value == NULL || length == NULL) {
        LOGE("Buffer to save is not valid.");
        return ERR_INVALID_ARGUMENT;
    }

    if (tlvBuffer[pos++] != TLV_START) {
        LOGE("TLV start byte is wrong.");
        return ERR_INVALID_ARGUMENT;
    }

    tlvTotalLen = GET_UINT16(tlvBuffer, pos);
    pos += LENGTH_FIELD_SIZE;

    tlvTotalLen += pos;
    if (tlvTotalLen > tlvBufferLen) {
        LOGE("tlvGet Invalid tlv buffer : %d %zu.", tlvTotalLen, tlvBufferLen);
        return ERR_INVALID_ARGUMENT;
    }

    while (tag != (TlvTag_t)tlvBuffer[pos++]) {
        tlvLen = GET_UINT16(tlvBuffer, pos);
        pos += LENGTH_FIELD_SIZE;
        pos += tlvLen;

        if (pos > (tlvTotalLen - TAGLENGTH_FIELD_SIZE)) {
            *length = 0;
            *value = NULL;
            LOGE("TLV with tag %d is not present", tag);
            return ERR_INVALID_ARGUMENT;
        }
    }

    tlvLen = GET_UINT16(tlvBuffer, pos);
    pos += LENGTH_FIELD_SIZE;

    if (tlvLen > tlvTotalLen || pos  > (tlvTotalLen - tlvLen)) {
        LOGE("TLV has length  %d is bigger then %d total length", pos + tlvLen, tlvTotalLen);
        return ERR_BUFFER_OVERFLOW;
    }

    if(tlvLen == 0) {
        *value = NULL;
    } else {
        *value = (uint8_t *)(tlvBuffer + pos);
    }

    *length = tlvLen;
    return NOT_ERROR;
}
