from __future__ import annotations
import asyncio
import logging
import math
import time
from asyncio import Server
from struct import unpack, pack
from typing import Any, NoReturn, cast


from pybrotlib.components import BROTTelescope

log = logging.getLogger(__name__)

MAX_INT = 2147483648


class StellariumProtocol(asyncio.Protocol):
    def __init__(self, server: Stellarium):
        self.server = server

    def connection_made(self, transport: asyncio.transports.BaseTransport) -> None:
        log.info("Client connected.")
        self.transport = cast(asyncio.transports.WriteTransport, transport)
        self.server.register_client(self)

    def connection_lost(self, exc: Exception | None) -> None:
        log.info("Client disconnected.")
        self.server.unregister_client(self)

    def data_received(self, data: bytes) -> None:
        # unpack data
        request = unpack("<BBBBQIi", data)

        # calculate RA/Dec
        ra_int, dec_int = request[5], request[6]
        ra = ra_int / MAX_INT * 180
        dec = dec_int / MAX_INT * 180

        # move
        log.info(f"Received command to move telescope to RA={ra}, Dec={dec}.")
        asyncio.create_task(self.server.move_telescope(ra, dec))

    def send(self, ra: float, dec: float, status: int) -> None:
        # to integer
        ra_int = math.floor(0.5 + ra * MAX_INT / 180.0)
        dec_int = math.floor(0.5 + dec * MAX_INT / 180.0)

        # build data and send it
        data = pack("<BBBBQIii", 24, 0, 0, 0, int(time.time() * 1000000), ra_int, dec_int, 0)
        self.transport.write(data)


class Stellarium:
    """A stellarium telescope."""

    def __init__(self, telescope: BROTTelescope, host: str = "localhost", port: int = 10001):
        """Initialize a new stellarium telescope proxy.

        Args:
            telescope: Telescope to use.
            port: Port to be accessed by Stellarium.

        """

        # store
        self._telescope = telescope
        self._host = host
        self._port = port
        self._clients: list[StellariumProtocol] = []

        # server
        self._server: Server | None = None

        # background task
        self._send_task: asyncio.Task | None = None

    async def start(self) -> None:
        loop = asyncio.get_running_loop()
        self._server = await loop.create_server(lambda: StellariumProtocol(self), self._host, self._port)
        await self._server.start_serving()
        self._send_task = asyncio.create_task(self._send_updates())

    async def stop(self) -> None:
        """Close module."""
        if self._server is not None:
            self._server.close()
            await self._server.wait_closed()

    def register_client(self, client: StellariumProtocol) -> None:
        self._clients.append(client)

    def unregister_client(self, client: StellariumProtocol) -> None:
        self._clients.remove(client)

    async def move_telescope(self, ra: float, dec: float) -> None:
        asyncio.create_task(self._telescope.move_radec(ra, dec))

    async def _send_updates(self) -> NoReturn:
        """Send coordinates to clients."""

        while True:
            # send to all clients
            for client in self._clients:
                client.send(self._telescope.right_ascension * 15.0, self._telescope.declination, 0)

            # sleep a second
            await asyncio.sleep(0.1)


__all__ = ["Stellarium"]
