/* T-Link interface using POSIX serial port */

/* (C) 2008-2011 by Harald Welte <laforge@gnumonks.org>
 *
 * All Rights Reserved
 *
 * Authors:	Harald Welte <laforge@gnumonks.org>
 *		Pablo Neira Ayuso <pablo@gnumonks.org>
 *
 * SPDX-License-Identifier: AGPL-3.0+
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation; either version 3 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <termios.h>
#include <fcntl.h>

#include <osmocom/core/select.h>
#include <osmocom/core/msgb.h>
#include <osmocom/core/logging.h>
#include <osmocom/core/talloc.h>
#include <osmocom/abis/e1_input.h>

static void *tall_rs232_ctx;

struct serial_handle {
	struct e1inp_line	*line;

	struct msgb		*rx_msg;
	unsigned int		rxmsg_bytes_missing;

	unsigned int		delay_ms;
};

#define CRAPD_HDR_LEN	10

static int handle_ser_write(struct osmo_fd *bfd);

static void rs232_build_msg(struct msgb *msg)
{
	uint8_t *crapd;
	unsigned int len;

	msg->l2h = msg->data;

	/* prepend CRAPD header */
	crapd = msgb_push(msg, CRAPD_HDR_LEN);

	len = msg->len - 2;

	crapd[0] = (len >> 8) & 0xff;
	crapd[1] = len & 0xff; /* length of bytes startign at crapd[2] */
	crapd[2] = 0x00;
	crapd[3] = 0x07;
	crapd[4] = 0x01;
	crapd[5] = 0x3e;
	crapd[6] = 0x00;
	crapd[7] = 0x00;
	crapd[8] = msg->len - 10; /* length of bytes starting at crapd[10] */
	crapd[9] = crapd[8] ^ 0x38;
}

/* select.c callback in case we can write to the rs232 */
static int handle_ser_write(struct osmo_fd *bfd)
{
	struct serial_handle *sh = bfd->data;
	struct e1inp_ts *e1i_ts = &sh->line->ts[0];
	struct e1inp_sign_link *sign_link;
	struct msgb *msg;
	int written;

	osmo_fd_write_disable(bfd);

	/* get the next msg for this timeslot */
	msg = e1inp_tx_ts(e1i_ts, &sign_link);
	if (!msg) {
		/* no message after tx delay timer */
		return 0;
	}
	LOGPITS(e1i_ts, DLMI, LOGL_DEBUG, "rs232 TX: %s\n", osmo_hexdump(msg->data, msg->len));

	rs232_build_msg(msg);

	/* send over serial line */
	written = write(bfd->fd, msg->data, msg->len);
	if (written < msg->len) {
		LOGPITS(e1i_ts, DLMI, LOGL_ERROR, "rs232: short write\n");
		msgb_free(msg);
		return -1;
	}

	msgb_free(msg);
	usleep(sh->delay_ms*1000);

	return 0;
}

#define SERIAL_ALLOC_SIZE	300

/* select.c callback in case we can read from the rs232 */
static int handle_ser_read(struct osmo_fd *bfd)
{
	struct serial_handle *sh = bfd->data;
	struct e1inp_ts *e1i_ts = &sh->line->ts[0];
	struct msgb *msg;
	int rc = 0;

	if (!sh->rx_msg) {
		sh->rx_msg = msgb_alloc(SERIAL_ALLOC_SIZE, "rs232 Rx");
		sh->rx_msg->l2h = NULL;
	}
	msg = sh->rx_msg;

	/* first read two byes to obtain length */
	if (msg->len < 2) {
		rc = read(bfd->fd, msg->tail, 2 - msg->len);
		if (rc < 0) {
			LOGPITS(e1i_ts, DLMI, LOGL_ERROR, "rs232: error reading from "
				"serial port: %s\n", strerror(errno));
			msgb_free(msg);
			return rc;
		}
		msgb_put(msg, rc);

		if (msg->len >= 2) {
			/* parse CRAPD payload length */
			if (msg->data[0] != 0) {
				LOGPITS(e1i_ts, DLMI, LOGL_ERROR, "Suspicious header byte 0: 0x%02x\n",
					msg->data[0]);
			}
			sh->rxmsg_bytes_missing = msg->data[0] << 8;
			sh->rxmsg_bytes_missing += msg->data[1];

			if (sh->rxmsg_bytes_missing < CRAPD_HDR_LEN -2) {
				LOGPITS(e1i_ts, DLMI, LOGL_ERROR, "Invalid length in hdr: %u\n",
					sh->rxmsg_bytes_missing);
			}
		}
	} else {
		/* try to read as many of the missing bytes as are available */
		rc = read(bfd->fd, msg->tail, sh->rxmsg_bytes_missing);
		if (rc < 0) {
			LOGPITS(e1i_ts, DLMI, LOGL_ERROR, "rs232: error reading from serial port: %s",
				strerror(errno));
			msgb_free(msg);
			return rc;
		}
		msgb_put(msg, rc);
		sh->rxmsg_bytes_missing -= rc;

		if (sh->rxmsg_bytes_missing == 0) {

			/* we have one complete message now */
			sh->rx_msg = NULL;

			if (msg->len > CRAPD_HDR_LEN)
				msg->l2h = msg->data + CRAPD_HDR_LEN;

			LOGPITS(e1i_ts, DLMI, LOGL_DEBUG, "rs232 RX: %s", osmo_hexdump(msg->data, msg->len));

			/* don't use e1inp_tx_ts() here, this header does not
			 * contain any SAPI and TEI values. */
			if (!e1i_ts->line->ops->sign_link) {
				LOGPITS(e1i_ts, DLMI, LOGL_ERROR, "rs232: no callback set, skipping message.\n");
				return -EINVAL;
			}
			e1i_ts->line->ops->sign_link(msg);
		}
	}

	return rc;
}

