"""Support for Z-Wave lights."""
import logging
from threading import Timer

from homeassistant.components.light import (
    ATTR_BRIGHTNESS,
    ATTR_COLOR_TEMP,
    ATTR_HS_COLOR,
    ATTR_TRANSITION,
    ATTR_WHITE_VALUE,
    DOMAIN,
    SUPPORT_BRIGHTNESS,
    SUPPORT_COLOR,
    SUPPORT_COLOR_TEMP,
    SUPPORT_TRANSITION,
    SUPPORT_WHITE_VALUE,
    LightEntity,
)
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.util.color as color_util

from . import CONF_REFRESH_DELAY, CONF_REFRESH_VALUE, ZWaveDeviceEntity, const

_LOGGER = logging.getLogger(__name__)

COLOR_CHANNEL_WARM_WHITE = 0x01
COLOR_CHANNEL_COLD_WHITE = 0x02
COLOR_CHANNEL_RED = 0x04
COLOR_CHANNEL_GREEN = 0x08
COLOR_CHANNEL_BLUE = 0x10

# Some bulbs have an independent warm and cool white light LEDs. These need
# to be treated differently, aka the zw098 workaround. Ensure these are added
# to DEVICE_MAPPINGS below.
# (Manufacturer ID, Product ID) from
# https://github.com/OpenZWave/open-zwave/blob/master/config/manufacturer_specific.xml
AEOTEC_ZW098_LED_BULB_LIGHT = (0x86, 0x62)
AEOTEC_ZWA001_LED_BULB_LIGHT = (0x371, 0x1)
AEOTEC_ZWA002_LED_BULB_LIGHT = (0x371, 0x2)
HANK_HKZW_RGB01_LED_BULB_LIGHT = (0x208, 0x4)
ZIPATO_RGB_BULB_2_LED_BULB_LIGHT = (0x131, 0x3)

WORKAROUND_ZW098 = "zw098"

DEVICE_MAPPINGS = {
    AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098,
    AEOTEC_ZWA001_LED_BULB_LIGHT: WORKAROUND_ZW098,
    AEOTEC_ZWA002_LED_BULB_LIGHT: WORKAROUND_ZW098,
    HANK_HKZW_RGB01_LED_BULB_LIGHT: WORKAROUND_ZW098,
    ZIPATO_RGB_BULB_2_LED_BULB_LIGHT: WORKAROUND_ZW098,
}

# Generate midpoint color temperatures for bulbs that have limited
# support for white light colors
TEMP_COLOR_MAX = 500  # mireds (inverted)
TEMP_COLOR_MIN = 154
TEMP_MID_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 2 + TEMP_COLOR_MIN
TEMP_WARM_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 3 * 2 + TEMP_COLOR_MIN
TEMP_COLD_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 3 + TEMP_COLOR_MIN


async def async_setup_entry(hass, config_entry, async_add_entities):
    """Set up Z-Wave Light from Config Entry."""

    @callback
    def async_add_light(light):
        """Add Z-Wave Light."""
        async_add_entities([light])

    async_dispatcher_connect(hass, "zwave_new_light", async_add_light)


def get_device(node, values, node_config, **kwargs):
    """Create Z-Wave entity device."""
    refresh = node_config.get(CONF_REFRESH_VALUE)
    delay = node_config.get(CONF_REFRESH_DELAY)
    _LOGGER.debug(
        "node=%d value=%d node_config=%s CONF_REFRESH_VALUE=%s"
        " CONF_REFRESH_DELAY=%s",
        node.node_id,
        values.primary.value_id,
        node_config,
        refresh,
        delay,
    )

    if node.has_command_class(const.COMMAND_CLASS_SWITCH_COLOR):
        return ZwaveColorLight(values, refresh, delay)
    return ZwaveDimmer(values, refresh, delay)


def brightness_state(value):
    """Return the brightness and state."""
    if value.data > 0:
        return round((value.data / 99) * 255), STATE_ON
    return 0, STATE_OFF


def byte_to_zwave_brightness(value):
    """Convert brightness in 0-255 scale to 0-99 scale.

    `value` -- (int) Brightness byte value from 0-255.
    """
    if value > 0:
        return max(1, round((value / 255) * 99))
    return 0


def ct_to_hs(temp):
    """Convert color temperature (mireds) to hs."""
    colorlist = list(
        color_util.color_temperature_to_hs(
            color_util.color_temperature_mired_to_kelvin(temp)
        )
    )
    return [int(val) for val in colorlist]


