text: # -*- 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)
  