/* (C) 2010,2017 by Harald Welte <laforge@gnumonks.org>
 *
 * All Rights Reserved
 *
 * SPDX-License-Identifier: GPL-2.0+
 *
 * 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.
 *
 */

/*! \addtogroup sercomm
 *  @{
 *  Serial communications layer, based on HDLC.
 *
 * \file sercomm.c */

#include "config.h"

#include <stdint.h>
#include <stdio.h>
#include <errno.h>

#include <osmocom/core/msgb.h>
#include <osmocom/core/utils.h>
#include <osmocom/core/sercomm.h>
#include <osmocom/core/linuxlist.h>

#ifndef EMBEDDED
# define DEFAULT_RX_MSG_SIZE	2048
/*! Protect against IRQ context */
void sercomm_drv_lock(unsigned long __attribute__((unused)) *flags) {}
/*! Release protection against IRQ context */
void sercomm_drv_unlock(unsigned long __attribute__((unused)) *flags) {}
#else
# define DEFAULT_RX_MSG_SIZE	256
#endif /* EMBEDDED */

/* weak symbols to be overridden by application */
__attribute__((weak)) void sercomm_drv_start_tx(struct osmo_sercomm_inst *sercomm) {};
__attribute__((weak)) int sercomm_drv_baudrate_chg(struct osmo_sercomm_inst *sercomm, uint32_t bdrt)
{
	return -1;
}

#define HDLC_FLAG	0x7E
#define HDLC_ESCAPE	0x7D

#define HDLC_C_UI	0x03
#define HDLC_C_P_BIT	(1 << 4)
#define HDLC_C_F_BIT	(1 << 4)

enum rx_state {
	RX_ST_WAIT_START,
	RX_ST_ADDR,
	RX_ST_CTRL,
	RX_ST_DATA,
	RX_ST_ESCAPE,
};

/*! Initialize an Osmocom sercomm instance
 *  \param sercomm Caller-allocated sercomm instance to be initialized
 *
 *  This function initializes the sercomm instance, including the
 *  registration of the ECHO service at the ECHO DLCI
 */
void osmo_sercomm_init(struct osmo_sercomm_inst *sercomm)
{
	unsigned int i;
	for (i = 0; i < ARRAY_SIZE(sercomm->tx.dlci_queues); i++)
		INIT_LLIST_HEAD(&sercomm->tx.dlci_queues[i]);

	sercomm->rx.msg = NULL;
	if (!sercomm->rx.msg_size)
		sercomm->rx.msg_size = DEFAULT_RX_MSG_SIZE;
	sercomm->initialized = 1;

	/* set up the echo dlci */
	osmo_sercomm_register_rx_cb(sercomm, SC_DLCI_ECHO, &osmo_sercomm_sendmsg);
}

/*! Determine if a given Osmocom sercomm instance has been initialized
 *  \param[in] sercomm Osmocom sercomm instance to be checked
 *  \returns 1 in case \a sercomm was previously initialized; 0 otherwise */
int osmo_sercomm_initialized(struct osmo_sercomm_inst *sercomm)
{
	return sercomm->initialized;
}

/*! User interface for transmitting messages for a given DLCI
 *  \param[in] sercomm Osmocom sercomm instance through which to transmit
 *  \param[in] dlci DLCI through whcih to transmit \a msg
 *  \param[in] msg Message buffer to be transmitted via \a dlci on \a *  sercomm
 **/
void osmo_sercomm_sendmsg(struct osmo_sercomm_inst *sercomm, uint8_t dlci, struct msgb *msg)
{
	unsigned long flags;
	uint8_t *hdr;

	/* prepend address + control octet */
	hdr = msgb_push(msg, 2);
	hdr[0] = dlci;
	hdr[1] = HDLC_C_UI;

	/* This functiion can be called from any context: FIQ, IRQ
	 * and supervisor context.  Proper locking is important! */
	sercomm_drv_lock(&flags);
	msgb_enqueue(&sercomm->tx.dlci_queues[dlci], msg);
	sercomm_drv_unlock(&flags);

	/* tell UART that we have something to send */
	sercomm_drv_start_tx(sercomm);
}

