// © Copyright The University of New South Wales 2018
//
// 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//
//
// adapted from firmata_test project
// https://github.com/firmata/firmata_test
// Copyright 2010, Paul Stoffregen (paul@pjrc.com)
// with permission to redistribute and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.


#include "serial.h"

#include <sys/ioctl.h>                  // required for: ioctl function
#include <sys/_select.h>                // required for: select function
#include <IOKit/serial/IOSerialKeys.h>  // required for: kIOCalloutDeviceKey, kCFAllocatorDefault consts

// Internals
void serial_inputdiscard(serial_t *port);
void serial_outputflush(serial_t *port);
int  serial_setcontrol(serial_t *port, int dtr, int rts);
int  macos_ports(io_iterator_t *PortIterator, char **list, int max_ports, int index);


int macos_ports(io_iterator_t *PortIterator, char **list, int max_ports, int index)
{
    io_object_t modemService;
    CFTypeRef nameCFstring;
    char s[MAXPATHLEN];
    int numPorts = index;
    
    if( ! PortIterator || ! list) return -1;
    
    
    while ((modemService = IOIteratorNext(*PortIterator)) && numPorts < max_ports) {
        nameCFstring = IORegistryEntryCreateCFProperty(modemService,
                                                       CFSTR(kIOCalloutDeviceKey), kCFAllocatorDefault, 0);
        if (nameCFstring) {
            if (CFStringGetCString((const struct __CFString *)nameCFstring,
                                   s, sizeof(s), kCFStringEncodingASCII)) {
                list[numPorts] = (char*) malloc((MAXPATHLEN) * sizeof(char));
                strcpy(list[numPorts], s);
                numPorts++;
            }
            CFRelease(nameCFstring);
        }
        IOObjectRelease(modemService);
    }
    return numPorts;
}

// Return a list of all serial ports
int serial_portlist(char **list, int max_ports)
{
    int numPorts = 0;
    
    // adapted from SerialPortSample.c, by Apple
    // http://developer.apple.com/samplecode/SerialPortSample/listing2.html
    // and also testserial.c, by Keyspan
    // http://www.keyspan.com/downloads-files/developer/macosx/KesypanTestSerial.c
    // www.rxtx.org, src/SerialImp.c seems to be based on Keyspan's testserial.c
    // neither keyspan nor rxtx properly release memory allocated.
    // more documentation at:
    // http://developer.apple.com/documentation/DeviceDrivers/Conceptual/WorkingWSerial/WWSerial_SerialDevs/chapter_2_section_6.html
    
    mach_port_t masterPort;
    CFMutableDictionaryRef classesToMatch;
    io_iterator_t serialPortIterator;
    
    if (IOMasterPort(0, &masterPort) != KERN_SUCCESS) return 0;
    
    // a usb-serial adaptor is usually considered a "modem",
    // especially when it implements the CDC class spec
    classesToMatch = IOServiceMatching(kIOSerialBSDServiceValue);
    if (!classesToMatch) return 0;
    CFDictionarySetValue(classesToMatch, CFSTR(kIOSerialBSDTypeKey),
                         CFSTR(kIOSerialBSDModemType));
    if (IOServiceGetMatchingServices(masterPort, classesToMatch,
                                     &serialPortIterator) != KERN_SUCCESS) return 0;
    numPorts += macos_ports(&serialPortIterator, list, max_ports, numPorts);
    IOObjectRelease(serialPortIterator);

    // but it might be considered a "rs232 port", so repeat this
    // search for rs232 ports
    classesToMatch = IOServiceMatching(kIOSerialBSDServiceValue);
    if (!classesToMatch) return 0;
    CFDictionarySetValue(classesToMatch, CFSTR(kIOSerialBSDTypeKey),
                         CFSTR(kIOSerialBSDRS232Type));
    if (IOServiceGetMatchingServices(masterPort, classesToMatch,
                                     &serialPortIterator) != KERN_SUCCESS) return 0;
    numPorts += macos_ports(&serialPortIterator, list, max_ports, numPorts);
    IOObjectRelease(serialPortIterator);
    
    printf("serial_portlist: %d\n", numPorts);
    return numPorts;
}

int serial_isopen(serial_t *port)
{
    if(port == NULL) return 0;
    return port->port_is_open;
}

// Open a port, by name.  Return 0 on success, non-zero for error
int serial_open(serial_t *port, char *name)
{
    if(port == NULL) return -1;    
    
    if (name == NULL) {
        sprintf(port->error_msg, "serial_open: No port name provided");
        return -1;
    }
    
    serial_close(port);
    
    int bits = 0;
    int port_fd = open(name, O_RDWR | O_NOCTTY | O_NONBLOCK);
    if (port_fd < 0) {
        sprintf(port->error_msg, "serial_open: Unable to open %s, %s\n", name, strerror(errno));
        //printf("Unable to open %s, %s\n", name, strerror(errno));
        return -1;
    }
    if (ioctl(port_fd, TIOCEXCL) == -1) {
        close(port_fd);
        sprintf(port->error_msg, "serial_open: Unable to get exclussive access to port %s\n", name);
        //printf("Unable to get exclussive access to port %s\n", name);
        return -1;
    }
    
    if (ioctl(port_fd, TIOCMGET, &bits) < 0) {
        close(port_fd);
        sprintf(port->error_msg, "serial_open: Unable to query serial port signals on %s\n", name);
        //printf("Unable to query serial port signals on %s\n", name);
        return -1;
    }
    
    bits &= ~(TIOCM_DTR | TIOCM_RTS);
    if (ioctl(port_fd, TIOCMSET, &bits) < 0) {
        close(port_fd);
        sprintf(port->error_msg, "serial_open: Unable to control serial port signals on %s\n", name);
        //printf("Unable to control serial port signals on %s\n", name);
        return -1;
    }
    if (tcgetattr(port_fd, &(port->settings_orig)) < 0) {
        close(port_fd);
        sprintf(port->error_msg, "serial_open: Unable to access baud rate on port %s\n", name);
        //printf("Unable to access baud rate on port %s\n", name);
        return -1;
    }
    
    port->port_fd = port_fd;
    memset(&(port->settings), 0, sizeof(port->settings));
    port->settings.c_cflag = CS8 | CLOCAL | CREAD | HUPCL;
    port->settings.c_iflag = IGNBRK | IGNPAR;
    serial_setbaud(port, port->baud_rate);
    tcflush(port->port_fd, TCIFLUSH);    

    strncpy(port->port_name, name, MAXPATHLEN);
    port->port_is_open = 1;

    sprintf(port->error_msg, "");
    return 0;
}

