/*
 *
 *  Connection Manager
 *
 *  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
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>

#include <gdbus.h>

#define CONNMAN_API_SUBJECT_TO_CHANGE
#include <connman/plugin.h>
#include <connman/log.h>

#define TIST_SYSFS_INSTALL "/sys/devices/platform/kim/install"
#define TIST_SYSFS_UART "/sys/devices/platform/kim/dev_name"
#define TIST_SYSFS_BAUD "/sys/devices/platform/kim/baud_rate"

/* Shared transport line discipline */
#define N_TI_WL 22

static GIOChannel *install_channel = NULL;
static GIOChannel *uart_channel = NULL;
static char uart_dev_name[32];
static unsigned long baud_rate = 0;

static guint install_watch = 0;
static guint uart_watch = 0;

static int install_count = 0;

#define NCCS2 19
struct termios2 {
	tcflag_t c_iflag;		/* input mode flags */
	tcflag_t c_oflag;		/* output mode flags */
	tcflag_t c_cflag;		/* control mode flags */
	tcflag_t c_lflag;		/* local mode flags */
	cc_t c_line;			/* line discipline */
	cc_t c_cc[NCCS2];		/* control characters */
	speed_t c_ispeed;		/* input speed */
	speed_t c_ospeed;		/* output speed */
};

#define  BOTHER         0x00001000

/* HCI definitions */
#define HCI_HDR_OPCODE          0xff36
#define HCI_COMMAND_PKT         0x01
#define HCI_EVENT_PKT           0x04
#define EVT_CMD_COMPLETE        0x0E

/* HCI Command structure to set the target baud rate */
struct speed_change_cmd {
	uint8_t uart_prefix;
	uint16_t opcode;
	uint8_t plen;
	uint32_t speed;
} __attribute__ ((packed));

/* HCI Event structure to set the cusrom baud rate*/
struct cmd_complete {
	uint8_t uart_prefix;
	uint8_t evt;
	uint8_t plen;
	uint8_t ncmd;
	uint16_t opcode;
	uint8_t status;
	uint8_t data[16];
} __attribute__ ((packed));

static int read_baud_rate(unsigned long *baud)
{
	int err;
	FILE *f;

	DBG("");

	f = fopen(TIST_SYSFS_BAUD, "r");
	if (!f)
		return -EIO;

	err = fscanf(f, "%lu", baud);
	fclose(f);

	DBG("baud rate %lu", *baud);

	return err;
}

static int read_uart_name(char uart_name[], size_t uart_name_len)
{
	int err;
	FILE *f;

	DBG("");

	memset(uart_name, 0, uart_name_len);

	f = fopen(TIST_SYSFS_UART, "r");
	if (!f)
		return -EIO;

        err = fscanf(f, "%s", uart_name);
	fclose(f);

	DBG("UART name %s", uart_name);

	return err;
}

static int read_hci_event(int fd, unsigned char *buf, int size)
{
	int prefix_len, param_len;

	if (size <= 0)
		return -EINVAL;

	/* First 3 bytes are prefix, event and param length */
	prefix_len = read(fd, buf, 3);
	if (prefix_len < 0)
		return prefix_len;

	if (prefix_len < 3) {
		connman_error("Truncated HCI prefix %d bytes 0x%x",
						prefix_len, buf[0]);
		return -EIO;
	}

	DBG("type 0x%x event 0x%x param len %d", buf[0], buf[1], buf[2]);

	param_len = buf[2];
	if (param_len > size - 3) {
		connman_error("Buffer is too small %d", size);
		return -EINVAL;
	}

	return read(fd, buf + 3, param_len);
}

