/*
 *******************************************************************************
 * Authors:
 *     Maxime Villard, 2017
 *     Jankovic Marco, 01/07/2014
 *******************************************************************************
 */

#include "common.h"

typedef enum fsm_state_t {
	FSM_INIT_COM,
	FSM_IDLE,
	FSM_READ_TTY,
	FSM_WRITE_TTY,
	FSM_READ_DEVICE,
	FSM_WRITE_DEVICE,
	FSM_READ_FIFO_RX,
	FSM_READ_FIFO_TX,
	FSM_WRITE_FIFO_RX,
	FSM_WRITE_FIFO_TX,
} fsm_state;

typedef enum flux_control_state_t {
	DONE,
	SEND_XON,
	SEND_XOFF
} flux_control_state;

typedef enum tx_state_t {
	ENABLE,
	DISABLE,
} tx_state;

/*
 * Variables used for thread communication.
 */
static flux_control_state flux_control = DONE;	// used for xon/xoff
static tx_state TX_STATE = ENABLE;				// used to ENABLE/DISABLE thread write

/*
 * This thread reads the fifo_rx for data, then writes those data to the correct
 * tty with the id contained in the packet. Then it checks if a tty is readable.
 * If there is one, write N packet from one tty to the fifo_tx then return to
 * idle state.
 */
void *thread_mux_demux(void *a)
{
	UNUSED(a);
	printf("[+] thread_mux launched\n");

	fd_set readfs;
	char packet_buffer[2 * READ_LEN];
	char tty_raw_buffer[READ_LEN];
	char *pkt;

	int res;
	int i,j;
	int tty_id;
	int tty_size_read = 0;
	int fifo_size_read = 0;
	fsm_state mux_state;

	sigset_t ens;
	sigfillset(&ens);
	sigdelset(&ens, SIGTERM);
	pthread_sigmask(SIG_SETMASK, &ens, NULL);

	struct timeval timeout;

	mux_state = FSM_IDLE;

	while (1)
	{
		switch (mux_state) {
			/*
			 * check if the fifo_rx is readable -> FSM_READ_FIFO_RX
			 * if not check if there is readable data in target_fd -> FSM_READ_TTY
			 */
			case FSM_IDLE:
				FD_ZERO(&readfs);
				FD_SET(fifo_rx_fd, &readfs);

				/*
				 * check if fifo_rx is readable
				 */
				timeout.tv_sec = TIMEOUT_DELAY_MUX_S;
				timeout.tv_usec = TIMEOUT_DELAY_MUX_US;
				res = select(FD_SETSIZE, &readfs, NULL, NULL, &timeout);
				if (res < 0) {
					err(1, "select");
				} else if (res == 0) {
					/* nothing to read, try to read a tty */
					mux_state = FSM_READ_TTY;
					break;
				}

				/* readable */
				mux_state = FSM_READ_FIFO_RX;
				break;

			/*
			 * Read fifo_rx.
			 */
			case FSM_READ_FIFO_RX:
				fifo_size_read = read(fifo_rx_fd, packet_buffer, READ_LEN);

				if (!fifo_size_read) {
					perror("read");
					mux_state = FSM_IDLE;
					break;
				}

				mux_state = FSM_WRITE_TTY;
				break;

			/*
			 * Write the data to the proper tty, then -> FSM_READ_TTY.
			 */
			case FSM_WRITE_TTY:
				j = 0;

				while (j < fifo_size_read) {
					pkt = packet_buffer + j;
					write(slave_fd[(uint8_t)pkt[PKT_TTY]], &pkt[PKT_DAT], 1);
#if DEBUG_MUX
					printf("[MUX::FSM_WRITE_TTY] tty_id=%x | data=%c\n",
					    pkt[PKT_TTY], pkt[PKT_DAT]);
#endif
					j += PACKET_SIZE;
				}
				mux_state = FSM_READ_TTY;
				break;

			/*
			 * Check if there is a readable tty. Return to idle if there is no
			 * tty ready, otherwise -> FSM_WRITE_FIFO_TX.
			 */
			case FSM_READ_TTY:
				timeout.tv_sec = TIMEOUT_DELAY_MUX_S;
				timeout.tv_usec = TIMEOUT_DELAY_MUX_US;

				FD_ZERO(&readfs);
				for (i = 0; i < NB_CHANNELS; i++) {
					FD_SET(slave_fd[i], &readfs);
				}

				res = select(FD_SETSIZE, &readfs, NULL, NULL, &timeout);
				if (res < 0) {
					err(1, "select");
				} else if (res == 0) {
					/* no readable tty */
					mux_state = FSM_IDLE;
					break;
				}

				/* one tty is readable, find it */
				tty_id = 0;
				while (tty_id < NB_CHANNELS && !(res = FD_ISSET(slave_fd[tty_id], &readfs))) {
					tty_id++;
				}

				/* read it */
				tty_size_read = read(slave_fd[tty_id], tty_raw_buffer, READ_LEN);

				if (tty_size_read == 0) {
					mux_state = FSM_IDLE;
					printf("thread MUX FSM_READ_TTY: error while reading tty_%d\n", tty_id);
					perror("read");
					break;
				}
#if DEBUG_MUX
				printf("[MUX::FSM_READ_TTY] read from tty_id=%d | %d bytes\n",
				    tty_id, tty_size_read);
#endif
				i = 0;
				j = 0;
				while (i < tty_size_read) {
					pkt = packet_buffer + j;
					pkt[PKT_TTY] = tty_id | 0x80;
					pkt[PKT_DAT] = tty_raw_buffer[i];
					i++;
					j += PACKET_SIZE;
				}

				mux_state = FSM_WRITE_FIFO_TX;
				break;

			/*
			 * Write packet_buffer into fifo_tx.
			 */
			case FSM_WRITE_FIFO_TX:
				write(fifo_tx_fd, packet_buffer, tty_size_read * PACKET_SIZE);
				mux_state = FSM_IDLE;
				break;

			default:
				err(1, "unreachable");
				break;
		}
	}
	return (void*)NULL;
}

