import datetime
from enum import Enum, unique
import isodate
import logging

import pyhomie

import pybalboa.clients as clients
import pybalboa.messages as messages

class Node(pyhomie.Node):

    def __init__(self, balboa_client: clients.Client, id, name, type):
        properties = []

        properties.append(pyhomie.Property("channel", "Channel", "integer", balboa_client.channel))
        properties.append(pyhomie.Property("state", "Client State", "string", balboa_client.state))

        # Byte string properties
        properties.append(pyhomie.Property("status", "Status", "string"))
        properties.append(pyhomie.Property("information", "Information", "string"))
        properties.append(pyhomie.Property("filter-cycles", "Filter Cycles", "string"))
        properties.append(pyhomie.Property("configuraiton", "Status", "string"))

        # Filter cycles
        properties.append(FilterCycleStartProperty(1))
        properties.append(FilterCycleDurationProperty(1))
        properties.append(FilterCycleStartProperty(2))
        properties.append(FilterCycleDurationProperty(2))

        # Properties in Status Update:
        properties.append(pyhomie.Property("priming", "Priming", "boolean"))
        properties.append(pyhomie.Property("current-temperature", "Current Temperature", "float"))
        properties.append(TimeProperty("time", "Time"))
        properties.append(Property("heating-mode", "Heating Mode", "enum", format="ready,rest,ready-in-rest", settable=True))
        properties.append(TemperatureScaleProperty("temperature-scale", "Temperature Scale"))
        properties.append(ClockModeProperty("clock-mode", "Clock Mode"))
        properties.append(pyhomie.Property("heating-state", "Heating State", "enum", format="off,heating,heat waiting"))
        properties.append(pyhomie.Property("circulation-pump", "Circulation Pump", "boolean"))
        properties.append(SetTemperatureProperty("set-temperature", "Set Temperature"))
        properties.append(pyhomie.Property("filter-mode", "Filter Mode", "enum", format="off,cycle 1,cycle 2,cycles 1 and 2"))

        # TODO: Query spa for configuration and assign properties accordingly
        # TODO: Change pump formats to boolean/enum if 1-speed or 2-speed?
        properties.append(ToggleItemProperty(0x03, "clear-notification", "Clear Notification", "boolean", retained=False))
        properties.append(ToggleItemProperty(messages.ToggleItemRequest.ItemCode.PUMP_1, "pump-1", "Pump 1", "enum", format="off,low,high"))
        properties.append(ToggleItemProperty(messages.ToggleItemRequest.ItemCode.PUMP_2, "pump-2", "Pump 2", "enum", format="off,low,high"))
        properties.append(ToggleItemProperty(messages.ToggleItemRequest.ItemCode.PUMP_3, "pump-3", "Pump 3", "enum", format="off,low,high"))
        properties.append(ToggleItemProperty(messages.ToggleItemRequest.ItemCode.PUMP_4, "pump-4", "Pump 4", "enum", format="off,low,high"))
        properties.append(ToggleItemProperty(messages.ToggleItemRequest.ItemCode.PUMP_5, "pump-5", "Pump 5", "enum", format="off,low,high"))
        properties.append(ToggleItemProperty(messages.ToggleItemRequest.ItemCode.PUMP_6, "pump-6", "Pump 6", "enum", format="off,low,high"))
        properties.append(ToggleItemProperty(messages.ToggleItemRequest.ItemCode.BLOWER, "blower", "Blower", "boolean"))
        properties.append(ToggleItemProperty(messages.ToggleItemRequest.ItemCode.MISTER, "mister", "Mister", "boolean"))
        properties.append(ToggleItemProperty(messages.ToggleItemRequest.ItemCode.LIGHT_1, "light-1", "Light 1", "boolean"))
        properties.append(ToggleItemProperty(messages.ToggleItemRequest.ItemCode.LIGHT_2, "light-2", "Light 2", "boolean"))
        properties.append(ToggleItemProperty(messages.ToggleItemRequest.ItemCode.AUX_1, "aux-1", "Aux 1", "boolean"))
        properties.append(ToggleItemProperty(messages.ToggleItemRequest.ItemCode.AUX_2, "aux-2", "Aux 2", "boolean"))
        properties.append(ToggleItemProperty(messages.ToggleItemRequest.ItemCode.HOLD_MODE, "hold-mode", "Hold Mode", "boolean"))
        properties.append(ToggleItemProperty(messages.ToggleItemRequest.ItemCode.TEMPERATURE_RANGE, "temperature-range", "Temperature Range", "enum", format="low,high"))
        properties.append(ToggleItemProperty(messages.ToggleItemRequest.ItemCode.HEAT_MODE, "heat-mode", "Heat Mode", "enum", format="ready,rest"))

        super().__init__(id, name, type, properties)
        self.balboa_client = balboa_client
        self.balboa_client.on_connect = self._on_balboa_connect

    def _on_balboa_connect(self):
        self.properties["channel"].value = self.balboa_client.channel

    def _on_connect(self, device):
        super()._on_connect(device)
        self.balboa_client.on_message = self._on_balboa_message
        self.balboa_client.request_configuration()
        self.balboa_client.request_information()
        self.balboa_client.request_filter_cycles()
        self.balboa_client.request_fault_log()

    def _on_balboa_message(self, client: clients.Client, msg: messages.Message):
        self.properties["state"].value = self.balboa_client.state
        if self.state != pyhomie.Device.State.READY:
            return
        if msg.type_code == messages.StatusUpdate.TYPE_CODE and self.properties["status"].value != "".join(map("{:02X}".format, msg.arguments)):
            self.properties["hold-mode"].value = msg.arguments[0] == 0x05
            self.properties["status"].value = "".join(map("{:02X}".format, msg.arguments))
            if msg.arguments[5] == 0x00:
                self.properties["heating-mode"].value = "ready"
            elif msg.arguments[5] == 0x01:
                self.properties["heating-mode"].value = "rest"
            elif msg.arguments[5] == 0x02:
                self.properties["heating-mode"].value = "ready-in-rest"
            if msg.arguments[9] & 0x18 == 0x00:
                self.properties["filter-mode"].value = "off"
            elif msg.arguments[9] & 0x18 == 0x08:
                self.properties["filter-mode"].value = "cycle 1"
            elif msg.arguments[9] & 0x18 == 0x10:
                self.properties["filter-mode"].value = "cycle 2"
            else:
                self.properties["filter-mode"].value = "cycles 1 and 2"
            if msg.arguments[10] & 0x30 == 0x00:
                self.properties["heating-state"].value = "off"
            elif msg.arguments[10] & 0x30 == 0x10:
                self.properties["heating-state"].value = "heating"
            elif msg.arguments[10] & 0x30 == 0x20:
                self.properties["heating-state"].value = "heat waiting"
            if msg.arguments[11] & 0x03 == 0:
                self.properties["pump-1"].value = "off"
            elif msg.arguments[11] & 0x03 == 1:
                self.properties["pump-1"].value = "low"
            else:
                self.properties["pump-1"].value = "high"
            if msg.arguments[11] & 0x0C == 0:
                self.properties["pump-2"].value = "off"
            elif msg.arguments[11] & 0x0C == 0x04:
                self.properties["pump-2"].value = "low"
            else:
                self.properties["pump-2"].value = "high"
            if msg.arguments[11] & 0x30 == 0:
                self.properties["pump-3"].value = "off"
            elif msg.arguments[11] & 0x30 == 0x10:
                self.properties["pump-3"].value = "low"
            else:
                self.properties["pump-3"].value = "high"
            if msg.arguments[11] & 0xC0 == 0:
                self.properties["pump-4"].value = "off"
            elif msg.arguments[11] & 0xC0 == 0x40:
                self.properties["pump-4"].value = "low"
            else:
                self.properties["pump-4"].value = "high"
            if (
                (msg.arguments[9] & 0x01 == 0 and self.properties["temperature-scale"].value != TemperatureScaleProperty.FAHRENHEIT) or
                (msg.arguments[9] & 0x01 == 1 and self.properties["temperature-scale"].value != TemperatureScaleProperty.CELSIUS)
            ):
                self.state = pyhomie.Device.State.INIT
                self.properties["temperature-scale"].value = TemperatureScaleProperty.CELSIUS if msg.arguments[9] & 0x01 else TemperatureScaleProperty.FAHRENHEIT
                self.properties["current-temperature"].unit = self.properties["temperature-scale"].value
                self.properties["set-temperature"].unit = self.properties["temperature-scale"].value
                self.state = pyhomie.Device.State.READY
            if msg.arguments[2] == 0xFF:
                self.properties["current-temperature"].value = None
            else:
                if self.properties["temperature-scale"].value == TemperatureScaleProperty.CELSIUS:
                    self.properties["current-temperature"].value = float(msg.arguments[2]) / 2
                else:
                    self.properties["current-temperature"].value = float(msg.arguments[2])
            self.properties["time"].value = datetime.datetime.combine(datetime.date.today(), datetime.time(msg.arguments[3], msg.arguments[4]))
            if msg.arguments[5] == 0:
                self.properties["heating-mode"].value = "ready"
            if msg.arguments[5] == 1:
                self.properties["heating-mode"].value = "rest"
            if msg.arguments[5] == 3:
                self.properties["heating-mode"].value = "ready-in-rest"
            self.properties["circulation-pump"].value = msg.arguments[13] & 0x02 == 0x02
            self.properties["blower"].value = msg.arguments[13] & 0xC0 == 0xC0
            self.properties["light-1"].value = msg.arguments[14] & 0x03 == 0x03
            self.properties["light-2"].value = msg.arguments[14] & 0x0C == 0x0C
            if self.properties["temperature-scale"].value == TemperatureScaleProperty.CELSIUS:
                self.properties["set-temperature"].value = float(msg.arguments[20]) / 2
            else:
                self.properties["set-temperature"].value = float(msg.arguments[20])
            self.properties["temperature-range"].value = "high" if msg.arguments[10] & 0x04 else "low"
        elif msg.type_code == messages.FilterCyclesResponse.TYPE_CODE:
            self.publish("filter-cycles", "".join(map("{:02X}".format, msg.arguments)))
            self.properties["filter-cycle-1-start"].value = datetime.datetime.combine(datetime.date.today(), datetime.time(msg.arguments[0], msg.arguments[1]))
            self.properties["filter-cycle-1-duration"].value = datetime.timedelta(0, msg.arguments[2] * 3600 + msg.arguments[3] * 60)
            if msg.arguments[4] & 0x80:
                self.properties["filter-cycle-2-start"].value = datetime.datetime.combine(datetime.date.today(), datetime.time(msg.arguments[4] & 0x7F, msg.arguments[5]))
                self.properties["filter-cycle-2-duration"].value = datetime.timedelta(0, msg.arguments[6] * 3600 + msg.arguments[7] * 60)
            else:
                self.properties["filter-cycle-2-duration"].value = datetime.timedelta()
        elif msg.type_code == messages.InformationResponse.TYPE_CODE:
            self.publish("information", "".join(map("{:02X}".format, msg.arguments)))
        elif msg.type_code == messages.PreferencesResponse.TYPE_CODE:
            self.publish("preferences", "".join(map("{:02X}".format, msg.arguments)))
            if (
                (msg.arguments[3] & 0x01 == 0 and self.properties["temperature-scale"].value != TemperatureScaleProperty.FAHRENHEIT) or
                (msg.arguments[3] & 0x01 == 1 and self.properties["temperature-scale"].value != TemperatureScaleProperty.CELSIUS)
            ):
                self.state = pyhomie.Device.State.INIT
                self.properties["temperature-scale"].value = TemperatureScaleProperty.CELSIUS if msg.arguments[9] & 0x01 else TemperatureScaleProperty.FAHRENHEIT
                self.properties["current-temperature"].unit = self.properties["temperature-scale"].value
                self.properties["set-temperature"].unit = self.properties["temperature-scale"].value
                self.state = pyhomie.Device.State.READY
            self.properties["temperature-scale"].value = TemperatureScaleProperty.CELSIUS if msg.arguments[3] == messages.SetTemperatureScaleRequest.CELSIUS else TemperatureScaleProperty.FAHRENHEIT
        elif msg.type_code == messages.FaultLogResponse.TYPE_CODE:
            self.publish("fault-log-{}".format(msg.arguments[1] + 1), "".join(map("{:02X}".format, msg.arguments)))
        elif msg.type_code == messages.ConfigurationResponse.TYPE_CODE:
            self.publish("configuration", "".join(map("{:02X}".format, msg.arguments)))
        self.on_balboa_message(client, msg)

    def on_balboa_message(self, client, msg):
        pass

