/****************************************************************************
 *
 *   Copyright (C) 2023 PX4 Development Team. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 * 3. Neither the name PX4 nor the names of its contributors may be
 *    used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 ****************************************************************************/

#include <SerialImpl.hpp>
#include <string.h> // strncpy
#include <termios.h>
#include <px4_log.h>
#include <fcntl.h>
#include <errno.h>
#include <poll.h>
#include <drivers/drv_hrt.h>

#define MODULE_NAME "SerialImpl"

namespace device
{

SerialImpl::SerialImpl(const char *port, uint32_t baudrate, ByteSize bytesize, Parity parity, StopBits stopbits,
		       FlowControl flowcontrol) :
	_baudrate(baudrate),
	_bytesize(bytesize),
	_parity(parity),
	_stopbits(stopbits),
	_flowcontrol(flowcontrol)
{
	if (validatePort(port)) {
		setPort(port);

	} else {
		_port[0] = 0;
	}
}

SerialImpl::~SerialImpl()
{
	if (isOpen()) {
		close();
	}
}

bool SerialImpl::configure()
{
	/* process baud rate */
	int speed;

	switch (_baudrate) {
	case 0:
		// special case, if baudrate is 0 it hangs entire system
		PX4_ERR("baudrate not specified");
		return false;

	case 9600:   speed = B9600;   break;

	case 19200:  speed = B19200;  break;

	case 38400:  speed = B38400;  break;

	case 57600:  speed = B57600;  break;

	case 115200: speed = B115200; break;

	case 230400: speed = B230400; break;

#ifndef B460800
#define B460800 460800
#endif

	case 460800: speed = B460800; break;

#ifndef B500000
#define B500000 500000
#endif

	case 500000: speed = B500000; break;

#ifndef B576000
#define B576000 576000
#endif

	case 576000: speed = B576000; break;

#ifndef B921600
#define B921600 921600
#endif

	case 921600: speed = B921600; break;

#ifndef B1000000
#define B1000000 1000000
#endif

	case 1000000: speed = B1000000; break;

#ifndef B1500000
#define B1500000 1500000
#endif

	case 1500000: speed = B1500000; break;

	default:
		speed = _baudrate;
#ifdef CONFIG_ARCH_CHIP_ESP32
		PX4_WARN("Using non-standard baudrate: %u", _baudrate);
#else
		PX4_WARN("Using non-standard baudrate: %lu", _baudrate);
#endif
		break;
	}

	struct termios uart_config;

	int termios_state;

	/* fill the struct for the new configuration */
	if ((termios_state = tcgetattr(_serial_fd, &uart_config)) < 0) {
		PX4_ERR("ERR: %d (tcgetattr)", termios_state);
		return false;
	}

	/* properly configure the terminal (see also https://en.wikibooks.org/wiki/Serial_Programming/termios ) */

	//
	// Input flags - Turn off input processing
	//
	// convert break to null byte, no CR to NL translation,
	// no NL to CR translation, don't mark parity errors or breaks
	// no input parity check, don't strip high bit off,
	// no XON/XOFF software flow control
	//
	uart_config.c_iflag &= ~(IGNBRK | BRKINT | ICRNL |
				 INLCR | PARMRK | INPCK | ISTRIP | IXON);

	//
	// Output flags - Turn off output processing
	//
	// no CR to NL translation, no NL to CR-NL translation,
	// no NL to CR translation, no column 0 CR suppression,
	// no Ctrl-D suppression, no fill characters, no case mapping,
	// no local output processing
	//
	// config.c_oflag &= ~(OCRNL | ONLCR | ONLRET |
	//                     ONOCR | ONOEOT| OFILL | OLCUC | OPOST);
	uart_config.c_oflag = 0;

	//
	// No line processing
	//
	// echo off, echo newline off, canonical mode off,
	// extended input processing off, signal chars off
	//
	uart_config.c_lflag &= ~(ECHO | ECHONL | ICANON | IEXTEN | ISIG);

	// Control modes
	uart_config.c_cflag = 0;

	switch (_bytesize) {
	case ByteSize::FiveBits:  uart_config.c_cflag |= CS5; break;

	case ByteSize::SixBits:   uart_config.c_cflag |= CS6; break;

	case ByteSize::SevenBits: uart_config.c_cflag |= CS7; break;

	case ByteSize::EightBits: uart_config.c_cflag |= CS8; break;
	}

	if (_flowcontrol == FlowControl::Enabled) {
		uart_config.c_cflag |= CRTSCTS;
	}

	if (_parity != Parity::None) {
		uart_config.c_cflag |= PARENB;
	}

	if (_parity == Parity::Odd) {
		uart_config.c_cflag |= PARODD;
	}

	if (_stopbits == StopBits::Two) {
		uart_config.c_cflag |= CSTOPB;
	}

	/* set baud rate */
	if ((termios_state = cfsetispeed(&uart_config, speed)) < 0) {
		PX4_ERR("ERR: %d (cfsetispeed)", termios_state);
		return false;
	}

	if ((termios_state = cfsetospeed(&uart_config, speed)) < 0) {
		PX4_ERR("ERR: %d (cfsetospeed)", termios_state);
		return false;
	}

	if ((termios_state = tcsetattr(_serial_fd, TCSANOW, &uart_config)) < 0) {
		PX4_ERR("ERR: %d (tcsetattr)", termios_state);
		return false;
	}

	return true;
}

bool SerialImpl::open()
{
	if (isOpen()) {
		return true;
	}

	if (!validatePort(_port)) {
		PX4_ERR("Invalid port %s", _port);
		return false;
	}

	// Open the serial port
	int serial_fd = ::open(_port, O_RDWR | O_NOCTTY | O_NONBLOCK);

	if (serial_fd < 0) {
		PX4_ERR("failed to open %s err: %d", _port, errno);
		return false;
	}

	_serial_fd = serial_fd;

	// Configure the serial port
	if (! configure()) {
		PX4_ERR("failed to configure %s err: %d", _port, errno);
		return false;
	}

	_open = true;

	// Do pin operations after port has been opened
	if (_single_wire_mode) {
		setSingleWireMode();
	}

	if (_swap_rx_tx_mode) {
		setSwapRxTxMode();
	}

	setInvertedMode(_inverted_mode);

	return _open;
}

bool SerialImpl::isOpen() const
{
	return _open;
}

bool SerialImpl::close()
{

	if (_serial_fd >= 0) {
		::close(_serial_fd);
	}

	_serial_fd = -1;
	_open = false;

	return true;
}

ssize_t SerialImpl::bytesAvailable()
{
	if (!_open) {
		PX4_ERR("Device not open!");
		errno = EBADF;
		return -1;
	}

	ssize_t bytes_available = 0;
	int ret = ioctl(_serial_fd, FIONREAD, &bytes_available);

	if (ret < 0) {
		return -1;
	}

	return bytes_available;
}

ssize_t SerialImpl::txSpaceAvailable()
{
	if (!_open) {
		PX4_ERR("Device not open!");
		errno = EBADF;
		return -1;
	}

	ssize_t space_available = 0;
	int ret = ioctl(_serial_fd, FIONSPACE, &space_available);

	if (ret < 0) {
		return -1;
	}

	return space_available;
}

ssize_t SerialImpl::read(uint8_t *buffer, size_t buffer_size)
{
	if (!_open) {
		PX4_ERR("Cannot read from serial device until it has been opened");
		return -1;
	}

	int ret = ::read(_serial_fd, buffer, buffer_size);

	if (ret < 0) {
		PX4_DEBUG("%s read error %d", _port, ret);
	}

	return ret;
}

ssize_t SerialImpl::readAtLeast(uint8_t *buffer, size_t buffer_size, size_t character_count, uint32_t timeout_ms)
{
	if (!_open) {
		PX4_ERR("Cannot readAtLeast from serial device until it has been opened");
		return -1;
	}

	if (buffer_size < character_count) {
		PX4_ERR("%s: Buffer not big enough to hold desired amount of read data", __FUNCTION__);
		return -1;
	}

	const hrt_abstime start_time_us = hrt_absolute_time();
	hrt_abstime timeout_us = timeout_ms * 1000;
	int total_bytes_read = 0;

	while ((total_bytes_read < (int) character_count) && (hrt_elapsed_time(&start_time_us) < timeout_us)) {
		// Poll for incoming UART data.
		pollfd fds[1];
		fds[0].fd = _serial_fd;
		fds[0].events = POLLIN;

		hrt_abstime elapsed_time_us = hrt_elapsed_time(&start_time_us);

		if (elapsed_time_us > timeout_us) { break; }

		int ret = poll(fds, sizeof(fds) / sizeof(fds[0]), (timeout_us - elapsed_time_us) / 1000);

		if (ret > 0) {
			if (fds[0].revents & POLLIN) {
				const unsigned sleeptime = character_count * 1000000 / (_baudrate / 10);

				int err = 0;
				int bytes_available = 0;
				err = ::ioctl(_serial_fd, FIONREAD, (unsigned long)&bytes_available);

				if (err != 0 || bytes_available < (int)character_count) {
					px4_usleep(sleeptime);
				}

				ret = read(&buffer[total_bytes_read], buffer_size - total_bytes_read);

				if (ret > 0) {
					total_bytes_read += ret;
				}

			} else {
				PX4_ERR("Got a poll error");
				return -1;
			}
		}
	}

	return total_bytes_read;
}

ssize_t SerialImpl::write(const void *buffer, size_t buffer_size)
{
	if (!_open) {
		PX4_ERR("Cannot write to serial device until it has been opened");
		return -1;
	}

	int written = ::write(_serial_fd, buffer, buffer_size);

	if (written < 0) {
		if (errno != EAGAIN) {
			PX4_ERR("%s write error %d", _port, written);
		}
	}

	return written;
}

ssize_t SerialImpl::writeBlocking(const void *buffer, size_t buffer_size, uint32_t timeout_ms)
{
	if (!_open) {
		PX4_ERR("Cannot writeBlocking to serial device until it has been opened");
		return -1;
	}

	const uint8_t *data = static_cast<const uint8_t *>(buffer);
	size_t total_written = 0;
	const hrt_abstime start_time_us = hrt_absolute_time();
	const hrt_abstime timeout_us = timeout_ms * 1000;

	while (total_written < buffer_size) {
		if (hrt_elapsed_time(&start_time_us) > timeout_us) {
			PX4_WARN("Write timeout, sent %zu", total_written);
			break;
		}

		pollfd fds[1];
		fds[0].fd = _serial_fd;
		fds[0].events = POLLOUT;

		hrt_abstime elapsed_us = hrt_elapsed_time(&start_time_us);
		int remaining_timeout_ms = (timeout_us - elapsed_us) / 1000;

		if (remaining_timeout_ms <= 0) {
			break;
		}

		int result = ::poll(fds, 1, remaining_timeout_ms);

		if (result < 0) {
			PX4_ERR("poll error %d", errno);
			return -1;
		}

		if (fds[0].revents & POLLOUT) {
			// Write as much as we can
			ssize_t written = ::write(_serial_fd, data + total_written, buffer_size - total_written);

			if (written < 0) {
				if (errno == EAGAIN) {
					// Buffer full, wait a bit and try again
					px4_usleep(1000);
					continue;
				}

				PX4_ERR("Write error: %d", errno);
				return -1;

			} else if (written > 0) {
				total_written += written;
			}
		}
	}

	return total_written;
}

void SerialImpl::flush()
{
	if (_open) {
		tcflush(_serial_fd, TCIOFLUSH);
	}
}

const char *SerialImpl::getPort() const
{
	return _port;
}

bool SerialImpl::validatePort(const char *port)
{
	return (port && (access(port, R_OK | W_OK) == 0));
}

bool SerialImpl::setPort(const char *port)
{
	if (_open) {
		PX4_ERR("Cannot set port after port has already been opened");
		return false;
	}

	if (validatePort(port)) {
		strncpy(_port, port, sizeof(_port) - 1);
		_port[sizeof(_port) - 1] = '\0';
		return true;
	}

	return false;
}

uint32_t SerialImpl::getBaudrate() const
{
	return _baudrate;
}

bool SerialImpl::setBaudrate(uint32_t baudrate)
{
	// check if already configured
	if ((baudrate == _baudrate) && _open) {
		return true;
	}

	_baudrate = baudrate;

	// process baud rate change now if port is already open
	if (_open) {
		return configure();
	}

	return true;
}

ByteSize SerialImpl::getBytesize() const
{
	return _bytesize;
}

bool SerialImpl::setBytesize(ByteSize bytesize)
{
	// check if already configured
	if ((bytesize == _bytesize) && _open) {
		return true;
	}

	_bytesize = bytesize;

	// process bytesize change now if port is already open
	if (_open) {
		return configure();
	}

	return true;
}

Parity SerialImpl::getParity() const
{
	return _parity;
}

bool SerialImpl::setParity(Parity parity)
{
	// check if already configured
	if ((parity == _parity) && _open) {
		return true;
	}

	_parity = parity;

	// process parity change now if port is already open
	if (_open) {
		return configure();
	}

	return true;
}

StopBits SerialImpl::getStopbits() const
{
	return _stopbits;
}

bool SerialImpl::setStopbits(StopBits stopbits)
{
	// check if already configured
	if ((stopbits == _stopbits) && _open) {
		return true;
	}

	_stopbits = stopbits;

	// process stopbits change now if port is already open
	if (_open) {
		return configure();
	}

	return true;
}

FlowControl SerialImpl::getFlowcontrol() const
{
	return _flowcontrol;
}

bool SerialImpl::setFlowcontrol(FlowControl flowcontrol)
{
	// check if already configured
	if ((flowcontrol == _flowcontrol) && _open) {
		return true;
	}

	_flowcontrol = flowcontrol;

	// process flowcontrol change now if port is already open
	if (_open) {
		return configure();
	}

	return true;
}

bool SerialImpl::getSingleWireMode() const
{
	return _single_wire_mode;
}

bool SerialImpl::setSingleWireMode()
{
#if defined(TIOCSSINGLEWIRE)

	if (_open) {
		ioctl(_serial_fd, TIOCSSINGLEWIRE, SER_SINGLEWIRE_ENABLED);
	}

	_single_wire_mode = true;
	return true;
#else
	return false;
#endif // TIOCSSINGLEWIRE
}

bool SerialImpl::getSwapRxTxMode() const
{
	return _swap_rx_tx_mode;
}

bool SerialImpl::setSwapRxTxMode()
{
#if defined(TIOCSSWAP)

	if (_open) {
		ioctl(_serial_fd, TIOCSSWAP, SER_SWAP_ENABLED);
	}

	_swap_rx_tx_mode = true;
	return true;
#else
	return false;
#endif // TIOCSSWAP
}

bool SerialImpl::getInvertedMode() const
{
	return _inverted_mode;
}

bool SerialImpl::setInvertedMode(bool enable)
{
#if defined(TIOCSINVERT)

	if (_open) {
		if (enable) {
			ioctl(_serial_fd, TIOCSINVERT, SER_INVERT_ENABLED_RX | SER_INVERT_ENABLED_TX);

		} else {
			ioctl(_serial_fd, TIOCSINVERT, 0);
		}
	}

	_inverted_mode = enable;
	return true;
#else
	return _inverted_mode == enable;
#endif // TIOCSINVERT
}

} // namespace device
