"""
Module for managing a binary sensor.

A binary sensor can be:
* A switch in the wall (as in the thing you press to switch on the light)
* A motion detector
* A reed sensor for detecting of a window/door is opened or closed.
"""

from __future__ import annotations

from collections.abc import Iterator
from functools import partial
import time
from typing import TYPE_CHECKING, cast

from xknx.core import Task
from xknx.remote_value import GroupAddressesType, RemoteValueSwitch
from xknx.telegram.apci import GroupValueWrite

from .device import Device, DeviceCallbackType

if TYPE_CHECKING:
    from xknx.telegram import Telegram
    from xknx.xknx import XKNX


class BinarySensor(Device):
    """Class for binary sensor."""

    def __init__(
        self,
        xknx: XKNX,
        name: str,
        group_address_state: GroupAddressesType = None,
        invert: bool = False,
        sync_state: bool | int | float | str = True,
        ignore_internal_state: bool = False,
        reset_after: float | None = None,
        context_timeout: float | None = None,
        device_updated_cb: DeviceCallbackType[BinarySensor] | None = None,
        always_callback: bool = False,
    ) -> None:
        """Initialize BinarySensor class."""
        super().__init__(xknx, name, device_updated_cb)

        self.ignore_internal_state = ignore_internal_state or bool(context_timeout)
        self.always_callback = always_callback
        self.state: bool | None = None

        self._context_timeout = context_timeout
        self._count_set_on = 0
        self._count_set_off = 0
        self._last_set: float | None = None
        if reset_after is not None:
            self._reset_task: Task | None = Task(
                name=f"binary_sensor.reset_{id(self)}",
                target=partial(self._set_internal_state, False),
                wait_before_start=reset_after,
            )
        else:
            self._reset_task = None
        if context_timeout:
            self._context_task: Task | None = Task(
                name=f"binary_sensor.context_{id(self)}",
                target=partial(self._counter_task, context_timeout),
                wait_before_start=context_timeout,
            )
        else:
            self._context_task = None

        self.remote_value = RemoteValueSwitch(
            xknx,
            group_address_state=group_address_state,
            invert=invert,
            sync_state=sync_state,
            device_name=self.name,
            # after_update called internally
            after_update_cb=self._set_internal_state,
        )

    def _iter_remote_values(self) -> Iterator[RemoteValueSwitch]:
        """Iterate the devices RemoteValue classes."""
        yield self.remote_value

    def async_remove_tasks(self) -> None:
        """Remove async tasks of device."""
        if self._context_task:
            self.xknx.task_registry.remove_task(self._context_task)
        if self._reset_task:
            self.xknx.task_registry.remove_task(self._reset_task)

    @property
    def last_telegram(self) -> Telegram | None:
        """Return the last telegram received from the RemoteValue."""
        return self.remote_value.telegram

    def _set_internal_state(self, state: bool) -> None:
        """Set the internal state of the device and schedule callbacks."""
        if state != self.state or (
            self.ignore_internal_state
            and self.remote_value.telegram is not None
            and isinstance(self.remote_value.telegram.payload, GroupValueWrite)
        ):
            self.state = state

            if self.ignore_internal_state and self._context_task:
                self.bump_and_get_counter(state)
                self.xknx.task_registry.start_task(self._context_task)
            else:
                self.after_update()
        elif self.always_callback:
            self.after_update()

    async def _counter_task(self, wait_seconds: float) -> None:
        """Trigger when context window has passed once with counter values and once reset."""
        self.after_update()

        self._count_set_on = 0
        self._count_set_off = 0
        self.after_update()

    @property
    def counter(self) -> int | None:
        """Return current counter for sensor."""
        if self._context_timeout:
            return self._count_set_on if self.state else self._count_set_off
        return None

    def bump_and_get_counter(self, state: bool) -> int:
        """Bump counter and return the number of times a state was set to the same value within context_timeout."""

        def within_same_context() -> bool:
            """Check if state change was within same context (e.g. 'Button was pressed twice')."""
            if self._last_set is None:
                self._last_set = time.time()
                return False
            new_set_time = time.time()
            time_diff = new_set_time - self._last_set
            self._last_set = new_set_time
            return time_diff < cast(float, self._context_timeout)

        if within_same_context():
            if state:
                self._count_set_on = self._count_set_on + 1
                return self._count_set_on
            self._count_set_off = self._count_set_off + 1
            return self._count_set_off

        if state:
            self._count_set_on = 1
            self._count_set_off = 0
        else:
            self._count_set_on = 0
            self._count_set_off = 1
        return 1

    def process_group_write(self, telegram: Telegram) -> None:
        """Process incoming and outgoing GroupValueWrite telegram."""
        if self.remote_value.process(telegram, always_callback=True):
            self._process_reset_after()

    def process_group_response(self, telegram: Telegram) -> None:
        """Process incoming GroupValueResponse telegrams."""
        if self.remote_value.process(telegram, always_callback=self.always_callback):
            self._process_reset_after()

    def _process_reset_after(self) -> None:
        """Create Task for resetting state if 'reset_after' is configured."""
        if self._reset_task is not None and self.state:
            self.xknx.task_registry.start_task(self._reset_task)

    def is_on(self) -> bool:
        """Return if binary sensor is 'on'."""
        return bool(self.state)

    def is_off(self) -> bool:
        """Return if binary sensor is 'off'."""
        return not self.state

    def __str__(self) -> str:
        """Return object as readable string."""
        return (
            f'<BinarySensor name="{self.name}" '
            f"remote_value={self.remote_value.group_addr_str()} "
            f"state={self.state.__repr__()} />"
        )
