/*
 * PDU Encode
 *
 * Copyright (C) 2010 by James Maki
 *
 * Author: James Maki <jmaki@multitech.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 */

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

#include "global.h"
#include "utils.h"
#include "pdu_encode.h"

#define ENCODE_FAIL(cond, name, ret) \
do { \
	if (cond) { \
		log_error("encode failed at %s", name); \
		return ret; \
	} \
} while (0)

int pdu_encode_timestamp(char *pdu_str, size_t len, struct tm *tm)
{
	int tmp;
	int off_upper;
	int off_lower;
	int off;
	int negative;

	if (len < PDU_TIMESTAMP_SIZE) {
		log_error("buffer is not large enough to hold a timestamp");
		return -1;
	}

	tmp = strftime(pdu_str, PDU_TIMESTAMP_SIZE, "%y%m%d%H%M%S", tm);
	ENCODE_FAIL(tmp != GMT_OFFSET_IDX, "timestamp", -1);

	off = tm->tm_gmtoff;
	if (off < 0) {
		off = -off;
		negative = 1;
	}
	off = off / 60 / 15;

	off_upper = off / 10;
	off_lower = off % 10;

	if (negative) {
		off_upper |= BIT(3);
	}

	snprintf(pdu_str + GMT_OFFSET_IDX, 2, "%X", off_upper & 0xF);
	snprintf(pdu_str + GMT_OFFSET_IDX + 1, 2, "%X", off_lower & 0xF);

	strpad(pdu_str, PDU_TIMESTAMP_LEN, 'F');
	nibble_swap(pdu_str, PDU_TIMESTAMP_LEN);

	log_debug("%s", pdu_str);

	return PDU_TIMESTAMP_LEN;
}

int pdu_encode_addr(char *pdu_str, size_t len, struct pdu_addr *addr, int smsc)
{
	char *begin = pdu_str;
	int addr_len;
	int tmp;

	addr_len = strlen(addr->addr);

	if (smsc) {
		if (addr_len) {
			if (addr_len & 1) {
				addr_len++;
			}
			addr_len = addr_len / HEX_BYTE_LEN + 1;
		}
	}

	log_debug("addr-len [transformed]: 0x%02X", addr_len);
	addr->len = addr_len;

	tmp = hex_byte_encode(pdu_str, len, addr_len);
	ENCODE_FAIL(tmp != HEX_BYTE_LEN, "addr-len", -1);
	pdu_str += tmp;
	len -= tmp;

	if (smsc && !addr_len) {
		log_debug("smsc addr is empty");
		goto done;
	}

	tmp = hex_byte_encode(pdu_str, len, addr->type);
	ENCODE_FAIL(tmp != HEX_BYTE_LEN, "addr-type", -1);
	pdu_str += tmp;
	len -= tmp;

	addr_len = strlen(addr->addr);
	if (addr_len & 1) {
		addr_len++;
	}

	if (len < addr_len + 1) {
		log_error("buffer is not large enough to hold the addr");
		return -1;
	}

	strcpy(pdu_str, addr->addr);
	strpad(pdu_str, addr_len, 'F');
	nibble_swap(pdu_str, addr_len);
	len -= addr_len;
	pdu_str += addr_len;

done:

	return pdu_str - begin;
}

#define septet_idx(n)		((n) * 8 / 7)

#define encode_octet(buf, n) \
shiftr(bit_maskl(cycledown(n, 7) + 1, 7) & buf[septet_idx(n)], cycleup(n, 7)) | \
shiftl(bit_maskr(cycleup(n, 7) + 1) & buf[septet_idx(n) + 1], cycledown(n, 7) + 1)

int pdu_encode_user_data(char *pdu_str, size_t len, struct pdu_info *pdu)
{
	char *begin = pdu_str;
	int tmp;
	uint8_t octets[PDU_OCTETS_MAX];
	int nr_octets;
	int i;

	if (pdu->data_coding.general.unused) {
		log_error("data coding group 0x%02X not implemented",
				pdu->data_coding.data_coding & 0xF0);
		return -1;
	}

	if (pdu->data_coding.general.alphabet == PDU_ALPHABET_DEFAULT) {
		log_debug("data coding alphabet is default");
	} else if (pdu->data_coding.general.alphabet == PDU_ALPHABET_EIGHT) {
		log_debug("data coding alphabet is eight");
	} else {
		log_debug("data coding alphabet 0x%02X not implemented",
				pdu->data_coding.general.alphabet);
		return -1;
	}

	if (pdu->data_coding.general.alphabet == PDU_ALPHABET_DEFAULT) {
		if (pdu->user_data_len > PDU_UD_7BIT_MAX) {
			log_error("string exceeds 7-bit data max length");
			return -1;
		}

		nr_octets = octet_align(pdu->user_data_len);
		for (i = 0; i < nr_octets; i++) {
			octets[i] = encode_octet(pdu->user_data, i);
		}
	} else {
		if (pdu->user_data_len > PDU_UD_8BIT_MAX) {
			log_error("string exceeds 8-bit data max length");
			return -1;
		}

		nr_octets = pdu->user_data_len;
		for (i = 0; i < pdu->user_data_len; i++) {
			octets[i] = pdu->user_data[i];
		}
	}

	if (len < nr_octets * 2 + HEX_BYTE_LEN + 1) {
		log_error("buffer is not large enough to hold user-data");
		return -1;
	}

	tmp = hex_byte_encode(pdu_str, len, pdu->user_data_len);
	ENCODE_FAIL(tmp != HEX_BYTE_LEN, "user-data-len", -1);
	pdu_str += tmp;
	len -= tmp;

	for (i = 0; i < nr_octets; i++) {
		hex_byte_encode(pdu_str, len, octets[i]);
		pdu_str += HEX_BYTE_LEN;
	}
	len -= nr_octets * 2;
	pdu_str[i] = '\0';

	log_debug("user-data-len: 0x%02X", pdu->user_data_len);
	log_debug("nr_octets: 0x%02X", nr_octets);

	return pdu_str - begin;
}