class ZwaveDimmer(ZWaveDeviceEntity, LightEntity):
    """Representation of a Z-Wave dimmer."""

    def __init__(self, values, refresh, delay):
        """Initialize the light."""
        ZWaveDeviceEntity.__init__(self, values, DOMAIN)
        self._brightness = None
        self._state = None
        self._supported_features = None
        self._delay = delay
        self._refresh_value = refresh
        self._zw098 = None

        # Enable appropriate workaround flags for our device
        # Make sure that we have values for the key before converting to int
        if self.node.manufacturer_id.strip() and self.node.product_id.strip():
            specific_sensor_key = (
                int(self.node.manufacturer_id, 16),
                int(self.node.product_id, 16),
            )
            if (
                specific_sensor_key in DEVICE_MAPPINGS
                and DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098
            ):
                _LOGGER.debug("AEOTEC ZW098 workaround enabled")
                self._zw098 = 1

        # Used for value change event handling
        self._refreshing = False
        self._timer = None
        _LOGGER.debug(
            "self._refreshing=%s self.delay=%s", self._refresh_value, self._delay
        )
        self.value_added()
        self.update_properties()

    def update_properties(self):
        """Update internal properties based on zwave values."""
        # Brightness
        self._brightness, self._state = brightness_state(self.values.primary)

    def value_added(self):
        """Call when a new value is added to this entity."""
        self._supported_features = SUPPORT_BRIGHTNESS
        if self.values.dimming_duration is not None:
            self._supported_features |= SUPPORT_TRANSITION

    def value_changed(self):
        """Call when a value for this entity's node has changed."""
        if self._refresh_value:
            if self._refreshing:
                self._refreshing = False
            else:

                def _refresh_value():
                    """Use timer callback for delayed value refresh."""
                    self._refreshing = True
                    self.values.primary.refresh()

                if self._timer is not None and self._timer.is_alive():
                    self._timer.cancel()

                self._timer = Timer(self._delay, _refresh_value)
                self._timer.start()
                return
        super().value_changed()

    @property
    def brightness(self):
        """Return the brightness of this light between 0..255."""
        return self._brightness

    @property
    def is_on(self):
        """Return true if device is on."""
        return self._state == STATE_ON

    @property
    def supported_features(self):
        """Flag supported features."""
        return self._supported_features

    def _set_duration(self, **kwargs):
        """Set the transition time for the brightness value.

        Zwave Dimming Duration values:
        0x00      = instant
        0x01-0x7F = 1 second to 127 seconds
        0x80-0xFE = 1 minute to 127 minutes
        0xFF      = factory default
        """
        if self.values.dimming_duration is None:
            if ATTR_TRANSITION in kwargs:
                _LOGGER.debug("Dimming not supported by %s", self.entity_id)
            return

        if ATTR_TRANSITION not in kwargs:
            self.values.dimming_duration.data = 0xFF
            return

        transition = kwargs[ATTR_TRANSITION]
        if transition <= 127:
            self.values.dimming_duration.data = int(transition)
        elif transition > 7620:
            self.values.dimming_duration.data = 0xFE
            _LOGGER.warning("Transition clipped to 127 minutes for %s", self.entity_id)
        else:
            minutes = int(transition / 60)
            _LOGGER.debug(
                "Transition rounded to %d minutes for %s", minutes, self.entity_id
            )
            self.values.dimming_duration.data = minutes + 0x7F

    def turn_on(self, **kwargs):
        """Turn the device on."""
        self._set_duration(**kwargs)

        # Zwave multilevel switches use a range of [0, 99] to control
        # brightness. Level 255 means to set it to previous value.
        if ATTR_BRIGHTNESS in kwargs:
            self._brightness = kwargs[ATTR_BRIGHTNESS]
            brightness = byte_to_zwave_brightness(self._brightness)
        else:
            brightness = 255

        if self.node.set_dimmer(self.values.primary.value_id, brightness):
            self._state = STATE_ON

    def turn_off(self, **kwargs):
        """Turn the device off."""
        self._set_duration(**kwargs)

        if self.node.set_dimmer(self.values.primary.value_id, 0):
            self._state = STATE_OFF