static int read_command_complete(int fd, unsigned short opcode)
{
	struct cmd_complete resp;
	int err;

	DBG("");

	err = read_hci_event(fd, (unsigned char *)&resp, sizeof(resp));
	if (err < 0)
		return err;

	DBG("HCI event %d bytes", err);

	if (resp.uart_prefix != HCI_EVENT_PKT) {
		connman_error("Not an event packet");
		return -EIO;
	}

	if (resp.evt != EVT_CMD_COMPLETE) {
	        connman_error("Not a cmd complete event");
		return -EIO;
	}

	if (resp.plen < 4) {
		connman_error("HCI header length %d", resp.plen);
		return -EIO;
	}

	if (resp.opcode != (unsigned short) opcode) {
		connman_error("opcode 0x%04x 0x%04x", resp.opcode, opcode);
		return -EIO;
	}

	return 0;
}

/* The default baud rate is 115200 */
static int set_default_baud_rate(int fd)
{
	struct termios ti;
	int err;

	DBG("");

	err = tcflush(fd, TCIOFLUSH);
	if (err < 0)
		goto err;

	err = tcgetattr(fd, &ti);
	if (err < 0)
		goto err;

	cfmakeraw(&ti);

	ti.c_cflag |= 1;
	ti.c_cflag |= CRTSCTS;

	err = tcsetattr(fd, TCSANOW, &ti);
	if (err < 0)
		goto err;

	cfsetospeed(&ti, B115200);
	cfsetispeed(&ti, B115200);

	err = tcsetattr(fd, TCSANOW, &ti);
	if (err < 0)
		goto err;

	err = tcflush(fd, TCIOFLUSH);
	if (err < 0)
		goto err;

	return 0;

err:
	connman_error("%s", strerror(errno));

	return err;
}

static int set_custom_baud_rate(int fd, unsigned long cus_baud_rate, int flow_ctrl)
{
	struct termios ti;
	struct termios2 ti2;
	int err;

	DBG("baud rate %lu flow_ctrl %d", cus_baud_rate, flow_ctrl);

	err = tcflush(fd, TCIOFLUSH);
	if (err < 0)
		goto err;

	err = tcgetattr(fd, &ti);
	if (err < 0)
		goto err;

	if (flow_ctrl)
		ti.c_cflag |= CRTSCTS;
	else
		ti.c_cflag &= ~CRTSCTS;

	/*
	 * Set the parameters associated with the UART
	 * The change will occur immediately by using TCSANOW.
	 */
	err = tcsetattr(fd, TCSANOW, &ti);
	if (err < 0)
		goto err;

	err = tcflush(fd, TCIOFLUSH);
	if (err < 0)
		goto err;

	/* Set the actual baud rate */
	err = ioctl(fd, TCGETS2, &ti2);
	if (err < 0)
		goto err;

	ti2.c_cflag &= ~CBAUD;
	ti2.c_cflag |= BOTHER;
	ti2.c_ospeed = cus_baud_rate;

	err = ioctl(fd, TCSETS2, &ti2);
	if (err < 0)
		goto err;

	return 0;

err:
	DBG("%s", strerror(errno));

	return err;
}

static gboolean uart_event(GIOChannel *channel,
				GIOCondition cond, gpointer data)
{
	int uart_fd, ldisc;

	DBG("");

	uart_fd = g_io_channel_unix_get_fd(channel);

	if (cond & (G_IO_NVAL | G_IO_HUP | G_IO_ERR)) {
		connman_error("UART event 0x%x", cond);
		if (uart_watch > 0)
			g_source_remove(uart_watch);

		goto err;
	}

	if (read_command_complete(uart_fd, HCI_HDR_OPCODE) < 0)
		goto err;

	if (set_custom_baud_rate(uart_fd, baud_rate, 1) < 0)
		goto err;

	ldisc = N_TI_WL;
	if (ioctl(uart_fd, TIOCSETD, &ldisc) < 0)
		goto err;

	install_count = 1;
	__sync_synchronize();

	return FALSE;

err:
	install_count = 0;
	__sync_synchronize();

	g_io_channel_shutdown(channel, TRUE, NULL);
	g_io_channel_unref(channel);

	return FALSE;
}

