from __future__ import annotations

import asyncio
from contextlib import asynccontextmanager
from datetime import datetime, timedelta
from typing import Any, AsyncIterator, Callable, Coroutine, Generic, Optional, TypeVar

from siobrultech_protocols.gem.packets import PacketFormatType

from .const import (
    CMD_GET_SERIAL_NUMBER,
    CMD_SET_DATE_AND_TIME,
    CMD_SET_PACKET_FORMAT,
    CMD_SET_PACKET_SEND_INTERVAL,
    CMD_SET_SECONDARY_PACKET_FORMAT,
    ESCAPE_SEQUENCE,
    TARGET_SERIAL_NUMBER_PREFIX,
)
from .protocol import PACKET_DELAY_CLEAR_TIME, BidirectionalProtocol

T = TypeVar("T")
R = TypeVar("R")


class ApiCall(Generic[T, R]):
    """
    Helper class for making API calls with BidirectionalProtocol. There is one instance of
    this class for each supported API call. This class handles the send_api_request and
    receive_api_response parts of driving the protocol, since those are specific to each API
    request type.
    """

    def __init__(self, formatter: Callable[[T], str], parser: Callable[[str], R]):
        """
        Constructs a new ApiCall.

        formatter: given the parameters for the call (if any) as a Python object, formats the request
        parser: given the response from the call as a string, parses it into a Python object
        """
        self._format_request = formatter
        self._parse_response = parser

    def send_request(
        self,
        protocol: BidirectionalProtocol,
        arg: T,
        serial_number: Optional[int] = None,
    ) -> timedelta:
        """
        Send the request using the given protocol and argument.

        Returns the length of time the caller should wait before attempting to receive the response.
        """
        formatted_request = self._format_request(arg)
        if serial_number:
            formatted_request = formatted_request.replace(
                ESCAPE_SEQUENCE,
                f"{TARGET_SERIAL_NUMBER_PREFIX}{serial_number%100000:05}",
            )

        return protocol.send_api_request(formatted_request)

    def receive_response(self, protocol: BidirectionalProtocol) -> R:
        """
        Receive the response. Should be called after send_request, after
        waiting the amount of time indicated in that method's return value.

        Returns the API call response, parsed into an appropriate Python type.
        """
        return self._parse_response(protocol.receive_api_response())


@asynccontextmanager
async def call_api(
    api: ApiCall[T, R],
    protocol: BidirectionalProtocol,
    serial_number: Optional[int] = None,
) -> AsyncIterator[Callable[[T], Coroutine[Any, None, R]]]:
    async def send(arg: T) -> R:
        delay = api.send_request(protocol, arg, serial_number)
        await asyncio.sleep(delay.seconds)

        return api.receive_response(protocol)

    delay = protocol.begin_api_request()
    try:
        await asyncio.sleep(delay.seconds)
        yield send
    finally:
        protocol.end_api_request()


GET_SERIAL_NUMBER = ApiCall[None, int](
    formatter=lambda _: CMD_GET_SERIAL_NUMBER,
    parser=lambda response: int(response),
)


async def get_serial_number(
    protocol: BidirectionalProtocol, serial_number: Optional[int] = None
) -> int:
    async with call_api(GET_SERIAL_NUMBER, protocol, serial_number) as f:
        return await f(None)


SET_DATE_AND_TIME = ApiCall[datetime, bool](
    formatter=lambda dt: f"{CMD_SET_DATE_AND_TIME}{dt.strftime('%y,%m,%d,%H,%M,%S')}\r",
    parser=lambda response: response == "DTM\r\n",
)
SET_PACKET_FORMAT = ApiCall[int, bool](
    formatter=lambda pf: f"{CMD_SET_PACKET_FORMAT}{pf:02}",
    parser=lambda response: response == "PKT\r\n",
)
SET_PACKET_SEND_INTERVAL = ApiCall[int, bool](
    formatter=lambda si: f"{CMD_SET_PACKET_SEND_INTERVAL}{si:03}",
    parser=lambda response: response == "IVL\r\n",
)
SET_SECONDARY_PACKET_FORMAT = ApiCall[int, bool](
    formatter=lambda pf: f"{CMD_SET_SECONDARY_PACKET_FORMAT}{pf:02}",
    parser=lambda response: response == "PKF\r\n",
)


async def set_date_and_time(
    protocol: BidirectionalProtocol, time: datetime, serial_number: Optional[int] = None
) -> bool:
    async with call_api(SET_DATE_AND_TIME, protocol, serial_number) as f:
        return await f(time)


async def set_packet_format(
    protocol: BidirectionalProtocol,
    format: PacketFormatType,
    serial_number: Optional[int] = None,
) -> bool:
    async with call_api(SET_PACKET_FORMAT, protocol, serial_number) as f:
        return await f(format)


async def set_packet_send_interval(
    protocol: BidirectionalProtocol,
    send_interval_seconds: int,
    serial_number: Optional[int] = None,
) -> bool:
    if send_interval_seconds < 0 or send_interval_seconds > 256:
        raise ValueError("send_interval must be a postive number no greater than 256")
    async with call_api(SET_PACKET_SEND_INTERVAL, protocol, serial_number) as f:
        return await f(send_interval_seconds)


async def set_secondary_packet_format(
    protocol: BidirectionalProtocol,
    format: PacketFormatType,
    serial_number: Optional[int] = None,
) -> bool:
    async with call_api(SET_SECONDARY_PACKET_FORMAT, protocol, serial_number) as f:
        return await f(format)


async def synchronize_time(
    protocol: BidirectionalProtocol, serial_number: Optional[int] = None
) -> bool:
    """
    Synchronizes the clock on the device to the time on the local device, accounting for the
    time waited for packets to clear.
    """
    time = datetime.now() + PACKET_DELAY_CLEAR_TIME
    return await set_date_and_time(protocol, time, serial_number)
