# -*- coding: utf-8 -*-
"""Multi-Threaded Color Changer

Contains several basic "Color-Following" functions, as well as custom Stop/Start threads for these effects.
"""
import logging
import threading
from functools import lru_cache

import numexpr as ne
import numpy as np
# from lib.color_functions import dominant_color
from PIL import Image
from desktopmagic.screengrab_win32 import getRectAsImage, getScreenAsImage
from lifxlan import utils

from ..ui.settings import config
from .utils import str2list


@lru_cache(maxsize=32)
def get_monitor_bounds(func):
    """ Returns the rectangular coordinates of the desired Avg. Screen area. Can pass a function to find the result
    procedurally """
    return func() or config["AverageColor"]["DefaultMonitor"]


def avg_screen_color(initial_color, func_bounds=lambda: None):
    """ Capture an image of the monitor defined by func_bounds, then get the average color of the image in HSBK"""
    monitor = get_monitor_bounds(func_bounds)
    if "full" in monitor:
        screenshot = getScreenAsImage()
    else:
        screenshot = getRectAsImage(str2list(monitor, int))
    # Resizing the image to 1x1 pixel will give us the average for the whole image (via HAMMING interpolation)
    color = screenshot.resize((1, 1), Image.HAMMING).getpixel((0, 0))
    color_hsbk = list(utils.RGBtoHSBK(color, temperature=initial_color[3]))
    return color_hsbk


def dominant_screen_color(initial_color, func_bounds=lambda: None):
    """
    https://stackoverflow.com/questions/50899692/most-dominant-color-in-rgb-image-opencv-numpy-python
    """
    monitor = get_monitor_bounds(func_bounds)
    if "full" in monitor:
        screenshot = getScreenAsImage()
    else:
        screenshot = getRectAsImage(str2list(monitor, int))

    downscale_width, downscale_height = screenshot.width // 4, screenshot.height // 4
    screenshot = screenshot.resize((downscale_width, downscale_height), Image.HAMMING)

    a = np.array(screenshot)
    a2D = a.reshape(-1, a.shape[-1])
    col_range = (256, 256, 256)  # generically : a2D.max(0)+1
    eval_params = {'a0': a2D[:, 0], 'a1': a2D[:, 1], 'a2': a2D[:, 2],
                   's0': col_range[0], 's1': col_range[1]}
    a1D = ne.evaluate('a0*s0*s1+a1*s0+a2', eval_params)
    color = np.unravel_index(np.bincount(a1D).argmax(), col_range)

    color_hsbk = list(utils.RGBtoHSBK(color, temperature=initial_color[3]))
    # color_hsbk[2] = initial_color[2]  # TODO Decide this
    return color_hsbk


class ColorThread(threading.Thread):
    """ A Simple Thread which runs when the _stop event isn't set """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, daemon=True, **kwargs)
        self._stop = threading.Event()

    def stop(self):
        """ Stop thread by setting event """
        self._stop.set()

    def stopped(self):
        """ Check if thread has been stopped """
        return self._stop.isSet()


class ColorThreadRunner:
    """ Manages an asynchronous color-change with a Device. Can be run continuously, stopped and started. """

    def __init__(self, bulb, color_function, parent, continuous=True, **kwargs):
        self.bulb = bulb
        self.color_function = color_function
        self.kwargs = kwargs
        self.parent = parent  # couple to parent frame
        self.logger = logging.getLogger(parent.logger.name + '.Thread({})'.format(color_function.__name__))
        self.prev_color = parent.get_color_values_hsbk()
        self.continuous = continuous
        self.thread = ColorThread(target=self.match_color, args=(self.bulb,))
        try:
            label = self.bulb.get_label()
        except:  # pylint: disable=bare-except
            # If anything goes wrong in getting the label just set it to ERR; we really don't care except for logging.
            label = "<LABEL-ERR>"
        self.logger.info(
            'Initialized Thread: Bulb: %s // Continuous: %s', label, self.continuous)

    def match_color(self, bulb):
        """ ColorThread target which calls the 'change_color' function on the bulb. """
        self.logger.debug('Starting color match.')
        self.prev_color = self.parent.get_color_values_hsbk()  # coupling to LightFrame from gui.py here
        while not self.thread.stopped():
            try:
                color = list(self.color_function(initial_color=self.prev_color, **self.kwargs))
                color[2] = min(color[2] + self.get_brightness_offset(), 65535)
                bulb.set_color(color, duration=self.get_duration() * 1000,
                               rapid=self.continuous)
                self.prev_color = color
            except OSError:
                # This is dirty, but we really don't care, just keep going
                self.logger.info("Hit an os error")
                continue
            if not self.continuous:
                self.stop()
        self.logger.debug('Color match finished.')

    def start(self):
        """ Start the match_color thread"""
        if self.thread.stopped():
            self.thread = ColorThread(target=self.match_color, args=(self.bulb,))
            self.thread.setDaemon(True)
        try:
            self.thread.start()
            self.logger.debug('Thread started.')
        except RuntimeError:
            self.logger.error('Tried to start ColorThread again.')

    def stop(self):
        """ Stop the match_color thread"""
        self.thread.stop()

    @staticmethod
    def get_duration():
        """ Read the transition duration from the config file. """
        return float(config["AverageColor"]["duration"])

    @staticmethod
    def get_brightness_offset():
        """ Read the brightness offset from the config file. """
        return int(config["AverageColor"]["brightnessoffset"])


def install_thread_excepthook():
    """
    Workaround for sys.excepthook thread bug
    (https://sourceforge.net/tracker/?func=detail&atid=105470&aid=1230540&group_label=5470).
    Call once from __main__ before creating any threads.
    If using psyco, call psycho.cannotcompile(threading.Thread.run)
    since this replaces a new-style class method.
    """
    import sys
    run_old = threading.Thread.run

    def run(*args, **kwargs):
        """ Monkey-patch for the run function that installs local excepthook """
        try:
            run_old(*args, **kwargs)
        except (KeyboardInterrupt, SystemExit):
            raise
        except:  # pylint: disable=bare-except
            sys.excepthook(*sys.exc_info())

    threading.Thread.run = run


install_thread_excepthook()
