"""Representation of Z-Wave binary_sensors."""
from openzwavemqtt.const import CommandClass, ValueIndex, ValueType

from homeassistant.components.binary_sensor import (
    DOMAIN as BINARY_SENSOR_DOMAIN,
    BinarySensorDeviceClass,
    BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import DATA_UNSUBSCRIBE, DOMAIN
from .entity import ZWaveDeviceEntity

NOTIFICATION_TYPE = "index"
NOTIFICATION_VALUES = "values"
NOTIFICATION_DEVICE_CLASS = "device_class"
NOTIFICATION_SENSOR_ENABLED = "enabled"
NOTIFICATION_OFF_VALUE = "off_value"

NOTIFICATION_VALUE_CLEAR = 0

# Translation from values in Notification CC to binary sensors
# https://github.com/OpenZWave/open-zwave/blob/master/config/NotificationCCTypes.xml
NOTIFICATION_SENSORS = [
    {
        # Index 1: Smoke Alarm - Value Id's 1 and 2
        # Assuming here that Value 1 and 2 are not present at the same time
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SMOKE_ALARM,
        NOTIFICATION_VALUES: [1, 2],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SMOKE,
    },
    {
        # Index 1: Smoke Alarm - All other Value Id's
        # Create as disabled sensors
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SMOKE_ALARM,
        NOTIFICATION_VALUES: [3, 4, 5, 6, 7, 8],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SMOKE,
        NOTIFICATION_SENSOR_ENABLED: False,
    },
    {
        # Index 2: Carbon Monoxide - Value Id's 1 and 2
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_MONOOXIDE,
        NOTIFICATION_VALUES: [1, 2],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS,
    },
    {
        # Index 2: Carbon Monoxide - All other Value Id's
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_MONOOXIDE,
        NOTIFICATION_VALUES: [4, 5, 7],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS,
        NOTIFICATION_SENSOR_ENABLED: False,
    },
    {
        # Index 3: Carbon Dioxide - Value Id's 1 and 2
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_DIOXIDE,
        NOTIFICATION_VALUES: [1, 2],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS,
    },
    {
        # Index 3: Carbon Dioxide - All other Value Id's
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_DIOXIDE,
        NOTIFICATION_VALUES: [4, 5, 7],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS,
        NOTIFICATION_SENSOR_ENABLED: False,
    },
    {
        # Index 4: Heat - Value Id's 1, 2, 5, 6 (heat/underheat)
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HEAT,
        NOTIFICATION_VALUES: [1, 2, 5, 6],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.HEAT,
    },
    {
        # Index 4: Heat - All other Value Id's
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HEAT,
        NOTIFICATION_VALUES: [3, 4, 8, 10, 11],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.HEAT,
        NOTIFICATION_SENSOR_ENABLED: False,
    },
    {
        # Index 5: Water - Value Id's 1, 2, 3, 4
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WATER,
        NOTIFICATION_VALUES: [1, 2, 3, 4],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.MOISTURE,
    },
    {
        # Index 5: Water - All other Value Id's
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WATER,
        NOTIFICATION_VALUES: [5],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.MOISTURE,
        NOTIFICATION_SENSOR_ENABLED: False,
    },
    {
        # Index 6: Access Control - Value Id's 1, 2, 3, 4 (Lock)
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_ACCESS_CONTROL,
        NOTIFICATION_VALUES: [1, 2, 3, 4],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.LOCK,
    },
    {
        # Index 6: Access Control - Value Id 22 (door/window open)
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_ACCESS_CONTROL,
        NOTIFICATION_VALUES: [22],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.DOOR,
        NOTIFICATION_OFF_VALUE: 23,
    },
    {
        # Index 7: Home Security - Value Id's 1, 2 (intrusion)
        # Assuming that value 1 and 2 are not present at the same time
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY,
        NOTIFICATION_VALUES: [1, 2],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SAFETY,
    },
    {
        # Index 7: Home Security - Value Id's 3, 4, 9 (tampering)
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY,
        NOTIFICATION_VALUES: [3, 4, 9],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SAFETY,
    },
    {
        # Index 7: Home Security - Value Id's 5, 6 (glass breakage)
        # Assuming that value 5 and 6 are not present at the same time
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY,
        NOTIFICATION_VALUES: [5, 6],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SAFETY,
    },
    {
        # Index 7: Home Security - Value Id's 7, 8 (motion)
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY,
        NOTIFICATION_VALUES: [7, 8],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.MOTION,
    },
    {
        # Index 8: Power management - Values 1...9
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_POWER_MANAGEMENT,
        NOTIFICATION_VALUES: [1, 2, 3, 4, 5, 6, 7, 8, 9],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.POWER,
        NOTIFICATION_SENSOR_ENABLED: False,
    },
    {
        # Index 8: Power management - Values 10...15
        # Battery values (mutually exclusive)
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_POWER_MANAGEMENT,
        NOTIFICATION_VALUES: [10, 11, 12, 13, 14, 15],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.POWER,
        NOTIFICATION_SENSOR_ENABLED: False,
        NOTIFICATION_OFF_VALUE: None,
    },
    {
        # Index 9: System - Value Id's 1, 2, 6, 7
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SYSTEM,
        NOTIFICATION_VALUES: [1, 2, 6, 7],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM,
        NOTIFICATION_SENSOR_ENABLED: False,
    },
    {
        # Index 10: Emergency - Value Id's 1, 2, 3
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_EMERGENCY,
        NOTIFICATION_VALUES: [1, 2, 3],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM,
    },
    {
        # Index 11: Clock - Value Id's 1, 2
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CLOCK,
        NOTIFICATION_VALUES: [1, 2],
        NOTIFICATION_DEVICE_CLASS: None,
        NOTIFICATION_SENSOR_ENABLED: False,
    },
    {
        # Index 12: Appliance - All Value Id's
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_APPLIANCE,
        NOTIFICATION_VALUES: [
            1,
            2,
            3,
            4,
            5,
            6,
            7,
            8,
            9,
            10,
            11,
            12,
            13,
            14,
            15,
            16,
            17,
            18,
            19,
            20,
            21,
        ],
        NOTIFICATION_DEVICE_CLASS: None,
    },
    {
        # Index 13: Home Health - Value Id's 1,2,3,4,5
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_APPLIANCE,
        NOTIFICATION_VALUES: [1, 2, 3, 4, 5],
        NOTIFICATION_DEVICE_CLASS: None,
    },
    {
        # Index 14: Siren
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SIREN,
        NOTIFICATION_VALUES: [1],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SOUND,
    },
    {
        # Index 15: Water valve
        # ignore non-boolean values
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WATER_VALVE,
        NOTIFICATION_VALUES: [3, 4],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM,
    },
    {
        # Index 16: Weather
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WEATHER,
        NOTIFICATION_VALUES: [1, 2],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM,
    },
    {
        # Index 17: Irrigation
        # ignore non-boolean values
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_IRRIGATION,
        NOTIFICATION_VALUES: [1, 2, 3, 4, 5],
        NOTIFICATION_DEVICE_CLASS: None,
    },
    {
        # Index 18: Gas
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_GAS,
        NOTIFICATION_VALUES: [1, 2, 3, 4],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS,
    },
    {
        # Index 18: Gas
        NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_GAS,
        NOTIFICATION_VALUES: [6],
        NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM,
    },
]


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    async_add_entities: AddEntitiesCallback,
) -> None:
    """Set up Z-Wave binary_sensor from config entry."""

    @callback
    def async_add_binary_sensor(values):
        """Add Z-Wave Binary Sensor(s)."""
        async_add_entities(VALUE_TYPE_SENSORS[values.primary.type](values))

    hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append(
        async_dispatcher_connect(
            hass, f"{DOMAIN}_new_{BINARY_SENSOR_DOMAIN}", async_add_binary_sensor
        )
    )


