# Plugin that manages YeeLight devices.

import yeelight
import time
import threading
import logging
import random
import weakref
import traceback

import iot_devices.device

import colorzero

logger = logging.Logger("plugins.yeelight")


lookup = {}

lastRefreshed = 0

lock = threading.Lock()


def maybeRefresh(t=30):
    global lastRefreshed
    if lastRefreshed < time.time() - t:
        refresh()
    elif lastRefreshed < time.time() - 5:
        refresh(3)


def refresh(timeout=8):
    global lastRefreshed, lookup
    from yeelight import discover_bulbs

    lastRefreshed = time.time()

    d = discover_bulbs()
    l = {}

    for i in d:
        if "name" in i["capabilities"]:
            l[i["capabilities"]["name"]] = i
        l[i["ip"]] = i
    lookup = l


def isIp(x):
    x = x.strip().split(".")
    try:
        x = [int(i) for i in x]
        if len(x) == 4:
            return True
    except Exception:
        return False


def getDevice(locator, timeout=10, klass=None):
    """Since plugs can change name, you should't keep a reference
    to a plug for too long. Instead use this function.
    """
    global lookup
    if not isIp(locator):
        if locator in lookup:
            return lookup[locator]

        maybeRefresh()
        if locator in lookup:
            return lookup[locator]

    return klass(locator)


def makeFlusher(wr):
    def f():
        while 1:
            try:
                time.sleep(3)
                d = wr()
                if not d:
                    return
                if d.closed:
                    return
                d.flush()
            except Exception as e:
                print(traceback.format_exc())

    t = threading.Thread(daemon=True, name="YeelightFlusher", target=f)
    t.start()


class YeelightDevice(iot_devices.device.Device):
    def __init__(self, data, **kw):
        iot_devices.device.Device.__init__(self, data, **kw)

        self.lock = threading.RLock()

        self.numeric_data_point("rssi", writable=False)
        self.set_alarm("Low Signal", "rssi", "value < -90")

        self.rssiCacheTime = 0
        self.lastLoggedUnreachable = 0

    def getRawDevice(self):
        return getDevice(self.data.get("device.locator"), 3, self.kdClass)

    def rssi(self, cacheFor=120, timeout=3):
        "These bulbs don't have RSSI that i found, instead we just detect reachability or not"
        with self.lock:
            "Returns the current RSSI value of the device"
            if time.time() - self.rssiCacheTime < cacheFor:
                return self.datapoints["rssi"].get()[0] or -75

            # Not ideal, but we really can't be retrying this too often.
            # if it's disconnected. Way too much slowdown
            self.rssiCacheTime = time.time()

            try:
                info = getDevice(
                    self.data.get("device.locator"), timeout, self.kdClass
                ).get_properties()

                self.set_data_point("rssi", -70)
            except Exception:
                self.set_data_point("rssi", -180)
                if self.lastLoggedUnreachable < time.time() - 30:
                    self.handle_error("Device was unreachable")
                    self.lastLoggedUnreachable = time.time()
                raise

            return self.datapoints["rssi"].get()[0] or -75


