# Copyright (c) Acconeer AB, 2022-2024
# All rights reserved

# THIS FILE IS AUTOMATICALLY GENERATED - DO NOT EDIT
from __future__ import annotations

import logging
import queue
import time
from typing import List


_LOG = logging.getLogger(__name__)


class CommandFailed(Exception):
    pass


class Packet:
    """
    Base class for all packages transmitted over UART protocol.
    """

    PROTOCOL_START_MARKER = 0xCC
    PROTOCOL_END_MARKER = 0xCD

    def __init__(self, payload):
        self.payload = payload

    @property
    def packet_type(self):
        raise NotImplementedError

    def get_byte_array(self):
        """
        Packet the data for this packet into a bytearray()
        that can be transmitted over UART.
        """
        data = bytearray([])
        data.append(Packet.PROTOCOL_START_MARKER)
        packet_length = int(len(self.payload))
        data.extend(packet_length.to_bytes(2, byteorder="little"))
        data.append(self.packet_type)
        data.extend(self.payload)
        data.append(Packet.PROTOCOL_END_MARKER)
        return data


class BlCommandResponsePacket(Packet):
    packet_type = 0xF3

    def __init__(self, payload):
        self.command_id = int.from_bytes(payload[0:2], byteorder="little")
        self.command_payload = payload[2:]
        super().__init__(payload)

    def get_command_packet(self):
        return _command_packets.get(self.command_id)(self.command_payload)


class BlCommandRequestPacket(Packet):
    packet_type = 0xF3

    def __init__(self, command_payload):
        command_payload[0:0] = self.command_id.to_bytes(2, byteorder="little")
        super().__init__(command_payload)


class GetLastErrorRequestPacket(BlCommandRequestPacket):
    command_id = 0x0002

    def __init__(
        self,
    ):
        payload = bytearray()
        super().__init__(payload)


class GetLastErrorResponsePacket:
    command_id = 0x0002

    def __init__(self, payload):
        self.payload = payload

    def get_status(self):
        return self.payload[0]

    def get_response_data(self):
        return self.payload[1:].decode("ascii")


class GetAppSwVersionRequestPacket(BlCommandRequestPacket):
    command_id = 0x0101

    def __init__(
        self,
    ):
        payload = bytearray()
        super().__init__(payload)


class GetAppSwVersionResponsePacket:
    command_id = 0x0101

    def __init__(self, payload):
        self.payload = payload

    def get_status(self):
        return self.payload[0]

    def get_version(self):
        return self.payload[1:].decode("ascii")


class GetAppSwNameRequestPacket(BlCommandRequestPacket):
    command_id = 0x0102

    def __init__(
        self,
    ):
        payload = bytearray()
        super().__init__(payload)


class GetAppSwNameResponsePacket:
    command_id = 0x0102

    def __init__(self, payload):
        self.payload = payload

    def get_status(self):
        return self.payload[0]

    def get_name(self):
        return self.payload[1:].decode("ascii")


class IsImageErasedRequestPacket(BlCommandRequestPacket):
    command_id = 0xF000

    def __init__(self, image_size):
        payload = bytearray()
        payload.extend(image_size.to_bytes(4, byteorder="little", signed=True))
        super().__init__(payload)


class IsImageErasedResponsePacket:
    command_id = 0xF000

    def __init__(self, payload):
        self.payload = payload
        assert len(payload) == 2

    def get_status(self):
        return self.payload[0]

    def get_is_erased(self):
        return self.payload[1] != 0


class ImageEraseRequestPacket(BlCommandRequestPacket):
    command_id = 0xF001

    def __init__(self, image_size):
        payload = bytearray()
        payload.extend(image_size.to_bytes(4, byteorder="little", signed=True))
        super().__init__(payload)


class ImageEraseResponsePacket:
    command_id = 0xF001

    def __init__(self, payload):
        self.payload = payload
        assert len(payload) == 1

    def get_status(self):
        return self.payload[0]


class ImageWriteBlockRequestPacket(BlCommandRequestPacket):
    command_id = 0xF002

    def __init__(self, offset, data):
        payload = bytearray()
        payload.extend(offset.to_bytes(4, byteorder="little", signed=True))
        payload.extend(data)
        super().__init__(payload)


class ImageWriteBlockResponsePacket:
    command_id = 0xF002

    def __init__(self, payload):
        self.payload = payload
        assert len(payload) == 1

    def get_status(self):
        return self.payload[0]