@callback
def async_get_legacy_binary_sensors(values):
    """Add Legacy/classic Z-Wave Binary Sensor."""
    return [ZWaveBinarySensor(values)]


@callback
def async_get_notification_sensors(values):
    """Convert Notification values into binary sensors."""
    sensors_to_add = []
    for list_value in values.primary.value["List"]:
        # check if we have a mapping for this value
        for item in NOTIFICATION_SENSORS:
            if item[NOTIFICATION_TYPE] != values.primary.index:
                continue
            if list_value["Value"] not in item[NOTIFICATION_VALUES]:
                continue
            sensors_to_add.append(
                ZWaveListValueSensor(
                    # required values
                    values,
                    list_value["Value"],
                    item[NOTIFICATION_DEVICE_CLASS],
                    # optional values
                    item.get(NOTIFICATION_SENSOR_ENABLED, True),
                    item.get(NOTIFICATION_OFF_VALUE, NOTIFICATION_VALUE_CLEAR),
                )
            )
    return sensors_to_add


VALUE_TYPE_SENSORS = {
    ValueType.BOOL: async_get_legacy_binary_sensors,
    ValueType.LIST: async_get_notification_sensors,
}


class ZWaveBinarySensor(ZWaveDeviceEntity, BinarySensorEntity):
    """Representation of a Z-Wave binary_sensor."""

    @property
    def is_on(self):
        """Return if the sensor is on or off."""
        return self.values.primary.value

    @property
    def entity_registry_enabled_default(self) -> bool:
        """Return if the entity should be enabled when first added to the entity registry."""
        # Legacy binary sensors are phased out (replaced by notification sensors)
        # Disable by default to not confuse users
        for item in self.values.primary.node.values():
            if item.command_class == CommandClass.NOTIFICATION:
                # This device properly implements the Notification CC, legacy sensor can be disabled
                return False
        return True


