"""Support for monitoring a Sense energy sensor."""
from homeassistant.components.sensor import (
    STATE_CLASS_MEASUREMENT,
    STATE_CLASS_TOTAL_INCREASING,
    SensorEntity,
)
from homeassistant.const import (
    ATTR_ATTRIBUTION,
    DEVICE_CLASS_ENERGY,
    DEVICE_CLASS_POWER,
    ELECTRIC_POTENTIAL_VOLT,
    ENERGY_KILO_WATT_HOUR,
    PERCENTAGE,
    POWER_WATT,
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect

from .const import (
    ACTIVE_NAME,
    ACTIVE_TYPE,
    ATTRIBUTION,
    CONSUMPTION_ID,
    CONSUMPTION_NAME,
    DOMAIN,
    FROM_GRID_ID,
    FROM_GRID_NAME,
    ICON,
    MDI_ICONS,
    NET_PRODUCTION_ID,
    NET_PRODUCTION_NAME,
    PRODUCTION_ID,
    PRODUCTION_NAME,
    PRODUCTION_PCT_ID,
    PRODUCTION_PCT_NAME,
    SENSE_DATA,
    SENSE_DEVICE_UPDATE,
    SENSE_DEVICES_DATA,
    SENSE_DISCOVERED_DEVICES_DATA,
    SENSE_TRENDS_COORDINATOR,
    SOLAR_POWERED_ID,
    SOLAR_POWERED_NAME,
    TO_GRID_ID,
    TO_GRID_NAME,
)


class SensorConfig:
    """Data structure holding sensor configuration."""

    def __init__(self, name, sensor_type):
        """Sensor name and type to pass to API."""
        self.name = name
        self.sensor_type = sensor_type


# Sensor types/ranges
ACTIVE_SENSOR_TYPE = SensorConfig(ACTIVE_NAME, ACTIVE_TYPE)

# Sensor types/ranges
TRENDS_SENSOR_TYPES = {
    "daily": SensorConfig("Daily", "DAY"),
    "weekly": SensorConfig("Weekly", "WEEK"),
    "monthly": SensorConfig("Monthly", "MONTH"),
    "yearly": SensorConfig("Yearly", "YEAR"),
}

# Production/consumption variants
SENSOR_VARIANTS = [(PRODUCTION_ID, PRODUCTION_NAME), (CONSUMPTION_ID, CONSUMPTION_NAME)]

# Trend production/consumption variants
TREND_SENSOR_VARIANTS = SENSOR_VARIANTS + [
    (PRODUCTION_PCT_ID, PRODUCTION_PCT_NAME),
    (NET_PRODUCTION_ID, NET_PRODUCTION_NAME),
    (FROM_GRID_ID, FROM_GRID_NAME),
    (TO_GRID_ID, TO_GRID_NAME),
    (SOLAR_POWERED_ID, SOLAR_POWERED_NAME),
]


def sense_to_mdi(sense_icon):
    """Convert sense icon to mdi icon."""
    return "mdi:{}".format(MDI_ICONS.get(sense_icon, "power-plug"))


async def async_setup_entry(hass, config_entry, async_add_entities):
    """Set up the Sense sensor."""
    data = hass.data[DOMAIN][config_entry.entry_id][SENSE_DATA]
    sense_devices_data = hass.data[DOMAIN][config_entry.entry_id][SENSE_DEVICES_DATA]
    trends_coordinator = hass.data[DOMAIN][config_entry.entry_id][
        SENSE_TRENDS_COORDINATOR
    ]

    # Request only in case it takes longer
    # than 60s
    await trends_coordinator.async_request_refresh()

    sense_monitor_id = data.sense_monitor_id
    sense_devices = hass.data[DOMAIN][config_entry.entry_id][
        SENSE_DISCOVERED_DEVICES_DATA
    ]

    devices = [
        SenseEnergyDevice(sense_devices_data, device, sense_monitor_id)
        for device in sense_devices
        if device["tags"]["DeviceListAllowed"] == "true"
    ]

    for variant_id, variant_name in SENSOR_VARIANTS:
        name = ACTIVE_SENSOR_TYPE.name
        sensor_type = ACTIVE_SENSOR_TYPE.sensor_type

        unique_id = f"{sense_monitor_id}-active-{variant_id}"
        devices.append(
            SenseActiveSensor(
                data,
                name,
                sensor_type,
                sense_monitor_id,
                variant_id,
                variant_name,
                unique_id,
            )
        )

    for i in range(len(data.active_voltage)):
        devices.append(SenseVoltageSensor(data, i, sense_monitor_id))

    for type_id, typ in TRENDS_SENSOR_TYPES.items():
        for variant_id, variant_name in TREND_SENSOR_VARIANTS:
            name = typ.name
            sensor_type = typ.sensor_type

            unique_id = f"{sense_monitor_id}-{type_id}-{variant_id}"
            devices.append(
                SenseTrendsSensor(
                    data,
                    name,
                    sensor_type,
                    variant_id,
                    variant_name,
                    trends_coordinator,
                    unique_id,
                )
            )

    async_add_entities(devices)


class SenseActiveSensor(SensorEntity):
    """Implementation of a Sense energy sensor."""

    _attr_icon = ICON
    _attr_native_unit_of_measurement = POWER_WATT
    _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
    _attr_should_poll = False
    _attr_available = False
    _attr_state_class = STATE_CLASS_MEASUREMENT

    def __init__(
        self,
        data,
        name,
        sensor_type,
        sense_monitor_id,
        variant_id,
        variant_name,
        unique_id,
    ):
        """Initialize the Sense sensor."""
        self._attr_name = f"{name} {variant_name}"
        self._attr_unique_id = unique_id
        self._data = data
        self._sense_monitor_id = sense_monitor_id
        self._sensor_type = sensor_type
        self._variant_id = variant_id
        self._variant_name = variant_name

    async def async_added_to_hass(self):
        """Register callbacks."""
        self.async_on_remove(
            async_dispatcher_connect(
                self.hass,
                f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}",
                self._async_update_from_data,
            )
        )

    @callback
    def _async_update_from_data(self):
        """Update the sensor from the data. Must not do I/O."""
        new_state = round(
            self._data.active_solar_power
            if self._variant_id == PRODUCTION_ID
            else self._data.active_power
        )
        if self._attr_available and self._attr_native_value == new_state:
            return
        self._attr_native_value = new_state
        self._attr_available = True
        self.async_write_ha_state()