class Property(pyhomie.Property):

    @property
    def balboa_client(self):
        return self.node.balboa_client


class ClockModeProperty(Property):

    def __init__(self, id, name):
        super().__init__(id, name, "enum", format="12-hour,24-hour", settable=True)

    def _on_set(self, value):
        self.balboa_client.set_preference(
           messages.SetPreferenceRequest.PreferenceCode.CLOCK_MODE,
            messages.SetClockModeRequest.MODE_24_HOUR if value == "24-hour" else messages.SetClockModeRequest.MODE_12_HOUR
        )


class FilterCycleStartProperty(Property):

    def __init__(self, n: int):
        self.n = n
        super().__init__("filter-cycle-{}-start".format(n), "Filter Cycle {} Start Time".format(n), "datetime", settable=True)

    def _on_set(self, value):
        if self.n == 1:
            start1 = value.timetz()
            start2 = self.node.properties["filter-cycle-2-start"].value.timetz()
        else:
            start1 = self.node.properties["filter-cycle-1-start"].value.timetz()
            start2 = value.timetz()
        duration1 = self.node.properties["filter-cycle-1-duration"].value
        duration2 = self.node.properties["filter-cycle-2-duration"].value
        self.balboa_client.set_filter_cycles(start1, duration1, start2=start2, duration2=duration2)
        super()._on_set(value)
        msg = messages.SetFilterCyclesRequest(self.balboa_client.channel, start1, duration1, start2=start2, duration2=duration2)
        self.node.properties["filter-cycles"].value = "".join(map("{:02X}".format, msg.arguments))