class ZWaveListValueSensor(ZWaveDeviceEntity, BinarySensorEntity):
    """Representation of a binary_sensor from values in the Z-Wave Notification CommandClass."""

    def __init__(
        self,
        values,
        on_value,
        device_class=None,
        default_enabled=True,
        off_value=NOTIFICATION_VALUE_CLEAR,
    ):
        """Initialize a ZWaveListValueSensor entity."""
        super().__init__(values)
        self._on_value = on_value
        self._device_class = device_class
        self._default_enabled = default_enabled
        self._off_value = off_value
        # make sure the correct value is selected at startup
        self._state = False
        self.on_value_update()

    @callback
    def on_value_update(self):
        """Call when a value is added/updated in the underlying EntityValues Collection."""
        if self.values.primary.value["Selected_id"] == self._on_value:
            # Only when the active ID exactly matches our watched ON value, set sensor state to ON
            self._state = True
        elif self.values.primary.value["Selected_id"] == self._off_value:
            # Only when the active ID exactly matches our watched OFF value, set sensor state to OFF
            self._state = False
        elif (
            self._off_value is None
            and self.values.primary.value["Selected_id"] != self._on_value
        ):
            # Off value not explicitly specified
            # Some values are reset by the simple fact they're overruled by another value coming in
            # For example the battery charging values in Power Management Index
            self._state = False

    @property
    def name(self):
        """Return the name of the entity."""
        # Append value label to base name
        base_name = super().name
        value_label = ""
        for item in self.values.primary.value["List"]:
            if item["Value"] == self._on_value:
                value_label = item["Label"]
                break
        # Strip "on location" / "at location" from name
        # Note: We're assuming that we don't retrieve 2 values with different location
        value_label = value_label.split(" on ")[0]
        value_label = value_label.split(" at ")[0]
        return f"{base_name}: {value_label}"

    @property
    def unique_id(self):
        """Return the unique_id of the entity."""
        unique_id = super().unique_id
        return f"{unique_id}.{self._on_value}"

    @property
    def is_on(self):
        """Return if the sensor is on or off."""
        return self._state

    @property
    def device_class(self):
        """Return the class of this device, from component DEVICE_CLASSES."""
        return self._device_class

    @property
    def entity_registry_enabled_default(self) -> bool:
        """Return if the entity should be enabled when first added to the entity registry."""
        # We hide the more advanced sensors by default to not overwhelm users
        return self._default_enabled
