text: from configparser import ConfigParser
  import keyboard
  import tkinter as tk
  import sys
  from win32clipboard import OpenClipboard, EmptyClipboard, SetClipboardText, CloseClipboard
  
  from typing import Dict, List, Tuple
  from DataClasses import PoeWindowInfo
  from ArchnemesisItemsMap import ArchnemesisItemsMap
  from ImageScanner import ImageScanner
  from DataClasses import RecipeItemNode
  from RecipeShopper import RecipeShopper
  from constants import COLOR_BG, COLOR_FG_GREEN, COLOR_FG_LIGHT_GREEN, COLOR_FG_ORANGE, COLOR_FG_WHITE, FONT_BIG, FONT_SMALL
  
  
  class UIOverlay:
      """
      Overlay window using tkinter '-topmost' property
      """
      def __init__(self, root: tk.Tk, info: PoeWindowInfo, items_map: ArchnemesisItemsMap, image_scanner: ImageScanner, recipe_shopper: RecipeShopper):
          self._window_info = info
          self._items_map = items_map
          self._image_scanner = image_scanner
          self._root = root
          self._recipe_shopper = recipe_shopper
          self._scan_results_window = None
          self._recipe_browser_window = None
          self._recipe_browser_current_root = ''
          self._tooltip_window = None
          self._highlight_windows_to_show = list()
          self._scan_results_window_saved_position = (-1, 0)
  
  
          self._settings = Settings(root, items_map, image_scanner)
  
          hotkey = self._settings.get_scan_hotkey()
          if hotkey:
              try:
                  keyboard.add_hotkey(hotkey, self._hotkey_pressed)
              except ValueError:
                  print('Invalid scan hotkey')
  
          self._create_controls()
  
          self._root.configure(bg='')
          self._root.geometry(f'+{info.x + 5}+{info.y + info.title_bar_height + 5}')
          if self._settings.should_run_as_overlay():
              self._root.overrideredirect(True)
              self._root.wm_attributes('-topmost', True)
          self._root.deiconify()
  
      @staticmethod
      def create_toplevel_window(bg=''):
          w = tk.Toplevel()
          w.configure(bg=bg)
          # Hide window outline/controls
          w.overrideredirect(True)
          # Make sure the window is always on top
          w.wm_attributes('-topmost', True)
          return w
  
      def _hotkey_pressed(self) -> None:
          if self._scan_label_text.get() == 'Scan':
              self._scan(None)
          elif self._scan_label_text.get() == 'Hide':
              self._hide(None)
  
      def _create_controls(self) -> None:
          l = tk.Button(self._root, text='[X]', fg=COLOR_FG_GREEN, bg=COLOR_BG, font=FONT_SMALL)
          l.bind('<Button-1>', sys.exit)
          l.bind('<B3-Motion>', lambda event: self._drag(self._root, -5, -5, event))
          l.grid(row=0, column=0)
  
          settings = tk.Button(self._root, text='Settings', fg=COLOR_FG_GREEN, bg=COLOR_BG, font=FONT_SMALL)
          settings.bind('<Button-1>', lambda _: self._settings.show())
          settings.bind('<B3-Motion>', lambda event: self._drag(self._root, -5, -5, event))
          settings.grid(row=0, column=1)
  
          self._scan_label_text = tk.StringVar(self._root, value='Scan')
          self._scan_label = tk.Button(self._root, textvariable=self._scan_label_text, fg=COLOR_FG_GREEN, bg=COLOR_BG, font=FONT_SMALL)
          self._scan_label.bind("<Button-1>", self._scan)
          self._scan_label.bind('<B3-Motion>', lambda event: self._drag(self._root, -5, -5, event))
          self._scan_label.grid(row=0, column=2)
  
      def _drag(self, window, offset_x: int, offset_y: int, event) -> Tuple[int, int]:
          x = offset_x + event.x + window.winfo_x()
          y = offset_y + event.y + window.winfo_y()
          window.geometry(f'+{x}+{y}')
          return (x, y)
  
      def _scan(self, _) -> None:
          self._scan_label_text.set('Scanning...')
          self._root.update()
          results = self._image_scanner.scan()
  
          shopping_list_mode = self._settings.is_shopping_list_mode() is True
          desired_items = self._settings.get_shopping_list().split(",")
          shopping_list = self._recipe_shopper.get_missing_items(desired_items, results)
          print("Missing Items:", shopping_list)
  
          main_recipe_list = self._items_map.recipes()
          shopping_list_recipe_list = [x for x in self._items_map.recipes() if x[0] in self._recipe_shopper._get_full_shopping_list(desired_items)]
          recipe_list = main_recipe_list if not shopping_list_mode else shopping_list_recipe_list
          if len(results) > 0:
              recipes = list()
              for item, recipe in recipe_list:
                  screen_items = [results.get(x) for x in recipe]
                  if (all(screen_items) or self._settings.should_display_unavailable_recipes()):
                      recipes.append((item, [x[0] for x in screen_items if x is not None], item in results, all(screen_items)))
  
              if shopping_list_mode:
                  trash_inventory = self._recipe_shopper.get_trash_inventory(desired_items, results)
                  trash_recipe_items = [None] * min(4, len(trash_inventory.keys()))
                  trash_recipe_items = [trash_inventory[list(trash_inventory.keys())[i]][0] for i,x in enumerate(trash_recipe_items)]
                  trash_recipe = ('Trash', trash_recipe_items, False, True)
                  recipes.append(trash_recipe)
                  
              self._show_scan_results(results, recipes)
  
              self._scan_label_text.set('Hide')
              self._scan_label.bind('<Button-1>', self._hide)
          else:
              self._hide(None)
  
      def _hide(self, _) -> None:
          if self._scan_results_window is not None:
              self._scan_results_window.destroy()
          if self._recipe_browser_window is not None:
              self._recipe_browser_window.destroy()
          if self._tooltip_window is not None:
              self._tooltip_window.destroy()
          self._clear_highlights(None)
          self._scan_label_text.set('Scan')
          self._scan_label.bind('<Button-1>', self._scan)
  
      def _show_scan_results(self, results: Dict[str, List[Tuple[int, int]]], recipes: List[Tuple[str, List[Tuple[int, int]], bool, bool]]) -> None:
          self._scan_results_window = UIOverlay.create_toplevel_window()
          x, y = self._scan_results_window_saved_position
          if x == -1:
              x = self._window_info.x + int(self._window_info.client_width / 3)
              y = self._window_info.y + self._window_info.title_bar_height
          self._scan_results_window.geometry(f'+{x}+{y}')
  
          last_column = 0
          if self._settings.should_display_inventory_items():
              last_column = self._show_inventory_list(results)
          self._show_recipes_list(results, recipes, last_column + 2)
  
      def _show_inventory_list(self, results: Dict[str, List[Tuple[int, int]]]) -> int:
          row = 0
          column = 0
  
          for item in self._items_map.items():
              inventory_items = results.get(item)
              if inventory_items is not None:
                  row, column = self._show_image_and_label(item, results, inventory_items, COLOR_FG_WHITE, f'x{len(inventory_items)} {item}', True, row, column)
          return column
  
      def _show_recipes_list(self, results: Dict[str, List[Tuple[int, int]]], recipes: List[Tuple[str, List[Tuple[int, int]], bool, bool]], column: int) -> None:
          row = 0
  
          for item, inventory_items, exists_in_inventory, available in recipes:
              if exists_in_inventory:
                  if available:
                      fg = COLOR_FG_GREEN
                  else:
                      fg = COLOR_FG_LIGHT_GREEN
              else:
                  if available:
                      fg = COLOR_FG_ORANGE
                  else:
                      fg = COLOR_FG_WHITE
              row, column = self._show_image_and_label(item, results, inventory_items, fg, item, available, row, column)
  
      def _show_image_and_label(self, item: str, results: Dict[str, List[Tuple[int, int]]], inventory_items: Tuple[int, int], highlight_color: str, label_text: str, highlight, row: int, column: int) -> Tuple[int, int]:
          image = tk.Label(self._scan_results_window, image=self._items_map.get_display_small_image(item), bg=COLOR_BG, pady=5)
          if highlight:
              image.bind('<Enter>', lambda _, arg=inventory_items, color=highlight_color: self._highlight_items_in_inventory(arg, color))
              image.bind('<Leave>', self._clear_highlights)
          image.bind('<Button-1>', lambda _, arg1=item, arg2=results: self._show_recipe_browser_tree(arg1, arg2))
          image.bind('<B3-Motion>', self._scan_results_window_drag_and_save)
          image.grid(row=row, column=column)
          tk.Label(self._scan_results_window, text=label_text, font=FONT_BIG, fg=highlight_color, bg=COLOR_BG).grid(row=row, column=column + 1, sticky='w', padx=5)
          row += 1
          if row % 10 == 0:
              column += 2
              row = 0
          return (row, column)
  
      def _scan_results_window_drag_and_save(self, event) -> None:
          self._scan_results_window_saved_position = self._drag(self._scan_results_window, -5, -5, event)
  
      def _show_recipe_browser_tree(self, item: str, results: Dict[str, List[Tuple[int, int]]]) -> None:
          if self._recipe_browser_window is not None:
              self._recipe_browser_window.destroy()
          self._destroy_tooltip_and_clear_highlights(None)
          # If the user clicks on the current root then close the tree
          if self._recipe_browser_current_root == item:
              return
          self._recipe_browser_current_root = item
          self._recipe_browser_window = UIOverlay.create_toplevel_window()
          self._recipe_browser_window.geometry(f'+{self._scan_results_window.winfo_x()}+{self._scan_results_window.winfo_y() + self._scan_results_window.winfo_height() + 40}')
  
          tree = self._items_map.get_subtree_for(item)
          if self._settings.should_copy_recipe_to_clipboard():
              self._copy_tree_items_to_clipboard(tree)
  
          def draw_tree(node, row, column):
              children_column = column
              for c in node.components:
                  children_column = draw_tree(c, row + 2, children_column)
              columnspan = max(1, children_column - column)
              if node.item in results:
                  bg = COLOR_FG_GREEN
              else:
                  bg = COLOR_BG
              l = tk.Label(self._recipe_browser_window, image=self._items_map.get_display_small_image(node.item), bg=bg, relief=tk.SUNKEN)
              l.bind('<Button-1>', lambda _, arg1=node.item, arg2=results: self._show_recipe_browser_tree(arg1, arg2))
              l.bind('<B3-Motion>', lambda event: self._drag(self._recipe_browser_window, -5, -5, event))
              l.bind('<Enter>', lambda _, arg1=self._recipe_browser_window, arg2=results.get(node.item), arg3=node.item: self._create_tooltip_and_highlight(arg1, arg2, arg3))
              l.bind('<Leave>', self._destroy_tooltip_and_clear_highlights)
              l.grid(row=row, column=column, columnspan=columnspan)
              if len(node.components) > 0:
                  f = tk.Frame(self._recipe_browser_window, bg=COLOR_BG, width=(self._items_map.small_image_size + 4) * columnspan, height=3)
                  f.grid(row=row + 1, column=column, columnspan=columnspan)
              return children_column + 1
          total_columns = draw_tree(tree, 1, 0)
          for c in range(total_columns):
              self._recipe_browser_window.grid_columnconfigure(c, minsize=self._items_map.small_image_size)
          # Show parents on row 0
          parents = [RecipeItemNode(p, []) for p in self._items_map.get_parent_recipes_for(item)]
          if len(parents) > 0:
              tk.Label(self._recipe_browser_window, text='Used in:', bg=COLOR_BG, fg=COLOR_FG_GREEN, font=FONT_BIG).grid(row=0, column=0)
              for column, p in enumerate(parents):
                  # Reuse the same function for convenience
                  draw_tree(p, 0, column + 1)
  
      def _highlight_items_in_inventory(self, inventory_items: List[Tuple[int, int]], color: str) -> None:
          self._highlight_windows_to_show = list()
          for (x, y) in inventory_items:
              x_offset, y_offset, _, _ = self._image_scanner.scanner_window_size
              x += x_offset
              y += y_offset
              width = int(self._items_map.image_size[0] * 0.7)
              height = int(self._items_map.image_size[1] * 0.7)
              w = UIOverlay.create_toplevel_window(bg=color)
              w.geometry(f'{width}x{height}+{x}+{y}')
              self._highlight_windows_to_show.append(w)
  
      def _clear_highlights(self, _) -> None:
          for w in self._highlight_windows_to_show:
              w.destroy()
  
      def _create_tooltip_and_highlight(self, window, inventory_items, text) -> None:
          if self._tooltip_window is not None:
              self._tooltip_window.destroy()
          self._tooltip_window = UIOverlay.create_toplevel_window()
          self._tooltip_window.geometry(f'+{window.winfo_x()}+{window.winfo_y() - 40}')
          tk.Label(self._tooltip_window, text=text, font=FONT_BIG, bg=COLOR_BG, fg=COLOR_FG_GREEN).pack()
  
          if inventory_items is not None:
              self._highlight_items_in_inventory(inventory_items, COLOR_FG_GREEN)
  
      def _copy_tree_items_to_clipboard(self, tree):
          if len(tree.components) > 0:
              search_string = '|'.join((str(x.item) for x in tree.components))
          else:
              search_string = tree.item
  
          OpenClipboard()
          EmptyClipboard()
          SetClipboardText('^('+search_string+')')
          CloseClipboard()
  
      def _destroy_tooltip_and_clear_highlights(self, _) -> None:
          if self._tooltip_window is not None:
              self._tooltip_window.destroy()
          self._clear_highlights(None)
  
      def run(self) -> None:
          self._root.mainloop()
  
  class Settings:
      def __init__(self, root: tk.Tk, items_map: ArchnemesisItemsMap, image_scanner):
          self._root = root
          self._items_map = items_map
          self._image_scanner = image_scanner
          self._window = None
  
          self._config = ConfigParser()
          self._config_file = 'settings.ini'
  
          self._config.read(self._config_file)
          if 'settings' not in self._config:
              self._config.add_section('settings')
          s = self._config['settings']
  
          scanner_window_size = s.get('scanner_window')
          if scanner_window_size is not None:
              self._image_scanner.scanner_window_size = tuple(map(int, scanner_window_size.replace('(', '').replace(')', '').replace(',', '').split()))
          self._items_map.scale = float(s.get('image_scale', self._items_map.scale))
          self._image_scanner.confidence_threshold = float(s.get('confidence_threshold', self._image_scanner.confidence_threshold))
          b = s.get('display_inventory_items')
          self._display_inventory_items = True if b is not None and b == 'True' else False
          b = s.get('display_unavailable_recipes')
          self._display_unavailable_recipes = True if b is not None and b == 'True' else False
          b = s.get('copy_recipe_to_clipboard')
          self._copy_recipe_to_clipboard = True if b is not None and b == 'True' else False
          b = s.get('scan_hotkey')
          self._scan_hotkey = b if b is not None else ''
          b = s.get('run_as_overlay')
          self._run_as_overlay = True if b is None or b == 'True' else False
          b = s.get('shopping_list_mode')
          self._shopping_list_mode = False if b is None or b == 'False' else True
          b = s.get('shopping_list')
          self._shopping_list = '' if b is None else b
  
  
      def show(self) -> None:
          if self._window is not None:
              return
          self._window = tk.Toplevel()
  
          self._window.geometry('+100+200')
          self._window.protocol('WM_DELETE_WINDOW', self._close)
  
          current_scanner_window = f'{self._image_scanner.scanner_window_size}'.replace('(', '').replace(')', '')
          v = tk.StringVar(self._window, value=current_scanner_window)
          self._scanner_window_entry = tk.Entry(self._window, textvariable=v)
          self._scanner_window_entry.grid(row=0, column=0)
          tk.Button(self._window, text='Set scanner window', command=self._update_scanner_window).grid(row=0, column=1)
  
          v = tk.DoubleVar(self._window, value=self._items_map.scale)
          self._scale_entry = tk.Entry(self._window, textvariable=v)
          self._scale_entry.grid(row=1, column=0)
          tk.Button(self._window, text='Set image scale', command=self._update_scale).grid(row=1, column=1)
  
          v = tk.DoubleVar(self._window, value=self._image_scanner.confidence_threshold)
          self._confidence_threshold_entry = tk.Entry(self._window, textvariable=v)
          self._confidence_threshold_entry.grid(row=2, column=0)
          tk.Button(self._window, text='Set confidence threshold', command=self._update_confidence_threshold).grid(row=2, column=1)
  
          v = tk.StringVar(self._window, value=self._scan_hotkey)
          self._scan_hotkey_entry = tk.Entry(self._window, textvariable=v)
          self._scan_hotkey_entry.grid(row=3, column=0)
          tk.Button(self._window, text='Set scan/hide hotkey', command=self._update_scan_hotkey).grid(row=3, column=1)
  
          c = tk.Checkbutton(self._window, text='Display inventory items', command=self._update_display_inventory_items)
          c.grid(row=4, column=0, columnspan=2)
          if self._display_inventory_items:
              c.select()
  
          c = tk.Checkbutton(self._window, text='Display unavailable recipes', command=self._update_display_unavailable_recipes)
          c.grid(row=5, column=0, columnspan=2)
          if self._display_unavailable_recipes:
              c.select()
  
          c = tk.Checkbutton(self._window, text='Copy recipe to clipboard', command=self._update_copy_recipe_to_clipboard)
          c.grid(row=6, column=0, columnspan=2)
          if self._copy_recipe_to_clipboard:
              c.select()
  
          c = tk.Checkbutton(self._window, text='Run as overlay', command=self._update_run_as_overlay)
          c.grid(row=7, column=0, columnspan=2)
          if self._run_as_overlay:
              c.select()
  
          
          c = tk.Checkbutton(self._window, text='Shopping List Mode', command=self._update_shopping_list_mode)
          c.grid(row=8, column=0, columnspan=2)
          if self._shopping_list_mode:
              c.select()
  
          self._shopping_list_label = tk.StringVar()
          self._shopping_list_label.set("Enter a comma separated list of items")
          c = tk.Label(self._window, textvariable=self._shopping_list_label).grid(row=9, column=0, columnspan=2)
  
          v = tk.StringVar(self._window, value=self._shopping_list)
          self._shopping_list_entry = tk.Entry(self._window, textvariable=v)
          self._shopping_list_entry.grid(row=10, column=0)
          tk.Button(self._window, text='Set shopping list', command=self._update_shopping_list).grid(row=10, column=1)
  
      def _close(self) -> None:
          if self._window is not None:
              self._window.destroy()
          self._window = None
  
      def _save_config(self) -> None:
          self._config['settings']['scanner_window'] = str(self._image_scanner.scanner_window_size)
          self._config['settings']['image_scale'] = str(self._items_map.scale)
          self._config['settings']['confidence_threshold'] = str(self._image_scanner.confidence_threshold)
          self._config['settings']['display_inventory_items'] = str(self._display_inventory_items)
          self._config['settings']['display_unavailable_recipes'] = str(self._display_unavailable_recipes)
          self._config['settings']['copy_recipe_to_clipboard'] = str(self._copy_recipe_to_clipboard)
          self._config['settings']['scan_hotkey'] = str(self._scan_hotkey)
          self._config['settings']['run_as_overlay'] = str(self._run_as_overlay)
          self._config['settings']['shopping_list_mode'] = str(self._shopping_list_mode)
          self._config['settings']['shopping_list'] = str(self._shopping_list)
          with open(self._config_file, 'w') as f:
              self._config.write(f)
  
      def _update_scanner_window(self) -> None:
          try:
              x, y, width, height = map(int, self._scanner_window_entry.get().replace(',', '').split())
          except ValueError:
              print('Unable to parse scanner window parameters')
              return
  
          scanner_window_to_show = UIOverlay.create_toplevel_window(bg='white')
          scanner_window_to_show.geometry(f'{width}x{height}+{x}+{y}')
          self._image_scanner.scanner_window_size = (x, y, width, height)
          scanner_window_to_show.after(200, scanner_window_to_show.destroy)
          self._save_config()
  
      def _update_scale(self) -> None:
          try:
              new_scale = float(self._scale_entry.get())
          except ValueError:
              print('Unable to parse image scale parameter')
              return
          self._items_map.scale = new_scale
          self._save_config()
  
      def _update_confidence_threshold(self) -> None:
          try:
              new_threshold = float(self._confidence_threshold_entry.get())
          except ValueError:
              print('Unable to parse confidence threshold parameter')
              return
          self._image_scanner.confidence_threshold = new_threshold
          self._save_config()
  
      def _update_display_inventory_items(self) -> None:
          self._display_inventory_items = not self._display_inventory_items
          self._save_config()
  
      def _update_display_unavailable_recipes(self) -> None:
          self._display_unavailable_recipes = not self._display_unavailable_recipes
          self._save_config()
  
      def _update_copy_recipe_to_clipboard(self) -> None:
          self._copy_recipe_to_clipboard = not self._copy_recipe_to_clipboard
          self._save_config()
  
      def _update_scan_hotkey(self) -> None:
          self._scan_hotkey = self._scan_hotkey_entry.get()
          self._save_config()
  
      def _update_run_as_overlay(self) -> None:
          self._run_as_overlay = not self._run_as_overlay
          self._save_config()
  
      def _update_shopping_list_mode(self) -> None:
          self._shopping_list_mode = not self._shopping_list_mode
          self._save_config()
  
      def _update_shopping_list(self) -> None:
          shopping_list = list(map(lambda x: x.strip(), self._shopping_list_entry.get().split(",")))
          if len(shopping_list) == 0 or len(self._shopping_list_entry.get().strip()) == 0:
              self._update_shopping_list_label("Error: Must enter at least one item")
              return
          for item in shopping_list:
              if item not in self._items_map.items():
                  self._update_shopping_list_label('Error: unknown item "{0}"'.format(item))
                  return
          self._update_shopping_list_label("Shopping list updated!")
          self._shopping_list = ",".join(shopping_list)
          self._save_config()
      
      def _update_shopping_list_label(self, value) -> None:
          self._shopping_list_label.set(value)
          self._window.update_idletasks()
  
      def should_display_inventory_items(self) -> bool:
          return self._display_inventory_items
  
      def should_display_unavailable_recipes(self) -> bool:
          return self._display_unavailable_recipes
  
      def should_copy_recipe_to_clipboard(self) -> bool:
          return self._copy_recipe_to_clipboard
  
      def should_run_as_overlay(self) -> bool:
          return self._run_as_overlay
  
      def get_scan_hotkey(self) -> str:
          return self._scan_hotkey
  
      def is_shopping_list_mode(self) -> bool:
          return self._shopping_list_mode
  
      def get_shopping_list(self) -> str:
          return self._shopping_list
  