static int install_ldisc(GIOChannel *channel, bool install)
{
	int uart_fd, err;
	struct speed_change_cmd cmd;
	GIOFlags flags;

	DBG("%d %p", install, uart_channel);

	if (!install) {
		install_count = 0;
		__sync_synchronize();

		if (!uart_channel) {
			DBG("UART channel is NULL");
			return 0;
		}

		g_io_channel_shutdown(uart_channel, TRUE, NULL);
		g_io_channel_unref(uart_channel);

		uart_channel = NULL;

		return 0;
	}

	if (uart_channel) {
		g_io_channel_shutdown(uart_channel, TRUE, NULL);
		g_io_channel_unref(uart_channel);
		uart_channel = NULL;
	}

	DBG("opening %s custom baud %lu", uart_dev_name, baud_rate);

	uart_fd = open(uart_dev_name, O_RDWR | O_CLOEXEC);
	if (uart_fd < 0)
		return -EIO;

	uart_channel = g_io_channel_unix_new(uart_fd);
	g_io_channel_set_close_on_unref(uart_channel, TRUE);

	g_io_channel_set_encoding(uart_channel, NULL, NULL);
	g_io_channel_set_buffered(uart_channel, FALSE);

	flags = g_io_channel_get_flags(uart_channel);
	flags |= G_IO_FLAG_NONBLOCK;
	g_io_channel_set_flags(uart_channel, flags, NULL);

        err = set_default_baud_rate(uart_fd);
	if (err < 0) {
		g_io_channel_shutdown(uart_channel, TRUE, NULL);
		g_io_channel_unref(uart_channel);
		uart_channel = NULL;

		return err;
	}

	if (baud_rate == 115200) {
		int ldisc;

		ldisc = N_TI_WL;
		if (ioctl(uart_fd, TIOCSETD, &ldisc) < 0) {
			g_io_channel_shutdown(uart_channel, TRUE, NULL);
			g_io_channel_unref(uart_channel);
			uart_channel = NULL;
		}

		install_count = 0;
		__sync_synchronize();

		return 0;
	}

	cmd.uart_prefix = HCI_COMMAND_PKT;
	cmd.opcode = HCI_HDR_OPCODE;
	cmd.plen = sizeof(unsigned long);
	cmd.speed = baud_rate;

	uart_watch = g_io_add_watch(uart_channel,
			G_IO_IN | G_IO_NVAL | G_IO_HUP | G_IO_ERR,
				uart_event, NULL);

	err = write(uart_fd, &cmd, sizeof(cmd));
	if (err < 0) {
		connman_error("Write failed %d", err);

		g_io_channel_shutdown(uart_channel, TRUE, NULL);
		g_io_channel_unref(uart_channel);
		uart_channel = NULL;
	}

	return err;
}


static gboolean install_event(GIOChannel *channel,
				GIOCondition cond, gpointer data)
{
	GIOStatus status = G_IO_STATUS_NORMAL;
	unsigned int install_state;
	bool install;
	char buf[8];
	gsize len;

	DBG("");

	if (cond & (G_IO_HUP | G_IO_NVAL)) {
		connman_error("install event 0x%x", cond);
		return FALSE;
	}

	__sync_synchronize();
	if (install_count != 0) {
		status = g_io_channel_seek_position(channel, 0, G_SEEK_SET, NULL);
		if (status != G_IO_STATUS_NORMAL) {
			g_io_channel_shutdown(channel, TRUE, NULL);
			g_io_channel_unref(channel);
			return FALSE;
		}

		/* Read the install value */
		status = g_io_channel_read_chars(channel, (gchar *) buf,
						8, &len, NULL);
		if (status != G_IO_STATUS_NORMAL) {
			g_io_channel_shutdown(channel, TRUE, NULL);
			g_io_channel_unref(channel);
			return FALSE;
		}

		install_state = atoi(buf);
		DBG("install event while installing %d %c", install_state, buf[0]);

		return TRUE;
	} else {
		install_count = 1;
		__sync_synchronize();
	}

	status = g_io_channel_seek_position(channel, 0, G_SEEK_SET, NULL);
	if (status != G_IO_STATUS_NORMAL) {
		g_io_channel_shutdown(channel, TRUE, NULL);
		g_io_channel_unref(channel);
		return FALSE;
	}

	/* Read the install value */
	status = g_io_channel_read_chars(channel, (gchar *) buf, 8, &len, NULL);
	if (status != G_IO_STATUS_NORMAL) {
		g_io_channel_shutdown(channel, TRUE, NULL);
		g_io_channel_unref(channel);
		return FALSE;
	}

	install_state = atoi(buf);

	DBG("install state %d", install_state);

	install = !!install_state;

	if (install_ldisc(channel, install) < 0) {
		connman_error("ldisc installation failed");
		install_count = 0;
		__sync_synchronize();
		return TRUE;
	}

	return TRUE;
}


