"""
Connects to and handles the serial port.

This module consists of the core serial functionality.
This can handle reading and writing to a serial.

| author     = "Seth Miers"
| copyright  = "Copyright, Xilinx Inc. All rights reserved."
| credits    = ["Seth Miers"]
| license    = "GPL"
| version    = "3.0"
| maintainer = "Seth Miers"
| email      = "sethm@xilinx.com"
| status     = "Development"

"""

##########################################
#               Imports                  #
##########################################


# --------------Packages---------------- #
try:
    import queue
except ImportError:
    import Queue as queue
import re
import threading
import time
import glob
import os
import sys
import serial
from serial.tools import list_ports
from serial.tools import list_ports_common
from serial.tools import list_ports_linux

# --------------Local------------------- #


##########################################
#              Main Code                 #
##########################################

class SerialComm():
    '''
    Handle a single connection to a serial port.
    It can determine between a readline with a timeout,
    a readline with a linelength cap, and a readline
    with a timeout. This makes it so data is always being
    returned even if no carriage return or new line is
    sent through the serial port.

    Args:
        logger (logging): The logger that should be used.
        timeout (int): How long the serial port should block
            before returning a timeout
        port_regex (string): The regular expression
            used to connect to a particular com/tty serial port
        baud (int): The baud rate of this serial connection.
        stopbits (int): How many stop bits this serial
            connection should use.

    Attributes:
        port (ListPortInfo): The specific port that this connection is
            attached to
        inqueue (queue): The receive queue from the serial. I.e. What
            has been received fromt he serial port.
        outqueue (queue): What should be sent to the serial port.
        returned_early (bool): If the last readline method
            returned early then this will be True
        last_byte (string): The last byte pulled from the inqueue
            by the readline method
        reader_thread (threading): The reader thread
        writer_thread (threading): The writer thread

    Todo:
        * Reduce the number of attributes used
    '''
    # pylint: disable=too-many-instance-attributes
    # 14 may be too many but it works for now
    def __init__(self, logger, timeout,
                 port_regex="", baud=115200,
                 stopbits=serial.STOPBITS_ONE):
        # pylint: disable=too-many-arguments
        # Six arguments is reasonalbe for serial ports
        self.logger = logger
        self.timeout = timeout
        self.port_regex = port_regex
        self.baud = baud
        self.stopbits = stopbits
        self.stop = False
        self.port = None
        self.connection = None
        self.inqueue = queue.Queue()
        self.outqueue = queue.Queue()
        self.returned_early = False
        self.last_byte = ''
        self.reader_thread = None
        self.writer_thread = None
        self.logger.debug("Created SerialComm class")

    @staticmethod
    def find_port(logger, port_regex):
        '''
        Use the port_regex to figure out which port
        that this class should connect to.

        Args:
            logger (logging): The logger that this
                method will record to
            port_regex (string): The regular expression
                that will be used to match a particular port

        Attributes:
            port (comport): The particular comport that
                matched the regular expression
            ports (list): All of the available ports that
                this class could connect to.
            search_ports (list): All of the available ports
                that this class could connect to formatted
                in a human readable format.

        Todo:
            * Change the return type to be more suited for this method
              or change the classes that use this class to actually
              handle the return properly.
        '''
        port = None
        if os.name == 'posix':
            plat = sys.platform.lower()
            if plat[:5] == 'linux':
                ports = list(comports())
        else:
            ports = list(list_ports.comports())
        search_ports = []

        # Format the ports into a human readble format
        for port in ports:
            search_ports.append("D:[" + str(port.device) +
                                "]N:[" + str(port.name) +
                                "]I:[" + str(port.interface) +
                                "]H:[" + str(port.hwid) +
                                "]V:[" + str(port.vid) +
                                "]P:[" + str(port.pid) +
                                "]S:[" + str(port.serial_number) +
                                "]L:[" + str(port.location) +
                                "]P:[" + str(port.product) +
                                "]M:[" + str(port.manufacturer) +
                                "]D:[" + str(port.description) + "]")
        logger.debug("Found serial ports:")
        logger.debug(search_ports)
        logger.debug("Searching for serial port with regex: " +
                     port_regex)

        # Match the port_regex to the search_ports
        for i, search_port in enumerate(search_ports):
            if re.search(port_regex, str(search_port)):
                port = ports[i]

        if port is None:
            logger.warning("Did not find a serial port to connect to")
            return None

        logger.debug("Found serial port with regex: " +
                     str(port))
        return port

    def connect_port(self):
        '''
        After a port has been selected (port is not None) then
        we can establish a connection to that port.

        Todo:
            * Change the return type or make sure
              that classes using this class are actually
              using the return
            * The connection timeout should be changeable
        '''
        try:
            self.connection = serial.Serial()
            self.connection.port = self.port.device
            self.logger.debug("Connecting to serial port: " +
                              str(self.port.device))
            self.connection.baudrate = self.baud
            self.logger.debug("Baud rate: " + str(self.baud))
            self.connection.timeout = 1
            self.connection.stopbits = self.stopbits
            self.logger.debug("Stopbits: " + str(self.stopbits))
            self.connection.open()
            self.connection.reset_input_buffer()
            self.connection.reset_output_buffer()
            self.connection.flush()
        except (OSError, serial.SerialException):
            self.logger.exception("Failed to connect to serial port")
            self.stop = True
            self.connection = None
            return False
        self.logger.debug("Connected to serial port: " + str(self.port.device))
        return True

    def start_serial(self):
        '''
        Go through all the motions to start a serial conneciton.
        This finds the port, connects to the port, and starts
        the reader and writer threads necessary to communicate
        with the serial serial connection.

        Todo:
            * Should this have a return?
        '''
        self.port = self.find_port(self.logger, self.port_regex)
        self.connect_port()

        self.reader_thread = threading.Thread(target=self.reader,
                                              name='serial_rx')
        self.reader_thread.setDaemon(True)
        self.reader_thread.start()
        self.writer_thread = threading.Thread(target=self.writer,
                                              name='serial_tx')
        self.writer_thread.setDaemon(True)
        self.writer_thread.start()

    def add_fake_input(self, fake_input):
        '''
        Adds fake input to the serial port. Good if you want
        to force the program to do something.

        Args:
            fake_input (string): Used to create some kind of false
                traffic over the serial connection. Good if you
                need to force a test to do something.
        '''
        for char in fake_input:
            self.inqueue.put(char.encode('utf-8'))

        return True

    def read(self, return_time):
        '''
        Read a single character out of the queue and return it

        Args:
            return_time (int): If there are no characters
                read from the serial port for this amount of time
                this method will return everything in the
                buffer so far and tab over the next readline

        Attributes:
            inqueue_pull (char): The character pulled from the
                reader queue.
        '''
        try:
            inqueue_pull = self.inqueue.get(timeout=return_time)\
                           .decode('utf-8')
        except queue.Empty:
            return ''
        return inqueue_pull

    def readline(self, return_time, character_limit):
        '''
        Read a single line out of the read function and
        return it.

        Args:
            return_time (int): If there are no characters
                read from the serial port for this amount of time
                this method will return everything in the
                buffer so far and tab over the next readline
            character_limit (int): If this many characters
                or more are in the buffer then the method
                will return everything in the buffer
                so far and tab over the next readline.

        Attributes:
            buff_in (string): A temporary object that
                contains the line that was read.
            time_start (int): The seconds since epoch. This
                variable is used to determine a timeout condition.
            char (byte): A single byte pulled from
                the serial buffer.

        Returns:
            string: The line that was read

        Todo:
            * More elegant tabbing over of line continuations.
              Perhaps by passing in a 'tab' character, and something
              that will work when it overflows 80 characters too.
        '''
        buff_in = ""
        time_start = time.time()
        if self.returned_early:
            buff_in = "     "
            self.returned_early = False
        while not self.stop:
            char = self.read(self.timeout)
            if char == '\n':
                if self.last_byte != '\r':
                    self.last_byte = char
                    return buff_in
                self.last_byte = ''
            elif char == '\r':
                if self.last_byte != '\n':
                    self.last_byte = char
                    return buff_in
                self.last_byte = ''
            elif char == '':
                self.last_byte = char
                if time.time()-time_start > self.timeout:
                    return None
                if buff_in and \
                        (buff_in != "     ") and \
                        (time.time()-time_start > return_time):
                    self.returned_early = True
                    return buff_in
            elif len(buff_in) >= character_limit-1:
                self.last_byte = char
                buff_in += char
                self.returned_early = True
                return buff_in
            else:
                self.last_byte = char
                buff_in += char

    def stop_serial(self):
        '''
        Set the stop variable to true so that all the running
        threads will safely exit. Then wait for each of the
        threads to join back with the main thread.
        '''
        self.stop = True
        self.logger.debug("Waiting for reader thread to join...")
        self.reader_thread.join()
        self.logger.debug("Waiting for writer thread to join...")
        self.writer_thread.join()
        self.logger.debug("Closing connection...")
        self.connection.close()
        self.logger.debug("Stopped successfully.")

    def set_timeout(self, timeout):
        '''
        in case the timeout needs to be changed

        Args:
            timeout (int): the new timeout value
                for the serial port
        '''
        self.timeout = timeout

    def reader(self):
        '''
        Reads from the serial connection and then
        places that data into a queue.

        Attributes:
            char_in (byte): A single byte that was read in
                by the serial port

        Todo:
            * Should this have a proper return?
        '''
        while not self.stop:
            try:
                while not self.stop:
                    char_in = self.connection.read(1)
                    self.inqueue.put(char_in)
            except serial.SerialTimeoutException:
                self.logger.error("Failed to read")
            except serial.SerialException:
                self.logger.error("Device reports readiness " +
                                  "to read but returned no data")

    def writer(self):
        '''
        Writes to the serial connection by pulling
        data from the outqueue

        Attributes:
            buff_out (string): A temporary location
                to store the data about to be written
        '''
        while not self.stop:
            try:
                buff_out = self.outqueue.get(timeout=0.1)
                print(repr(buff_out))
                self.logger.debug("Writing \"" + buff_out +
                                  "\" to " + self.port.device)
                if buff_out:
                    self.connection.write(buff_out.encode('utf-8'))
            except queue.Empty:
                pass
            finally:
                buff_out = None
        return True


