/*
 *
 *  Near Field Communication nfctool
 *
 *  Copyright (C) 2012  Intel Corporation. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  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., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */
#include <stdio.h>
#include <glib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/time.h>

#include <near/nfc_copy.h>

#include "nfctool.h"
#include "sniffer.h"
#include "ndef-decode.h"
#include "snep-decode.h"
#include "llcp-decode.h"

/* Raw socket + LLCP headers */
#define RAW_LLCP_HEADERS_SIZE 4

#define LLCP_PTYPE_SYMM		0
#define LLCP_PTYPE_PAX		1
#define LLCP_PTYPE_AGF		2
#define LLCP_PTYPE_UI		3
#define LLCP_PTYPE_CONNECT	4
#define LLCP_PTYPE_DISC		5
#define LLCP_PTYPE_CC		6
#define LLCP_PTYPE_DM		7
#define LLCP_PTYPE_FRMR		8
#define LLCP_PTYPE_SNL		9
#define LLCP_PTYPE_I		12
#define LLCP_PTYPE_RR		13
#define LLCP_PTYPE_RNR		14

#define LLCP_DM_NORMAL			0x00
#define LLCP_DM_NO_ACTIVE_CONN		0x01
#define LLCP_DM_NOT_BOUND		0x02
#define LLCP_DM_REJECTED		0x03
#define LLCP_DM_PERM_SAP_FAILURE	0x10
#define LLCP_DM_PERM_ALL_SAP_FAILURE	0x11
#define LLCP_DM_TMP_SAP_FAILURE		0x20
#define LLCP_DM_TMP_ALL_SAP_FAILURE	0x21

enum llcp_param_t {
	LLCP_PARAM_VERSION = 1,
	LLCP_PARAM_MIUX,
	LLCP_PARAM_WKS,
	LLCP_PARAM_LTO,
	LLCP_PARAM_RW,
	LLCP_PARAM_SN,
	LLCP_PARAM_OPT,
	LLCP_PARAM_SDREQ,
	LLCP_PARAM_SDRES,

	LLCP_PARAM_MIN = LLCP_PARAM_VERSION,
	LLCP_PARAM_MAX = LLCP_PARAM_SDRES
};

static guint8 llcp_param_length[] = {
	0,
	1,
	2,
	2,
	1,
	1,
	0,
	1,
	0,
	2
};

static char *llcp_ptype_str[] = {
	"Symmetry (SYMM)",
	"Parameter Exchange (PAX)",
	"Aggregated Frame (AGF)",
	"Unnumbered Information (UI)",
	"Connect (CONNECT)",
	"Disconnect (DISC)",
	"Connection Complete (CC)",
	"Disconnected Mode (DM)",
	"Frame Reject (FRMR)",
	"Service Name Lookup (SNL)",
	"reserved",
	"reserved",
	"Information (I)",
	"Receive Ready (RR)",
	"Receive Not Ready (RNR)",
	"reserved",
	"Unknown"
};

static char *llcp_ptype_short_str[] = {
	"SYMM",
	"PAX",
	"AGF",
	"UI",
	"CONNECT",
	"DISC",
	"CC",
	"DM",
	"FRMR",
	"SNL",
	NULL,
	NULL,
	"I",
	"RR",
	"RNR",
	NULL,
	"Unknown"
};

static const gchar *llcp_param_str[] = {
	"",
	"Version Number",
	"Maximum Information Unit Extensions",
	"Well-Known Service List",
	"Link Timeout",
	"Receive Window Size",
	"Service Name",
	"Option",
	"Service Discovery Request",
	"Service Discovery Response"
};

