#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>

#include <osmocom/core/select.h>
#include <osmocom/core/talloc.h>
#include <osmocom/core/msgb.h>
#include <osmocom/core/logging.h>
#include <osmocom/core/application.h>
#include <osmocom/core/endian.h>

#include <osmocom/netif/rs232.h>

#define DRS232TEST 0

struct log_info_cat osmo_rs232_test_cat[] = {
	[DRS232TEST] = {
		.name = "DRS232TEST",
		.description = "rs232 test",
		.color = "\033[1;35m",
		.enabled = 1, .loglevel = LOGL_DEBUG,
	},
};

const struct log_info osmo_rs232_test_log_info = {
	.filter_fn = NULL,
	.cat = osmo_rs232_test_cat,
	.num_cat = ARRAY_SIZE(osmo_rs232_test_cat),
};

static struct osmo_rs232 *r;

void sighandler(int foo)
{
	LOGP(DRS232TEST, LOGL_NOTICE, "closing rs232.\n");
	osmo_rs232_close(r);
	osmo_rs232_destroy(r);
	exit(EXIT_SUCCESS);
}

static int read_cb(struct osmo_rs232 *r)
{
	struct msgb *msg;

	LOGP(DRS232TEST, LOGL_DEBUG, "received data from rs232\n");

	msg = msgb_alloc(1024, "rs232/test");
	if (msg == NULL) {
		LOGP(DRS232TEST, LOGL_ERROR, "cannot allocate message\n");
		return 0;
	}
	if (osmo_rs232_read(r, msg) < 0) {
		LOGP(DRS232TEST, LOGL_ERROR, "cannot receive message\n");
		return 0;
	}
	LOGP(DRS232TEST, LOGL_DEBUG, "received %d bytes\n", msg->len);

	printf("received %d bytes ", msg->len);

	int i;
	printf("(");
	for (i=0; i<msg->len; i++)
		printf("\\x%.2x", 0xff & msg->data[i]);
	printf(") %s\n", msg->data);

	msgb_free(msg);

	return 0;
}

static void *tall_test;

/* u-blox6_ReceiverDescriptionProtocolSpec_(GPS.G6-SW-10018).pdf */

/* See Sect 23. */
struct ubx_hdr {
	uint8_t	sync_char1;	/* 0xb5 */
	uint8_t sync_char2;	/* 0x62 */
	uint8_t	class;
	uint8_t	id;
} __attribute__((packed));

static void ubx_header(struct msgb *msg, uint8_t class, uint8_t id)
{
	/* See Sect. 31.24 */
	struct ubx_hdr ubxhdr = {
		.sync_char1	= 0xb5,
		.sync_char2	= 0x62,
		.class		= class,
		.id		= id,
	};
	memcpy(msg->data, &ubxhdr, sizeof(struct ubx_hdr));
	msgb_put(msg, sizeof(struct ubx_hdr));
}

/* See Sect 26. */
static void ubx_checksum(struct msgb *msg, uint8_t *ck)
{
	struct ubx_hdr *ubxhdr = (struct ubx_hdr *)msg->data;
	/* skip sync chars in checksum calculation. */
	uint8_t *buf = ((uint8_t *)ubxhdr) + 2;
	int i;

	memset(ck, 0, sizeof(uint16_t));

	for (i=0; i<msg->len-2; i++) {
		ck[0] += buf[i];
		ck[1] += ck[0];
	}
}

# if OSMO_IS_LITTLE_ENDIAN
# define utohl(x)       (x)
# define utohs(x)       (x)
# define htoul(x)       (x)
# define htous(x)       (x)
# else
#  if OSMO_IS_BIG_ENDIAN
#   define utohl(x)     __bswap_32 (x)
#   define utohs(x)     __bswap_16 (x)
#   define htoul(x)     __bswap_32 (x)
#   define htous(x)     __bswap_16 (x)
#  endif
# endif

static void ubx_payload_start(struct msgb *msg)
{
	uint16_t len = 0;
	/* make room for payload length. */
	memcpy(msg->data + msg->len, &len, sizeof(len));
	msgb_put(msg, sizeof(len));
}

static void ubx_payload_put_u8(struct msgb *msg, uint8_t data)
{
	memcpy(msg->data + msg->len, &data, sizeof(data));
	msgb_put(msg, sizeof(data));
}

static void ubx_payload_put_le16(struct msgb *msg, uint16_t data)
{
	uint16_t le_data = htous(data);
	memcpy(msg->data + msg->len, &le_data, sizeof(data));
	msgb_put(msg, sizeof(data));
}