/*! How deep is the Tx queue for a given DLCI?
 *  \param[n] sercomm Osmocom sercomm instance on which to operate
 *  \param[in] dlci DLCI whose queue depthy is to be determined
 *  \returns number of elements in the per-DLCI transmit queue */
unsigned int osmo_sercomm_tx_queue_depth(struct osmo_sercomm_inst *sercomm, uint8_t dlci)
{
	struct llist_head *le;
	unsigned int num = 0;

	llist_for_each(le, &sercomm->tx.dlci_queues[dlci]) {
		num++;
	}

	return num;
}

/*! wait until everything has been transmitted, then grab the lock and
 *	   change the baud rate as requested
 *  \param[in] sercomm Osmocom sercomm instance
 *  \param[in] bdrt New UART Baud Rate
 *  \returns result of the operation as provided by  sercomm_drv_baudrate_chg()
 */
int osmo_sercomm_change_speed(struct osmo_sercomm_inst *sercomm, uint32_t bdrt)
{
	unsigned int i, count;
	unsigned long flags;

	while (1) {
		/* count the number of pending messages */
		count = 0;
		for (i = 0; i < ARRAY_SIZE(sercomm->tx.dlci_queues); i++)
			count += osmo_sercomm_tx_queue_depth(sercomm, i);
		/* if we still have any in the queue, restart */
		if (count == 0)
			break;
	}

	while (1) {
		/* no messages in the queue, grab the lock to ensure it
		 * stays that way */
		sercomm_drv_lock(&flags);
		if (!sercomm->tx.msg && !sercomm->tx.next_char) {
			int rc;
			/* change speed */
			rc = sercomm_drv_baudrate_chg(sercomm, bdrt);
			sercomm_drv_unlock(&flags);
			return rc;
		} else
			sercomm_drv_unlock(&flags);
	}
	return -1;
}

/*! fetch one octet of to-be-transmitted serial data
 *  \param[in] sercomm Sercomm Instance from which to fetch pending data
 *  \param[out] ch pointer to caller-allocaed output memory
 *  \returns 1 in case of succss; 0 if no data available; negative on error */
int osmo_sercomm_drv_pull(struct osmo_sercomm_inst *sercomm, uint8_t *ch)
{
	unsigned long flags;

	/* we may be called from interrupt context, but we stiff need to lock
	 * because sercomm could be accessed from a FIQ context ... */

	sercomm_drv_lock(&flags);

	if (!sercomm->tx.msg) {
		unsigned int i;
		/* dequeue a new message from the queues */
		for (i = 0; i < ARRAY_SIZE(sercomm->tx.dlci_queues); i++) {
			sercomm->tx.msg = msgb_dequeue(&sercomm->tx.dlci_queues[i]);
			if (sercomm->tx.msg)
				break;
		}
		if (sercomm->tx.msg) {
			/* start of a new message, send start flag octet */
			*ch = HDLC_FLAG;
			sercomm->tx.next_char = sercomm->tx.msg->data;
			sercomm_drv_unlock(&flags);
			return 1;
		} else {
			/* no more data avilable */
			sercomm_drv_unlock(&flags);
			return 0;
		}
	}

	if (sercomm->tx.state == RX_ST_ESCAPE) {
		/* we've already transmitted the ESCAPE octet,
		 * we now need to transmit the escaped data */
		*ch = *sercomm->tx.next_char++;
		sercomm->tx.state = RX_ST_DATA;
	} else if (sercomm->tx.next_char >= sercomm->tx.msg->tail) {
		/* last character has already been transmitted,
		 * send end-of-message octet */
		*ch = HDLC_FLAG;
		/* we've reached the end of the message buffer */
		msgb_free(sercomm->tx.msg);
		sercomm->tx.msg = NULL;
		sercomm->tx.next_char = NULL;
	/* escaping for the two control octets */
	} else if (*sercomm->tx.next_char == HDLC_FLAG ||
		   *sercomm->tx.next_char == HDLC_ESCAPE ||
		   *sercomm->tx.next_char == 0x00) {
		/* send an escape octet */
		*ch = HDLC_ESCAPE;
		/* invert bit 5 of the next octet to be sent */
		*sercomm->tx.next_char ^= (1 << 5);
		sercomm->tx.state = RX_ST_ESCAPE;
	} else {
		/* standard case, simply send next octet */
		*ch = *sercomm->tx.next_char++;
	}

	sercomm_drv_unlock(&flags);
	return 1;
}

