import attr
import structlog

from dlms_cosem.hdlc import address, exceptions, frames
from dlms_cosem.hdlc.exceptions import LocalProtocolError
from dlms_cosem.hdlc.state import (
    AWAITING_CONNECTION,
    AWAITING_DISCONNECT,
    AWAITING_RESPONSE,
    NEED_DATA,
    HdlcConnectionState,
)

LOG = structlog.get_logger()


@attr.s(auto_attribs=True)
class HdlcConnection:
    """
    HDLC - High-level Data Link Control

    In DLMS/COSEM HDLC is used to send DLMS data over serial interfaces. Like the optical
    probe for diagnostic. Some meters also send data over TCP sockets sing HDLC when they
    have not implemented the TCP transport variant of DLMS/COSEM.

    Tracks the state of HDLC communication and transforming bytes to frames and
    frames to bytes.
    """

    client_address: address.HdlcAddress
    server_address: address.HdlcAddress
    client_ssn: int = attr.ib(init=False, default=0)
    client_rsn: int = attr.ib(init=False, default=0)
    server_ssn: int = attr.ib(init=False, default=0)
    server_rsn: int = attr.ib(init=False, default=0)
    max_data_size: int = attr.ib(default=128)
    state: HdlcConnectionState = attr.ib(factory=HdlcConnectionState)
    buffer: bytearray = attr.ib(factory=bytearray)
    buffer_search_position: int = 1

    def send(self, frame) -> bytes:
        """
        Returns the bytes to be sent over I/O for a frame and changes the connection
        state depending on frame.
        :param frame: HDLC frame:
        :return: bytes
        """
        self.state.process_frame(frame)

        if isinstance(frame, frames.InformationFrame):
            self.handle_sequence_numbers(
                frame_ssn=frame.send_sequence_number,
                frame_rsn=frame.receive_sequence_number,
                response=False,
            )

        return frame.to_bytes()

    def handle_sequence_numbers(self, frame_ssn: int, frame_rsn: int, response: bool):
        if not response:
            if frame_ssn != self.server_ssn or frame_rsn != self.server_rsn:
                raise LocalProtocolError(
                    f"Frame sequence numbers are wrong: frame(ssn: {frame_ssn}, rsn: "
                    f"{frame_rsn}) =! client(ssn:{self.server_ssn}, "
                    f"rsn:{self.server_rsn})"
                )
            self.server_ssn += 1
            self.client_rsn += 1
        else:
            if frame_ssn != self.client_ssn or frame_rsn != self.client_rsn:
                raise LocalProtocolError(
                    f"Frame sequence numbers are wrong: frame(ssn: {frame_ssn}, rsn: "
                    f"{frame_rsn}) =! client(ssn:{self.server_ssn}, "
                    f"rsn:{self.server_rsn})"
                )
            self.server_rsn += 1
            self.client_ssn += 1

        if self.server_rsn > 7:
            self.server_rsn = 0
        if self.server_ssn > 7:
            self.server_ssn = 0
        if self.client_rsn > 7:
            self.client_rsn = 0
        if self.client_ssn > 7:
            self.client_ssn = 0

    def receive_data(self, data: bytes):
        """
        Add data into the receive buffer.
        After this you could call next_event
        """
        if data:
            LOG.debug(f"Added data to buffer", data=data)
            self.buffer += data

    def next_event(self):
        """
        Will try to parse a frame from the buffer. If a frame is found the buffer is
        cleared of the bytes making up the frame and the frame is returned.
        If the frame is not parsable we assume it is not compleate and we return a
        NEED_DATA event to signal we need to receive more data.
        :return:
        """
        frame_bytes = self._find_frame()
        if frame_bytes is None:
            return NEED_DATA

        if (
            self.state.current_state == AWAITING_CONNECTION
            or self.state.current_state == AWAITING_DISCONNECT
        ):

            try:
                frame = frames.UnNumberedAcknowledgmentFrame.from_bytes(frame_bytes)
            except (exceptions.HdlcParsingError, ValueError):
                frame = None

        elif self.state.current_state == AWAITING_RESPONSE:
            # It can be a InformationFrame or a ReceiveReadyFrame in case we have sent
            # a segmented frame.
            try:
                frame = frames.InformationFrame.from_bytes(frame_bytes)
            except (exceptions.HdlcParsingError, ValueError):
                # Not an information frame. Should be a receive ready frame
                frame = None

            if frame is None:
                try:
                    frame = frames.ReceiveReadyFrame.from_bytes(frame_bytes)
                except (exceptions.HdlcParsingError, ValueError):
                    frame = None
        else:
            frame = None

        if frame is None:
            LOG.debug("HDLC frame could not be parsed. Need more data")
            return NEED_DATA

        LOG.debug(f"Received HDLC frame", frame=frame)
        self.state.process_frame(frame)
        self._tidy_buffer()

        if isinstance(frame, frames.InformationFrame):
            self.handle_sequence_numbers(
                frame_ssn=frame.send_sequence_number,
                frame_rsn=frame.receive_sequence_number,
                response=True,
            )

        return frame

    def _find_frame(self):
        """
        To find a frame in the buffer we need to assume somethings.
        1. The first character in the buffer should be the HDLC_FLAG.
            During normal operations we will have frames with flags on both ends.
            But with windowing one might be omitted in long information frame exchanges
            Ex: 7e{frame}7e{frame}7e. The second one would not have an initial 7e after
             we take out the first frame.
            So if the intial byte is not 7e we should manually add it.

        2. We might find an incomplete frame if the second 7e was found as data and not
            actually an end flag. So we need to keep the current end memory so we can
            extend the search if we cant parse the frame.

        3. Once we have parsed a proper frame we shoudl call clear buffer that will
        remove the data for the frame from the buffer.
        :return:
        """
        try:
            frame_end = (
                self.buffer.index(frames.HDLC_FLAG, self.buffer_search_position) + 1
            )
        except ValueError:
            # .index raises ValueError on not finding subsection
            return None

        frame_bytes = self.buffer[:frame_end]
        self.buffer_search_position = frame_end

        if not frame_bytes.startswith(frames.HDLC_FLAG):
            frame_bytes.insert(0, ord(frames.HDLC_FLAG))

        return frame_bytes

    def _tidy_buffer(self):
        """
        Remove the bytes we have extracted.
        """
        del self.buffer[: self.buffer_search_position]
        self.buffer_search_position = 1