static void ubx_payload_put_le32(struct msgb *msg, uint32_t data)
{
	uint32_t le_data = htoul(data);
	memcpy(msg->data + msg->len, &le_data, sizeof(data));
	msgb_put(msg, sizeof(data));
}

static void ubx_payload_stop(struct msgb *msg)
{
	uint16_t *length = (uint16_t *) &(msg->data[4]);
	uint8_t checksum[2];

	/* length does not includes the header, ID, length.
	 * note that checksum has not been yet added.
	 */
	*length = htous(msg->len - 6);

	ubx_checksum(msg, checksum);
	memcpy(msg->data + msg->len, checksum, sizeof(checksum));
	msgb_put(msg, sizeof(checksum));
}

static void cfg_prt(void)
{
	struct msgb *msg;

	msg = msgb_alloc(512, "CFG-PRT for USB");
	if (msg == NULL)
		exit(EXIT_FAILURE);

	ubx_header(msg, 0x06, 0x00);	/* CFG-PRT */

	ubx_payload_start(msg);
	ubx_payload_put_u8(msg, 0x03);		/* Port ID is (=3 USB). */
	ubx_payload_put_u8(msg, 0x00);		/* Reserved. */
	ubx_payload_put_le16(msg, 0x0000);	/* TX ready. */
	ubx_payload_put_le32(msg, 0x00000000);	/* Reserved. */
	ubx_payload_put_le32(msg, 0x00000000);	/* Reserved. */
	ubx_payload_put_le16(msg, 0x0003);	/* InProtoMask (NMEA+UBX). */
	ubx_payload_put_le16(msg, 0x0001);	/* OutProtoMask (UBX). */
	ubx_payload_put_le16(msg, 0x0000);	/* Flags. */
	ubx_payload_put_le16(msg, 0x0000);	/* Reserved. */
	ubx_payload_stop(msg);

	int i;
	for (i=0; i<msg->len; i++)
		printf("\\x%.2x", 0xff & msg->data[i]);
	printf("\n");

	if (osmo_rs232_write(r, msg) < 0) {
		LOGP(DRS232TEST, LOGL_ERROR, "cannot write to rs232\n");
		exit(EXIT_FAILURE);
	}
}

static int nmea_checksum(char *nmea_cmd, uint8_t *checksum)
{
	int i, ret = 0;
	uint8_t from, to;
	char *start, *end;

	/* find starting $ */
	start = strtok(nmea_cmd, "$");
	if (start == NULL)
		return -1;

	from = start - nmea_cmd;

	end = strtok(start+1, "*");
	if (end == NULL)
		return -1;

	to = end - nmea_cmd;

	ret = (uint8_t)nmea_cmd[0];
	for (i=from+1; i<to; i++)
		ret ^= (uint8_t)nmea_cmd[i];

	*checksum = ret;

	return 0;
}

static void send_pubx(void)
{
	struct msgb *msg;

	/* See 21.8: UBX,41.
	 *
	 * $PUBX,41,portId,inProto,outProto,baudrate,autobauding*cs
	 *
	 * [in|out]Proto: bit = 0 (ubx), bit = 1 (nmea)
	 *
	 * Sect 4. Serial Communication Ports Description
	 *
	 * 0 DDC
	 * 1 UART1
	 * 2 UART2
	 * 3 USB
	 * 4 SPI
	 * 5 reserved
	 *
	 * The NMEA command below comes without the checksum calculated.
	 */
	char nmea_cmd[128] = "$PUBX,41,3,0001,0001,9600,0*";
	uint8_t checksum;

	if (nmea_checksum(nmea_cmd, &checksum) < 0) {
		LOGP(DRS232TEST, LOGL_ERROR, "error calculating checksum\n");
		exit(EXIT_FAILURE);
	}
	sprintf(nmea_cmd + strlen(nmea_cmd), "%u\r\n", checksum);

	msg = msgb_alloc(300, "rs232/test");
	if (msg == NULL) {
		LOGP(DRS232TEST, LOGL_ERROR, "cannot allocate message\n");
		exit(EXIT_FAILURE);
	}
	memcpy(msg->data, nmea_cmd, strlen(nmea_cmd));
	msgb_put(msg, strlen(nmea_cmd));

	if (osmo_rs232_write(r, msg) < 0) {
		LOGP(DRS232TEST, LOGL_ERROR, "cannot write to rs232\n");
		exit(EXIT_FAILURE);
	}
}