const char *serial_getname(serial_t *port)
{
    if ( ! port || ! port->port_is_open) return "";
    return port->port_name;
}

// set the baud rate
int serial_setbaud(serial_t *port, int baud)
{
    if (port == NULL || baud <= 0) return -1;
    if (!port->port_is_open) return -1;
   
    if (-1 == cfsetospeed(&(port->settings), (speed_t)baud)) {
        sprintf(port->error_msg, "serial_setbaud: Couldn't set BAUD rate");
        return -1;
    }
    if (-1 == cfsetispeed(&(port->settings), (speed_t)baud)) {
        sprintf(port->error_msg, "serial_setbaud: Couldn't set BAUD rate");
        return -1;
    }
    
    if (-1 == tcsetattr(port->port_fd, TCSANOW, &(port->settings))) {
        sprintf(port->error_msg, "serial_setbaud: Couldn't set BAUD rate");
        return -1;
    }

    port->baud_rate = baud;
    sprintf(port->error_msg, "");
    return 0;
}

// Read from the serial port.  Returns only the bytes that are
// already received, up to count.  This always returns without delay,
// returning 0 if nothing has been received
long serial_read(serial_t *port, void *ptr, int count)
{
    if (port == NULL || !port->port_is_open) return -1;
    if (count <= 0 || ptr == NULL) return 0;
    
    long n;
    n = read(port->port_fd, ptr, count);
    //printf("serial_read %ld bytes\n", n);
    if (n < 0 && (errno == EAGAIN || errno == EINTR)) return 0;
    return n;
}

// Write to the serial port.  This blocks until the data is
// sent (or in a buffer to be sent).  All bytes are written,
// unless there is some sort of error.
long serial_write(serial_t *port, const void *ptr, int len)
{
    //printf("Write %d\n", len);
    if (port == NULL || !port->port_is_open) return -1;
    if (len == 0 || ptr == NULL) return 0;
    
    long n, written=0;
    fd_set wfds;
    struct timeval tv;
    while (written < len) {
        n = write(port->port_fd, (const char *)ptr + written, len - written);
        if (n < 0 && (errno == EAGAIN || errno == EINTR)) n = 0;
        //printf("Write, n = %d\n", n);
        if (n < 0) return -1;
        if (n > 0) {
            written += n;
        } else {
            tv.tv_sec = 10;
            tv.tv_usec = 0;
            FD_ZERO(&wfds);
            FD_SET(port->port_fd, &wfds);
            n = select(port->port_fd+1, NULL, &wfds, NULL, &tv);
            if (n < 0 && errno == EINTR) n = 1;
            if (n <= 0) return -1;
        }
    }
    return written;
}

// Wait up to msec for data to become available for reading.
// return 0 if timeout, or non-zero if one or more bytes are
// received and can be read.  -1 if an error occurs
int serial_inputwait(serial_t *port, int msec)
{
    if (port == NULL || !port->port_is_open) return -1;
    
    fd_set rfds;
    struct timeval tv;
    tv.tv_sec = msec / 1000;
    tv.tv_usec = (msec % 1000) * 1000;
    FD_ZERO(&rfds);
    FD_SET(port->port_fd, &rfds);
    return select(port->port_fd+1, &rfds, NULL, NULL, &tv);
}

// Discard all received data that hasn't been read
void serial_inputdiscard(serial_t *port)
{
    if (port == NULL || !port->port_is_open) return;
    tcflush(port->port_fd, TCIFLUSH);
}

// Wait for all transmitted data with Write to actually leave the serial port
void serial_outputflush(serial_t *port)
{
    if (port == NULL || !port->port_is_open) return;
    tcdrain(port->port_fd);
}

// set DTR and RTS,  0 = low, 1=high, -1=unchanged
int serial_setcontrol(serial_t *port, int dtr, int rts)
{
    if (port == NULL || !port->port_is_open) return -1;
    
    // on Mac OS X, "man 4 tty"
    int bits;
    if (ioctl(port->port_fd, TIOCMGET, &bits) < 0) return -1;
    if (dtr == 1) {
        bits |= TIOCM_DTR;
    } else if (dtr == 0) {
        bits &= ~TIOCM_DTR;
    }
    if (rts == 1) {
        bits |= TIOCM_RTS;
    } else if (rts == 0) {
        bits &= ~TIOCM_RTS;
    }
    if (ioctl(port->port_fd, TIOCMSET, &bits) < 0) return -1;;
    
    return 0;
}

// Close the port
void serial_close(serial_t *port)
{
    if ( ! port || !port->port_is_open) return;
    
    serial_outputflush(port);
    serial_inputdiscard(port);
    tcsetattr(port->port_fd, TCSANOW, &(port->settings_orig));
    close(port->port_fd);
    
    port->port_is_open = 0;
    port->baud_rate = 0;
    sprintf(port->port_name, "");
    sprintf(port->error_msg, "");
    port->port_fd = 0;
}




