class ZwaveColorLight(ZwaveDimmer):
    """Representation of a Z-Wave color changing light."""

    def __init__(self, values, refresh, delay):
        """Initialize the light."""
        self._color_channels = None
        self._hs = None
        self._ct = None
        self._white = None

        super().__init__(values, refresh, delay)

    def value_added(self):
        """Call when a new value is added to this entity."""
        super().value_added()

        self._supported_features |= SUPPORT_COLOR
        if self._zw098:
            self._supported_features |= SUPPORT_COLOR_TEMP
        elif self._color_channels is not None and self._color_channels & (
            COLOR_CHANNEL_WARM_WHITE | COLOR_CHANNEL_COLD_WHITE
        ):
            self._supported_features |= SUPPORT_WHITE_VALUE

    def update_properties(self):
        """Update internal properties based on zwave values."""
        super().update_properties()

        if self.values.color is None:
            return
        if self.values.color_channels is None:
            return

        # Color Channels
        self._color_channels = self.values.color_channels.data

        # Color Data String
        data = self.values.color.data

        # RGB is always present in the openzwave color data string.
        rgb = [int(data[1:3], 16), int(data[3:5], 16), int(data[5:7], 16)]
        self._hs = color_util.color_RGB_to_hs(*rgb)

        # Parse remaining color channels. Openzwave appends white channels
        # that are present.
        index = 7

        # Warm white
        if self._color_channels & COLOR_CHANNEL_WARM_WHITE:
            warm_white = int(data[index : index + 2], 16)
            index += 2
        else:
            warm_white = 0

        # Cold white
        if self._color_channels & COLOR_CHANNEL_COLD_WHITE:
            cold_white = int(data[index : index + 2], 16)
            index += 2
        else:
            cold_white = 0

        # Color temperature. With the AEOTEC ZW098 bulb, only two color
        # temperatures are supported. The warm and cold channel values
        # indicate brightness for warm/cold color temperature.
        if self._zw098:
            if warm_white > 0:
                self._ct = TEMP_WARM_HASS
                self._hs = ct_to_hs(self._ct)
            elif cold_white > 0:
                self._ct = TEMP_COLD_HASS
                self._hs = ct_to_hs(self._ct)
            else:
                # RGB color is being used. Just report midpoint.
                self._ct = TEMP_MID_HASS

        elif self._color_channels & COLOR_CHANNEL_WARM_WHITE:
            self._white = warm_white

        elif self._color_channels & COLOR_CHANNEL_COLD_WHITE:
            self._white = cold_white

        # If no rgb channels supported, report None.
        if not (
            self._color_channels & COLOR_CHANNEL_RED
            or self._color_channels & COLOR_CHANNEL_GREEN
            or self._color_channels & COLOR_CHANNEL_BLUE
        ):
            self._hs = None

    @property
    def hs_color(self):
        """Return the hs color."""
        return self._hs

    @property
    def white_value(self):
        """Return the white value of this light between 0..255."""
        return self._white

    @property
    def color_temp(self):
        """Return the color temperature."""
        return self._ct

    def turn_on(self, **kwargs):
        """Turn the device on."""
        rgbw = None

        if ATTR_WHITE_VALUE in kwargs:
            self._white = kwargs[ATTR_WHITE_VALUE]

        if ATTR_COLOR_TEMP in kwargs:
            # Color temperature. With the AEOTEC ZW098 bulb, only two color
            # temperatures are supported. The warm and cold channel values
            # indicate brightness for warm/cold color temperature.
            if self._zw098:
                if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS:
                    self._ct = TEMP_WARM_HASS
                    rgbw = "#000000ff00"
                else:
                    self._ct = TEMP_COLD_HASS
                    rgbw = "#00000000ff"
        elif ATTR_HS_COLOR in kwargs:
            self._hs = kwargs[ATTR_HS_COLOR]
            if ATTR_WHITE_VALUE not in kwargs:
                # white LED must be off in order for color to work
                self._white = 0

        if (
            ATTR_WHITE_VALUE in kwargs or ATTR_HS_COLOR in kwargs
        ) and self._hs is not None:
            rgbw = "#"
            for colorval in color_util.color_hs_to_RGB(*self._hs):
                rgbw += format(colorval, "02x")
            if self._white is not None:
                rgbw += format(self._white, "02x") + "00"
            else:
                rgbw += "0000"

        if rgbw and self.values.color:
            self.values.color.data = rgbw

        super().turn_on(**kwargs)