// HardwareSerial.cpp
//
// Copyright (C) 2015 Mike McCauley
// $Id: HardwareSerial.cpp,v 1.3 2015/08/13 02:45:47 mikem Exp mikem $

#include <RadioHead.h>
#if (RH_PLATFORM == RH_PLATFORM_UNIX)

#include <HardwareSerial.h>

#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <sys/select.h>

HardwareSerial::HardwareSerial(const char* deviceName)
    : _deviceName(deviceName),
      _device(-1)
{
    // Override device name from environment
    char* e = getenv("RH_HARDWARESERIAL_DEVICE_NAME");
    if (e)
	_deviceName = e;
}

void HardwareSerial::begin(int baud)
{
    if (openDevice())
	setBaud(baud);
}

void HardwareSerial::end()
{
    closeDevice();
}

void HardwareSerial::flush()
{
    tcdrain(_device);
}

int HardwareSerial::peek(void)
{
    printf("HardwareSerial::peek not implemented\n");
    return 0;
}

int HardwareSerial::available()
{
    int bytes;

    if (ioctl(_device, FIONREAD, &bytes) != 0)
    {
	fprintf(stderr, "HardwareSerial::available ioctl failed: %s\n", strerror(errno));
	return 0;
    }
    return bytes;
}

int HardwareSerial::read()
{
    uint8_t data;
    ssize_t result = ::read(_device, &data, 1);
    if (result != 1)
    {
	fprintf(stderr, "HardwareSerial::read read failed: %s\n", strerror(errno));
	return 0;
    }
//    printf("got: %02x\n", data);
    return data;
}

size_t HardwareSerial::write(uint8_t ch)
{
    size_t result = ::write(_device, &ch, 1);
    if (result != 1)
    {
	fprintf(stderr, "HardwareSerial::write failed: %s\n", strerror(errno));
	return 0;
    }
//    printf("sent: %02x\n", ch);
    return 1; // OK
}

bool HardwareSerial::openDevice()
{
    if (_device == -1)
	closeDevice();
    _device = open(_deviceName, O_RDWR | O_NOCTTY | O_NDELAY);
    if (_device == -1)
    {
	// Could not open the port.
	fprintf(stderr, "HardwareSerial::openDevice could not open %s: %s\n", _deviceName, strerror(errno));
	return false;
    }

    // Device opened
    fcntl(_device, F_SETFL, 0);
    return true;
}

bool HardwareSerial::closeDevice()
{
    if (_device != -1)
	close(_device);
    _device = -1;
    return true;
}

bool HardwareSerial::setBaud(int baud)
{
    speed_t speed;

    // This is kind of ugly, but its prob better than a case
    if (baud == 50)
	speed = B50;
    else if (baud == 75)
	speed = B75;
    else if (baud == 110)
	speed = B110;
    else if (baud == 134)
	speed = B134;
    else if (baud == 150)
	speed = B150;
    else if (baud == 200)
	speed = B200;
    else if (baud == 300)
	speed = B300;
    else if (baud == 600)
	speed = B600;
    else if (baud == 1200)
	speed = B1200;
    else if (baud == 1800)
	speed = B1800;
    else if (baud == 2400)
	speed = B2400;
    else if (baud == 4800)
	speed = B4800;
    else if (baud == 9600)
	speed = B9600;
    else if (baud == 19200)
	speed = B19200;
    else if (baud == 38400)
	speed = B38400;
    else if (baud == 57600)
	speed = B57600;
#ifdef B76800
    else if (baud == 76800)  // Not available on Linux
	speed = B76800;
#endif
    else if (baud == 115200)
	speed = B115200;
    else if (baud == 230400)
	speed = B230400;
#ifdef B460800
    else if (baud == 460800) // Not available on OSX
	speed = B460800;
#endif
#ifdef B921600
    else if (baud == 921600) // Not available on OSX
	speed = B921600;
#endif
    else 
    {
	fprintf(stderr, "HardwareSerial::setBaud: unsupported baud rate %d\n", baud);
	return false;
    }

    struct termios options;
    // Get current options
    if (tcgetattr(_device, &options) != 0)
    {
	fprintf(stderr, "HardwareSerial::setBaud: could not tcgetattr %s\n", strerror(errno));
	return false;
    }

    // Set new speed options
    cfsetispeed(&options, speed);
    cfsetospeed(&options, speed);
    // Enable the receiver and set local mode...
    options.c_cflag |= (CLOCAL | CREAD);

    // Force mode to 8,N,1
    // to be compatible with Arduino HardwareSerial
    // Should this be configurable? Prob not, must have 8 bits, dont need parity.
    options.c_cflag &= ~(PARENB | CSTOPB | CSIZE);
    options.c_cflag |= CS8;
   
    // Disable flow control and input character conversions
    options.c_iflag &= ~(IXON | IXOFF | IXANY | ICRNL | INLCR);

    // Raw input:
    options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);

    // Raw output
    options.c_oflag &= ~(OPOST | OCRNL | ONLCR);

    // Set the options in the port
    if (tcsetattr(_device, TCSANOW, &options) != 0)
    {
	fprintf(stderr, "HardwareSerial::setBaud: could not tcsetattr %s\n", strerror(errno));
	return false;
    }

    _baud = baud;
    return true;
}

// Block until something is available
void HardwareSerial::waitAvailable()
{
    waitAvailableTimeout(0); // 0 = Wait forever
}

// Block until something is available or timeout expires
bool HardwareSerial::waitAvailableTimeout(uint16_t timeout)
{
    int            max_fd;
    fd_set         input;
    int            result;

    FD_ZERO(&input);
    FD_SET(_device, &input);
    max_fd = _device + 1;

    if (timeout)
    {
	struct timeval timer;
	// Timeout is in milliseconds
	timer.tv_sec  = timeout / 1000;
	timer.tv_usec = (timeout % 1000) * 1000;
	result = select(max_fd, &input, NULL, NULL, &timer);
    }
    else
    {
	result = select(max_fd, &input, NULL, NULL, NULL);
    }
    if (result < 0)
	fprintf(stderr, "HardwareSerial::waitAvailableTimeout: select failed %s\n", strerror(errno));
    return result > 0;
}

#endif