#define llcp_printf_header(prefix, color, fmt, ...) \
				print_indent_prefix( LLCP_HEADER_INDENT,\
				color, prefix, \
				LLCP_COLOR, fmt, ## __VA_ARGS__)

#define llcp_printf_msg(fmt, ...) print_indent(LLCP_MSG_INDENT, \
						LLCP_COLOR, fmt, ## __VA_ARGS__)

#define llcp_printf_error(fmt, ...) print_indent(LLCP_MSG_INDENT, \
						COLOR_ERROR, fmt, ## __VA_ARGS__)

#define llcp_get_param_str(param_type) llcp_param_str[param_type]

#define llcp_get_param_len(param_type) llcp_param_length[param_type]

#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0]))

static struct timeval start_timestamp;

static GHashTable *connection_hash;

/* We associate an SN from a CONNECT to its sender */
static void llcp_add_connection_sn(struct sniffer_packet *packet, gchar *sn)
{
	guint8 ssap;

	if (packet->llcp.ptype != LLCP_PTYPE_CONNECT)
		return;

	/* If we're sending the CONNECT, we use our local SAP*/
	if (packet->llcp.local_sap != 0x1)
		ssap = packet->llcp.local_sap;
	else
		ssap = packet->llcp.remote_sap;

	g_hash_table_replace(connection_hash,
			    GINT_TO_POINTER(ssap), g_strdup(sn));
}

/* Check if there's a pending SN for this dsap */
static void llcp_check_cc(struct sniffer_packet *packet)
{
	guint8 dsap;
	gchar *sn;

	if (packet->llcp.ptype != LLCP_PTYPE_CC)
		return;

	/* Find the real destination SAP for this CC */
	if (packet->direction == NFC_DIRECTION_RX)
		dsap = packet->llcp.local_sap;
	else
		dsap = packet->llcp.remote_sap;

	/* Do we have a CONNECT pending for this SAP ?*/
	sn = g_hash_table_lookup(connection_hash,
					GINT_TO_POINTER(dsap));
	if (!sn)
		return;

	if (strcmp(sn, "urn:nfc:sn:handover") == 0)
		opts.handover_sap = dsap;
}

static void free_connection(gpointer data)
{
	gchar *sn = data;

	g_free(sn);
}

static void llcp_print_params(struct sniffer_packet *packet)
{
	guint8 major, minor;
	guint16 miux, wks, tid;
	guint32 offset = 0;
	guint32 rmng;
	guint8 *param;
	guint8 param_len;
	gchar *sn, param_str[64];

	while (packet->llcp.data_len - offset >= 3) {
		param = packet->llcp.data + offset;
		rmng = packet->llcp.data_len - offset;

		if (param[0] < LLCP_PARAM_MIN || param[0] > LLCP_PARAM_MAX) {
			llcp_printf_error("Error decoding params");
			return;
		}

		param_len = llcp_get_param_len(param[0]);

		if (param_len == 0)
			param_len = param[1];

		if (param_len != param[1] || rmng < 2u + param_len) {
			llcp_printf_error("Error decoding params");
			return;
		}

		switch ((enum llcp_param_t)param[0]) {
		case LLCP_PARAM_VERSION:
			major = (param[2] & 0xF0) >> 4;
			minor = param[2] & 0x0F;
			sprintf(param_str, "%d.%d", major, minor);
			break;

		case LLCP_PARAM_MIUX:
			miux = ((param[2] & 0x07) << 8) | param[3];
			sprintf(param_str, "%d", miux);
			break;

		case LLCP_PARAM_WKS:
			wks = (param[2] << 8) | param[3];
			sprintf(param_str, "0x%02hX", wks);
			break;

		case LLCP_PARAM_LTO:
			sprintf(param_str, "%d", param[2]);
			break;

		case LLCP_PARAM_RW:
			sprintf(param_str, "%d", param[2] & 0x0F);
			break;

		case LLCP_PARAM_SN:
			sn = g_strndup((gchar *)param + 2, param_len);
			llcp_add_connection_sn(packet, sn);
			snprintf(param_str, 64, "%s", sn);
			g_free(sn);
			break;

		case LLCP_PARAM_OPT:
			sprintf(param_str, "0x%X", (unsigned int)param[2] & 0x03);
			break;

		case LLCP_PARAM_SDREQ:
			tid = param[2];
			sn = g_strndup((gchar *)param + 3, param_len - 1);
			snprintf(param_str, 64, "TID:%d, SN:%s", tid, sn);
			g_free(sn);
			break;

		case LLCP_PARAM_SDRES:
			sprintf(param_str, "TID:%d, SAP:%d", param[2], param[3] & 0x3F);
			break;
		}

		llcp_printf_msg("%s: %s", llcp_get_param_str(param[0]),
				param_str);

		offset += 2 + param_len;
	}
}

static int llcp_decode_packet(guint8 *data, guint32 data_len,
			      struct sniffer_packet *packet)
{
	if (data_len < RAW_LLCP_HEADERS_SIZE)
		return -EINVAL;

	memset(packet, 0, sizeof(struct sniffer_packet));

	/* LLCP raw socket header */
	packet->adapter_idx = data[0];
	packet->direction = data[1] & 0x01;

	/* LLCP header */
	if (packet->direction == NFC_DIRECTION_TX) {
		packet->llcp.remote_sap = (data[2] & 0xFC) >> 2;
		packet->llcp.local_sap = data[3] & 0x3F;
	} else {
		packet->llcp.remote_sap = data[3] & 0x3F;
		packet->llcp.local_sap = (data[2] & 0xFC) >> 2;
	}

	packet->llcp.ptype = ((data[2] & 0x03) << 2) | ((data[3] & 0xC0) >> 6);

	if (packet->llcp.ptype >= ARRAY_SIZE(llcp_ptype_str))
		return -EINVAL;

	packet->llcp.data = data + RAW_LLCP_HEADERS_SIZE;
	packet->llcp.data_len = data_len - RAW_LLCP_HEADERS_SIZE;

	/* Sequence field */
	if (packet->llcp.ptype >= LLCP_PTYPE_I) {
		if (packet->llcp.data_len == 0)
			return -EINVAL;

		packet->llcp.send_seq = ((packet->llcp.data[0] & 0xF0) >> 4);
		packet->llcp.recv_seq = packet->llcp.data[0] & 0x0F;

		packet->llcp.data++;
		packet->llcp.data_len--;
	}

	return 0;
}

static void llcp_print_sequence(struct sniffer_packet *packet)
{
	llcp_printf_msg("N(S):%d N(R):%d",
			packet->llcp.send_seq, packet->llcp.recv_seq);
}

static int llcp_print_agf(struct sniffer_packet *packet,
			  struct timeval *timestamp)
{
	guint8 *pdu;
	gsize pdu_size;
	gsize size;
	guint16 offset;
	guint16 count;
	int err;

	if (packet->llcp.data_len < 2) {
		llcp_printf_error("Error parsing AGF PDU");
		return -EINVAL;
	}

	printf("\n");

	pdu = NULL;
	pdu_size = 0;
	offset = 0;
	count = 0;

	while (offset < packet->llcp.data_len - 2) {
		size = (packet->llcp.data[offset] << 8) |
			packet->llcp.data[offset + 1];

		offset += 2;

		if (size == 0 || offset + size > packet->llcp.data_len) {
			llcp_printf_error("Error parsing AGF PDU");
			err = -EINVAL;
			goto exit;
		}

		if (size + NFC_RAW_HEADER_SIZE > pdu_size) {
			pdu_size = size + NFC_RAW_HEADER_SIZE;
			pdu = g_realloc(pdu, pdu_size);

			pdu[0] = packet->adapter_idx;
			pdu[1] = packet->direction;
		}

		memcpy(pdu + NFC_RAW_HEADER_SIZE,
			packet->llcp.data + offset, size);

		llcp_printf_msg("-- AGF LLC PDU %02u:", count++);

		llcp_print_pdu(pdu, size + NFC_RAW_HEADER_SIZE, timestamp);

		offset += size;
	}

	llcp_printf_msg("-- End of AGF LLC PDUs");

	err = 0;
exit:
	g_free(pdu);

	return err;
}

static int llcp_print_dm(struct sniffer_packet *packet)
{
	gchar *reason;

	if (packet->llcp.data_len != 1)
		return -EINVAL;

	switch (packet->llcp.data[0]) {
	case LLCP_DM_NORMAL:
	default:
		reason = "Normal disconnect";
		break;

	case LLCP_DM_NO_ACTIVE_CONN:
		reason =
		      "No active connection for connection-oriented PDU at SAP";
		break;

	case LLCP_DM_NOT_BOUND:
		reason = "No service bound to target SAP";
		break;

	case LLCP_DM_REJECTED:
		reason = "CONNECT PDU rejected by service layer";
		break;

	case LLCP_DM_PERM_SAP_FAILURE:
		reason = "Permanent failure for target SAP";
		break;

	case LLCP_DM_PERM_ALL_SAP_FAILURE:
		reason = "Permanent failure for any target SAP";
		break;

	case LLCP_DM_TMP_SAP_FAILURE:
		reason = "Temporary failure for target SAP";
		break;

	case LLCP_DM_TMP_ALL_SAP_FAILURE:
		reason = "Temporary failure for any target SAP";
		break;
	}

	llcp_printf_msg("Reason: %d (%s)", packet->llcp.data[0], reason);

	return 0;
}

static int llcp_print_i(struct sniffer_packet *packet)
{
	llcp_print_sequence(packet);

	if (packet->llcp.local_sap == opts.snep_sap ||
	    packet->llcp.remote_sap == opts.snep_sap) {
		int err;

		err = snep_print_pdu(packet);
		if (err != 0)
			llcp_printf_error("Error decoding SNEP frame");

		return err;
	} else 	if (packet->llcp.local_sap == opts.handover_sap ||
			packet->llcp.remote_sap == opts.handover_sap) {
		int err;

		err = ndef_print_records(packet->llcp.data,
					 packet->llcp.data_len);
		if (err != 0)
			llcp_printf_error("Error decoding Handover frame");
		return err;
	}

	sniffer_print_hexdump(stdout, packet->llcp.data,  packet->llcp.data_len,
				LLCP_MSG_INDENT, TRUE);

	return 0;
}

static int llcp_print_frmr(struct sniffer_packet *packet)
{
	guint8 info, ptype;

	if (packet->llcp.data_len != 4)
		return -EINVAL;

	info = packet->llcp.data[0];
	ptype = info & 0x0F;
	if (ptype >= ARRAY_SIZE(llcp_ptype_str))
		ptype = ARRAY_SIZE(llcp_ptype_str) - 1;

	llcp_printf_msg("W:%d I:%d R:%d S:%d PTYPE:%s SEQ: %d V(S):"
			" %d V(R): %d V(SA): %d V(RA): %d",
			(info & 0x80) >> 7, (info & 0x40) >> 6,
			(info & 0x20) >> 5, (info & 0x10) >> 4,
			llcp_ptype_short_str[ptype], packet->llcp.data[1],
			(packet->llcp.data[2] & 0xF0) >> 4,
			packet->llcp.data[2] & 0x0F,
			(packet->llcp.data[3] & 0xF0) >> 4,
			packet->llcp.data[3] & 0x0F);

	return 0;
}

int llcp_print_pdu(guint8 *data, guint32 data_len, struct timeval *timestamp)
{
	struct timeval msg_timestamp;
	struct sniffer_packet packet;
	gchar *direction_str, time_str[32];
	gchar *direction_color;
	int err;

	if (!timestamp)
		return -EINVAL;

	if (!timerisset(&start_timestamp))
		start_timestamp = *timestamp;

	err = llcp_decode_packet(data, data_len, &packet);
	if (err)
		goto exit;

	if (!opts.dump_symm && packet.llcp.ptype == LLCP_PTYPE_SYMM)
		return 0;

	if (packet.direction == NFC_DIRECTION_RX) {
		direction_str = ">>";
		direction_color = COLOR_RED;
	} else {
		direction_str = "<<";
		direction_color = COLOR_GREEN;
	}

	time_str[0] = 0;

	if (opts.show_timestamp != SNIFFER_SHOW_TIMESTAMP_NONE) {
		char prefix = ' ';

		if (opts.show_timestamp == SNIFFER_SHOW_TIMESTAMP_ABS) {
			msg_timestamp = *timestamp;
		} else {
			timersub(timestamp, &start_timestamp, &msg_timestamp);
			prefix = '+';
		}

		sprintf(time_str,  "%c%ld.%06lds", prefix, msg_timestamp.tv_sec,
							msg_timestamp.tv_usec);
	}

	llcp_printf_header(direction_str, direction_color,
				" nfc%d: local:0x%02x remote:0x%02x %s",
				packet.adapter_idx, packet.llcp.local_sap,
				packet.llcp.remote_sap, time_str);

	llcp_printf_msg("%s", llcp_ptype_str[packet.llcp.ptype]);

	switch (packet.llcp.ptype) {
	case LLCP_PTYPE_AGF:
		llcp_print_agf(&packet, timestamp);
		break;

	case LLCP_PTYPE_I:
		llcp_print_i(&packet);
		break;

	case LLCP_PTYPE_RR:
	case LLCP_PTYPE_RNR:
		llcp_print_sequence(&packet);
		break;

	case LLCP_PTYPE_PAX:
	case LLCP_PTYPE_CONNECT:
	case LLCP_PTYPE_CC:
	case LLCP_PTYPE_SNL:
		llcp_check_cc(&packet);
		llcp_print_params(&packet);
		break;

	case LLCP_PTYPE_DM:
		llcp_print_dm(&packet);
		break;

	case LLCP_PTYPE_FRMR:
		llcp_print_frmr(&packet);
		break;

	default:
		sniffer_print_hexdump(stdout, packet.llcp.data,
					packet.llcp.data_len,
					LLCP_MSG_INDENT, TRUE);
		break;
	}

	printf("\n");

	err = 0;

exit:
	return err;
}

void llcp_decode_cleanup(void)
{
	timerclear(&start_timestamp);

	snep_decode_cleanup();

	if (connection_hash)
		g_hash_table_destroy(connection_hash);
}

int llcp_decode_init(void)
{
	int err;

	timerclear(&start_timestamp);

	connection_hash = g_hash_table_new_full(g_direct_hash,
						g_direct_equal, NULL,
						free_connection);

	err = snep_decode_init();

	return err;
}