static int tist_init(void)
{
	GIOStatus status = G_IO_STATUS_NORMAL;
	GIOFlags flags;
	unsigned int install_state;
	char buf[8];
	int fd, err;
	gsize len;

	err = read_uart_name(uart_dev_name, sizeof(uart_dev_name));
	if (err < 0) {
		connman_error("Could not read the UART name");
		return err;
	}

	err = read_baud_rate(&baud_rate);
	if (err < 0) {
		connman_error("Could not read the baud rate");
		return err;
	}

	fd = open(TIST_SYSFS_INSTALL, O_RDONLY | O_CLOEXEC);
	if (fd < 0) {
		connman_error("Failed to open TI ST sysfs install file");
		return -EIO;
	}

	install_channel = g_io_channel_unix_new(fd);
	g_io_channel_set_close_on_unref(install_channel, TRUE);

	g_io_channel_set_encoding(install_channel, NULL, NULL);
	g_io_channel_set_buffered(install_channel, FALSE);

	flags = g_io_channel_get_flags(install_channel);
	flags |= G_IO_FLAG_NONBLOCK;
	g_io_channel_set_flags(install_channel, flags, NULL);

	status = g_io_channel_read_chars(install_channel, (gchar *) buf, 8,
								&len, NULL);
	if (status != G_IO_STATUS_NORMAL) {
		g_io_channel_shutdown(install_channel, TRUE, NULL);
		g_io_channel_unref(install_channel);
		return status;
	}

	status = g_io_channel_seek_position(install_channel, 0, G_SEEK_SET, NULL);
	if (status != G_IO_STATUS_NORMAL) {
		connman_error("Initial seek failed");
		g_io_channel_shutdown(install_channel, TRUE, NULL);
		g_io_channel_unref(install_channel);
		return -EIO;
	}

	install_state = atoi(buf);

	DBG("Initial state %d", install_state);

	install_watch = g_io_add_watch_full(install_channel, G_PRIORITY_HIGH,
				G_IO_PRI | G_IO_ERR,
					    install_event, NULL, NULL);

	if (install_state) {
		install_count = 1;
		__sync_synchronize();

		err = install_ldisc(install_channel, true);
		if (err < 0) {
			connman_error("ldisc installation failed");
			return err;
		}
	}

	return 0;
}


static void tist_exit(void)
{

	if (install_watch > 0)
		g_source_remove(install_watch);

	DBG("uart_channel %p", uart_channel);

	g_io_channel_shutdown(install_channel, TRUE, NULL);
	g_io_channel_unref(install_channel);

	if (uart_channel) {
		g_io_channel_shutdown(uart_channel, TRUE, NULL);
		g_io_channel_unref(uart_channel);
		uart_channel = NULL;
	}

}

CONNMAN_PLUGIN_DEFINE(tist, "TI shared transport support", VERSION,
		CONNMAN_PLUGIN_PRIORITY_DEFAULT, tist_init, tist_exit)
