# -*- coding: utf-8 -*-
"""UI Logic and Interface Elements for Settings

This module contains several ugly God-classes that control the settings GUI functions and reactions.

Notes
-----
    Uses a really funky design pattern for a dialog that I copied from an old project. It's bad and I should probably
    ript it out
"""
import configparser
import logging
import tkinter.ttk as ttk
from tkinter import (Toplevel, Frame, Button, ACTIVE, LEFT, YES, Label, Listbox, FLAT, X, BOTH, RAISED, FALSE,
                     VERTICAL, Y, Scrollbar, END, BooleanVar, Checkbutton, StringVar, OptionMenu, Scale, HORIZONTAL,
                     Entry)
from tkinter.colorchooser import askcolor

from desktopmagic.screengrab_win32 import getDisplayRects
from lifxlan.utils import RGBtoHSBK

from ..utilities.keypress import KeybindManager
from ..utilities.utils import resource_path, str2list

config = configparser.ConfigParser()  # pylint: disable=invalid-name
config.read([resource_path("default.ini"), "config.ini"])

# Compare datetimes
DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%f"


# boilerplate code from http://effbot.org/tkinterbook/tkinter-dialog-windows.htm
class Dialog(Toplevel):
    """ Template for dialogs that include an Ok and Cancel button, and return validated user input data. """

    def __init__(self, parent, title=None):
        Toplevel.__init__(self, parent)
        self.transient(parent)
        if title:
            self.title(title)
        self.parent = parent
        self.result = None
        body = Frame(self)
        self.initial_focus = self.body(body)
        body.pack(padx=5, pady=5)
        self.buttonbox()
        self.grab_set()
        if not self.initial_focus:
            self.initial_focus = self
        self.protocol("WM_DELETE_WINDOW", self.cancel)
        self.geometry("+%d+%d" % (parent.winfo_rootx() + 50,
                                  parent.winfo_rooty() + 50))
        self.initial_focus.focus_set()
        self.wait_window(self)

    # construction hooks
    def body(self, master):
        """create dialog body.  return widget that should have initial focus. This method should be overridden"""
        pass

    def buttonbox(self):
        """ add standard button box. override if you don't want the standard buttons """
        box = Frame(self)
        # pylint: disable=invalid-name
        ok = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE)
        ok.pack(side=LEFT, padx=5, pady=5)
        cancel = Button(box, text="Cancel", width=10, command=self.cancel)
        cancel.pack(side=LEFT, padx=5, pady=5)

        self.bind("<Return>", self.ok)
        self.bind("<Escape>", self.cancel)

        box.pack()

    def ok(self, _=None):  # pylint: disable=invalid-name
        """ Standard ok semantics """
        if not self.validate():
            self.initial_focus.focus_set()  # put focus back
            return
        self.withdraw()
        self.update_idletasks()
        self.apply()
        self.cancel()

    def cancel(self, _=None):
        """put focus back to the parent window"""
        self.parent.focus_set()
        self.destroy()
        return 0

    # command hooks
    def validate(self):  # pylint: disable=no-self-use
        """ Override """
        return 1  # override

    def apply(self):
        """ Override """
        pass  # override


class MultiListbox(Frame):  # pylint: disable=too-many-ancestors
    """ Shows information about items in a column-format
    https://www.safaribooksonline.com/library/view/python-cookbook/0596001673/ch09s05.html """

    def __init__(self, master, lists):
        Frame.__init__(self, master)
        self.lists = []
        for list_, widget in lists:
            frame = Frame(self)
            frame.pack(side=LEFT, expand=YES, fill=BOTH)
            Label(frame, text=list_, borderwidth=1, relief=RAISED).pack(fill=X)
            list_box = Listbox(frame, width=widget, borderwidth=0, selectborderwidth=0,
                               relief=FLAT, exportselection=FALSE)
            list_box.pack(expand=YES, fill=BOTH)
            self.lists.append(list_box)
            list_box.bind('<B1-Motion>', lambda e, s=self: s._select(e.y))
            list_box.bind('<Button-1>', lambda e, s=self: s._select(e.y))
            list_box.bind('<Leave>', lambda e: 'break')
            list_box.bind('<B2-Motion>', lambda e, s=self: s._b2motion(e.x, e.y))
            list_box.bind('<Button-2>', lambda e, s=self: s._button2(e.x, e.y))
        frame = Frame(self)
        frame.pack(side=LEFT, fill=Y)
        Label(frame, borderwidth=1, relief=RAISED).pack(fill=X)
        scroll = Scrollbar(frame, orient=VERTICAL, command=self._scroll)
        scroll.pack(expand=YES, fill=Y)
        self.lists[0]['yscrollcommand'] = scroll.set

    def _select(self, y):  # pylint: disable=invalid-name
        """ Select a row when clicked """
        row = self.lists[0].nearest(y)
        self.selection_clear(0, END)
        self.selection_set(row)
        return 'break'

    def _button2(self, x, y):  # pylint: disable=invalid-name
        """ TODO Docs """
        for list_ in self.lists:
            list_.scan_mark(x, y)
        return 'break'

    def _b2motion(self, x, y):  # pylint: disable=invalid-name
        """ TODO Docs """
        for list_ in self.lists:
            list_.scan_dragto(x, y)
        return 'break'

    def _scroll(self, *args):
        """ Move the list down """
        for list_ in self.lists:
            list_.yview(*args)

    def curselection(self):
        """ Return currently selected list item """
        return self.lists[0].curselection()

    def delete(self, first, last=None):
        """ Remove an item from the list and GUI """
        for list_ in self.lists:
            list_.delete(first, last)

    def get(self, first, last=None):
        """ Get specific item from the list """
        result = []
        for list_ in self.lists:
            result.append(list_.get(first, last))
        if last:
            return map(*([None] + result))
        return result

    def index(self, index):
        """ Get index of item at index"""
        self.lists[0].index(index)

    def insert(self, index, *elements):
        """ Insert element into list"""
        for elm in elements:
            i = 0
            for list_ in self.lists:
                list_.insert(index, elm[i])
                i = i + 1

    def size(self):
        """ Size of internal list at call time """
        return self.lists[0].size()

    def see(self, index):
        """ Wrapper for see function that calls on each list """
        for list_ in self.lists:
            list_.see(index)

    def selection_anchor(self, index):
        """ TODO Docs """
        for list_ in self.lists:
            list_.selection_anchor(index)

    def selection_clear(self, first, last=None):
        """ Clear selection highlight """
        for list_ in self.lists:
            list_.selection_clear(first, last)

    def selection_includes(self, index):
        """ Check if item at index is in user selection """
        return self.lists[0].selection_includes(index)

    def selection_set(self, first, last=None):
        """ Manually change the selection """
        for list_ in self.lists:
            list_.selection_set(first, last)


