import enum
from dataclasses import dataclass
from typing import Iterable, List

from .uds import NegativeResponseCode


class AutomotiveError(Exception):
    pass


class NoResponseFromBus(AutomotiveError):
    def __init__(self, message: str = "") -> None:
        super().__init__(message)


class UdsNegativeResponse(AutomotiveError):
    def __init__(self, code: NegativeResponseCode) -> None:
        super().__init__(f"UDS negative response: {code.name}")
        self.code = code


class RequestSequenceError(AutomotiveError):
    pass


class DTCStatusMask(enum.IntFlag):
    test_failed = 1 << 0
    test_failed_this_operation_cycle = 1 << 1
    pending_dtc = 1 << 2
    confirmed_dtc = 1 << 3
    test_not_completed_since_last_clear = 1 << 4
    test_failed_since_last_clear = 1 << 5
    test_not_completed_this_operation_cycle = 1 << 6
    warning_indicator_requested = 1 << 7

    any = 0xFF
    none = 0x00


class DTCStatusMaskValue(enum.Enum):
    none = "none"
    test_failed = "testFailed"
    test_failed_this_operation_cycle = "testFailedThisOperationCycle"
    pending_dtc = "pendingDTC"
    confirmed_dtc = "confirmedDTC"
    test_not_completed_since_last_clear = "testNotCompletedSinceLastClear"
    test_failed_since_last_clear = "testFailedSinceLastClear"
    test_not_completed_this_operation_cycle = "testNotCompletedThisOperationCycle"
    warning_indicator_requested = "warningIndicatorRequested"

    @property
    def status_mask(self) -> DTCStatusMask:
        mapping = {
            DTCStatusMaskValue.none: DTCStatusMask.none,
            DTCStatusMaskValue.test_failed: DTCStatusMask.test_failed,
            DTCStatusMaskValue.test_failed_this_operation_cycle: DTCStatusMask.test_failed_this_operation_cycle,
            DTCStatusMaskValue.pending_dtc: DTCStatusMask.pending_dtc,
            DTCStatusMaskValue.confirmed_dtc: DTCStatusMask.confirmed_dtc,
            DTCStatusMaskValue.test_not_completed_since_last_clear: DTCStatusMask.test_not_completed_since_last_clear,
            DTCStatusMaskValue.test_failed_since_last_clear: DTCStatusMask.test_failed_since_last_clear,
            DTCStatusMaskValue.test_not_completed_this_operation_cycle: DTCStatusMask.test_not_completed_this_operation_cycle,
            DTCStatusMaskValue.warning_indicator_requested: DTCStatusMask.warning_indicator_requested,
        }
        return mapping[self]


@dataclass(frozen=True)
class AutomotiveDTC:
    bytes: List[int]
    state: DTCStatusMask

    @staticmethod
    def from_bytes(payload: Iterable[int], state: DTCStatusMask) -> "AutomotiveDTC":
        data = list(payload)
        if len(data) != 3:
            raise ValueError("DTC must be 3 bytes")
        return AutomotiveDTC(bytes=data, state=state)

    @property
    def obd2(self) -> str:
        if not self.bytes:
            return ""
        a = self.bytes[0]
        first_character = a >> 6
        kind = {0b00: "P", 0b01: "C", 0b10: "B", 0b11: "U"}.get(first_character, "?")
        values = [(a & 0b00110000) >> 4, a & 0x0F]
        b = self.bytes[1]
        values.extend([b >> 4, b & 0x0F])
        if len(self.bytes) == 3:
            c = self.bytes[2]
            values.extend([c >> 4, c & 0x0F])
        return kind + "".join(f"{value:X}" for value in values)


@dataclass
class MemorySlice:
    block: "MemoryBlock"
    offset: int
    length: int