def comports(include_links=False):
    '''
    This is simply a copy of the comports function
    in serial.tools.list_ports_linux. However, this
    function removes the platform restriction and
    adds '/dev/ttyPS*' to support Xilinx serial
    devices that are built into the processor.

    Args:
        include_links (bool): Should the function
            add in the symbolic links on the system
            and search through those as well?

    Attributes:
        devices (list): A list of all the devices
            that pyserial will look for serial
            ports on.

    Returns:
        list: A list of available serial ports

    Todo:
        * I don't think over-riding the pyserial
          function is the best way to do this. What
          if the pyserial library changes, then this
          might not work anymore.
    '''
    # built-in serial ports
    devices = glob.glob('/dev/ttyS*')
    # built-in serial ports
    devices.extend(glob.glob('/dev/ttyPS*'))
    # usb-serial with own driver
    devices.extend(glob.glob('/dev/ttyUSB*'))
    # usb-serial with CDC-ACM profile
    devices.extend(glob.glob('/dev/ttyACM*'))
    # ARM internal port (raspi)
    devices.extend(glob.glob('/dev/ttyAMA*'))
    # BT serial devices
    devices.extend(glob.glob('/dev/rfcomm*'))
    # Advantech multi-port serial controllers
    devices.extend(glob.glob('/dev/ttyAP*'))
    if include_links:
        devices.extend(list_ports_common.list_links(devices))
    return list([list_ports_linux.SysFS(d) for d in devices])
