# Copyright 2020-2026 Ternaris
# SPDX-License-Identifier: Apache-2.0
"""Check and benchmark rosbag2 read implementations."""

from __future__ import annotations

import sys
from math import isnan
from pathlib import Path
from timeit import timeit
from typing import TYPE_CHECKING, cast

import numpy as np
from rclpy.serialization import deserialize_message  # type: ignore[import-not-found]
from rosbag2_py import (  # type: ignore[import-not-found]
    ConverterOptions,
    SequentialReader,
    StorageOptions,
)
from rosidl_runtime_py.utilities import get_message  # type: ignore[import-not-found]

from rosbags.rosbag2 import Reader
from rosbags.typesys import Stores, get_typestore

if TYPE_CHECKING:
    from collections.abc import Generator
    from typing import Protocol

    class NativeMSG(Protocol):
        """Minimal native ROS message interface used for benchmark."""

        def get_fields_and_field_types(self) -> dict[str, str]:
            """Introspect message type."""
            raise NotImplementedError


class ReaderPy:
    """Minimal shim using rosbag2_py to emulate rosbag2 API."""

    def __init__(self, path: Path) -> None:
        """Initialize reader shim."""
        soptions = StorageOptions(str(path), 'sqlite3')
        coptions = ConverterOptions('', '')
        self.reader = SequentialReader()
        self.reader.open(soptions, coptions)
        self.typemap = {x.name: x.type for x in self.reader.get_all_topics_and_types()}

    def messages(self) -> Generator[tuple[str, str, int, bytes], None, None]:
        """Expose rosbag2 like generator behavior."""
        while self.reader.has_next():
            topic, data, timestamp = self.reader.read_next()
            yield topic, self.typemap[topic], timestamp, data


def deserialize_py(data: bytes, msgtype: str) -> NativeMSG:
    """Deserialization helper for rosidl_runtime_py + rclpy."""
    pytype = get_message(msgtype)
    return cast('NativeMSG', deserialize_message(data, pytype))


def compare_msg(lite: object, native: NativeMSG) -> None:
    """Compare rosbag2 (lite) vs rosbag2_py (native) message content.

    Args:
        lite: Message from rosbag2.
        native: Message from rosbag2_py.

    Raises:
        AssertionError: If messages are not identical.

    """
    for fieldname in native.get_fields_and_field_types():
        native_val = getattr(native, fieldname)
        lite_val = getattr(lite, fieldname)

        if hasattr(lite_val, '__dataclass_fields__'):
            compare_msg(lite_val, native_val)

        elif isinstance(lite_val, np.ndarray):
            assert not (native_val != lite_val).any(), f'{fieldname}: {native_val} != {lite_val}'

        elif isinstance(lite_val, list):
            assert len(native_val) == len(lite_val), f'{fieldname} length mismatch'
            for sub1, sub2 in zip(native_val, lite_val, strict=False):
                compare_msg(sub2, sub1)
        elif isinstance(lite_val, float) and isnan(lite_val):
            assert isnan(native_val)
        else:
            assert native_val == lite_val, f'{fieldname}: {native_val} != {lite_val}'


def compare(path: Path) -> None:
    """Compare raw and deserialized messages."""
    typestore = get_typestore(Stores.ROS2_FOXY)
    with Reader(path) as reader:
        gens = (reader.messages(), ReaderPy(path).messages())
        for item, item_py in zip(*gens, strict=False):
            connection, timestamp, data = item
            topic_py, msgtype_py, timestamp_py, data_py = item_py

            assert connection.topic == topic_py
            assert connection.msgtype == msgtype_py
            assert timestamp == timestamp_py
            assert data == data_py

            msg_py = deserialize_py(data_py, msgtype_py)
            msg = typestore.deserialize_cdr(data, connection.msgtype)

            compare_msg(msg, msg_py)
        assert not list(gens[0])
        assert not list(gens[1])


def read_deser_rosbag2_py(path: Path) -> None:
    """Read testbag with rosbag2_py."""
    soptions = StorageOptions(str(path), 'sqlite3')
    coptions = ConverterOptions('', '')
    reader = SequentialReader()
    reader.open(soptions, coptions)
    typemap = {x.name: x.type for x in reader.get_all_topics_and_types()}

    while reader.has_next():
        topic, rawdata, _ = reader.read_next()
        msgtype = typemap[topic]
        pytype = get_message(msgtype)
        deserialize_message(rawdata, pytype)


def read_deser_rosbag2(path: Path) -> None:
    """Read testbag with rosbag2lite."""
    typestore = get_typestore(Stores.ROS2_FOXY)
    with Reader(path) as reader:
        for connection, _, data in reader.messages():
            typestore.deserialize_cdr(data, connection.msgtype)


def main() -> None:
    """Benchmark rosbag2 against rosbag2_py."""
    path = Path(sys.argv[1])
    try:
        print('Comparing messages from rosbag2 and rosbag2_py.')  # noqa: T201
        compare(path)
    except AssertionError as err:
        print(f'Comparison failed {err!r}')  # noqa: T201
        sys.exit(1)

    print('Measuring execution times of rosbag2 and rosbag2_py.')  # noqa: T201
    time_py = timeit(lambda: read_deser_rosbag2_py(path), number=1)
    time = timeit(lambda: read_deser_rosbag2(path), number=1)
    print(  # noqa: T201
        f'Processing times:\n'
        f'rosbag2_py {time_py:.3f}\n'
        f'rosbag2    {time:.3f}\n'
        f'speedup    {time_py / time:.2f}\n',
    )


if __name__ == '__main__':
    main()