static void cfg_tp5(void)
{
	struct msgb *msg;

	msg = msgb_alloc(512, "CFG-TP5 for USB");
	if (msg == NULL)
		exit(EXIT_FAILURE);

	ubx_header(msg, 0x06, 0x31);		/* CFG-TP5 */

	ubx_payload_start(msg);
	ubx_payload_put_u8(msg, 0x01);		/* TIMEPULSE2 (=1) */
	ubx_payload_put_u8(msg, 0x00);		/* Reserved. */
	ubx_payload_put_le16(msg, 0x0000);	/* Reserved. */
	ubx_payload_put_le16(msg, 0);		/* Antenna Delay (ns) */
	ubx_payload_put_le16(msg, 0);		/* RF Group Delay (ns) */
	ubx_payload_put_le32(msg, 8192000);	/* freqPeriod (Hz/us) */
	ubx_payload_put_le32(msg, 8192000);	/* freqPeriodLoc (Hz/us) */
	ubx_payload_put_le32(msg, 0x80000000);	/* pulseLenRation:
						   1/2^-32 (us() */
	ubx_payload_put_le32(msg, 0x80000000);	/* pulseLenRationLock:
						   1/2^-32 (us() */
	ubx_payload_put_le32(msg, 0);		/* userConfigDelay (ns) */
	ubx_payload_put_le32(msg, (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3));
						/* flags: bits 0, 1 and 3. */
	ubx_payload_stop(msg);

	int i;
	for (i=0; i<msg->len; i++)
		printf("\\x%.2x", 0xff & msg->data[i]);
	printf("\n");

	if (osmo_rs232_write(r, msg) < 0) {
		LOGP(DRS232TEST, LOGL_ERROR, "cannot write to rs232\n");
		exit(EXIT_FAILURE);
	}
}

static int kbd_cb(struct osmo_fd *fd, unsigned int what)
{
        char buf[1024];
        int ret, val;

        ret = read(STDIN_FILENO, buf, sizeof(buf));
	if (ret < 0) {
		LOGP(DRS232TEST, LOGL_ERROR, "cannot write to read from "
					     "keyboard\n");
		exit(EXIT_FAILURE);
	}

	val = atoi(buf);
	switch(val) {
	case 1:
		printf("sending command PUBX to switch to UBX mode\n");
		send_pubx();
		break;
	case 2:
		printf("sending command TP5\n");
		cfg_tp5();
		break;
	case 3:
		printf("sending command CFG-PRT\n");
		cfg_prt();
		break;
	default:
		printf("wrong option: select 1, 2 or 3\n");
		break;
	}
	return 0;
}

int main(void)
{
	struct osmo_fd *kbd_ofd;
	int rc;

	tall_test = talloc_named_const(NULL, 1, "osmo_rs232_test");
	msgb_talloc_ctx_init(tall_test, 0);
	osmo_init_logging2(tall_test, &osmo_rs232_test_log_info);
	log_set_log_level(osmo_stderr_target, LOGL_NOTICE);

	r = osmo_rs232_create(tall_test);
	if (r == NULL) {
		LOGP(DRS232TEST, LOGL_ERROR, "cannot create rs232 object\n");
		exit(EXIT_FAILURE);
	}
	osmo_rs232_set_serial_port(r, "/dev/ttyACM0");
	osmo_rs232_set_baudrate(r, 9600);
	osmo_rs232_set_delay_us(r, 3330);
	osmo_rs232_set_read_cb(r, read_cb);

	if (osmo_rs232_open(r) < 0) {
		LOGP(DRS232TEST, LOGL_ERROR, "cannot open rs232\n");
		exit(EXIT_FAILURE);
	}

	LOGP(DRS232TEST, LOGL_NOTICE, "Entering main loop\n");

        kbd_ofd = talloc_zero(tall_test, struct osmo_fd);
        if (!kbd_ofd) {
                LOGP(DRS232TEST, LOGL_ERROR, "OOM\n");
                exit(EXIT_FAILURE);
        }
	osmo_fd_setup(kbd_ofd, STDIN_FILENO, OSMO_FD_READ, kbd_cb, NULL, 0);
        rc = osmo_fd_register(kbd_ofd);
	if (rc < 0) {
		LOGP(DRS232TEST, LOGL_ERROR, "FD Register\n");
		exit(EXIT_FAILURE);
	}

	while(1) {
		osmo_select_main(0);
	}
}
