#! /usr/bin/env python3

import asyncio
import aioserial
import pyubx2
import io
import logging
import aioserial
import asyncio
import signal

from mqtthandler.command import command
from mqtthandler.handler import MQTTHandler, task

# The pyubx2 library spams the console with errors that aren't errors.
# I don't care if you failed to parse an incomplete buffer.
logging.getLogger("pyubx2").setLevel(logging.FATAL)


class UBXAsyncParser:
    def __init__(self):
        self.buffer = bytearray()

    def feed(self, data: bytes):
        self.buffer.extend(data)

    def parse(self):
        while True:
            stream = io.BytesIO(self.buffer)
            try:
                ubr = pyubx2.UBXReader(stream, parsing=True)
                raw, parsed = ubr.read()
                if raw is None:
                    return

                consumed = stream.tell()
                del self.buffer[:consumed]
                yield parsed

            except pyubx2.UBXStreamError:
                # incomplete frame
                return


class UBXHandler(MQTTHandler):
    def __init__(
        self,
        name: str,
        serial_port: aioserial.AioSerial,
    ):
        super().__init__(name)
        self.serial_port = serial_port

    @task
    async def handle_ubx_messages(self):

        async def read_serial():
            parser = UBXAsyncParser()
            while True:
                chunk = await self.serial_port.read_async(64)
                if not chunk:
                    await asyncio.sleep(0)
                    continue

                parser.feed(chunk)
                for message in parser.parse():
                    yield message

        async for message in read_serial():
            if isinstance(message, pyubx2.UBXMessage):
                for name, value in vars(message).items():
                    # Skip 'private' members
                    if name.startswith("_"):
                        continue

                    property = f"{message.identity}/{name}"
                    await self.set_property(property, value, qos=0, retain=True)
            else:
                # print("Unexpected response:", message)
                pass

    @command({"type": "number"}, "An example command", foo="bar")
    async def example_cmd(self, args):
        print(f"Executing command with args {args}")

    @command(
        {
            "type": "object",
            "properties": {
                "portID": {
                    "type": "integer",
                },
                "baudRate": {
                    "type": "integer",
                    "enum": [
                        110,
                        300,
                        600,
                        1200,
                        2400,
                        4800,
                        9600,
                        14400,
                        19200,
                        38400,
                        57600,
                        115200,
                        230400,
                        460800,
                        921600,
                    ],
                    "default": 9600,
                },
            },
            "required": ["portID", "baudRate"],
            "additionalProperties": False,
        },
        "Reconfigure the serial port for the UBX simulator.",
    )
    async def configure_port(self, args):
        message = pyubx2.UBXMessage(
            "CFG",
            "CFG-PRT",
            pyubx2.ubxtypes_core.SET,
            portID=args["portID"],
            baudRate=args["baudRate"],
        )
        num_bytes = await self.serial_port.write_async(message.serialize())
        return num_bytes

    @command(
        {
            "type": "object",
            "properties": {
                "measRate": {
                    "type": "integer",
                    "minimum": 50,
                    "maximum": 60000,
                    "description": "Measurement period in milliseconds",
                    "default": 1000,
                },
                "navRate": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 127,
                    "description": "Number of measurement cycles per navigation solution",
                    "default": 1,
                },
                "timeRef": {
                    "type": "integer",
                    "enum": [0, 1],
                    "description": "Time reference (0=UTC, 1=GPS)",
                },
            },
            "required": ["measRate", "navRate", "timeRef"],
            "additionalProperties": False,
        },
        "Reconfigure the rate properties for the UBX device.",
    )
    async def configure_rate(self, args):
        message = pyubx2.UBXMessage(
            "CFG",
            "CFG-RATE",
            pyubx2.ubxtypes_core.SET,
            measRate=args["measRate"],
            navRate=args["navRate"],
            timeRef=args["timeRef"],
        )
        num_bytes = await self.serial_port.write_async(message.serialize())
        return num_bytes


async def main():
    handler = UBXHandler(
        "example-gps",
        aioserial.AioSerial(
            port="/tmp/ttyV0",
            baudrate=115200,
            timeout=0.05,  # 50 ms
        ),
    )
    signal.signal(signal.SIGINT, lambda signum, frame: handler.stop())

    await handler.run(
        "127.0.0.1", port=1883, username="device", password="devicesecret"
    )


if __name__ == "__main__":
    asyncio.run(main())
