import logging
from logging import debug, info, warning, error, critical
import serial
from serial.tools import list_ports
import time
import threading
from collections.abc import Iterable
from motty.config import Config


class HistoryObserver(object):
    def __init__(self):
        pass

    def onNewHistoryEntry(self, entry, history_index):
        pass

    def onUpdateHistoryEntry(self, entry, history_index, update_start_index):
        pass

    def onRemoveOldHistory(self, history_index, count):
        pass

    def notify(self, event_name, entry=None, history_index=None, update_start_index=0, count=0):
        if event_name == "new":
            self.onNewHistoryEntry(entry, history_index)
        elif event_name == "update":
            self.onUpdateHistoryEntry(entry, history_index, update_start_index)
        elif event_name == "pop":
            self.onRemoveOldHistory(history_index, count)



class HistoryEntry(object):
    """Holds a timed data stream record"""

    def __init__(self, direction, port, ts=[], data=b""):
        """:param direction: "rx" or "tx" for received or transmitted data
        :param port: system name of the port, data was received / transmitted
        :param ts: list of timestamp touples (index of corresponding byte, ts)
        :param data: bytes string"""

        self.direction = direction
        self.port = port
        self.ts = ts
        self.data = data

    def is_terminated(self, termbytes, timeout):
        if (termbytes == b""):
            return True
        if (termbytes != None) and (self.data[-len(termbytes):] == termbytes):
            return True
        else:
            # termbytes deactivated
            # or no termbytes at end of data
            # => check for timeout which would terminate as well:
            if (timeout != None) and (timeout >= 0):
                now = time.time()
                if (now - self.endtime()) > timeout:
                    return True
        return False

    def update(self, ts, data):
        offset = len(self.data)
        self.ts += [(i+offset, s) for i, s in ts]
        self.data += data

    def starttime(self):
        """returns timestamp for the timestamped byte with the lowest index"""
        # assuming the first timestamp is the earliest:
        return self.ts[0][1]

    def endtime(self):
        """returns latest timestamp"""
        # assuming the last timestamp is the latest:
        return self.ts[-1][1]


class History(object):
    """class used by the Communication class to hold received and send data"""
    def __init__(self, maxentries=None, terminator=None, timeout=None, observers=[]):
        self.maxentries = maxentries
        self.terminator = terminator
        self.timeout = timeout
        assert(isinstance(observers, Iterable))
        self.observers = observers
        self.entries = []

    def registerObserver(self, observer):
        self.observers.append(observer)

    def notifyObservers(self, event_name, **kwargs):
        for observer in self.observers:
            observer.notify(event_name, **kwargs)

    def find(self, port, direction):
        i = len(self.entries)
        while i > 0:
            i -= 1;
            entry = self.entries[i]
            if entry.port == port and entry.direction == direction:
                return (i, entry)
        return (0, None)

    def update(self, entry):
        """append a new entry to the last existing one for the same port and
        direction, if that entry was not terminated"""

        i, match = self.find(entry.port, entry.direction)
        if match and not match.is_terminated(self.terminator, self.timeout):
            start = len(entry.data)
            match.update(entry.ts, entry.data)
            self.notifyObservers("update", entry=match, history_index=i, update_start_index=start)
        else:
            self.entries.append(entry)
            i = len(self.entries) - 1
            self.notifyObservers("new", entry=entry, history_index=i)

        if self.entries[i].is_terminated(self.terminator, self.timeout):
            self.entries.pop(i)
            self.notifyObservers("pop",history_index=i, count=1)



class Port(serial.Serial):
    """Extends the pySerial.Serial class with some attributes to maintain
    multiple open ports"""

    def __init__(self, port, description="", rx=False, tx=False, **kwargs):
        serial.Serial.__init__(self, port=port, **kwargs)
        object.__setattr__(self, 'rx', rx)
        object.__setattr__(self, 'tx', tx)
        self.description = description or port
        self.update_state()

    def __setattr__(self, name, value):
        serial.Serial.__setattr__(self, name, value)
        if name in ("rx", "tx"):
            self.update_state()

    def update_state(self):
        if self.rx or self.tx:
            self.open()
        else:
            self.close()

    def open(self):
        if not self.isOpen():
            serial.Serial.open(self)

    def close(self):
        if self.isOpen():
            serial.Serial.flush(self)
            serial.Serial.close(self)

    def send(self, data):
        self.write(data)


class Communication(threading.Thread):
    """manage communication with multiple serial ports in a seperate thread"""
    def __init__(self, history):
        threading.Thread.__init__(self)
        cfg = Config()
        self.ports = {}
        for port, desc, _unused01 in list_ports.comports():
            if cfg.ports[port]:
                desc = cfg.ports[port]['description'] or desc
                baudrate = cfg.ports[port]['baud'] or 9600
                listen = cfg.ports[port]['listen']
                listen = True if listen == None else listen
            try:
                self.ports[port] = Port(port, desc, rx=listen, tx=True, baudrate=baudrate)
            except serial.SerialException as emsg:
                warning("Unable to open port {}. {}".format(port,emsg))
        assert(isinstance(history, History))
        self.history = history

    def run(self):
        while (self.alive):
            for port in iter(self.ports.values()):
                if port.isOpen(): # TODO and port.rx:
                    ts = (0, time.time())
                    try:
                        data = port.read(port.inWaiting())
                        if len(data) > 0:
                            debug("[{}]<={} ".format(port.name,len(data)))
                    except serial.SerialException as e:
                        debug("E[{}]: {}".format(port.name,e))
                        data = b""
                    if len(data) > 0:
                        self.history.update(HistoryEntry("rx", port.name, [ts], data))

        for port in iter(self.ports.values()):
            port.close()

    def start(self):
        self.alive = True
        threading.Thread.start(self)

    def join(self, timeout=None):
        self.alive = False
        threading.Thread.join(self, timeout)

    def send(self, port_name: str, data: bytes):
        """Send data on the serial port with name port_name
        :param port_name: Name of the serial port given by the OS
        :param data: bytes string to send
        :note: Not Thread-safe. Calling only from one other thread
               is safe because Communication.send() is blocking
        """
        if port_name and data:
            ts = (0, time.time())
            num = self.ports[port_name].write(data)
            self.history.update(HistoryEntry("tx", port_name, [ts], data))
        else:
            debug("Did not send anything (port: '{}', data: '{}')".format(port_name, data))


