from __future__ import annotations

import queue
import threading
import time
from typing import Dict, List, Optional

from pymulator.core.bus import CANBusEmulation
from pymulator.core.can import CANFrame
from pymulator.core.isotp import FlowControlFrame, FlowStatus, ISOTPTransceiver

from ..errors import AdapterError


class ResponseStream:
    def __init__(self) -> None:
        self._queue: queue.Queue[str] = queue.Queue()
        self._done = threading.Event()
        self._error: Optional[Exception] = None

    def yield_output(self, output: str) -> None:
        self._queue.put(output)

    def finish(self, error: Optional[Exception] = None) -> None:
        self._error = error
        self._done.set()

    def __iter__(self):
        while True:
            if self._done.is_set() and self._queue.empty():
                if self._error:
                    raise self._error
                break
            try:
                output = self._queue.get(timeout=0.05)
            except queue.Empty:
                continue
            yield output


class BusCommandHandler:
    def __init__(self) -> None:
        self._isotp: Dict[int, ISOTPTransceiver] = {}
        self._adapter: Optional["Adapter"] = None
        self._timer: Optional[threading.Timer] = None
        self._response_stream: Optional[ResponseStream] = None
        self._lock = threading.Lock()

    def handle(self, bytes_: List[int], adapter: "Adapter") -> ResponseStream:
        self._adapter = adapter
        self._isotp[adapter.current_request_header] = ISOTPTransceiver(
            block_size=0,
            separation_time_ms=1,
            frame_length=adapter.isotp_frame_length,
        )
        self._response_stream = ResponseStream()
        self._start_transmission(bytes_)
        self._launch_timer_if_necessary()
        return self._response_stream

    def _start_transmission(self, bytes_: List[int]) -> None:
        adapter = self._require_adapter()
        bus = self._require_bus(adapter)
        if adapter.auto_segmentation_tx:
            isotp = self._transceiver_for_header(adapter.current_request_header)
            action = isotp.write(bytes_)
            if action.kind != "write_frames" or not action.frames:
                raise RuntimeError("Unexpected ISOTP action")
            if len(action.frames) != 1:
                raise RuntimeError("Expected single frame for initial transmit")
            frame_bytes = action.frames[0]
            payload = adapter.frame_bytes_for_transmit(frame_bytes)
            frame = CANFrame(arbitration_id=adapter.current_request_header, data=payload)
            bus.write(frame, originator=adapter)
        else:
            manual_length_provided = (
                len(bytes_) > 1
                and bytes_[0] <= 0x07
                and bytes_[0] == len(bytes_) - 1
                and bytes_[1] >= 0x10
            )
            auto_format_enabled = adapter.can_auto_format and not manual_length_provided
            maximum_length = 7 if adapter.request_header_extension is not None else 8
            payload_limit = min(7, maximum_length) if auto_format_enabled else maximum_length
            if len(bytes_) > payload_limit:
                raise AdapterError.invalid_command()
            base_bytes = ([len(bytes_)] + bytes_) if auto_format_enabled else bytes_
            frame_bytes = adapter.frame_bytes_for_transmit(base_bytes)
            frame = CANFrame(arbitration_id=adapter.current_request_header, data=frame_bytes)
            bus.write(frame, originator=adapter)

    def _launch_timer_if_necessary(self) -> None:
        adapter = self._require_adapter()
        request = adapter.active_request
        if request is None:
            return
        if request.is_completed:
            if self._response_stream:
                self._response_stream.finish()
            return
        timeout_ms = request.effective_timeout(
            default=adapter.bus_timeout,
            busy_timeout=adapter.nrc_busy_bus_timeout,
            is_busy=request.received_busy_please_wait,
        )
        if self._timer:
            self._timer.cancel()
        self._timer = threading.Timer(timeout_ms / 1000.0, self._on_timeout, args=(request,))
        self._timer.daemon = True
        self._timer.start()

    def _on_timeout(self, request: "Request") -> None:
        stream = self._response_stream
        if not stream:
            return
        error = AdapterError.no_data() if not request.received_anything else None
        stream.finish(error=error)

    def _transceiver_for_header(self, header: int) -> ISOTPTransceiver:
        transceiver = self._isotp.get(header)
        if transceiver is None:
            transceiver = ISOTPTransceiver(block_size=0, separation_time_ms=1, frame_length=self._require_adapter().isotp_frame_length)
            self._isotp[header] = transceiver
        transceiver.frame_length = self._require_adapter().isotp_frame_length
        return transceiver

    def continue_transmission(self, frame: CANFrame, request: "Request") -> None:
        adapter = self._require_adapter()
        if self._timer:
            self._timer.cancel()
        payload = adapter.stripped_response_bytes(frame.data)
        if payload is None:
            self._launch_timer_if_necessary()
            return
        if adapter.auto_segmentation_rx or (adapter.auto_segmentation_tx and self._transceiver_for_header(adapter.current_request_header).state == "sending"):
            isotp = self._transceiver_for_header(frame.arbitration_id)
            action = isotp.did_read(payload)
            if action.kind == "wait":
                pass
            elif action.kind == "process" and action.bytes is not None:
                request.did_receive_message(action.bytes)
                output = adapter.format_received_bytes(frame.arbitration_id, action.bytes)
                if output:
                    self._response_stream and self._response_stream.yield_output(output)
            elif action.kind == "write_frames" and action.frames:
                for frame_bytes in action.frames:
                    adjusted = adapter.frame_bytes_for_transmit(frame_bytes)
                    out_frame = CANFrame(arbitration_id=adapter.current_request_header, data=adjusted)
                    adapter.bus.write(out_frame, originator=adapter)
                    if action.separation_time > 0:
                        time.sleep(action.separation_time / 1000.0)
        else:
            request.did_receive_frame(payload)
            output = adapter.format_received_bytes(frame.arbitration_id, payload)
            if output:
                self._response_stream and self._response_stream.yield_output(output)
            if payload and (payload[0] & 0xF0) == 0x10:
                flow_control = FlowControlFrame(flow_status=FlowStatus.clear_to_send, block_size=0, separation_time=1)
                adjusted = adapter.frame_bytes_for_transmit(flow_control.bytes)
                out_frame = CANFrame(arbitration_id=adapter.current_request_header, data=adjusted)
                adapter.bus.write(out_frame, originator=adapter)
        self._launch_timer_if_necessary()

    def _require_adapter(self) -> "Adapter":
        if not self._adapter:
            raise RuntimeError("Adapter not set")
        return self._adapter

    def _require_bus(self, adapter: "Adapter") -> CANBusEmulation:
        if not adapter.bus:
            raise AdapterError.no_data()
        return adapter.bus