class YeelightRGB(YeelightDevice):
    device_type = "YeelightRGB"
    kdClass = yeelight.Bulb
    descriptors = {
        "kaithem.device.hsv": 1,
        "kaithem.device.powerswitch": 1,
    }

    def flush(self):
        if not self.hasData:
            return

        # Rate limits will prevent getting rejected
        self.allowedOperations = min(
            self.allowedOperations + (time.time() - self.lastRecalcedAllowed), 40
        )
        self.lastRecalcedAllowed = time.time()

        # Drop frames randomly. This will look a lot better with flickery type effects.
        if (random.random() * 20 + 1) > self.allowedOperations:
            return

        self.allowedOperations -= 1
        self.hasData = False

        color = colorzero.Color(self.datapoints["color"].get()[0] or "white")
        rgb = color.rgb

        # Very crappy color correction done by trial and error
        hsv = colorzero.Color.from_rgb(
            rgb[0], max(0, rgb[1] - rgb[0] * 0.1), max(0, rgb[2] - rgb[0] * 0.1)
        ).hsv

        duration = self.datapoints["fade"].get()[0] or 0

        if hsv.v < 0.0001:
            self.wasOff = True
            self.setSwitch(0, False, duration)
        else:
            try:
                self.getRawDevice().set_hsv(
                    int(hsv.h * 359.9),
                    int(hsv.s * 100),
                    int(hsv.v * 100),
                    effect="smooth" if duration else "sudden",
                    duration=int(duration * 1000) if not self.wasOff else 0,
                )
            except Exception:
                # Assume turned off.  Turn on. Retry.
                self.setSwitch(0, True, duration)
                self.getRawDevice().set_hsv(
                    int(hsv.h * 359.9),
                    int(hsv.s * 100),
                    int(hsv.v * 100),
                    effect="smooth" if duration else "sudden",
                    duration=int(duration * 1000) if not self.wasOff else 0,
                )

            if self.wasOff:
                self.wasOff = False
                self.setSwitch(0, True, duration)

    def on_before_close(self):
        return super().close()
        self.closed = True

    def __init__(self, name, data):
        YeelightDevice.__init__(self, name, data)
        self.closed = False
        self.lastHueChange = time.time()

        self.allowedOperations = 60
        self.lastRecalcedAllowed = time.time()

        # Has color data to flush
        self.hasData = False

        def swhandle(v, t, a):
            try:
                self.setSwitch(0, v >= 0.5)
            except Exception:
                pass

        self.numeric_data_point(
            "switch", min=0, max=1, subtype="bool", interval=300, handler=swhandle
        )
        self.numeric_data_point("fade", min=0, max=10, subtype="light_fade_duration")

        self.huesat = -1
        self.lastVal = -1
        self.wasOff = True
        self.oldTransitionRate = -1

        def colorhandle(v, t, a):
            self.hasData = True
            self.flush()

        self.string_data_point("color", subtype="color", handler=colorhandle)

        makeFlusher(weakref.ref(self))

    def getSwitch(self, channel, state):
        if channel > 0:
            raise ValueError("Bulb has 1 master power channel only")
        return self.getRawDevice().is_on

    def setSwitch(self, channel, state, duration=1):
        logger.debug(
            "Setting smartplug "
            + self.data.get("device.locator")
            + "to state "
            + str(state)
        )
        self.set_data_point("fade", duration)
        self.set_data_point("switch", 1 if state else 0)

        duration = self.datapoints["fade"].get()[0] or 0

        with self.lock:
            "Set the state of switch channel N"
            if channel > 0:
                raise ValueError("This is a 1 channel device")
            try:
                if state:
                    getDevice(self.data.get("device.locator"), 3, self.kdClass).turn_on(
                        effect="smooth", duration=int(duration * 1000)
                    )
                    self.wasOff = False
                else:
                    getDevice(
                        self.data.get("device.locator"), 3, self.kdClass
                    ).turn_off(effect="smooth", duration=int(duration * 1000))
                    self.wasOff = True
                self.oldTransitionRate = duration
            except Exception:
                if self.lastLoggedUnreachable < time.time() - 30:
                    self.handle_error("Device was unreachable")
                    self.lastLoggedUnreachable = time.time()
                self.set_data_point("rssi", -180)
                raise

            self.set_data_point("rssi", -75)

    def setHSV(self, channel, hue, sat, val, duration=1):
        if channel > 0:
            raise ValueError("Bulb has 1 color only")
        self.set_data_point("fade", duration)
        self.set_data_point("color", colorzero.Color.from_hsv(hue / 360, sat, val).html)

    @classmethod
    def discover_devices(cls, config={}, current_device=None, intent=None, **kw):
        global lookup
        maybeRefresh()
        l = {}
        for i in lookup:
            config2 = config.copy()

            config2.update(
                {
                    "type": cls.device_type,
                    "device.locator": lookup[i]["capabilities"].get("name", "")
                    or lookup[i]["ip"],
                }
            )

            l[lookup[i]["capabilities"].get("name", "") or lookup[i]["ip"]] = config2

        return l
