"""Support for Buienradar.nl weather service."""
import logging

from buienradar.constants import (
    ATTRIBUTION,
    CONDCODE,
    CONDITION,
    DETAILED,
    EXACT,
    EXACTNL,
    FORECAST,
    IMAGE,
    MEASURED,
    PRECIPITATION_FORECAST,
    STATIONNAME,
    TIMEFRAME,
    VISIBILITY,
    WINDGUST,
    WINDSPEED,
)
import voluptuous as vol

from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
    ATTR_ATTRIBUTION,
    CONF_LATITUDE,
    CONF_LONGITUDE,
    CONF_MONITORED_CONDITIONS,
    CONF_NAME,
    DEGREE,
    IRRADIATION_WATTS_PER_SQUARE_METER,
    LENGTH_KILOMETERS,
    SPEED_KILOMETERS_PER_HOUR,
    TEMP_CELSIUS,
    TIME_HOURS,
    UNIT_PERCENTAGE,
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import dt as dt_util

from .const import DEFAULT_TIMEFRAME
from .util import BrData

_LOGGER = logging.getLogger(__name__)

MEASURED_LABEL = "Measured"
TIMEFRAME_LABEL = "Timeframe"
SYMBOL = "symbol"

# Schedule next call after (minutes):
SCHEDULE_OK = 10
# When an error occurred, new call after (minutes):
SCHEDULE_NOK = 2