class ResetRequestPacket(BlCommandRequestPacket):
    command_id = 0xFFFE

    def __init__(
        self,
    ):
        payload = bytearray()
        super().__init__(payload)


_command_packets = {
    0x0002: GetLastErrorResponsePacket,
    0x0101: GetAppSwVersionResponsePacket,
    0x0102: GetAppSwNameResponsePacket,
    0xF000: IsImageErasedResponsePacket,
    0xF001: ImageEraseResponsePacket,
    0xF002: ImageWriteBlockResponsePacket,
}


class BLCommunication:
    PROTOCOL_OVERHEAD = 5
    PACKET_TIMEOUT = 0.2
    PROTOCOL_TIMEOUT = 2

    def __init__(self, serial_device):
        self._ser = serial_device
        self._ser.timeout = self.PACKET_TIMEOUT

    def _send_packet(self, packet):
        data = packet.get_byte_array()
        _LOG.debug("Sending packet %s", data)
        self._ser.write(data)

    def _read_response(self):
        response_bytes = bytearray()
        length = 0
        start = time.monotonic()
        while len(response_bytes) < (length + self.PROTOCOL_OVERHEAD):
            data = self._ser.read()
            response_bytes.extend(data)
            if len(response_bytes) > 0 and response_bytes[0] != Packet.PROTOCOL_START_MARKER:
                msg = f"Bad response, start marker '0x{response_bytes[-1]:x}'"
                raise CommandFailed(msg)

            if length == 0 and len(response_bytes) > 2:
                length = response_bytes[1] | response_bytes[2] << 8

            if (time.monotonic() - start) > self.PROTOCOL_TIMEOUT:
                msg = "Timeout when receiving data from device"
                raise CommandFailed(msg)

        if response_bytes[-1] != Packet.PROTOCOL_END_MARKER:
            msg = f"Bad response, end marker '0x{response_bytes[-1]:x}'"
            raise CommandFailed(msg)

        if response_bytes[3] != BlCommandResponsePacket.packet_type:
            msg = f"Bad response, packet type '0x{response_bytes[3]:x}'"
            raise CommandFailed(msg)

        return BlCommandResponsePacket(response_bytes[4:-1])

    def _execute_request(self, packet):
        try:
            self._send_packet(packet)
            response = self._read_response()
            return response.get_command_packet()
        except queue.Empty:
            _LOG.error("read timeout")
            msg = "_execute_request timeout"
            raise TimeoutError(msg)

    def get_last_error(self):
        response = self._execute_request(GetLastErrorRequestPacket())
        status = response.get_status()
        response_data = response.get_response_data()
        if status != 0:
            error_msg = self.get_last_error()
            msg = f"Command failed with code {status} [{error_msg}]"
            raise CommandFailed(msg)
        return response_data

    def get_app_sw_version(self):
        response = self._execute_request(GetAppSwVersionRequestPacket())
        status = response.get_status()
        version = response.get_version()
        if status != 0:
            error_msg = self.get_last_error()
            msg = f"Command failed with code {status} [{error_msg}]"
            raise CommandFailed(msg)
        return version

    def get_app_sw_name(self):
        response = self._execute_request(GetAppSwNameRequestPacket())
        status = response.get_status()
        name = response.get_name()
        if status != 0:
            error_msg = self.get_last_error()
            msg = f"Command failed with code {status} [{error_msg}]"
            raise CommandFailed(msg)
        return name

    def is_image_erased(self, image_size: int):
        response = self._execute_request(IsImageErasedRequestPacket(image_size))
        status = response.get_status()
        is_erased = response.get_is_erased()
        if status != 0:
            error_msg = self.get_last_error()
            msg = f"Command failed with code {status} [{error_msg}]"
            raise CommandFailed(msg)
        return is_erased

    def image_erase(self, image_size: int):
        response = self._execute_request(ImageEraseRequestPacket(image_size))
        status = response.get_status()
        if status != 0:
            error_msg = self.get_last_error()
            msg = f"Command failed with code {status} [{error_msg}]"
            raise CommandFailed(msg)

    def image_write_block(self, offset: int, data: List[int]):
        response = self._execute_request(ImageWriteBlockRequestPacket(offset, data))
        status = response.get_status()
        if status != 0:
            error_msg = self.get_last_error()
            msg = f"Command failed with code {status} [{error_msg}]"
            raise CommandFailed(msg)

    def reset(self):
        self._send_packet(ResetRequestPacket())
        try:
            self._ser.read()  # Dummy read to make sure the previous write is finished
        except Exception:  # noqa: S110
            pass
