"""Helpers for sun events."""
from __future__ import annotations

import datetime
from typing import TYPE_CHECKING, Optional, Union

from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET
from homeassistant.core import callback
from homeassistant.loader import bind_hass
from homeassistant.util import dt as dt_util

from .typing import HomeAssistantType

if TYPE_CHECKING:
    import astral

DATA_LOCATION_CACHE = "astral_location_cache"


@callback
@bind_hass
def get_astral_location(hass: HomeAssistantType) -> astral.Location:
    """Get an astral location for the current Home Assistant configuration."""
    from astral import Location  # pylint: disable=import-outside-toplevel

    latitude = hass.config.latitude
    longitude = hass.config.longitude
    timezone = str(hass.config.time_zone)
    elevation = hass.config.elevation
    info = ("", "", latitude, longitude, timezone, elevation)

    # Cache astral locations so they aren't recreated with the same args
    if DATA_LOCATION_CACHE not in hass.data:
        hass.data[DATA_LOCATION_CACHE] = {}

    if info not in hass.data[DATA_LOCATION_CACHE]:
        hass.data[DATA_LOCATION_CACHE][info] = Location(info)

    return hass.data[DATA_LOCATION_CACHE][info]


@callback
@bind_hass
def get_astral_event_next(
    hass: HomeAssistantType,
    event: str,
    utc_point_in_time: Optional[datetime.datetime] = None,
    offset: Optional[datetime.timedelta] = None,
) -> datetime.datetime:
    """Calculate the next specified solar event."""
    location = get_astral_location(hass)
    return get_location_astral_event_next(location, event, utc_point_in_time, offset)


@callback
def get_location_astral_event_next(
    location: "astral.Location",
    event: str,
    utc_point_in_time: Optional[datetime.datetime] = None,
    offset: Optional[datetime.timedelta] = None,
) -> datetime.datetime:
    """Calculate the next specified solar event."""
    from astral import AstralError  # pylint: disable=import-outside-toplevel

    if offset is None:
        offset = datetime.timedelta()

    if utc_point_in_time is None:
        utc_point_in_time = dt_util.utcnow()

    mod = -1
    while True:
        try:
            next_dt: datetime.datetime = (
                getattr(location, event)(
                    dt_util.as_local(utc_point_in_time).date()
                    + datetime.timedelta(days=mod),
                    local=False,
                )
                + offset
            )
            if next_dt > utc_point_in_time:
                return next_dt
        except AstralError:
            pass
        mod += 1


@callback
@bind_hass
def get_astral_event_date(
    hass: HomeAssistantType,
    event: str,
    date: Union[datetime.date, datetime.datetime, None] = None,
) -> Optional[datetime.datetime]:
    """Calculate the astral event time for the specified date."""
    from astral import AstralError  # pylint: disable=import-outside-toplevel

    location = get_astral_location(hass)

    if date is None:
        date = dt_util.now().date()

    if isinstance(date, datetime.datetime):
        date = dt_util.as_local(date).date()

    try:
        return getattr(location, event)(date, local=False)  # type: ignore
    except AstralError:
        # Event never occurs for specified date.
        return None


@callback
@bind_hass
def is_up(
    hass: HomeAssistantType, utc_point_in_time: Optional[datetime.datetime] = None
) -> bool:
    """Calculate if the sun is currently up."""
    if utc_point_in_time is None:
        utc_point_in_time = dt_util.utcnow()

    next_sunrise = get_astral_event_next(hass, SUN_EVENT_SUNRISE, utc_point_in_time)
    next_sunset = get_astral_event_next(hass, SUN_EVENT_SUNSET, utc_point_in_time)

    return next_sunrise > next_sunset