class MemoryBlock:
    def __init__(
        self,
        name: str,
        identifier: int,
        address: int,
        size: int,
        mtu: int,
        file_to_load: str | None = None,
        file_to_compare_with: str | None = None,
        file_to_store: str | None = None,
        skip_erase_check: bool = False,
        skip_size_check: bool = False,
        debug_swallow_response_likeliness: float = 0.0,
    ) -> None:
        self.name = name
        self.identifier = identifier
        self.address = address
        self.size = size
        self.mtu = mtu
        self.file_to_load = file_to_load
        self.file_to_compare_with = file_to_compare_with
        self.file_to_store = file_to_store
        self.skip_erase_check = skip_erase_check
        self.skip_size_check = skip_size_check
        self.debug_swallow_response_likeliness = debug_swallow_response_likeliness

        self.erased = False
        self.incoming: List[int] | None = None
        self.outgoing: List[int] | None = None
        self.content: List[int] = []
        self.next_block = 0

    @property
    def has_active_transfer(self) -> bool:
        return self.has_incoming_transfer or self.has_outgoing_transfer

    @property
    def has_outgoing_transfer(self) -> bool:
        return self.outgoing is not None

    @property
    def has_incoming_transfer(self) -> bool:
        return self.incoming is not None

    def erase(self) -> None:
        self.erased = True

    def start_download(self) -> None:
        if not self.skip_erase_check and not self.erased:
            raise UdsNegativeResponse(NegativeResponseCode.conditions_not_correct)
        self.incoming = []
        self.next_block = 1

    def download(self, payload: List[int]) -> None:
        if self.incoming is None:
            raise UdsNegativeResponse(NegativeResponseCode.request_sequence_error)
        self.incoming.extend(payload)
        self.next_block = 0 if self.next_block == 0xFF else self.next_block + 1

    def exit_download(self) -> None:
        if self.incoming is None:
            raise UdsNegativeResponse(NegativeResponseCode.general_programming_failure)
        if not self.skip_size_check and len(self.incoming) != self.size:
            raise UdsNegativeResponse(NegativeResponseCode.general_programming_failure)
        if self.file_to_compare_with:
            try:
                with open(self.file_to_compare_with, "rb") as handle:
                    compare_bytes = handle.read()
                if compare_bytes != bytes(self.incoming):
                    raise UdsNegativeResponse(NegativeResponseCode.general_programming_failure)
            except FileNotFoundError:
                pass
        if self.file_to_store:
            with open(self.file_to_store, "wb") as handle:
                handle.write(bytes(self.incoming))
        self.incoming = None
        self.erased = False

    def start_upload(self) -> None:
        if self.file_to_load:
            try:
                with open(self.file_to_load, "rb") as handle:
                    self.outgoing = list(handle.read())
            except FileNotFoundError:
                self.outgoing = [0xFF] * self.size
        else:
            self.outgoing = [0xFF] * self.size
        self.next_block = 1

    def upload(self) -> List[int]:
        if not self.outgoing:
            raise UdsNegativeResponse(NegativeResponseCode.conditions_not_correct)
        block_size = min(len(self.outgoing), int(self.mtu))
        block = self.outgoing[:block_size]
        self.outgoing = self.outgoing[block_size:]
        self.next_block = 0 if self.next_block == 0xFF else self.next_block + 1
        return block

    def exit_upload(self) -> None:
        if self.outgoing is None or self.outgoing:
            raise UdsNegativeResponse(NegativeResponseCode.conditions_not_correct)
        self.outgoing = None

    def read(self, offset: int, length: int) -> List[int]:
        if offset < 0 or length <= 0:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        end_offset = offset + length
        if end_offset > int(self.size):
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        if not self.content:
            if self.file_to_load:
                try:
                    with open(self.file_to_load, "rb") as handle:
                        self.content = list(handle.read())
                except FileNotFoundError:
                    self.content = [0xFF] * int(self.size)
            else:
                self.content = [0xFF] * int(self.size)
        if end_offset > len(self.content):
            self.content.extend([0xFF] * (end_offset - len(self.content)))
        return self.content[offset:end_offset]

    def write(self, offset: int, data: List[int]) -> None:
        if offset < 0 or not data:
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        end_offset = offset + len(data)
        if end_offset > int(self.size):
            raise UdsNegativeResponse(NegativeResponseCode.request_out_of_range)
        if not self.content:
            if self.file_to_load:
                try:
                    with open(self.file_to_load, "rb") as handle:
                        self.content = list(handle.read())
                except FileNotFoundError:
                    self.content = [0xFF] * int(self.size)
            else:
                self.content = [0xFF] * int(self.size)
        if end_offset > len(self.content):
            self.content.extend([0xFF] * (end_offset - len(self.content)))
        for idx, byte in enumerate(data):
            self.content[offset + idx] = byte


class IdentifierValueType(enum.Enum):
    bytes = "bytes"
    dynamic = "dynamic"
    sequence = "sequence"


class Identifier:
    def __init__(
        self,
        identifier: int,
        name: str,
        data_type,
        static_content: List[int],
        value_type,
        read_levels: List[int],
        write_levels: List[int],
    ) -> None:
        self.identifier = identifier
        self.name = name
        self.data_type = data_type
        self._static_content = static_content
        self.value_type = value_type
        self.read_levels = read_levels
        self.write_levels = write_levels

    @property
    def content(self) -> List[int]:
        if self.value_type[0] == IdentifierValueType.bytes:
            return list(self._static_content)
        if self.value_type[0] == IdentifierValueType.dynamic:
            return self.value_type[1].current_bytes()
        if self.value_type[0] == IdentifierValueType.sequence:
            return self.value_type[1].next_bytes()
        return list(self._static_content)

    @content.setter
    def content(self, value: List[int]) -> None:
        self._static_content = list(value)
        self.value_type = (IdentifierValueType.bytes, None)

    @property
    def length(self) -> int:
        typed_length = getattr(self.data_type, "length", 0)
        if typed_length:
            return typed_length
        if self.value_type[0] == IdentifierValueType.dynamic:
            return self.value_type[1].encoding.byte_width
        if self.value_type[0] == IdentifierValueType.sequence:
            return self.value_type[1].response_length
        return min(255, len(self._static_content))

    def has_range(self, rng: range) -> bool:
        return rng.start >= 0 and rng.stop <= self.length

    def subrange(self, rng: range) -> List[int]:
        return self.content[rng.start:rng.stop]


class DynamicIdentifier:
    max_slices = 40
    max_total_bytes = 88

    def __init__(self, identifier: int) -> None:
        self.identifier = identifier
        self.slices: List[tuple] = []

    @property
    def slice_count(self) -> int:
        return len(self.slices)

    @property
    def total_length(self) -> int:
        total = 0
        for kind, payload in self.slices:
            if kind == "identifier":
                _, rng = payload
                total += rng.stop - rng.start
            elif kind == "memory":
                _, _, length = payload
                total += length
        return total


class SecurityLevel:
    class SeedKeyPair:
        def __init__(self, seed: List[int], key: List[int]) -> None:
            self.seed = seed
            self.key = key

    def __init__(self, level: int, seed_key_pairs: List["SecurityLevel.SeedKeyPair"], delay: int, actions) -> None:
        self.level = level
        self.seed_key_pairs = seed_key_pairs
        self.delay = delay
        self.actions = actions