class SettingsDisplay(Dialog):
    """ Settings form User Interface"""

    def body(self, master):
        self.root_window = master.master.master  # This is really gross. I'm sorry.
        self.logger = logging.getLogger(self.root_window.logger.name + '.SettingsDisplay')
        self.key_listener = KeybindManager(self, sticky=True)
        # Labels
        Label(master, text="Start Minimized?: ").grid(row=0, column=0)
        Label(master, text="Avg. Monitor Default: ").grid(row=1, column=0)
        Label(master, text="Smooth Transition Time (sec): ").grid(row=2, column=0)
        Label(master, text="Brightness Offset: ").grid(row=3, column=0)
        Label(master, text="Add Preset Color: ").grid(row=4, column=0)
        Label(master, text="Audio Input Source: ").grid(row=5, column=0)
        Label(master, text="Add keyboard shortcut").grid(row=6, column=0)

        # Widgets
        # Starting minimized
        self.start_mini = BooleanVar(master, value=config.getboolean("AppSettings", "start_minimized"))
        self.start_mini_check = Checkbutton(master, variable=self.start_mini)

        # Avg monitor color match
        self.avg_monitor = StringVar(master, value=config["AverageColor"]["DefaultMonitor"])
        options = ['full', 'get_primary_monitor', *getDisplayRects()]
        # lst = getDisplayRects()
        # for i in range(1, len(lst) + 1):
        #    els = [list(x) for x in itertools.combinations(lst, i)]
        #    options.extend(els)
        self.avg_monitor_dropdown = OptionMenu(master, self.avg_monitor, *options)

        self.duration_scale = Scale(master, from_=0, to_=2, resolution=1 / 15, orient=HORIZONTAL)
        self.duration_scale.set(float(config["AverageColor"]["Duration"]))

        self.brightness_offset = Scale(master, from_=0, to_=65535, resolution=1, orient=HORIZONTAL)
        self.brightness_offset.set(int(config["AverageColor"]["brightnessoffset"]))

        # Custom preset color
        self.preset_color_name = Entry(master)
        self.preset_color_name.insert(END, "Enter color name...")
        self.preset_color_button = Button(master, text="Choose and add!", command=self.get_color)

        # Audio dropdown
        device_names = self.master.audio_interface.get_device_names()
        try:
            init_string = " " + config["Audio"]["InputIndex"] + " " + device_names[int(config["Audio"]["InputIndex"])]
        except ValueError:
            init_string = " None"
        self.audio_source = StringVar(master,
                                      init_string)  # AudioSource index is grabbed from [1], so add a space at [0]
        as_choices = device_names.items()
        self.as_dropdown = OptionMenu(master, self.audio_source, *as_choices)

        # Add keybindings
        lightnames = list(self.root_window.lightsdict.keys())
        self.keybind_bulb_selection = StringVar(master, value=lightnames[0])
        self.keybind_bulb_dropdown = OptionMenu(master, self.keybind_bulb_selection,
                                                *lightnames)
        self.keybind_keys_select = Entry(master)
        self.keybind_keys_select.insert(END, "Add key-combo...")
        self.keybind_keys_select.config(state='readonly')
        self.keybind_keys_select.bind('<FocusIn>', self.on_keybind_keys_click)
        self.keybind_keys_select.bind('<FocusOut>', lambda *_: self.keybind_keys_select.config(state='readonly'))
        self.keybind_color_selection = StringVar(master, value="Color")
        self.keybind_color_dropdown = OptionMenu(master, self.keybind_color_selection,
                                                 *self.root_window.framesdict[
                                                     self.keybind_bulb_selection.get()].default_colors,
                                                 *(
                                                     [*config["PresetColors"].keys()] if any(
                                                         config["PresetColors"].keys()) else [None])
                                                 )
        self.keybind_add_button = Button(master, text="Add keybind",
                                         command=lambda *_: self.register_keybinding(
                                             self.keybind_bulb_selection.get(), self.keybind_keys_select.get(),
                                             self.keybind_color_selection.get()))
        self.keybind_delete_button = Button(master, text="Delete keybind", command=self.delete_keybind)

        # Insert
        self.start_mini_check.grid(row=0, column=1)
        ttk.Separator(master, orient=HORIZONTAL).grid(row=0, sticky='esw', columnspan=100)
        self.avg_monitor_dropdown.grid(row=1, column=1)
        self.duration_scale.grid(row=2, column=1)
        self.brightness_offset.grid(row=3, column=1)
        ttk.Separator(master, orient=HORIZONTAL).grid(row=3, sticky='esw', columnspan=100)
        self.preset_color_name.grid(row=4, column=1)
        self.preset_color_button.grid(row=4, column=2)
        ttk.Separator(master, orient=HORIZONTAL).grid(row=4, sticky='esw', columnspan=100)
        self.as_dropdown.grid(row=5, column=1)
        ttk.Separator(master, orient=HORIZONTAL).grid(row=5, sticky='esw', columnspan=100)
        self.keybind_bulb_dropdown.grid(row=6, column=1)
        self.keybind_keys_select.grid(row=6, column=2)
        self.keybind_color_dropdown.grid(row=6, column=3)
        self.keybind_add_button.grid(row=6, column=4)
        self.mlb = MultiListbox(master, (('Bulb', 5), ('Keybind', 5), ('Color', 5)))
        for keypress, fnx in dict(config['Keybinds']).items():
            label, color = fnx.split(':')
            self.mlb.insert(END, (label, keypress, color))
        self.mlb.grid(row=7, columnspan=100, sticky='esw')
        self.keybind_delete_button.grid(row=8, column=0)

    def validate(self):
        config["AppSettings"]["start_minimized"] = str(self.start_mini.get())
        config["AverageColor"]["DefaultMonitor"] = str(self.avg_monitor.get())
        config["AverageColor"]["Duration"] = str(self.duration_scale.get())
        config["AverageColor"]["BrightnessOffset"] = str(self.brightness_offset.get())
        config["Audio"]["InputIndex"] = str(self.audio_source.get()[1])
        # Write to config file
        with open('config.ini', 'w') as cfg:
            config.write(cfg)

        self.key_listener.shutdown()

        return 1

    def get_color(self):
        """ Present user with color pallette dialog and return color in HSBK """
        color = askcolor()[0]
        if color:
            # RGBtoHBSK sometimes returns >65535, so we have to clamp
            hsbk = [min(c, 65535) for c in RGBtoHSBK(color)]
            config["PresetColors"][self.preset_color_name.get()] = str(hsbk)

    def register_keybinding(self, bulb: str, keys: str, color: str):
        """ Get the keybind from the input box and pass the color off to the root window. """
        try:
            color = self.root_window.framesdict[self.keybind_bulb_selection.get()].default_colors[color]
        except KeyError:  # must be using a custom color
            color = str2list(config["PresetColors"][color], int)
        self.root_window.save_keybind(bulb, keys, color)
        config["Keybinds"][str(keys)] = str(bulb + ":" + str(color))
        self.mlb.insert(END, (str(bulb), str(keys), str(color)))
        self.keybind_keys_select.config(state='normal')
        self.keybind_keys_select.delete(0, 'end')
        self.keybind_keys_select.insert(END, "Add key-combo...")
        self.keybind_keys_select.config(state='readonly')
        self.preset_color_name.focus_set()  # Set focus to a dummy widget to reset the Entry

    def on_keybind_keys_click(self, event):
        """ Call when cursor is in key-combo entry """
        self.update()
        self.update_idletasks()
        self.key_listener.restart()
        self.keybind_keys_select.config(state='normal')
        self.update()
        self.update_idletasks()
        while self.focus_get() == self.keybind_keys_select:
            self.keybind_keys_select.delete(0, 'end')
            self.keybind_keys_select.insert(END, self.key_listener.get_key_combo_code())
            self.update()
            self.update_idletasks()

    def delete_keybind(self):
        """ Delete keybind currently selected in the multi-list box. """
        _, keybind, _ = self.mlb.get(ACTIVE)
        self.mlb.delete(ACTIVE)
        self.root_window.delete_keybind(keybind)
        config.remove_option("Keybinds", keybind)