/*! Register a handler for a given DLCI
 *  \param sercomm Sercomm Instance in which caller wishes to register
 *  \param[in] dlci Data Ling Connection Identifier to register
 *  \param[in] cb Callback function for \a dlci
 *  \returns 0 on success; negative on error */
int osmo_sercomm_register_rx_cb(struct osmo_sercomm_inst *sercomm, uint8_t dlci, dlci_cb_t cb)
{
	if (dlci >= ARRAY_SIZE(sercomm->rx.dlci_handler))
		return -EINVAL;

	if (sercomm->rx.dlci_handler[dlci])
		return -EBUSY;

	sercomm->rx.dlci_handler[dlci] = cb;
	return 0;
}

/* dispatch an incoming message once it is completely received */
static void dispatch_rx_msg(struct osmo_sercomm_inst *sercomm, uint8_t dlci, struct msgb *msg)
{
	if (dlci >= ARRAY_SIZE(sercomm->rx.dlci_handler) ||
	    !sercomm->rx.dlci_handler[dlci]) {
		msgb_free(msg);
		return;
	}
	sercomm->rx.dlci_handler[dlci](sercomm, dlci, msg);
}

/*! the driver has received one byte, pass it into sercomm layer
 *  \param[in] sercomm Sercomm Instance for which a byte was received
 *  \param[in] ch byte that was received from line for said instance
 *  \returns 1 on success; 0 on unrecognized char; negative on error */
int osmo_sercomm_drv_rx_char(struct osmo_sercomm_inst *sercomm, uint8_t ch)
{
	uint8_t *ptr;

	/* we are always called from interrupt context in this function,
	 * which means that any data structures we use need to be for
	 * our exclusive access */
	if (!sercomm->rx.msg)
		sercomm->rx.msg = osmo_sercomm_alloc_msgb(sercomm->rx.msg_size);

	if (msgb_tailroom(sercomm->rx.msg) == 0) {
		//cons_puts("sercomm_drv_rx_char() overflow!\n");
		msgb_free(sercomm->rx.msg);
		sercomm->rx.msg = osmo_sercomm_alloc_msgb(sercomm->rx.msg_size);
		sercomm->rx.state = RX_ST_WAIT_START;
		return 0;
	}

	switch (sercomm->rx.state) {
	case RX_ST_WAIT_START:
		if (ch != HDLC_FLAG)
			break;
		sercomm->rx.state = RX_ST_ADDR;
		break;
	case RX_ST_ADDR:
		sercomm->rx.dlci = ch;
		sercomm->rx.state = RX_ST_CTRL;
		break;
	case RX_ST_CTRL:
		sercomm->rx.ctrl = ch;
		sercomm->rx.state = RX_ST_DATA;
		break;
	case RX_ST_DATA:
		if (ch == HDLC_ESCAPE) {
			/* drop the escape octet, but change state */
			sercomm->rx.state = RX_ST_ESCAPE;
			break;
		} else if (ch == HDLC_FLAG) {
			/* message is finished */
			dispatch_rx_msg(sercomm, sercomm->rx.dlci, sercomm->rx.msg);
			/* allocate new buffer */
			sercomm->rx.msg = NULL;
			/* start all over again */
			sercomm->rx.state = RX_ST_WAIT_START;

			/* do not add the control char */
			break;
		}
		/* default case: store the octet */
		ptr = msgb_put(sercomm->rx.msg, 1);
		*ptr = ch;
		break;
	case RX_ST_ESCAPE:
		/* store bif-5-inverted octet in buffer */
		ch ^= (1 << 5);
		ptr = msgb_put(sercomm->rx.msg, 1);
		*ptr = ch;
		/* transition back to normal DATA state */
		sercomm->rx.state = RX_ST_DATA;
		break;
	}

	return 1;
}

/*! @} */
