#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
"""
stringtools.stp (Serial transfer protocol)
==========================================

**Author:**

* Dirk Alders <sudo-dirk@mount-mockery.de>

**Description:**

    This module is a submodule of :mod:`stringtools` and creates an serial frame to transmit and receive messages via an serial interface.

**Submodules:**

* :class:`stringtools.stp.stp`
* :func:`stringtools.stp.build_frame`
"""

import stringtools

import logging
import sys

try:
    from config import APP_NAME as ROOT_LOGGER_NAME
except ImportError:
    ROOT_LOGGER_NAME = 'root'
logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)

DATA_SYNC = b'\x3a'
"""The data sync byte"""
DATA_CLEAR_BUFFER = b'\x3c'
"""The clear buffer byte ('\\\\x3a\\\\x3c' -> start of message)"""
DATA_VALID_MSG = b'\x3e'
"""The valid message byte ('\\\\x3a\\\\x3e' -> end of message)"""
DATA_STORE_SYNC_VALUE = b'\x3d'
"""The store sync value byte ('\\\\x3a\\\\x3d' -> '\\\\x3a' inside a message)"""

STP_STATE_IDLE = 0x00
"""Idle state definition (default)"""
STP_STATE_ESCAPE_1 = 0x01
"""Escape 1 state definition ('\\\\x3a\\\\x3c' found)"""
STP_STATE_ESCAPE_2 = 0x02
"""Escape 2 state definition ('\\\\x3a' found inside a message)"""
STP_STATE_STORE_DATA = 0x03
"""Store data state definition (start of message found; data will be stored)"""


class stp(object):
    """This class extracts messages from an "stp-stream".

    **Example:**

    .. literalinclude:: stringtools/_examples_/stp.stp.py

    Will result to the following output:

    .. literalinclude:: stringtools/_examples_/stp.stp.log
    """
    LOG_PREFIX = 'STP:'

    def __init__(self):
        self.state = STP_STATE_IDLE
        self.__buffer__ = b''
        self.__clear_buffer__()

    def __clear_buffer__(self):
        if len(self.__buffer__) > 0:
            logger.warning('%s Chunking "%s" from buffer', self.LOG_PREFIX, stringtools.hexlify(self.__buffer__))
        self.__buffer__ = b''

    def process(self, data):
        """
        This processes a byte out of a "stp-stream".

        :param bytes data: A byte stream
        :returns: The extracted message or None, if no message is identified yet
        :rtype: str
        """
        if type(data) is list:
            raise TypeError
        #
        rv = []
        #
        while len(data) > 0:
            b = bytes([data[0]])
            data = data[1:]
            #
            if self.state == STP_STATE_IDLE:
                if b == DATA_SYNC:
                    self.state = STP_STATE_ESCAPE_1
                    logger.debug('%s data sync (%02x) received => changing state STP_STATE_IDLE -> STP_STATE_ESCAPE_1', self.LOG_PREFIX, ord(b))
                else:
                    logger.warning('%s no data sync (%02x) received => ignoring byte', self.LOG_PREFIX, ord(b))
            elif self.state == STP_STATE_ESCAPE_1:
                if b == DATA_CLEAR_BUFFER:
                    logger.debug('%s start pattern (%02x %02x) received => changing state STP_STATE_ESCAPE_1 -> STP_STATE_STORE_DATA', self.LOG_PREFIX, ord(DATA_SYNC), ord(b))
                    self.state = STP_STATE_STORE_DATA
                    self.__clear_buffer__()
                elif b != DATA_SYNC:
                    self.state = STP_STATE_IDLE
                    logger.warning('%s no start pattern (%02x %02x) received => changing state STP_STATE_ESCAPE_1 -> STP_STATE_IDLE', self.LOG_PREFIX, ord(DATA_SYNC), ord(b))
                else:
                    logger.warning('%s 2nd data sync (%02x) received => keep state', self.LOG_PREFIX, ord(b))
            elif self.state == STP_STATE_STORE_DATA:
                if b == DATA_SYNC:
                    self.state = STP_STATE_ESCAPE_2
                    logger.debug('%s data sync (%02x) received => changing state STP_STATE_STORE_DATA -> STP_STATE_ESCAPE_2', self.LOG_PREFIX, ord(b))
                else:
                    self.__buffer__ += b
            elif self.state == STP_STATE_ESCAPE_2:
                if b == DATA_CLEAR_BUFFER:
                    logger.warning('%s start pattern (%02x %02x) received => changing state STP_STATE_ESCAPE_2 -> STP_STATE_STORE_DATA', self.LOG_PREFIX, ord(DATA_SYNC), ord(b))
                    self.state = STP_STATE_STORE_DATA
                    self.__clear_buffer__()
                elif b == DATA_VALID_MSG:
                    self.state = STP_STATE_IDLE
                    logger.debug('%s end pattern (%02x %02x) received => storing message and changing state STP_STATE_ESCAPE_2 -> STP_STATE_IDLE', self.LOG_PREFIX, ord(DATA_SYNC), ord(b))
                    rv.append(self.__buffer__)
                    self.__buffer__ = b''
                elif b == DATA_STORE_SYNC_VALUE:
                    self.state = STP_STATE_STORE_DATA
                    logger.debug('%s store sync pattern (%02x %02x) received => changing state STP_STATE_ESCAPE_2 -> STP_STATE_STORE_DATA', self.LOG_PREFIX, ord(DATA_SYNC), ord(b))
                    self.__buffer__ += DATA_SYNC
                elif b == DATA_SYNC:
                    self.state = STP_STATE_ESCAPE_1
                    logger.warning('%s second data sync (%02x) received => changing state STP_STATE_ESCAPE_2 -> STP_STATE_ESCAPE_1', self.LOG_PREFIX, ord(b))
                    self.__clear_buffer__()
                else:
                    self.state = STP_STATE_IDLE
                    logger.warning('%s data (%02x) received => changing state STP_STATE_ESCAPE_2 -> STP_STATE_IDLE', self.LOG_PREFIX, ord(b))
                    self.__clear_buffer__()
            else:
                logger.error('%s unknown state (%s) => adding value (%02x) back to data again and changing state -> STP_STATE_IDLE', self.LOG_PREFIX, repr(self.state), ord(b))
                self.state = STP_STATE_IDLE
                self.__clear_buffer__()
                data = b + data
        for msg in rv:
            logger.info('%s message identified - %s', self.LOG_PREFIX, stringtools.hexlify(msg))
        return rv


def build_frame(data):
    """This Method builds an "stp-frame" to be transfered via a stream.

    :param str data: A String (Bytes) to be framed
    :returns: The "stp-framed" message to be sent
    :rtype: str

    **Example:**

    .. literalinclude:: stringtools/_examples_/stp.build_frame.py

    Will result to the following output:

    .. literalinclude:: stringtools/_examples_/stp.build_frame.log
    """
    rv = DATA_SYNC + DATA_CLEAR_BUFFER

    for byte in data:
        byte = bytes([byte])
        if byte == DATA_SYNC:
            rv += DATA_SYNC + DATA_STORE_SYNC_VALUE
        else:
            rv += byte

    rv += DATA_SYNC + DATA_VALID_MSG
    return rv