class SenseVoltageSensor(SensorEntity):
    """Implementation of a Sense energy voltage sensor."""

    _attr_native_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT
    _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
    _attr_icon = ICON
    _attr_should_poll = False
    _attr_available = False

    def __init__(
        self,
        data,
        index,
        sense_monitor_id,
    ):
        """Initialize the Sense sensor."""
        line_num = index + 1
        self._attr_name = f"L{line_num} Voltage"
        self._attr_unique_id = f"{sense_monitor_id}-L{line_num}"
        self._data = data
        self._sense_monitor_id = sense_monitor_id
        self._voltage_index = index

    async def async_added_to_hass(self):
        """Register callbacks."""
        self.async_on_remove(
            async_dispatcher_connect(
                self.hass,
                f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}",
                self._async_update_from_data,
            )
        )

    @callback
    def _async_update_from_data(self):
        """Update the sensor from the data. Must not do I/O."""
        new_state = round(self._data.active_voltage[self._voltage_index], 1)
        if self._attr_available and self._attr_native_value == new_state:
            return
        self._attr_available = True
        self._attr_native_value = new_state
        self.async_write_ha_state()


class SenseTrendsSensor(SensorEntity):
    """Implementation of a Sense energy sensor."""

    _attr_device_class = DEVICE_CLASS_ENERGY
    _attr_state_class = STATE_CLASS_TOTAL_INCREASING
    _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR
    _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
    _attr_icon = ICON
    _attr_should_poll = False

    def __init__(
        self,
        data,
        name,
        sensor_type,
        variant_id,
        variant_name,
        trends_coordinator,
        unique_id,
    ):
        """Initialize the Sense sensor."""
        self._attr_name = f"{name} {variant_name}"
        self._attr_unique_id = unique_id
        self._data = data
        self._sensor_type = sensor_type
        self._coordinator = trends_coordinator
        self._variant_id = variant_id
        self._had_any_update = False

        if variant_id in [PRODUCTION_PCT_ID, SOLAR_POWERED_ID]:
            self._attr_native_unit_of_measurement = PERCENTAGE
            self._attr_entity_registry_enabled_default = False
            self._attr_state_class = None
            self._attr_device_class = None

    @property
    def native_value(self):
        """Return the state of the sensor."""
        return round(self._data.get_trend(self._sensor_type, self._variant_id), 1)

    @property
    def available(self):
        """Return if entity is available."""
        return self._had_any_update and self._coordinator.last_update_success

    @callback
    def _async_update(self):
        """Track if we had an update so we do not report zero data."""
        self._had_any_update = True
        self.async_write_ha_state()

    async def async_update(self):
        """Update the entity.

        Only used by the generic entity update service.
        """
        await self._coordinator.async_request_refresh()

    async def async_added_to_hass(self):
        """When entity is added to hass."""
        self.async_on_remove(self._coordinator.async_add_listener(self._async_update))


class SenseEnergyDevice(SensorEntity):
    """Implementation of a Sense energy device."""

    _attr_available = False
    _attr_state_class = STATE_CLASS_MEASUREMENT
    _attr_native_unit_of_measurement = POWER_WATT
    _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
    _attr_device_class = DEVICE_CLASS_POWER
    _attr_should_poll = False

    def __init__(self, sense_devices_data, device, sense_monitor_id):
        """Initialize the Sense binary sensor."""
        self._attr_name = f"{device['name']} {CONSUMPTION_NAME}"
        self._id = device["id"]
        self._sense_monitor_id = sense_monitor_id
        self._attr_unique_id = f"{sense_monitor_id}-{self._id}-{CONSUMPTION_ID}"
        self._attr_icon = sense_to_mdi(device["icon"])
        self._sense_devices_data = sense_devices_data

    async def async_added_to_hass(self):
        """Register callbacks."""
        self.async_on_remove(
            async_dispatcher_connect(
                self.hass,
                f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}",
                self._async_update_from_data,
            )
        )

    @callback
    def _async_update_from_data(self):
        """Get the latest data, update state. Must not do I/O."""
        device_data = self._sense_devices_data.get_device_by_id(self._id)
        if not device_data or "w" not in device_data:
            new_state = 0
        else:
            new_state = int(device_data["w"])
        if self._attr_available and self._attr_native_value == new_state:
            return
        self._attr_native_value = new_state
        self._attr_available = True
        self.async_write_ha_state()