/* select.c callback */
static int serial_fd_cb(struct osmo_fd *bfd, unsigned int what)
{
	int rc = 0;

	if (what & OSMO_FD_READ)
		rc = handle_ser_read(bfd);

	if (rc < 0)
		return rc;

	if (what & OSMO_FD_WRITE)
		rc = handle_ser_write(bfd);

	return rc;
}

static int rs232_want_write(struct e1inp_ts *e1i_ts)
{
	osmo_fd_write_enable(&e1i_ts->driver.rs232.fd);

	return 0;
}

static int
rs232_setup(struct e1inp_line *line, const char *serial_port, unsigned int delay_ms)
{
	int rc;
	struct osmo_fd *bfd = &line->ts[0].driver.rs232.fd;
	struct serial_handle *ser_handle;
	struct termios tio;

	rc = open(serial_port, O_RDWR);
	if (rc < 0) {
		LOGPIL(line, DLMI, LOGL_ERROR, "rs232: cannot open serial port: %s", strerror(errno));
		return rc;
	}
	bfd->fd = rc;

	/* set baudrate */
	rc = tcgetattr(bfd->fd, &tio);
	if (rc < 0) {
		LOGPIL(line, DLMI, LOGL_ERROR, "rs232: tcgetattr says: %s", strerror(errno));
		return rc;
	}
	cfsetispeed(&tio, B19200);
	cfsetospeed(&tio, B19200);
	tio.c_cflag |=  (CREAD | CLOCAL | CS8);
	tio.c_cflag &= ~(PARENB | CSTOPB | CSIZE | CRTSCTS);
	tio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
	tio.c_iflag |=  (INPCK | ISTRIP);
	tio.c_iflag &= ~(ISTRIP | IXON | IXOFF | IGNBRK | INLCR | ICRNL | IGNCR);
	tio.c_oflag &= ~(OPOST);
	rc = tcsetattr(bfd->fd, TCSADRAIN, &tio);
	if (rc < 0) {
		LOGPIL(line, DLMI, LOGL_ERROR, "rs232: tcsetattr says: %s", strerror(errno));
		return rc;
	}

	ser_handle = talloc_zero(tall_rs232_ctx, struct serial_handle);
	if (ser_handle == NULL) {
		close(bfd->fd);
		LOGPIL(line, DLMI, LOGL_ERROR, "rs232: cannot allocate memory for serial handler\n");
		return -ENOMEM;
	}
	ser_handle->line = line;
	ser_handle->delay_ms = delay_ms;

	osmo_fd_setup(bfd, bfd->fd, OSMO_FD_READ, serial_fd_cb, ser_handle, 0);

	rc = osmo_fd_register(bfd);
	if (rc < 0) {
		close(bfd->fd);
		LOGPIL(line, DLMI, LOGL_ERROR, "rs232: could not register FD: %s\n", strerror(-rc));
		return rc;
	}

	return 0;
}

static int rs232_line_update(struct e1inp_line *line);

static struct e1inp_driver rs232_driver = {
	.name		= "rs232",
	.want_write	= rs232_want_write,
	.line_update	= rs232_line_update,
};

static int rs232_line_update(struct e1inp_line *line)
{
	if (line->driver != &rs232_driver)
		return -EINVAL;

	return rs232_setup(line, line->ops->cfg.rs232.port,
				 line->ops->cfg.rs232.delay);
}

int e1inp_rs232_init(void)
{
	return e1inp_driver_register(&rs232_driver);
}
