##
# @file   comm_interface.py
# @brief
#
# @copyright
#
#
# @{

import serial
import time
import logging
import threading
import __init__
from __init__ import Singleton
from comm_commands import *
from comm_message import *
from comm_structures import *
from Queue import Queue

READ_DATA_REPEAT = 3  # Number of attempts to receive data
SERIAL_TIMEOUT = 1

INTER_MESSAGE_TIMEOUT = 0.002
QUEUE_EMPTY_TIMEOUT = 0.025
SERIAL_PORT = 'COM10'
SERIAL_BAUD = 57600

COMM_RSP_ACK = 0  # Acknowledge
COMM_RSP_CRC = 1  # Invalid CRC
COMM_RSP_LENGTH = 2  # Invalid data length
COMM_RSP_UNKNOWN = 3  # Unknown command
COMM_RSP_FAIL = 4  # Processing failed


class CommState(Enum):
    '''

    '''
    SOF     = 0  # Awaiting start of frame
    LEN     = 1  # Awaiting length
    CMD     = 2  # Awaiting command
    DATA    = 3  # Awaiting data
    CRC_1   = 4  # Awaiting CRC


class CommunicationError(Exception):
    pass


class CommInterface(object):
    # this makes us a singleton
    __metaclass__ = Singleton


    def __init__(self):
        self.log = logging.getLogger(self.__class__.__name__)
        self.port = None
        self.lock = threading.Lock()
        self.period = 0.15
        self.periodic_commands = []
        self.messageQueue = Queue()


    def connect(self, port=SERIAL_PORT, baud=SERIAL_BAUD, auto_retry=True):
        '''
        Tries to connect to the specified serial port.
        :param port: Port to connect to
        :param baud: Baud rate of communication
        :param auto_retry: How many times should I try it.
        :return:
        '''
        if self.port is not None and self.port.isOpen():
            self.log.info('Serial port already open - skipping opening')
        else:
            self.log.info('Opening serial port')
            retries = READ_DATA_REPEAT if auto_retry else 1
            for i in range(retries):
                try:
                    self.port = serial.Serial(port, baud)
                    self.sending = True
                    self.feeding = True
                    self.feedingThread = threading.Thread(None, self._feed)
                    self.sendingThread = threading.Thread(None, self._transmit)
                    self.sendingThread.start()
                    self.feedingThread.start()
                    break
                except serial.SerialException, ex:
                    if i == retries - 1:  # last attempt throws the exception
                        raise ex
                    else:
                        self.log.debug(ex, exc_info=True)
                        self.log.warning('Connection error. Trying again...')
                        time.sleep(0.1)
            # DTR is used for STM8 reset signal - deactivate it
            self.port.setDTR(False)
            self.port.timeout = SERIAL_TIMEOUT


    def disconnect(self):
        '''
        Closes the opened serial port.
        :return:
        '''
        self.sending = False
        self.feeding = False
        self.log.info('Closing serial port')
        self.port.close()
        self.port = None


    def set_refresh_freq(self, frequency):
        '''
        Sets new frequency of the auto refresh command.
        :param frequency: Desired frequency of the auto refresh command
        :return: None
        '''
        if frequency == 0:
            self.period = float("inf")
        else:
            self.period = 1.0 / frequency


    def get_refresh_freq(self,):
        '''
        Gets the frequency of the auto refresh command.
        :return: The frequency of the auto refresh command .
        '''
        if self.period == float("inf"):
            return 0
        else:
            return 1.0 / self.period

    def command_periodic(self, command, data=None, callback=None):
        cmd = command.value if isinstance(command, Commands) else command
        msg = Message(cmd, data, callback)
        self.periodic_commands.append(msg)


    def command_periodic_detach(self, command, data=None, callback=None):
        cmd = command.value if isinstance(command, Commands) else command
        self.periodic_commands = filter(lambda m: m.cmd != cmd, self.periodic_commands)


    def command(self, command, data=None, callback=None):
        '''
        Sends command via serial port.
        :param group: Group byte of the message
        :param cmd: Command byte of the message
        :param data: Data bytes of the message
        :return: (res, rec_group, rec_cmd, rec_data)
        '''
        cmd = command.value if isinstance(command, Commands) else command
        msg = Message(cmd, data, callback)
        self.messageQueue.put(msg)


    def _feed(self):
        while self.feeding:
            if self.messageQueue.qsize() < 32:
                for msg in self.periodic_commands:
                    self.messageQueue.put(msg)
            time.sleep(self.period)


    def _transmit(self):
        while self.sending:
            self.lock.acquire()
            try:
                if not self.messageQueue.empty():
                    msg = self.messageQueue.get()
                    self.log.debug('Sending: %s', ''.join('%02X ' % b for b in msg.bytes))
                    self.port.write(msg.binary)
                    response = self._wait_for_response()
                    if not (msg.callback is None):
                        msg.callback(response)
            except CommunicationError as e:
                print 'ERROR: {}\n'.format(e.message)
                while not self.messageQueue.empty():
                    self.messageQueue.get()
            finally:
                self.lock.release()
                if self.messageQueue.empty():
                    time.sleep(QUEUE_EMPTY_TIMEOUT)
                else:
                    time.sleep(INTER_MESSAGE_TIMEOUT)


    def _wait_for_response(self):
        state = CommState.SOF
        resp = COMM_RSP_ACK
        length = 0
        data = []
        crc_received = 0
        ok = False
        msg = None
        received_length = 0

        while True:
            ch = self.port.read(1)

            if len(ch) > 0:
                b = ord(ch)

                if CommState.SOF == state:
                    if Message.START == b:
                        state = CommState.SOF  # reset the reception, just for sure
                        data = [b]
                        state = CommState.LEN
                        continue

                data.append(b)

                if CommState.LEN == state:
                    length = b

                    # if the payload length is more than max => reset
                    if length > Message.MAX_LEN:
                        resp = COMM_RSP_LENGTH
                        self.log.debug('Received: %s', ''.join('%02X ' % b for b in data))
                        raise CommunicationError('Message is too large')
                    else:
                        received_length = length;
                        state = CommState.CMD

                elif CommState.CMD == state:
                    if length > 4:
                        state = CommState.DATA
                    else:
                        state = CommState.CRC_1

                elif CommState.DATA == state:
                    if len(data) >= (2 + length - 1):
                        state = CommState.CRC_1

                elif CommState.CRC_1 == state:
                    crc_received = b
                    self.log.debug('Received: %s', ''.join('%02X ' % b for b in data))
                    msg = Message.unpack(data)

                    if crc_received != msg.crc:
                        resp = COMM_RSP_CRC
                        raise CommunicationError(
                            'UART received data body malformed: CRC rx=%04X, cmp=%04X' % (crc_received, msg.crc))
                    ok = True
                    break

            else:
                ok = False
                break

        if ok:
            self.log.debug('CMD = %02X, resp = %d, length = %d, data = %s',
                           msg.cmd, resp, msg.length, ''.join('%02X ' % b for b in msg.data))

            if COMM_RSP_UNKNOWN == resp:
                raise CommunicationError('NACK - unknown instruction')
            elif COMM_RSP_LENGTH == resp:
                raise CommunicationError('NACK - message length not correct')
            elif COMM_RSP_FAIL == resp:
                raise CommunicationError('NACK - instruction execution failed')
            elif COMM_RSP_CRC == resp:
                raise CommunicationError('NACK - CRC does not match')

            return msg
        else:
            resp = COMM_RSP_UNKNOWN
            raise CommunicationError('Response timed out')


if __name__ == '__main__':
    ci = CommInterface()

    ci.connect()

    # ~ ci.write(SystemLogMask(output=SystemLogMask.LOG_OUTPUT_SWO, severity_p1p2=SystemLogMask.LOG_SEVERITY_INFO))
    # ~ status = ci.read(SystemLogMask(output=SystemLogMask.LOG_OUTPUT_SWO))
    # ~ print status
    # ~ time.sleep(0.1)

    print ci.read(Inputs)

    ci.disconnect()

# @}
