"""

Contains the `Timer` class.
Timer objects are created by [set_interval][textual.message_pump.MessagePump.set_interval] or
    [set_timer][textual.message_pump.MessagePump.set_timer].
"""

from __future__ import annotations

import weakref
from asyncio import CancelledError, Event, Task, create_task, gather
from typing import Any, Awaitable, Callable, Iterable, Union

from rich.repr import Result, rich_repr

from textual import _time, events
from textual._callback import invoke
from textual._compat import cached_property
from textual._context import active_app
from textual._time import sleep
from textual._types import MessageTarget

TimerCallback = Union[Callable[[], Awaitable[Any]], Callable[[], Any]]
"""Type of valid callbacks to be used with timers."""


class EventTargetGone(Exception):
    """Raised if the timer event target has been deleted prior to the timer event being sent."""


@rich_repr
class Timer:
    """A class to send timer-based events.

    Args:
        event_target: The object which will receive the timer events.
        interval: The time between timer events, in seconds.
        name: A name to assign the event (for debugging).
        callback: An optional callback to invoke when the event is handled.
        repeat: The number of times to repeat the timer, or None to repeat forever.
        skip: Enable skipping of scheduled events that couldn't be sent in time.
        pause: Start the timer paused.
    """

    _timer_count: int = 1

    def __init__(
        self,
        event_target: MessageTarget,
        interval: float,
        *,
        name: str | None = None,
        callback: TimerCallback | None = None,
        repeat: int | None = None,
        skip: bool = True,
        pause: bool = False,
    ) -> None:
        self._target_repr = repr(event_target)
        self._target = weakref.ref(event_target)
        self._interval = interval
        self.name = f"Timer#{self._timer_count}" if name is None else name
        self._timer_count += 1
        self._callback = callback
        self._repeat = repeat
        self._skip = skip
        self._task: Task | None = None
        self._reset: bool = False
        self._original_pause = pause

    @cached_property
    def _active(self) -> Event:
        event = Event()
        if not self._original_pause:
            event.set()
        return event

    def __rich_repr__(self) -> Result:
        yield self._interval
        yield "name", self.name
        yield "repeat", self._repeat, None

    @property
    def target(self) -> MessageTarget:
        target = self._target()
        if target is None:
            raise EventTargetGone()
        return target

    def _start(self) -> None:
        """Start the timer."""
        self._task = create_task(self._run_timer(), name=self.name)

    def stop(self) -> None:
        """Stop the timer."""
        if self._task is None:
            return

        self._active.set()
        self._task.cancel()
        self._task = None

    @classmethod
    async def _stop_all(cls, timers: Iterable[Timer]) -> None:
        """Stop a number of timers, and await their completion.

        Args:
            timers: A number of timers.
        """

        async def stop_timer(timer: Timer) -> None:
            """Stop a timer and wait for it to finish.

            Args:
                timer: A Timer instance.
            """
            if timer._task is not None:
                timer._active.set()
                timer._task.cancel()
                try:
                    await timer._task
                except CancelledError:
                    pass
                timer._task = None

        await gather(*[stop_timer(timer) for timer in list(timers)])

    def pause(self) -> None:
        """Pause the timer.

        A paused timer will not send events until it is resumed.
        """
        self._active.clear()

    def reset(self) -> None:
        """Reset the timer, so it starts from the beginning."""
        self._active.set()
        self._reset = True

    def resume(self) -> None:
        """Resume a paused timer."""
        self._active.set()

    async def _run_timer(self) -> None:
        """Run the timer task."""
        try:
            await self._run()
        except CancelledError:
            pass

    async def _run(self) -> None:
        """Run the timer."""
        count = 0
        _repeat = self._repeat
        _interval = self._interval
        self._active  # Force instantiation in same thread
        await self._active.wait()
        start = _time.get_time()

        while _repeat is None or count <= _repeat:
            next_timer = start + ((count + 1) * _interval)
            now = _time.get_time()
            if self._skip and next_timer < now:
                count = int((now - start) / _interval + 1)
                continue
            now = _time.get_time()
            wait_time = max(0, next_timer - now)
            await sleep(wait_time)
            count += 1
            await self._active.wait()
            if self._reset:
                start = _time.get_time()
                count = 0
                self._reset = False
                continue
            try:
                await self._tick(next_timer=next_timer, count=count)
            except EventTargetGone:
                break

    async def _tick(self, *, next_timer: float, count: int) -> None:
        """Triggers the Timer's action: either call its callback, or sends an event to its target"""

        app = active_app.get()
        if app._exit:
            return

        if self._callback is not None:
            try:
                await invoke(self._callback)
            except CancelledError:
                # https://github.com/Textualize/textual/pull/2895
                # Re-raise CancelledErrors that would be caught by the following exception block in Python 3.7
                raise
            except Exception as error:
                app._handle_exception(error)
        else:
            event = events.Timer(
                timer=self,
                time=next_timer,
                count=count,
                callback=self._callback,
            )
            self.target.post_message(event)