class FilterCycleDurationProperty(Property):

    def __init__(self, n: int):
        self.n = n
        super().__init__("filter-cycle-{}-duration".format(n), "Filter Cycle {} Duration".format(n), "duration", settable=True)

    def _on_set(self, value):
        start1 = self.node.properties["filter-cycle-1-start"].value.timetz()
        start2 = self.node.properties["filter-cycle-2-start"].value.timetz()
        if self.n == 1:
            duration1 = value
            duration2 = self.node.properties["filter-cycle-2-duration"].value
        else:
            duration1 = self.node.properties["filter-cycle-1-duration"].value
            duration2 = value
        self.balboa_client.set_filter_cycles(start1, duration1, start2=start2, duration2=duration2)
        super()._on_set(value)
        msg = messages.SetFilterCyclesRequest(self.balboa_client.channel, start1, duration1, start2=start2, duration2=duration2)
        self.node.properties["filter-cycles"].value = "".join(map("{:02X}".format, msg.arguments))


class SettingsLockProperty(Property):

    def __init__(self):
        super().__init__("settings-lock", "Settings Lock", "boolean", settable=True)

    def _on_set(self, value):
        self.balboa_client.lock_settings()


class SetTemperatureProperty(Property):

    def __init__(self, id, name):
        super().__init__(id, name, "float", settable=True)

    def _on_set(self, value):
        if self.node.properties["temperature-scale"].value == TemperatureScaleProperty.CELSIUS:
            value *= 2
        self.balboa_client.set_temperature(int(value))


