# Copyright 2016 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from collections.abc import Callable
from types import TracebackType
from typing import Generic, List, Optional, Type, TypeVar, Union

from rclpy.callback_groups import CallbackGroup
from rclpy.duration import Duration
from rclpy.event_handler import EventHandler, PublisherEventCallbacks
from rclpy.impl.implementation_singleton import rclpy_implementation as _rclpy
from rclpy.qos import QoSProfile
from rclpy.type_support import MsgT

from typing_extensions import Self


# Left to support Legacy TypeVars.
MsgType = TypeVar('MsgType')


class BasePublisher(Generic[MsgT]):

    def __init__(
        self,
        publisher_impl: '_rclpy.Publisher[MsgT]',
        msg_type: Type[MsgT],
        topic: str,
        qos_profile: QoSProfile,
        *,
        on_destroy: Optional[Callable[['BasePublisher[MsgT]'], None]] = None,
    ) -> None:
        """
        Create a container for a ROS publisher.

        .. warning:: Users should not create a publisher with this constructor, instead they
           should call :meth:`.Node.create_publisher` or :meth:`.AsyncNode.create_publisher`.
        """
        self.__publisher = publisher_impl
        self.msg_type = msg_type
        self.topic = topic
        self.qos_profile = qos_profile
        self._on_destroy = on_destroy
        self._destroyed = False

    def publish(self, msg: Union[MsgT, bytes]) -> None:
        """
        Send a message to the topic for the publisher.

        :param msg: The ROS message to publish.
        :raises: TypeError if the type of the passed message isn't an instance
          of the provided type when the publisher was constructed.
        """
        with self.handle:
            if isinstance(msg, self.msg_type):
                self.__publisher.publish(msg)
            elif isinstance(msg, bytes):
                self.__publisher.publish_raw(msg)
            else:
                raise TypeError('Expected {}, got {}'.format(self.msg_type, type(msg)))

    def get_subscription_count(self) -> int:
        """Get the amount of subscribers that this publisher has."""
        with self.handle:
            return self.__publisher.get_subscription_count()

    @property
    def topic_name(self) -> str:
        with self.handle:
            return self.__publisher.get_topic_name()

    @property
    def handle(self) -> '_rclpy.Publisher[MsgT]':
        return self.__publisher

    @property
    def logger_name(self) -> str:
        """Get the name of the logger associated with the node of the publisher."""
        with self.handle:
            return self.__publisher.get_logger_name()

    def destroy(self) -> None:
        """Destroy the publisher, notifying the owning node and releasing the handle."""
        if self._destroyed:
            return
        self._destroyed = True
        if self._on_destroy is not None:
            self._on_destroy(self)
            self._on_destroy = None
        self._destroy()

    def _destroy(self) -> None:
        self.handle.destroy_when_not_in_use()

    def assert_liveliness(self) -> None:
        """
        Manually assert that this Publisher is alive.

        If the QoS Liveliness policy is set to MANUAL_BY_TOPIC, the
        application must call this at least as often as ``QoSProfile.liveliness_lease_duration``.
        """
        with self.handle:
            _rclpy.rclpy_assert_liveliness(self.handle)

    def __enter__(self) -> Self:
        return self

    def __exit__(
        self,
        exc_type: Optional[Type[BaseException]],
        exc_val: Optional[BaseException],
        exc_tb: Optional[TracebackType],
    ) -> None:
        self.destroy()


class Publisher(BasePublisher[MsgT], Generic[MsgT]):

    def __init__(
        self,
        publisher_impl: '_rclpy.Publisher[MsgT]',
        msg_type: Type[MsgT],
        topic: str,
        qos_profile: QoSProfile,
        *,
        on_destroy: Optional[Callable[['Publisher[MsgT]'], None]] = None,
        event_callbacks: PublisherEventCallbacks,
        callback_group: CallbackGroup,
    ) -> None:
        """
        Create a container for a ROS publisher.

        .. warning:: Users should not create a publisher with this constructor, instead they should
           call :meth:`.Node.create_publisher`.

        A publisher is used as a primary means of communication in a ROS system by publishing
        messages on a ROS topic.

        :param publisher_impl: Publisher wrapping the underlying ``rcl_publisher_t`` object.
        :param msg_type: The type of ROS messages the publisher will publish.
        :param topic: The name of the topic the publisher will publish to.
        :param qos_profile: The quality of service profile to apply to the publisher.
        """
        super().__init__(
            publisher_impl=publisher_impl,
            msg_type=msg_type,
            topic=topic,
            qos_profile=qos_profile,
        )
        self._on_destroy = on_destroy

        self.event_handlers: List[EventHandler] = event_callbacks.create_event_handlers(
            callback_group, publisher_impl, topic)

    def wait_for_all_acked(self, timeout: Duration = Duration(seconds=-1)) -> bool:
        """
        Wait until all published message data is acknowledged or until the timeout elapses.

        If the timeout is negative then this function will block indefinitely until all published
        message data is acknowledged.
        If the timeout is 0 then it will check if all published message has been acknowledged
        without waiting.
        If the timeout is greater than 0 then it will return after that period of time has elapsed
        or all published message data is acknowledged.
        This function only waits for acknowledgments if the publisher's QOS profile is RELIABLE.
        Otherwise this function will immediately return true.

        :param timeout: the duration to wait for all published message data to be acknowledged.
        :returns: true if all published message data is acknowledged before the timeout, otherwise
            false.
        """
        with self.handle:
            return self.handle.wait_for_all_acked(timeout._duration_handle)

    def _destroy(self) -> None:
        for handler in self.event_handlers:
            handler.destroy()
        super()._destroy()
