"""Defines a SmartThings device."""
from collections import defaultdict, namedtuple
import colorsys
from enum import Enum
import re
from typing import Any, Dict, Mapping, Optional, Sequence

from .api import Api
from .capability import ATTRIBUTE_ON_VALUES, Attribute, Capability
from .entity import Entity

COLOR_HEX_MATCHER = re.compile('^#[A-Fa-f0-9]{6}$')
Status = namedtuple('status', 'value unit data')
STATUS_NONE = Status(None, None, None)


def hs_to_hex(hue: float, saturation: float) -> str:
    """Convert hue and saturation to a string hex color."""
    rgb = colorsys.hsv_to_rgb(hue/100, saturation/100, 100)
    return '#{:02x}{:02x}{:02x}'.format(
        round(rgb[0]), round(rgb[1]), round(rgb[2])).upper()


def hex_to_hs(color_hex: str) -> (int, int):
    """Convert a string hex color to hue and saturation components."""
    color_hex = color_hex.lstrip('#')
    rgb = [int(color_hex[i:i + len(color_hex) // 3], 16)/255.0
           for i in range(0, len(color_hex), len(color_hex) // 3)]
    hsv = colorsys.rgb_to_hsv(rgb[0], rgb[1], rgb[2])
    return round(hsv[0]*100, 3), round(hsv[1]*100, 3)


class Command:
    """Define common commands."""

    close = 'close'
    off = 'off'
    lock = 'lock'
    open = 'open'
    on = 'on'
    preset_position = 'presetPosition'
    set_color = 'setColor'
    set_color_temperature = 'setColorTemperature'
    set_fan_speed = 'setFanSpeed'
    set_cooling_setpoint = 'setCoolingSetpoint'
    set_heating_setpoint = 'setHeatingSetpoint'
    set_hue = 'setHue'
    set_level = 'setLevel'
    set_thermostat_fan_mode = 'setThermostatFanMode'
    set_thermostat_mode = 'setThermostatMode'
    set_saturation = 'setSaturation'
    unlock = 'unlock'


class DeviceType(Enum):
    """Define the device type."""

    UNKNOWN = 'UNKNOWN'
    DTH = 'DTH'
    ENDPOINT_APP = 'ENDPOINT_APP'


class Device:
    """Represents a SmartThings device."""

    def __init__(self):
        """Initialize a new device."""
        self._device_id = None
        self._name = None
        self._label = None
        self._location_id = None
        self._room_id = None
        self._type = DeviceType.UNKNOWN
        self._device_type_id = None
        self._device_type_name = None
        self._device_type_network = None
        self._components = dict()
        self._capabilities = []

    def apply_data(self, data: dict):
        """Apply the given data dictionary."""
        self._device_id = data['deviceId']
        self._name = data['name']
        self._label = data['label']
        self._location_id = data['locationId']
        self._room_id = data.get('roomId')
        self._type = DeviceType(data['type'])
        self._components.clear()
        self._capabilities.clear()
        for component in data['components']:
            capabilities = [c['id'] for c in component['capabilities']]
            component_id = component['id']
            if component_id == 'main':
                self._capabilities.extend(capabilities)
            else:
                self._components[component_id] = capabilities
        if self._type is DeviceType.DTH:
            dth = data['dth']
            self._device_type_id = dth["deviceTypeId"]
            self._device_type_name = dth["deviceTypeName"]
            self._device_type_network = dth["deviceNetworkType"]

    def get_capability(self, *capabilities) -> Optional[str]:
        """Return the first capability held by the device."""
        for capability in capabilities:
            if capability in self._capabilities:
                return capability
        return None

    @property
    def device_id(self) -> str:
        """Get the SmartThings device id."""
        return self._device_id

    @property
    def name(self) -> str:
        """Get the SmartThings device name."""
        return self._name

    @property
    def label(self) -> str:
        """Get the SmartThings user assigned label."""
        return self._label

    @property
    def location_id(self) -> str:
        """Get the SmartThings location assigned to the device."""
        return self._location_id

    @property
    def room_id(self):
        """Get the room assigned to the device."""
        return self._room_id

    @property
    def type(self) -> DeviceType:
        """Get the SmartThings device type."""
        return self._type

    @property
    def device_type_id(self) -> str:
        """Get the SmartThings device type handler id."""
        return self._device_type_id

    @property
    def device_type_name(self) -> str:
        """Get the SmartThings device type handler name."""
        return self._device_type_name

    @property
    def device_type_network(self) -> str:
        """Get the SmartThings device type handler network."""
        return self._device_type_network

    @property
    def components(self) -> Dict[str, Sequence[str]]:
        """Get the components of the device."""
        return self._components

    @property
    def capabilities(self) -> Sequence[str]:
        """Get a unique list of all capabilities across components."""
        return self._capabilities


class DeviceStatusBase:
    """Define the base status of device components."""

    def __init__(self, component_id: str,
                 attributes: Optional[Mapping[str, Status]] = None):
        """Initialize the status class."""
        self._attributes = defaultdict(lambda: STATUS_NONE, attributes or {})
        self._component_id = component_id

    def is_on(self, attribute: str) -> bool:
        """Determine if a specific attribute contains an on/True value."""
        if attribute not in ATTRIBUTE_ON_VALUES:
            return bool(self._attributes[attribute].value)
        return self._attributes[attribute].value == \
            ATTRIBUTE_ON_VALUES[attribute]

    def update_attribute_value(self, attribute: str, value):
        """Update the value of an attribute while mantaining unit and data."""
        status = self._attributes[attribute]
        self._attributes[attribute] = Status(value, status.unit, status.data)

    @property
    def attributes(self) -> Dict[str, Status]:
        """Get all of the attribute status objects."""
        return self._attributes

    @property
    def values(self) -> Dict[str, Any]:
        """Get the values of the attributes."""
        return defaultdict(
            lambda: None, {k: v.value for k, v in self._attributes.items()})

    @property
    def color(self) -> Optional[str]:
        """Get the color attribute."""
        return self._attributes[Attribute.color].value

    @color.setter
    def color(self, value: str):
        """Set the color attribute."""
        if not COLOR_HEX_MATCHER.match(value):
            raise ValueError(
                "value was not a properly formatted color hex, i.e. #000000.")
        self.update_attribute_value(Attribute.color, value)

    @property
    def color_temperature(self) -> int:
        """Get the color temperature attribute."""
        return int(self._attributes[Attribute.color_temperature].value or 1)

    @color_temperature.setter
    def color_temperature(self, value: int):
        """Set the color temperature attribute."""
        if not 1 <= value <= 30000:
            raise ValueError("value must be scaled between 1-30000.")
        self.update_attribute_value(Attribute.color_temperature, value)

    @property
    def component_id(self) -> str:
        """Get the component id of the status."""
        return self._component_id

    @property
    def fan_speed(self) -> int:
        """Get the fan speed attribute."""
        return int(self._attributes[Attribute.fan_speed].value or 0)

    @fan_speed.setter
    def fan_speed(self, value: int):
        """Set the fan speed attribute."""
        if value < 0:
            raise ValueError("value must be >= 0.")
        self.update_attribute_value(Attribute.fan_speed, value)

    @property
    def hue(self) -> float:
        """Get the hue attribute, scaled 0-100."""
        return float(self._attributes[Attribute.hue].value or 0.0)

    @hue.setter
    def hue(self, value: float):
        """Set the hue attribute, scaled 0-100."""
        if not 0 <= value <= 100:
            raise ValueError("value must be scaled between 0-100.")
        self.update_attribute_value(Attribute.hue, value)

    @property
    def level(self) -> int:
        """Get the level attribute, scaled 0-100."""
        return int(self._attributes[Attribute.level].value or 0)

    @level.setter
    def level(self, value: int):
        """Set the level of the attribute, scaled 0-100."""
        if not 0 <= value <= 100:
            raise ValueError("value must be scaled between 0-100.")
        self.update_attribute_value(Attribute.level, value)

    @property
    def saturation(self) -> float:
        """Get the saturation attribute, scaled 0-100."""
        return float(self._attributes[Attribute.saturation].value or 0.0)

    @saturation.setter
    def saturation(self, value: float):
        """Set the saturation attribute, scaled 0-100."""
        if not 0 <= value <= 100:
            raise ValueError("value must be scaled between 0-100.")
        self.update_attribute_value(Attribute.saturation, value)

    @property
    def motion(self) -> bool:
        """Get the motion attribute."""
        return self.is_on(Attribute.motion)

    @property
    def switch(self) -> bool:
        """Get the switch attribute."""
        return self.is_on(Attribute.switch)

    @switch.setter
    def switch(self, value: bool):
        """Set the value of the switch attribute."""
        status_value = ATTRIBUTE_ON_VALUES[Attribute.switch] \
            if value else 'off'
        self.update_attribute_value(Attribute.switch, status_value)

    @property
    def thermostat_fan_mode(self):
        """Get the thermostatFanMode attribute."""
        return self._attributes[Attribute.thermostat_fan_mode].value

    @thermostat_fan_mode.setter
    def thermostat_fan_mode(self, value: str):
        """Update the thermostatFanMode attribute."""
        self.update_attribute_value(Attribute.thermostat_fan_mode, value)

    @property
    def humidity(self) -> Optional[int]:
        """Get the humidity in percentage."""
        return self._attributes[Attribute.humidity].value

    @property
    def thermostat_mode(self) -> Optional[str]:
        """Get the thermostatMode attribute."""
        return self._attributes[Attribute.thermostat_mode].value

    @thermostat_mode.setter
    def thermostat_mode(self, value: str):
        """Set the thermostatMode attribute."""
        self.update_attribute_value(Attribute.thermostat_mode, value)

    @property
    def temperature(self) -> Optional[int]:
        """Get the temperature attribute."""
        return self._attributes[Attribute.temperature].value

    @property
    def thermostat_operating_state(self) -> Optional[str]:
        """Get the thermostatOperatingState attribute."""
        return self._attributes[Attribute.thermostat_operating_state].value

    @property
    def supported_thermostat_fan_modes(self) -> Optional[str]:
        """Get the supportedThermostatFanModes attribute."""
        return self._attributes[
            Attribute.supported_thermostat_fan_modes].value

    @property
    def supported_thermostat_modes(self) -> Optional[str]:
        """Get the supportedThermostatModes attribute."""
        return self._attributes[Attribute.supported_thermostat_modes].value

    @property
    def cooling_setpoint(self) -> Optional[int]:
        """Get the coolingSetpoint attribute."""
        return self._attributes[Attribute.cooling_setpoint].value

    @cooling_setpoint.setter
    def cooling_setpoint(self, value: int):
        """Set the coolingSetpoint attribute."""
        self.update_attribute_value(Attribute.cooling_setpoint, value)

    @property
    def heating_setpoint(self) -> Optional[int]:
        """Get the heatingSetpoint attribute."""
        return self._attributes[Attribute.heating_setpoint].value

    @heating_setpoint.setter
    def heating_setpoint(self, value):
        """Set the heatingSetpoint attribute."""
        self.update_attribute_value(Attribute.heating_setpoint, value)

    @property
    def lock(self):
        """Get the lock attribute."""
        return self._attributes[Attribute.lock].value

    @property
    def door(self):
        """Get the door attribute."""
        return self._attributes[Attribute.door].value

    @property
    def window_shade(self):
        """Get the windowShade attribute."""
        return self._attributes[Attribute.window_shade].value


class DeviceStatus(DeviceStatusBase):
    """Define the device status."""

    def __init__(self, api: Api, device_id: str, data=None):
        """Create a new instance of the DeviceStatusEntity class."""
        super().__init__('main')
        self._api = api
        self._device_id = device_id
        self._components = {}
        if data:
            self.apply_data(data)

    def apply_attribute_update(self, component_id: str, capability: str,
                               attribute: str, value: Any):
        """Apply an update to a specific attribute."""
        # capability future usage.
        if component_id == 'main':
            self.update_attribute_value(attribute, value)
        elif component_id in self._components.keys():
            self._components[component_id].update_attribute_value(
                attribute, value)

    def apply_data(self, data: dict):
        """Apply the values from the given data structure."""
        self._components.clear()
        for component_id, component in data['components'].items():
            attributes = {}
            for capabilities in component.values():
                for attribute, value in capabilities.items():
                    attributes[attribute] = Status(
                        value.get('value'), value.get('unit'),
                        value.get('data'))
            if component_id == 'main':
                self._attributes.clear()
                self._attributes.update(attributes)
            else:
                self._components[component_id] = \
                    DeviceStatusBase(component_id, attributes)

    @property
    def components(self) -> Dict[str, DeviceStatusBase]:
        """Get the component status instances."""
        return self._components

    @property
    def device_id(self) -> str:
        """Get the device id."""
        return self._device_id

    @device_id.setter
    def device_id(self, value: str):
        """Set the device id."""
        self._device_id = value

    async def refresh(self):
        """Refresh the values of the entity."""
        data = await self._api.get_device_status(self.device_id)
        if data:
            self.apply_data(data)


class DeviceEntity(Entity, Device):
    """Define a device entity."""

    def __init__(self, api: Api, data: Optional[dict] = None,
                 device_id: Optional[str] = None):
        """Create a new instance of the DeviceEntity class."""
        Entity.__init__(self, api)
        Device.__init__(self)
        if data:
            self.apply_data(data)
        if device_id:
            self._device_id = device_id
        self._status = DeviceStatus(api, self._device_id)

    async def refresh(self):
        """Refresh the device information using the API."""
        data = await self._api.get_device(self._device_id)
        if data:
            self.apply_data(data)
        self._status.device_id = self._device_id

    async def save(self):
        """Save the changes made to the device."""
        raise NotImplementedError

    async def command(self, component_id: str, capability, command,
                      args=None) -> bool:
        """Execute a command on the device."""
        response = await self._api.post_device_command(
            self._device_id, component_id, capability, command, args)
        return response == {}

    async def set_color(
            self, hue: Optional[float] = None,
            saturation: Optional[float] = None,
            color_hex: Optional[str] = None,
            set_status: bool = False,
            *, component_id: str = 'main') -> bool:
        """Call the set color command."""
        color_map = {}
        if color_hex:
            if not COLOR_HEX_MATCHER.match(color_hex):
                raise ValueError("color_hex was not a properly formatted "
                                 "color hex, i.e. #000000.")
            color_map['hex'] = color_hex
        else:
            if not 0 <= hue <= 100:
                raise ValueError("hue must be scaled between 0-100.")
            if not 0 <= saturation <= 100:
                raise ValueError("saturation must be scaled between 0-100.")
            color_map['hue'] = hue
            color_map['saturation'] = saturation

        result = await self.command(
            component_id, Capability.color_control,
            Command.set_color, [color_map])
        if result and set_status:
            if color_hex:
                self.status.color = color_hex
                self.status.hue, self.status.saturation = hex_to_hs(color_hex)
            else:
                self.status.color = hs_to_hex(hue, saturation)
                self.status.hue = hue
                self.status.saturation = saturation
        return result

    async def set_color_temperature(self, temperature: int,
                                    set_status: bool = False,
                                    *, component_id: str = 'main') -> bool:
        """Call the color temperature device command."""
        if not 1 <= temperature <= 30000:
            raise ValueError("temperature must be scaled between 1-30000.")

        result = await self.command(
            component_id, Capability.color_temperature,
            Command.set_color_temperature, [temperature])
        if result and set_status:
            self.status.color_temperature = temperature
        return result

    async def set_fan_speed(
            self, speed: int, set_status: bool = False,
            *, component_id: str = 'main') -> bool:
        """Call the set fan speed device command."""
        if speed < 0:
            raise ValueError("value must be >= 0.")

        result = await self.command(
            component_id, Capability.fan_speed, Command.set_fan_speed,
            [speed])
        if result and set_status:
            self.status.fan_speed = speed
            self.status.switch = speed > 0
        return result

    async def set_hue(self, hue: int, set_status: bool = False,
                      *, component_id: str = 'main') -> bool:
        """Call the set hue device command."""
        if not 0 <= hue <= 100:
            raise ValueError("hue must be scaled between 0-100.")

        result = await self.command(
            component_id, Capability.color_control, Command.set_hue, [hue])
        if result and set_status:
            self.status.hue = hue
        return result

    async def set_level(self, level: int, duration: int = 0,
                        set_status: bool = False,
                        *, component_id: str = 'main') -> bool:
        """Call the set level device command."""
        if not 0 <= level <= 100:
            raise ValueError("level must be scaled between 0-100.")
        if duration < 0:
            raise ValueError("duration must be >= 0.")

        result = await self.command(
            component_id, Capability.switch_level, Command.set_level,
            [level, duration])
        if result and set_status:
            self.status.level = level
            self.status.switch = level > 0
        return result

    async def set_saturation(
            self, saturation: int, set_status: bool = False,
            *, component_id: str = 'main') -> bool:
        """Call the set saturation device command."""
        if not 0 <= saturation <= 100:
            raise ValueError("saturation must be scaled between 0-100.")

        result = await self.command(
            component_id, Capability.color_control, Command.set_saturation,
            [saturation])
        if result and set_status:
            self.status.saturation = saturation
        return result

    async def set_thermostat_fan_mode(
            self, mode: str, set_status: bool = False,
            *, component_id: str = 'main') -> bool:
        """Call the setThermostatFanMode device command."""
        capability = self.get_capability(
            Capability.thermostat_fan_mode, Capability.thermostat)
        result = await self.command(
            component_id, capability, Command.set_thermostat_fan_mode,
            [mode])
        if result and set_status:
            self.status.thermostat_fan_mode = mode
        return result

    async def set_thermostat_mode(
            self, mode: str, set_status: bool = False,
            *, component_id: str = 'main') -> bool:
        """Call the setThermostatMode deivce command."""
        capability = self.get_capability(
            Capability.thermostat_mode, Capability.thermostat)
        result = await self.command(
            component_id, capability, Command.set_thermostat_mode,
            [mode])
        if result and set_status:
            self.status.thermostat_mode = mode
        return result

    async def set_cooling_setpoint(
            self, temperature: int, set_status: bool = False,
            *, component_id: str = 'main') -> bool:
        """Call the setThermostatMode deivce command."""
        capability = self.get_capability(
            Capability.thermostat_cooling_setpoint, Capability.thermostat)
        result = await self.command(
            component_id, capability, Command.set_cooling_setpoint,
            [temperature])
        if result and set_status:
            self.status.cooling_setpoint = temperature
        return result

    async def set_heating_setpoint(
            self, temperature: int, set_status: bool = False,
            *, component_id: str = 'main') -> bool:
        """Call the setThermostatMode deivce command."""
        capability = self.get_capability(
            Capability.thermostat_heating_setpoint, Capability.thermostat)
        result = await self.command(
            component_id, capability, Command.set_heating_setpoint,
            [temperature])
        if result and set_status:
            self.status.heating_setpoint = temperature
        return result

    async def switch_off(self, set_status: bool = False,
                         *, component_id: str = 'main') -> bool:
        """Call the switch off device command."""
        result = await self.command(
            component_id, Capability.switch, Command.off)
        if result and set_status:
            self.status.switch = False
        return result

    async def switch_on(self, set_status: bool = False,
                        *, component_id: str = 'main') -> bool:
        """Call the switch on device command."""
        result = await self.command(
            component_id, Capability.switch, Command.on)
        if result and set_status:
            self.status.switch = True
        return result

    async def lock(self, set_status: bool = False,
                   *, component_id: str = 'main') -> bool:
        """Call the lock device command."""
        result = await self.command(
            component_id, Capability.lock, Command.lock)
        if result and set_status:
            self.status.update_attribute_value(Attribute.lock, 'locked')
        return result

    async def unlock(self, set_status: bool = False,
                     *, component_id: str = 'main') -> bool:
        """Call the unlock device command."""
        result = await self.command(
            component_id, Capability.lock, Command.unlock)
        if result and set_status:
            self.status.update_attribute_value(Attribute.lock, 'unlocked')
        return result

    async def open(self, set_status: bool = False,
                   *, component_id: str = 'main') -> bool:
        """Call the open device command."""
        capability = self.get_capability(
            Capability.door_control, Capability.window_shade,
            Capability.garage_door_control)
        result = await self.command(component_id, capability, Command.open)
        if result and set_status:
            attribute = Attribute.window_shade if \
                capability == Capability.window_shade else Attribute.door
            self.status.update_attribute_value(attribute, 'opening')
        return result

    async def close(self, set_status: bool = False,
                    *, component_id: str = 'main') -> bool:
        """Call the close device command."""
        capability = self.get_capability(
            Capability.door_control, Capability.window_shade,
            Capability.garage_door_control)
        result = await self.command(component_id, capability, Command.close)
        if result and set_status:
            attribute = Attribute.window_shade if \
                capability == Capability.window_shade else Attribute.door
            self.status.update_attribute_value(attribute, 'closing')
        return result

    async def preset_position(self, *, component_id: str = 'main') -> bool:
        """Call the close device command."""
        return await self.command(
            component_id, Capability.window_shade, Command.close)

    @property
    def status(self):
        """Get the status entity of the device."""
        return self._status