# Supported sensor types:
# Key: ['label', unit, icon]
SENSOR_TYPES = {
    "stationname": ["Stationname", None, None],
    # new in json api (>1.0.0):
    "barometerfc": ["Barometer value", None, "mdi:gauge"],
    # new in json api (>1.0.0):
    "barometerfcname": ["Barometer", None, "mdi:gauge"],
    # new in json api (>1.0.0):
    "barometerfcnamenl": ["Barometer", None, "mdi:gauge"],
    "condition": ["Condition", None, None],
    "conditioncode": ["Condition code", None, None],
    "conditiondetailed": ["Detailed condition", None, None],
    "conditionexact": ["Full condition", None, None],
    "symbol": ["Symbol", None, None],
    # new in json api (>1.0.0):
    "feeltemperature": ["Feel temperature", TEMP_CELSIUS, "mdi:thermometer"],
    "humidity": ["Humidity", UNIT_PERCENTAGE, "mdi:water-percent"],
    "temperature": ["Temperature", TEMP_CELSIUS, "mdi:thermometer"],
    "groundtemperature": ["Ground temperature", TEMP_CELSIUS, "mdi:thermometer"],
    "windspeed": ["Wind speed", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy"],
    "windforce": ["Wind force", "Bft", "mdi:weather-windy"],
    "winddirection": ["Wind direction", None, "mdi:compass-outline"],
    "windazimuth": ["Wind direction azimuth", DEGREE, "mdi:compass-outline"],
    "pressure": ["Pressure", "hPa", "mdi:gauge"],
    "visibility": ["Visibility", LENGTH_KILOMETERS, None],
    "windgust": ["Wind gust", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy"],
    "precipitation": ["Precipitation", f"mm/{TIME_HOURS}", "mdi:weather-pouring"],
    "irradiance": ["Irradiance", IRRADIATION_WATTS_PER_SQUARE_METER, "mdi:sunglasses"],
    "precipitation_forecast_average": [
        "Precipitation forecast average",
        f"mm/{TIME_HOURS}",
        "mdi:weather-pouring",
    ],
    "precipitation_forecast_total": [
        "Precipitation forecast total",
        "mm",
        "mdi:weather-pouring",
    ],
    # new in json api (>1.0.0):
    "rainlast24hour": ["Rain last 24h", "mm", "mdi:weather-pouring"],
    # new in json api (>1.0.0):
    "rainlasthour": ["Rain last hour", "mm", "mdi:weather-pouring"],
    "temperature_1d": ["Temperature 1d", TEMP_CELSIUS, "mdi:thermometer"],
    "temperature_2d": ["Temperature 2d", TEMP_CELSIUS, "mdi:thermometer"],
    "temperature_3d": ["Temperature 3d", TEMP_CELSIUS, "mdi:thermometer"],
    "temperature_4d": ["Temperature 4d", TEMP_CELSIUS, "mdi:thermometer"],
    "temperature_5d": ["Temperature 5d", TEMP_CELSIUS, "mdi:thermometer"],
    "mintemp_1d": ["Minimum temperature 1d", TEMP_CELSIUS, "mdi:thermometer"],
    "mintemp_2d": ["Minimum temperature 2d", TEMP_CELSIUS, "mdi:thermometer"],
    "mintemp_3d": ["Minimum temperature 3d", TEMP_CELSIUS, "mdi:thermometer"],
    "mintemp_4d": ["Minimum temperature 4d", TEMP_CELSIUS, "mdi:thermometer"],
    "mintemp_5d": ["Minimum temperature 5d", TEMP_CELSIUS, "mdi:thermometer"],
    "rain_1d": ["Rain 1d", "mm", "mdi:weather-pouring"],
    "rain_2d": ["Rain 2d", "mm", "mdi:weather-pouring"],
    "rain_3d": ["Rain 3d", "mm", "mdi:weather-pouring"],
    "rain_4d": ["Rain 4d", "mm", "mdi:weather-pouring"],
    "rain_5d": ["Rain 5d", "mm", "mdi:weather-pouring"],
    # new in json api (>1.0.0):
    "minrain_1d": ["Minimum rain 1d", "mm", "mdi:weather-pouring"],
    "minrain_2d": ["Minimum rain 2d", "mm", "mdi:weather-pouring"],
    "minrain_3d": ["Minimum rain 3d", "mm", "mdi:weather-pouring"],
    "minrain_4d": ["Minimum rain 4d", "mm", "mdi:weather-pouring"],
    "minrain_5d": ["Minimum rain 5d", "mm", "mdi:weather-pouring"],
    # new in json api (>1.0.0):
    "maxrain_1d": ["Maximum rain 1d", "mm", "mdi:weather-pouring"],
    "maxrain_2d": ["Maximum rain 2d", "mm", "mdi:weather-pouring"],
    "maxrain_3d": ["Maximum rain 3d", "mm", "mdi:weather-pouring"],
    "maxrain_4d": ["Maximum rain 4d", "mm", "mdi:weather-pouring"],
    "maxrain_5d": ["Maximum rain 5d", "mm", "mdi:weather-pouring"],
    "rainchance_1d": ["Rainchance 1d", UNIT_PERCENTAGE, "mdi:weather-pouring"],
    "rainchance_2d": ["Rainchance 2d", UNIT_PERCENTAGE, "mdi:weather-pouring"],
    "rainchance_3d": ["Rainchance 3d", UNIT_PERCENTAGE, "mdi:weather-pouring"],
    "rainchance_4d": ["Rainchance 4d", UNIT_PERCENTAGE, "mdi:weather-pouring"],
    "rainchance_5d": ["Rainchance 5d", UNIT_PERCENTAGE, "mdi:weather-pouring"],
    "sunchance_1d": ["Sunchance 1d", UNIT_PERCENTAGE, "mdi:weather-partly-cloudy"],
    "sunchance_2d": ["Sunchance 2d", UNIT_PERCENTAGE, "mdi:weather-partly-cloudy"],
    "sunchance_3d": ["Sunchance 3d", UNIT_PERCENTAGE, "mdi:weather-partly-cloudy"],
    "sunchance_4d": ["Sunchance 4d", UNIT_PERCENTAGE, "mdi:weather-partly-cloudy"],
    "sunchance_5d": ["Sunchance 5d", UNIT_PERCENTAGE, "mdi:weather-partly-cloudy"],
    "windforce_1d": ["Wind force 1d", "Bft", "mdi:weather-windy"],
    "windforce_2d": ["Wind force 2d", "Bft", "mdi:weather-windy"],
    "windforce_3d": ["Wind force 3d", "Bft", "mdi:weather-windy"],
    "windforce_4d": ["Wind force 4d", "Bft", "mdi:weather-windy"],
    "windforce_5d": ["Wind force 5d", "Bft", "mdi:weather-windy"],
    "windspeed_1d": ["Wind speed 1d", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy"],
    "windspeed_2d": ["Wind speed 2d", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy"],
    "windspeed_3d": ["Wind speed 3d", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy"],
    "windspeed_4d": ["Wind speed 4d", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy"],
    "windspeed_5d": ["Wind speed 5d", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy"],
    "winddirection_1d": ["Wind direction 1d", None, "mdi:compass-outline"],
    "winddirection_2d": ["Wind direction 2d", None, "mdi:compass-outline"],
    "winddirection_3d": ["Wind direction 3d", None, "mdi:compass-outline"],
    "winddirection_4d": ["Wind direction 4d", None, "mdi:compass-outline"],
    "winddirection_5d": ["Wind direction 5d", None, "mdi:compass-outline"],
    "windazimuth_1d": ["Wind direction azimuth 1d", DEGREE, "mdi:compass-outline"],
    "windazimuth_2d": ["Wind direction azimuth 2d", DEGREE, "mdi:compass-outline"],
    "windazimuth_3d": ["Wind direction azimuth 3d", DEGREE, "mdi:compass-outline"],
    "windazimuth_4d": ["Wind direction azimuth 4d", DEGREE, "mdi:compass-outline"],
    "windazimuth_5d": ["Wind direction azimuth 5d", DEGREE, "mdi:compass-outline"],
    "condition_1d": ["Condition 1d", None, None],
    "condition_2d": ["Condition 2d", None, None],
    "condition_3d": ["Condition 3d", None, None],
    "condition_4d": ["Condition 4d", None, None],
    "condition_5d": ["Condition 5d", None, None],
    "conditioncode_1d": ["Condition code 1d", None, None],
    "conditioncode_2d": ["Condition code 2d", None, None],
    "conditioncode_3d": ["Condition code 3d", None, None],
    "conditioncode_4d": ["Condition code 4d", None, None],
    "conditioncode_5d": ["Condition code 5d", None, None],
    "conditiondetailed_1d": ["Detailed condition 1d", None, None],
    "conditiondetailed_2d": ["Detailed condition 2d", None, None],
    "conditiondetailed_3d": ["Detailed condition 3d", None, None],
    "conditiondetailed_4d": ["Detailed condition 4d", None, None],
    "conditiondetailed_5d": ["Detailed condition 5d", None, None],
    "conditionexact_1d": ["Full condition 1d", None, None],
    "conditionexact_2d": ["Full condition 2d", None, None],
    "conditionexact_3d": ["Full condition 3d", None, None],
    "conditionexact_4d": ["Full condition 4d", None, None],
    "conditionexact_5d": ["Full condition 5d", None, None],
    "symbol_1d": ["Symbol 1d", None, None],
    "symbol_2d": ["Symbol 2d", None, None],
    "symbol_3d": ["Symbol 3d", None, None],
    "symbol_4d": ["Symbol 4d", None, None],
    "symbol_5d": ["Symbol 5d", None, None],
}

CONF_TIMEFRAME = "timeframe"

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
    {
        vol.Optional(
            CONF_MONITORED_CONDITIONS, default=["symbol", "temperature"]
        ): vol.All(cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES.keys())]),
        vol.Inclusive(
            CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together"
        ): cv.latitude,
        vol.Inclusive(
            CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together"
        ): cv.longitude,
        vol.Optional(CONF_TIMEFRAME, default=DEFAULT_TIMEFRAME): vol.All(
            vol.Coerce(int), vol.Range(min=5, max=120)
        ),
        vol.Optional(CONF_NAME, default="br"): cv.string,
    }
)


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
    """Create the buienradar sensor."""

    latitude = config.get(CONF_LATITUDE, hass.config.latitude)
    longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
    timeframe = config[CONF_TIMEFRAME]

    if None in (latitude, longitude):
        _LOGGER.error("Latitude or longitude not set in Home Assistant config")
        return False

    coordinates = {CONF_LATITUDE: float(latitude), CONF_LONGITUDE: float(longitude)}

    _LOGGER.debug(
        "Initializing buienradar sensor coordinate %s, timeframe %s",
        coordinates,
        timeframe,
    )

    dev = []
    for sensor_type in config[CONF_MONITORED_CONDITIONS]:
        dev.append(BrSensor(sensor_type, config.get(CONF_NAME), coordinates))
    async_add_entities(dev)

    data = BrData(hass, coordinates, timeframe, dev)
    # schedule the first update in 1 minute from now:
    await data.schedule_update(1)


class BrSensor(Entity):
    """Representation of an Buienradar sensor."""

    def __init__(self, sensor_type, client_name, coordinates):
        """Initialize the sensor."""

        self.client_name = client_name
        self._name = SENSOR_TYPES[sensor_type][0]
        self.type = sensor_type
        self._state = None
        self._unit_of_measurement = SENSOR_TYPES[self.type][1]
        self._entity_picture = None
        self._attribution = None
        self._measured = None
        self._stationname = None
        self._unique_id = self.uid(coordinates)

        # All continuous sensors should be forced to be updated
        self._force_update = self.type != SYMBOL and not self.type.startswith(CONDITION)

        if self.type.startswith(PRECIPITATION_FORECAST):
            self._timeframe = None

    def uid(self, coordinates):
        """Generate a unique id using coordinates and sensor type."""
        # The combination of the location, name and sensor type is unique
        return "{:2.6f}{:2.6f}{}".format(
            coordinates[CONF_LATITUDE], coordinates[CONF_LONGITUDE], self.type
        )

    @callback
    def data_updated(self, data):
        """Update data."""
        if self._load_data(data) and self.hass:
            self.async_write_ha_state()

    @callback
    def _load_data(self, data):
        """Load the sensor with relevant data."""
        # Find sensor

        # Check if we have a new measurement,
        # otherwise we do not have to update the sensor
        if self._measured == data.get(MEASURED):
            return False

        self._attribution = data.get(ATTRIBUTION)
        self._stationname = data.get(STATIONNAME)
        self._measured = data.get(MEASURED)

        if (
            self.type.endswith("_1d")
            or self.type.endswith("_2d")
            or self.type.endswith("_3d")
            or self.type.endswith("_4d")
            or self.type.endswith("_5d")
        ):

            # update forcasting sensors:
            fcday = 0
            if self.type.endswith("_2d"):
                fcday = 1
            if self.type.endswith("_3d"):
                fcday = 2
            if self.type.endswith("_4d"):
                fcday = 3
            if self.type.endswith("_5d"):
                fcday = 4

            # update weather symbol & status text
            if self.type.startswith(SYMBOL) or self.type.startswith(CONDITION):
                try:
                    condition = data.get(FORECAST)[fcday].get(CONDITION)
                except IndexError:
                    _LOGGER.warning("No forecast for fcday=%s...", fcday)
                    return False

                if condition:
                    new_state = condition.get(CONDITION)
                    if self.type.startswith(SYMBOL):
                        new_state = condition.get(EXACTNL)
                    if self.type.startswith("conditioncode"):
                        new_state = condition.get(CONDCODE)
                    if self.type.startswith("conditiondetailed"):
                        new_state = condition.get(DETAILED)
                    if self.type.startswith("conditionexact"):
                        new_state = condition.get(EXACT)

                    img = condition.get(IMAGE)

                    if new_state != self._state or img != self._entity_picture:
                        self._state = new_state
                        self._entity_picture = img
                        return True
                return False

            if self.type.startswith(WINDSPEED):
                # hass wants windspeeds in km/h not m/s, so convert:
                try:
                    self._state = data.get(FORECAST)[fcday].get(self.type[:-3])
                    if self._state is not None:
                        self._state = round(self._state * 3.6, 1)
                    return True
                except IndexError:
                    _LOGGER.warning("No forecast for fcday=%s...", fcday)
                    return False

            # update all other sensors
            try:
                self._state = data.get(FORECAST)[fcday].get(self.type[:-3])
                return True
            except IndexError:
                _LOGGER.warning("No forecast for fcday=%s...", fcday)
                return False

        if self.type == SYMBOL or self.type.startswith(CONDITION):
            # update weather symbol & status text
            condition = data.get(CONDITION)
            if condition:
                if self.type == SYMBOL:
                    new_state = condition.get(EXACTNL)
                if self.type == CONDITION:
                    new_state = condition.get(CONDITION)
                if self.type == "conditioncode":
                    new_state = condition.get(CONDCODE)
                if self.type == "conditiondetailed":
                    new_state = condition.get(DETAILED)
                if self.type == "conditionexact":
                    new_state = condition.get(EXACT)

                img = condition.get(IMAGE)

                if new_state != self._state or img != self._entity_picture:
                    self._state = new_state
                    self._entity_picture = img
                    return True

            return False

        if self.type.startswith(PRECIPITATION_FORECAST):
            # update nested precipitation forecast sensors
            nested = data.get(PRECIPITATION_FORECAST)
            self._timeframe = nested.get(TIMEFRAME)
            self._state = nested.get(self.type[len(PRECIPITATION_FORECAST) + 1 :])
            return True

        if self.type == WINDSPEED or self.type == WINDGUST:
            # hass wants windspeeds in km/h not m/s, so convert:
            self._state = data.get(self.type)
            if self._state is not None:
                self._state = round(data.get(self.type) * 3.6, 1)
            return True

        if self.type == VISIBILITY:
            # hass wants visibility in km (not m), so convert:
            self._state = data.get(self.type)
            if self._state is not None:
                self._state = round(self._state / 1000, 1)
            return True

        # update all other sensors
        self._state = data.get(self.type)
        return True

    @property
    def attribution(self):
        """Return the attribution."""
        return self._attribution

    @property
    def unique_id(self):
        """Return the unique id."""
        return self._unique_id

    @property
    def name(self):
        """Return the name of the sensor."""
        return f"{self.client_name} {self._name}"

    @property
    def state(self):
        """Return the state of the device."""
        return self._state

    @property
    def should_poll(self):
        """No polling needed."""
        return False

    @property
    def entity_picture(self):
        """Weather symbol if type is symbol."""
        return self._entity_picture

    @property
    def device_state_attributes(self):
        """Return the state attributes."""

        if self.type.startswith(PRECIPITATION_FORECAST):
            result = {ATTR_ATTRIBUTION: self._attribution}
            if self._timeframe is not None:
                result[TIMEFRAME_LABEL] = "%d min" % (self._timeframe)

            return result

        result = {
            ATTR_ATTRIBUTION: self._attribution,
            SENSOR_TYPES["stationname"][0]: self._stationname,
        }
        if self._measured is not None:
            # convert datetime (Europe/Amsterdam) into local datetime
            local_dt = dt_util.as_local(self._measured)
            result[MEASURED_LABEL] = local_dt.strftime("%c")

        return result

    @property
    def unit_of_measurement(self):
        """Return the unit of measurement of this entity, if any."""
        return self._unit_of_measurement

    @property
    def icon(self):
        """Return possible sensor specific icon."""
        return SENSOR_TYPES[self.type][2]

    @property
    def force_update(self):
        """Return true for continuous sensors, false for discrete sensors."""
        return self._force_update