int pdu_encode(char *pdu_str, size_t len, struct pdu_info *pdu)
{
	char *begin = pdu_str;
	char *msg_begin = pdu_str;
	int tmp;

	tmp = pdu_encode_addr(pdu_str, len, &pdu->smsc_addr, 1);
	ENCODE_FAIL(tmp < 0, "smsc-addr", -1);
	len -= tmp;
	pdu_str += tmp;

	msg_begin = pdu_str;

	tmp = hex_byte_encode(pdu_str, len, pdu->type.type);
	ENCODE_FAIL(tmp != HEX_BYTE_LEN, "type", -1);
	len -= tmp;
	pdu_str += tmp;

	if (pdu->type.msg_type == PDU_MTI_SUBMIT ||
	    pdu->type.msg_type == PDU_MTI_REPORT) {
		tmp = hex_byte_encode(pdu_str, len, pdu->msg_reference);
		ENCODE_FAIL(tmp != HEX_BYTE_LEN, "msg-reference", -1);
		len -= tmp;
		pdu_str += tmp;
	}

	tmp = pdu_encode_addr(pdu_str, len, &pdu->addr, 0);
	ENCODE_FAIL(tmp < 0, "addr", -1);
	len -= tmp;
	pdu_str += tmp;

	switch (pdu->type.msg_type) {
	case PDU_MTI_REPORT:
		tmp = pdu_encode_timestamp(pdu_str, len, &pdu->timestamp);
		ENCODE_FAIL(tmp < 0, "report-timestamp", -1);
		len -= tmp;
		pdu_str += tmp;

		break;
	case PDU_MTI_DELIVER:
	case PDU_MTI_SUBMIT:
		tmp = hex_byte_encode(pdu_str, len, pdu->protocol_id);
		ENCODE_FAIL(tmp != HEX_BYTE_LEN, "protocol-id", -1);
		len -= tmp;
		pdu_str += tmp;

		tmp = hex_byte_encode(pdu_str, len, pdu->data_coding.data_coding);
		ENCODE_FAIL(tmp != HEX_BYTE_LEN, "data-coding-scheme", -1);
		len -= tmp;
		pdu_str += tmp;

		if (pdu->type.msg_type == PDU_MTI_SUBMIT) {
			switch (pdu->type.validity_period_format) {
			case PDU_VPF_RELATIVE:
				tmp = hex_byte_encode(pdu_str, len, pdu->validity_period);
				ENCODE_FAIL(tmp != HEX_BYTE_LEN, "validity-period", -1);
				len -= tmp;
				pdu_str += tmp;

				break;

			case PDU_VPF_ENHANCED:
				log_warning("PDU_VPF_ENHANCED? Falling through to absolute");
			case PDU_VPF_ABSOUTE:
				tmp = pdu_encode_timestamp(pdu_str, len, &pdu->timestamp);
				ENCODE_FAIL(tmp < 0, "validity-period-timestamp", -1);
				len -= tmp;
				pdu_str += tmp;

				break;

			case PDU_VPF_NOT_PRESENT:
			default:
				break;
			}
		} else if (pdu->type.msg_type == PDU_MTI_DELIVER) {
			tmp = pdu_encode_timestamp(pdu_str, len, &pdu->timestamp);
			ENCODE_FAIL(tmp < 0, "delivery-timestamp", -1);
			len -= tmp;
			pdu_str += tmp;
		}

		tmp = pdu_encode_user_data(pdu_str, len, pdu);
		ENCODE_FAIL(tmp < 0, "user-data", -1);
		len -= tmp;
		pdu_str += tmp;
	}

	pdu->msg_len = (pdu_str - msg_begin) / 2;
	log_debug("msg-len: %d", pdu->msg_len);

	return pdu_str - begin;
}