"""Helpers for sun events."""
import datetime
from typing import Optional, Union, TYPE_CHECKING

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

if TYPE_CHECKING:
    import astral  # pylint: disable=unused-import

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

    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

    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 = getattr(location, event)(
                dt_util.as_local(utc_point_in_time).date() +
                datetime.timedelta(days=mod),
                local=False) + offset  # type: datetime.datetime
            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

    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