/*
 * This thread initiates communication with the device by sending XON
 * -> read the RX_BUFFER
 * -> check packet correctness
 * -> check fifo_rx size
 * -> software flux control
 * -> then write to the fifo_rx
 */
void *thread_read_rx(void *a)
{
	UNUSED(a);
	printf("[+] thread_read_rx launched\n");
	int size_read = 0;
	int bytes_avail = 0;
	int id_ok = 0;
	int res = 0;
	int x_off = 0;
	int device_raw_offset = 0;
	fd_set readfs;
	int fd;

#if TEST_BENCH
	fd = test_rx_fd;
#else
	fd = device_fd;
#endif

	unsigned char device_raw_data[READ_LEN];
	unsigned char packet_buffer[2];

	struct timeval timeout;

#if TEST_BENCH
	fsm_state read_state = FSM_IDLE;
#else
	fsm_state read_state = FSM_INIT_COM;
#endif

	while (1)
	{
		switch (read_state)
		{
			/*
			 * This state is used to initiate communication with the device
			 * by sending periodically XON while the RX_BUFFER is empty.
			 * Then we reach FSM_IDLE, and never to this state afterwards.
			 */
			case FSM_INIT_COM:
				FD_ZERO(&readfs);
				FD_SET(fd, &readfs);

				timeout.tv_sec = TIMEOUT_DELAY_READ_S;
				timeout.tv_usec = TIMEOUT_DELAY_READ_US;

				res = select(FD_SETSIZE, &readfs, NULL, NULL, &timeout);
				if (res > 0) {
					read_state = FSM_IDLE;
					flux_control = DONE;
					break;
				} else if (res < 0) {
					err(1, "select");
				} else {
					/* nothing to read */
				}

				if (flux_control == DONE)
					flux_control = SEND_XON;
				break;

			/*
			 * This state first checks the number of bytes available in fifo_rx.
			 * If FIFO_LIMIT_SIZE is reached a XOFF is sent. After checking the
			 * fifo, check if the device is ready for i/o -> FSM_READ_DEVICE.
			 */
			case FSM_IDLE:
				FD_ZERO(&readfs);
				FD_SET(fd, &readfs);

				if (ioctl(fifo_rx_fd, FIONREAD, &bytes_avail) < 0) {
					err(1, "ioctl");
				}

				if (bytes_avail > FIFO_LIMIT_SIZE && !x_off) {
					if (flux_control == DONE) {
						flux_control = SEND_XOFF;
						x_off = 1;
					}
				} else if ((bytes_avail < FIFO_LIMIT_SIZE) && x_off) {
					if (flux_control == DONE) {
						flux_control = SEND_XON;
						x_off = 0;
					}
				}

				/*
				 * check if the device is readable
				 */
				timeout.tv_sec = TIMEOUT_DELAY_READ_S;
				timeout.tv_usec = TIMEOUT_DELAY_READ_US;
				res = select(FD_SETSIZE, &readfs, NULL, NULL, &timeout);
				if (res > 0) {
					read_state = FSM_READ_DEVICE;
					break;
				} else if (res < 0) {
					err(1, "select");
				} else {
					/* nothing to read */
				}

				break;

				/*
				 * Read the device, then -> FSM_WRITE_FIFO_RX.
				 */
			case FSM_READ_DEVICE:
				size_read = read(fd, device_raw_data, READ_LEN);
				if (size_read == 0) {
					read_state = FSM_IDLE;
					break;
				}

				read_state = FSM_WRITE_FIFO_RX;
				break;

				/*
				 * Control packet correctness
				 * if a data is XON -> ENABLE thread_write
				 * if a data is XOFF -> DISABLE thread_write
				 * non correct packet are dropped
				 * then back to idle
				 */
			case FSM_WRITE_FIFO_RX:
				device_raw_offset = 0;
				while (device_raw_offset < size_read)
				{
#if DEBUG_READ
					printf("-> got packet from device: data=%x\n", device_raw_data[device_raw_offset]);
#endif
					if (device_raw_data[device_raw_offset] == XOFF) {
						printf("XOFF received\n");
						TX_STATE = DISABLE;
						device_raw_offset++;
						continue;
					} else if (device_raw_data[device_raw_offset] == XON) {
						printf("XON received\n");
						TX_STATE = ENABLE;
						device_raw_offset++;
						continue;
					} else if (device_raw_data[device_raw_offset] & 0x80) {
						id_ok = 1;
						packet_buffer[PKT_TTY] = device_raw_data[device_raw_offset] & 0x7F; /* XXX: should be in the demux thread */

						device_raw_offset++;
#if DEBUG_READ
						printf("--> detected ID in packet: id=%x\n", packet_buffer[PKT_TTY]);
#endif
						continue;
					}
					/* check if data is not ID*/
					else if (id_ok && !(device_raw_data[device_raw_offset] & 0x80))
					{
						if (device_raw_data[device_raw_offset] == XOFF) {
#if DEBUG_READ
							printf("--> detected DATA in packet: XOFF\n");
#endif
							device_raw_offset++;
							TX_STATE = DISABLE;
							continue;
						} else if (device_raw_data[device_raw_offset] == XON) {
#if DEBUG_READ
							printf("--> detected DATA in packet: XON\n");
#endif
							device_raw_offset++;
							TX_STATE = ENABLE;
							continue;
						}
#if DEBUG_READ
						printf("--> detected DATA in packet: '%c'\n", device_raw_data[device_raw_offset]);
#endif
						packet_buffer[PKT_DAT] = device_raw_data[device_raw_offset];
						id_ok = 0;

						write(fifo_rx_fd, packet_buffer, PACKET_SIZE);
						device_raw_offset++;
						continue;
					}
					printf("-> dropping data=%x\n", device_raw_data[device_raw_offset]);
					device_raw_offset++;
				}

				read_state = FSM_IDLE;
				break;

			default:
				err(1, "unreachable");
				break;
		}
	}
	return NULL;
}