class TemperatureScaleProperty(Property):

    FAHRENHEIT = "°F"
    CELSIUS = "°C"

    def __init__(self, id, name):
        super().__init__(id, name, "enum", format=",".join([self.FAHRENHEIT, self.CELSIUS]), settable=True)

    def _on_message(self, msg):
        if msg.topic == "set":
            self.balboa_client.set_preference(
                messages.SetPreferenceRequest.PreferenceCode.TEMPERATURE_SCALE,
                messages.SetTemperatureScaleRequest.CELSIUS if msg.payload.decode("utf-8") == self.CELSIUS else messages.SetTemperatureScaleRequest.FAHRENHEIT
            )
        else:
            super()._on_message(self, msg)


class TimeProperty(Property):

    def __init__(self, id, name):
        super().__init__(id, name, "datetime", settable=True)

    def _on_set(self, value):
        logging.debug('Set time')
        logging.debug(value.utcoffset())
        logging.debug(value.timetz().utcoffset())
        self.balboa_client.set_time(value)


class ToggleItemProperty(Property):

    def __init__(self, item_code, id, name, data_type, format=None, retained=True):
        self.item_code = item_code
        super().__init__(id, name, data_type, format=format, settable=True, retained=retained)

    def _on_set(self, value):
        self.balboa_client.toggle_item(self.item_code)
