from __future__ import annotations

from typing import Dict, List, Optional

from .base import CommandHandler
from ..automotive import NoResponseFromBus
from ..message import Message
from ..addressing import Addressing
from ..obd2.frame import Frame
from ..obd2.pid import PIDRegistry
from ..uds import ServiceId


class OBD2DiagnosticSession(CommandHandler):
    def __init__(
        self,
        arbitration_id: int,
        reply_id: int,
        auxiliary_ids: List[int],
        vin: Optional[str],
        frames: List[Frame],
        stored_dtcs: List,
        pending_dtcs: List,
        permanent_dtcs: List,
        informational: Dict[int, object],
        oxygen_sensor_monitoring: Dict[int, object],
        on_board_monitoring: Dict[int, object],
        control_operations: Dict[int, object],
    ) -> None:
        if not frames:
            raise ValueError("OBD2 handler needs at least one frame")
        self._arbitration_id = arbitration_id
        self._reply_id = reply_id
        self._auxiliary_ids = auxiliary_ids
        self._vin = vin
        self._frames = frames
        self._stored_dtcs = stored_dtcs
        self._pending_dtcs = pending_dtcs
        self._permanent_dtcs = permanent_dtcs
        self._informational = informational
        self._oxygen_sensor_monitoring = oxygen_sensor_monitoring
        self._on_board_monitoring = on_board_monitoring
        self._control_operations = control_operations

    @property
    def arbitration_id(self) -> int:
        return self._arbitration_id

    @property
    def reply_id(self) -> int:
        return self._reply_id

    @property
    def request_extension(self) -> Optional[int]:
        return None

    @property
    def reply_extension(self) -> Optional[int]:
        return None

    @property
    def auxiliary_ids(self) -> List[int]:
        return self._auxiliary_ids

    @property
    def auxiliary_request_extensions(self) -> List[int]:
        return []

    @property
    def p2_server_max(self) -> float:
        return 0.0

    @property
    def p2ext_server_max(self) -> float:
        return 0.0

    def configure(self, property_name: str, value: str) -> None:
        return

    def current_frame(self, pid: int) -> List[int]:
        frame = self._frames[0]
        return frame.value_for_pid(pid)

    def freeze_frame(self, frame_id: int, pid: int) -> List[int]:
        frame_number = frame_id + 1
        if frame_number >= len(self._frames):
            raise NoResponseFromBus()
        frame = self._frames[frame_number]
        return frame.value_for_pid(pid)

    def dtcs(self, dtcs: List) -> List[int]:
        return [len(dtcs)] + [byte for dtc in dtcs for byte in dtc.bytes[:2]]

    @staticmethod
    def _mode09_data_pid_for_count_pid(pid: int) -> Optional[int]:
        if pid <= 0 or pid % 2 == 0:
            return None
        if pid % 0x20 == 0x1F:
            return None
        data_pid = pid + 1
        if data_pid % 0x20 == 0:
            return None
        return data_pid

    @staticmethod
    def _mode09_count_pid_for_data_pid(pid: int) -> Optional[int]:
        if pid <= 0 or pid % 2 != 0:
            return None
        if pid % 0x20 == 0:
            return None
        count_pid = pid - 1
        if count_pid % 0x20 == 0:
            return None
        return count_pid

    @staticmethod
    def _classic_isotp_message_count(payload_length: int) -> int:
        if payload_length <= 0:
            return 0
        if payload_length <= 7:
            return 1
        remaining_payload = payload_length - 6
        return 1 + ((remaining_payload + 6) // 7)

    def _mode09_supported_registry(self) -> PIDRegistry:
        registry = PIDRegistry(self._informational.copy())
        for pid in list(self._informational.keys()):
            count_pid = self._mode09_count_pid_for_data_pid(pid)
            if count_pid is not None and count_pid not in registry:
                registry[count_pid] = self._informational[pid]
        return registry

    def _mode09_auto_message_count(self, pid: int) -> Optional[int]:
        data_pid = self._mode09_data_pid_for_count_pid(pid)
        if data_pid is None or data_pid not in self._informational:
            return None
        response_payload_length = 2 + len(self._informational[data_pid].content)
        count = self._classic_isotp_message_count(response_payload_length)
        return min(count, 0xFF)

    def informational(self, pid: int) -> List[int]:
        if pid % 0x20 == 0:
            return self._mode09_supported_registry().supported(pid)
        auto_count = self._mode09_auto_message_count(pid)
        if auto_count is not None:
            return [auto_count]
        if pid not in self._informational:
            raise NoResponseFromBus()
        return self._informational[pid].content

    def oxygen_sensor_monitoring(self, pid: int) -> List[int]:
        if pid % 0x20 == 0:
            return PIDRegistry(self._oxygen_sensor_monitoring).supported(pid)
        if pid not in self._oxygen_sensor_monitoring:
            raise NoResponseFromBus()
        return self._oxygen_sensor_monitoring[pid].content

    def on_board_monitoring(self, pid: int) -> List[int]:
        if pid % 0x20 == 0:
            return PIDRegistry(self._on_board_monitoring).supported(pid)
        if pid not in self._on_board_monitoring:
            raise NoResponseFromBus()
        return self._on_board_monitoring[pid].content

    def control_operation(self, pid: int) -> List[int]:
        if pid % 0x20 == 0:
            return PIDRegistry(self._control_operations).supported(pid)
        if pid not in self._control_operations:
            raise NoResponseFromBus()
        return self._control_operations[pid].content

    def handle(self, message: Message, sink):
        if not message.bytes:
            return None
        pid = message.bytes[0]
        if pid < 0x01 or pid > 0x0F:
            return None
        try:
            response_bytes = self.response_for_message(message)
            response_addressing = Addressing.id_only(self.reply_id)
            return Message(addressing=response_addressing, bytes=response_bytes)
        except NoResponseFromBus:
            return None

    def response_for_message(self, message: Message) -> List[int]:
        bytes_in = message.bytes
        response_bytes = [bytes_in[0] | 0x40]
        if len(bytes_in) >= 2:
            response_bytes.append(bytes_in[1])
        sid = bytes_in[0]
        if sid == ServiceId.current_powertrain_diagnostics_data:
            if len(bytes_in) < 2:
                raise NoResponseFromBus()
            response_bytes += self.current_frame(bytes_in[1])
            return response_bytes
        if sid == ServiceId.powertrain_freeze_frame_data:
            if len(bytes_in) < 3:
                raise NoResponseFromBus()
            response_bytes += self.freeze_frame(bytes_in[1], bytes_in[2])
            return response_bytes
        if sid == ServiceId.emission_related_dtcs:
            response_bytes += self.dtcs(self._stored_dtcs)
            return response_bytes
        if sid == ServiceId.reset_emission_related_dtcs:
            self._stored_dtcs.clear()
            self._pending_dtcs.clear()
            return response_bytes
        if sid == ServiceId.pending_emission_related_dtcs:
            response_bytes += self.dtcs(self._pending_dtcs)
            return response_bytes
        if sid == ServiceId.permanent_emission_related_dtcs:
            response_bytes += self.dtcs(self._permanent_dtcs)
            return response_bytes
        if sid == ServiceId.oxygen_sensor_monitoring_test_results:
            if len(bytes_in) < 2:
                raise NoResponseFromBus()
            response_bytes += self.oxygen_sensor_monitoring(bytes_in[1])
            return response_bytes
        if sid == ServiceId.on_board_monitoring_test_results:
            if len(bytes_in) < 2:
                raise NoResponseFromBus()
            response_bytes += self.on_board_monitoring(bytes_in[1])
            return response_bytes
        if sid == ServiceId.control_on_board_system_test_or_component:
            if len(bytes_in) < 2:
                raise NoResponseFromBus()
            response_bytes += self.control_operation(bytes_in[1])
            return response_bytes
        if sid == ServiceId.vehicle_information:
            if len(bytes_in) < 2:
                raise NoResponseFromBus()
            response_bytes += self.informational(bytes_in[1])
            return response_bytes
        raise NoResponseFromBus()

    def __str__(self) -> str:
        aux = "" if not self._auxiliary_ids else " + " + ", ".join(f"0x{value:X}" for value in self._auxiliary_ids)
        return f"{type(self).__name__} @ 0x{self._arbitration_id:X} => 0x{self._reply_id:X}{aux}"