/*
 * This thread writes packets from the fifo_tx to the TX_BUFFER. This thread is
 * controlled with TX_STATE and flux_control.
 */
void *thread_write_tx(void *a)
{
	UNUSED(a);
#if DEBUG_WRITE
	int i,j;
#endif

	int res = 0;
	int size_read = 0;
	int fd;

	unsigned char xon = XON;
	unsigned char xoff = XOFF;
	fd_set readfs;

#if TEST_BENCH
	fd = test_tx_fd;
#else
	fd = device_fd;
#endif

	unsigned char device_buf[READ_LEN];

	struct timeval timeout;

	fsm_state write_state = FSM_IDLE;

	printf("[+] thread_write_tx launched\n");

	while (1)
	{
		switch (write_state)
		{
			/*
			 * Depending on TX_STATE and flux_control, send a xon or xoff or
			 * yield if DISABLE. Otherwise, check if the fifo_tx is readable
			 * -> FSM_READ_FIFO_TX.
			 */
			case FSM_IDLE:
				/* software flux control section */
				if (TX_STATE == DISABLE && flux_control != DONE) {
					sched_yield();
					break;
				} else if (flux_control == SEND_XOFF) {
					write(fd, &xoff, 1);
					flux_control = DONE;
					printf("XOFF sent\n");
					break;
				} else if (flux_control == SEND_XON) {
					write(fd, &xon, 1);
					flux_control = DONE;
					printf("XON sent\n");
					break;
				}
				/**********************************/

				/*
				 * check if the fifo is readable
				 */
				FD_ZERO(&readfs);
				FD_SET(fifo_tx_fd, &readfs);
				timeout.tv_sec = TIMEOUT_DELAY_WRITE_S;
				timeout.tv_usec = TIMEOUT_DELAY_WRITE_US;
				res = select(FD_SETSIZE, &readfs, NULL, NULL, &timeout);
				if (res > 0) {
					if (TX_STATE != DISABLE)
						write_state = FSM_READ_FIFO_TX;
					break;
				} else if (res < 0) {
					err(1, "select");
				} else {
					/* nothing to read */
				}

				break;

			/*
			 * Read from fifo_tx, then -> FSM_WRITE_DEVICE.
			 */
			case FSM_READ_FIFO_TX:
				size_read = read(fifo_tx_fd, device_buf, READ_LEN);

#if DEBUG_WRITE
				i = 0;
				j = 0;
				while (i < size_read) {
					printf("thread WRITE id=%x data='%c'\n", device_buf[j + PKT_TTY],device_buf[j + PKT_DAT]);
					i += PACKET_SIZE;
					j += PACKET_SIZE;
				}
#endif

				if (size_read == 0) {
					write_state = FSM_IDLE;
					break;
				}

				write_state = FSM_WRITE_DEVICE;
				break;

				/*
				 * Write to the device, then return to idle.
				 */
			case FSM_WRITE_DEVICE:
				size_read = write(fd, device_buf, size_read);
				write_state = FSM_IDLE;
				break;

			default:
				err(1, "unreachable");
				break;
		}
	}
	return NULL;
}
