from sys import exit
import os
import tkinter as tk
from tkinter import messagebox
import ttkbootstrap as ttk
# from ttkbootstrap.dialogs import Messagebox
from PIL import Image, ImageTk
from .db_functions import db_update, db_select_all, db_update_record, db_delete_record
from .vam_functions import read_varbr_file, save_varbrowser_dict, move_files, copy_files, create_symlinks, find_packages_in_json, start_vam, restore_dummyvars_dir
from .dedupe_tools import find_duplicate_files
from .packages import add_var_paths, find_var_paths, find_var_names
from .unpacking import find_unpacked_vars, delete_unpacked_vars, unpack_files

import pywinstyles, sys
import darkdetect

# This allows better control of of title bar color  
# (cloned function from https://github.com/rdbende/Sun-Valley-ttk-theme)
def apply_theme_to_titlebar(root):
    version = sys.getwindowsversion()
    if version.major == 10 and version.build >= 22000:
        # Set the title bar color to the background color on Windows 11 for better appearance
        pywinstyles.change_header_color(root, "#1c1c1c" if darkdetect.isDark() else "#fafafa")
    elif version.major == 10:
        pywinstyles.apply_style(root, "dark" if darkdetect.isDark() else "normal")
        # A hacky way to update the title bar's color on Windows 10 (it doesn't update instantly like on Windows 11)
        root.wm_attributes("-alpha", 0.99)
        root.wm_attributes("-alpha", 1)

# Directory levels/id
VAM = 1  # Active packages level, in AddonPackages directory
VBR = 2  # VarBrowser packages level, in AllPackages directory
EXT = 3  # External/offline packages level, in another directory, unseen from game.

# Application class
class PackagesApp():
    def __init__(self, root: tk.Tk, db_name=''):
        self.root = root
        self.db = db_name
        self.root.title("Vambropy")

        #default values
        self.image_path = ''
        self.level_values = ()
        self.level_d_values = ()
        self.num_deps = 0

        # Var lists (profiles) directory
        self.varlists_dir = 'lists'

        #config from file
        self.VAM_dir = ''
        self.common_dir = ''
        self.Repo1 = ''
        self.Repo2 = ''
        self.Repo3 = ''
        self.Repo4 = ''
        self.Repo5 = ''
        self.autoview = False
        self.dummyvars = False
        self.max_threads = 5
        self.thumbnail_size = 256
        self.theme = 'darkly'
        self.theme_font_size = 9
        self.autoi_set = set()
        self.fav_set = set()

        # Colors for packages' levels of availability
        self.color_missing='#EE4040'
        self.color_installed=''
        self.color_available='#00AA00'
        self.color_external='#EE802E'
        self.color_autoinst='#80A0EE'
        self.color_unpacked='#EE80EE'
        self.bgcolor_favorite='#202070'
        self.bgcolor_default=''

        #config from outside or refresh
        self.VAM_addonpackages = ''
        self.VAM_allpackages = ''
        self.autoinstall_path = ''
        self.favorites_path = ''
        self.packages_dict = {}
        self.dependencies = set ()

        #control values - do not change
        # self.is_autoi_changed = False
        # self.is_fav_changed = False
        # self.selected_package = None  # not used yet  - check out that option

        # UI Theme customization
        # apply_theme_to_titlebar(root)
        ttk.Style(self.theme)

        # Frames for the widgets
        frame_tl = ttk.Frame(root)
        frame_tl.grid(row=0, column=0, rowspan=2, sticky='nw')

        frame_tr = ttk.Frame(root)
        frame_tr.grid(row=0, column=2, rowspan=2, sticky='nw')

        frame_ml = ttk.Frame(root) 
        frame_ml.grid(row=1, column=0, rowspan=2, sticky='nw')

        frame_mla = ttk.Frame(frame_ml)
        frame_mla.grid(row=0, column=0, rowspan=3, sticky='nw')

        frame_mlb = ttk.Frame(frame_ml)
        frame_mlb.grid(row=0, column=1, rowspan=3, padx=8, sticky='nw')

        frame_mlc = ttk.Frame(frame_ml)
        frame_mlc.grid(row=0, column=2, rowspan=3, padx=8, sticky='nw')

        frame_mld = ttk.Frame(frame_ml) #, background='lightgreen')
        frame_mld.grid(row=0, column=3, rowspan=1, padx=8, sticky='news')
        frame_mld2 = ttk.Frame(frame_ml) #, background='lightblue')
        frame_mld2.grid(row=1, column=3, rowspan=2, padx=8, sticky= 'news')


        frame_mr = ttk.Frame(root)
        frame_mr.grid(row=1, column=2, sticky="nsew")

        # top-left frame : packages and package's dependencies section

        # all columns used but not displayed
        self.columns_p = ('var_name', 'var_tags', 'var_date', 'var_deps', 'var_info', 'var_instr', 'var_thumbs', 'level', 'autoi', 'unp', 'var_morphn', 'fav')
        # all columns used for filtering
        self.fcolumns_p = ('var_name', 'var_tags', 'var_date', 'var_deps', 'var_info', 'var_morphs')
        # all columns displayed
        self.dcolumns_p = ('var_name', 'var_tags', 'var_date', 'var_morphn')

        # Treeview widget for displaying packages data
        self.tree_p = ttk.Treeview(frame_tl, columns=self.columns_p, displaycolumns=self.dcolumns_p, show='headings')
        self.tree_p.grid(row=0, column=0, sticky='new')
        self.tree_p.configure(height=25)
        # Column headers and their sorting order
        headings_pt = ('Name', 'Tags', 'Date', 'Morphs count')
        self.sort_order_p = {}
        for n, col in enumerate(self.dcolumns_p):
            self.tree_p.heading(col, text=headings_pt[n], command=lambda c=col: self.sort_column(self.tree_p, c, self.sort_order_p))
            # self.sort_order_p[col] = True
        self.tree_p.column('var_name', width=285, stretch=True)        
        self.tree_p.column('var_tags', width=275, stretch=True)        
        self.tree_p.column('var_date', width=75, stretch=False)        
        self.tree_p.column('var_morphn', width=40, stretch=False)        
        # Vertical scrollbar
        vsb_p = ttk.Scrollbar(frame_tl, orient="vertical", command=self.tree_p.yview)
        self.tree_p.configure(yscroll=vsb_p.set)
        vsb_p.grid(row=0, column=1, sticky='ns')
        # horizontal scrollbar
        hsb_p = ttk.Scrollbar(frame_tl, orient="horizontal", command=self.tree_p.xview)
        self.tree_p.configure(xscroll=hsb_p.set)
        hsb_p.grid(row=0, column=0, sticky='wes')

        # Context menu for treeview
        self.menu_p = tk.Menu(frame_tl, tearoff=0)
        # Add some options to the context menu
        self.menu_p.add_command(label="Related dependencies", accelerator='R', command=self.dependencies_from_selected)
        self.menu_p.add_command(label="Dependency usage", accelerator='D', command=self.dependency_usage_packages)
        self.menu_p.add_separator()
        self.menu_p.add_command(label="Install/Uninstall", accelerator='I', command=lambda f='i': self.process_installable_packages(f))
        self.menu_p.add_command(label="Install+Deps/Uninstall", accelerator='P', command=lambda f='p': self.process_installable_packages(f))
        self.menu_p.add_command(label="Toggle Autoinstall", accelerator='A', command=lambda f='a': self.process_installable_packages(f))
        self.menu_p.add_command(label="Install as symlinked", accelerator='L', command=lambda f='l': self.process_installable_packages(f))
        self.menu_p.add_command(label="Unpack/Remove unpacked", accelerator='U', command=self.process_unpackable_packages)
        self.menu_p.add_command(label="Delete from database", accelerator='Ctr+Del', command=self.delete_from_database)

        # All Hotkeys and mouse binds here
        self.tree_p.bind('<FocusIn>', self.on_focusin_treeview) # command= lambda t=self.tree_p: self.onfocusview(t))
        self.tree_p.bind('<ButtonRelease-1>', self.view_info)
        self.tree_p.bind('<Return>', self.view_info)
        # Keyboard navigation (only up/down) with autoview if enabled  (worth adding pgup/pgdown? )
        self.tree_p.bind('<Up>', self.nav_autoviewable)
        self.tree_p.bind('<Down>', self.nav_autoviewable)
        # Menu options
        self.tree_p.bind('<r>', self.dependencies_from_selected)
        self.tree_p.bind('<d>', self.dependency_usage_packages)
        self.tree_p.bind("<i>", lambda event: self.process_installable_packages('i'))
        self.tree_p.bind("<p>", lambda event: self.process_installable_packages('p'))
        self.tree_p.bind("<a>", lambda event: self.process_installable_packages('a'))
        self.tree_p.bind("<l>", lambda event: self.process_installable_packages('l'))
        self.tree_p.bind("<u>", self.process_unpackable_packages)
        self.tree_p.bind('<Double-1>', self.dependencies_from_selected)
        self.tree_p.bind("<Button-3>", self.packages_context_menu)

        # Treeview for package dependencies
        self.tree_pd = ttk.Treeview(frame_tl, show='headings', takefocus=0) #show='tree' for  tree only not headers
        self.tree_pd.configure(height=26)
        self.tree_pd.grid(row=0, column=2, sticky='n')
        self.tree_pd["columns"] = ('level', 'var_deps')
        # Hide default column (with ID "")
        self.tree_pd.column("#0", width=0, stretch=tk.NO)  # "#0" is default column identifier
        self.tree_pd.column("level", width=30)
        self.tree_pd.column("var_deps", width=200)
        self.tree_pd.heading(column='var_deps', text='Var_Deps')

        # Hotkeys for the Package's dependencies treeview
        self.tree_pd.bind('<ButtonRelease-1>', self.dependencies_from_selected)
        self.tree_pd.bind('<Return>', self.dependencies_from_selected)
        self.tree_pd.bind('<Double-1>', lambda event : self.dependency_usage(self.tree_pd))

        # Vertical scrollbar
        vsb_pd = ttk.Scrollbar(frame_tl, orient="vertical", command=self.tree_pd.yview)
        self.tree_pd.configure(yscroll=vsb_pd.set)
        vsb_pd.grid(row=0, column=3, sticky='ns')

        # Entry widgets for filtering
        self.filter_entries_p = {}
        filter_frame_p = ttk.Frame(frame_tl) #, background='cyan')
        filter_frame_p.grid(row=1, column=0, columnspan=4, sticky='nsew', )
        for col in self.fcolumns_p:
            label_p = ttk.Label(filter_frame_p, text=col.title())
            label_p.grid(row=0, column=self.fcolumns_p.index(col), padx=5, pady=0, sticky=tk.W+tk.S)
            entry_p = ttk.Entry(filter_frame_p)
            entry_p.grid(row=1, column=self.fcolumns_p.index(col), padx=5, pady=0, sticky=tk.W+tk.N)
            entry_p.bind('<Return>',  lambda event: self.apply_filters())
            # entry_p.bind('<Escape>', self.clear_filters)
            self.filter_entries_p[col] = entry_p

        self.filter_entries_p['var_name'].config(width=25)
        self.filter_entries_p['var_tags'].config(width=25)
        self.filter_entries_p['var_date'].config(width=10)
        self.filter_entries_p['var_deps'].config(width=20)
        # self.filter_entries_p['level'].config(width=5)

        # Button for updating/creating db
        self.update_button = ttk.Button(filter_frame_p, text="Refresh", command= self.refresh_wrapper)
        self.update_button.grid(row=1, column=7, padx=16, pady=0, sticky=tk.W+tk.N)

        # filter_frame_p_label = tk.Label(filter_frame_p, text='text free space 1')
        # filter_frame_p_label.grid(row=0, column=5, padx=4, sticky='w')
        # filter_frame_p_label = tk.Label(filter_frame_p, text='text free space 2')
        # filter_frame_p_label.grid(row=1, column=5, padx=4, sticky='w')

        # Button frame for filters / actions
        button_frame = ttk.Frame(frame_tl)
        button_frame.grid(row=2, column=0, columnspan=4, pady=0, sticky='nsew')
        self.var_filter = tk.StringVar()
        self.filters = (('All', []), ('VarBrowser',[1,2]), ('VB Installed', [1]), ('VB Available',[2]), ('External',[3]), ('VB AutoInstalled',[11]), ('VB Favorites',[12]), ('Unpacked',[31]), ('Missing', [0]))
        self.filter_buttons = []
        for index, filter in enumerate(self.filters):
            filter_button = ttk.Radiobutton(button_frame, text=filter[0], variable=self.var_filter, value=filter[0], command= lambda f=filter: self.filter_level_wrapper(f))
            filter_button.grid(row=0, column=index, padx=6, pady=8)
            self.filter_buttons.append(filter_button)

        # Checkbox status variable (True o False) for external packages scanning on refresh, enable/disable
        self.var_reveal_missing = tk.BooleanVar()
        self.var_reveal_missing.set(False)
        # Create a checkbox button
        checkbox = ttk.Checkbutton(button_frame, text="Reveal missing", padding=(10,10,0,10), variable=self.var_reveal_missing) 
        checkbox.grid(row=0, column=9, sticky='nse') 

        #mid-left frame - all packages' dependencies section

        self.columns_d = ('level', 'var_dep')
        self.tree_d = ttk.Treeview(frame_mla, columns=self.columns_d, show='headings')
        self.tree_d.grid(row=0, column=0, sticky='nw')
        self.tree_d.configure(height=21)
        self.tree_d.column('level', width=50)        
        self.tree_d.column('var_dep', width=270)        

        vsb_d = ttk.Scrollbar(frame_mla, orient="vertical", command=self.tree_d.yview)
        self.tree_d.configure(yscroll=vsb_d.set)
        vsb_d.grid(row=0, column=1, sticky='ns')
        # Column headers and their sorting order
        self.sort_order_d = {}
        for col in self.columns_d:
            self.tree_d.heading(col, text=col.title(), command=lambda c=col: self.sort_column(self.tree_d, c, self.sort_order_d))
            # self.sort_order_d[col] = True
       
        # Context menu for treeview
        self.menu_d = tk.Menu(frame_mla, tearoff=0)
        # Add some options to the context menu
        self.menu_d.add_command(label="Dependency usage", accelerator='D', command=self.dependency_usage)

        # Hotkeys for the Dependencies treeview
        self.tree_d.bind('<Double-1>', self.view_dep_info)
        self.tree_d.bind('<d>', self.dependency_usage)
        self.tree_d.bind('<Return>', self.view_dep_info)
        self.tree_d.bind("<Button-3>", self.dependencies_context_menu)


        # mid-center frame for dependencies filters and options

        # RadioButton frame for dependency filters
        button_frame_d = ttk.Frame(frame_mlb)
        button_frame_d.grid(row=0, column=0, rowspan=2, columnspan=4, pady=0, sticky='nw')
       
        self.button_frame_d_label = ttk.Label(button_frame_d, text=f"Related dependencies:")
        self.button_frame_d_label.grid(row=0, column=0, padx=4, pady=4, sticky='nw')
        
        self.var_filter_d = tk.StringVar()
        self.filters_d = (('All dependencies', []), ('Installed', [1]), ('Available',[2]), ('External',[3]), ('Missing',[0]))
        self.filter_d_buttons = []
        for index, filter in enumerate(self.filters_d):
            filter_d_button = ttk.Radiobutton(button_frame_d, text=filter[0], variable=self.var_filter_d, value=filter[0], command= lambda level_values=filter[1]: self.filter_dependencies_and_ready(level_values))
            filter_d_button.grid(row=index+1, column=0, padx=8, pady=6, sticky='w')
            self.filter_d_buttons.append(filter_d_button)

        button_frame_tasks = ttk.Frame(frame_mlb, border=2, borderwidth=2)
        button_frame_tasks.grid(row=6, column=0, sticky='nw')

        button_move = ttk.Button(button_frame_tasks, text="Install related (filtered)", width=26, command= lambda level=2: self.install_all_deps_wrapper(level))
        button_move.grid(row=0, column=0, pady=6, padx=4, sticky='w')

        button_dwnl = ttk.Button(button_frame_tasks, text="Create missing list ->", width=26, command=lambda level=0: self.create_varlist_from_level(level))
        button_dwnl.grid(row=2, column=0, pady=6, padx=4, sticky='w')
        button_savedscenes = ttk.Button(button_frame_tasks, text="<- Deps from saved scenes", width=26, command=self.dependencies_from_saved_scenes)
        button_savedscenes.grid(row=3, column=0, pady=6, padx=4, sticky='w')

        # mid-right frame widgets for information, instructions

        # Checkbox status variable (True o False)
        self.var_autoview = tk.BooleanVar()
        self.var_autoview.set(self.autoview)
        # Create a checkbox button
        checkbox = ttk.Checkbutton(frame_tr, text="Auto view", padding=(4,4), variable=self.var_autoview, takefocus=0) 
        checkbox.grid(row=1, column=0, sticky='nw') 

        # Label for thumbnail view
        self.image_label = ttk.Label(frame_tr)
        self.image_label.grid(row=1, column=0, sticky='nw')
        self.image_label.bind('<Double-1>', self.os_show_image)

        # Label for info view
        self.info_label = ttk.Label(frame_mr, text="Information:")
        self.info_label.grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
        # Information text with scrollbar
        scrollbar_info = ttk.Scrollbar(frame_mr)
        self.text_info = tk.Text(frame_mr, wrap="word", yscrollcommand=scrollbar_info.set, height=8, width=80)
        scrollbar_info.config(command=self.text_info.yview)
        # Position text and scrollbar
        self.text_info.grid(row=1, column=0, padx=5, pady=5, sticky=(tk.N, tk.S, tk.E, tk.W))
        scrollbar_info.grid(row=1, column=1, sticky=(tk.N, tk.S))

        # Label for instructions view
        label_instr = ttk.Label(frame_mr, text="Instructions:")
        label_instr.grid(row=2, column=0, padx=5, pady=5, sticky=tk.W)
        # Instructions text with scrollbar
        scrollbar_instr = ttk.Scrollbar(frame_mr)
        self.text_instr = tk.Text(frame_mr, wrap="word", yscrollcommand=scrollbar_instr.set, height=8, width=80)
        scrollbar_instr.config(command=self.text_instr.yview)
        # Position text and scrollbar
        self.text_instr.grid(row=3, column=0, padx=5, pady=5, sticky=(tk.N, tk.S, tk.E, tk.W))
        scrollbar_instr.grid(row=3, column=1, sticky=(tk.N, tk.S))

        # Status bar usable from other modules
        self.var_status = tk.StringVar(value="Ready")
        self.status_bar = ttk.Label(root, textvariable=self.var_status,  anchor='sw')
        self.status_bar.grid(row=3, column=0, columnspan=3, sticky='swe')

        # Theme combobox
        self.theme_var = tk.StringVar(value=self.theme)
        self.theme_box = ttk.Combobox(root, textvariable=self.theme_var)
        self.theme_box.grid(row=3, column=2, sticky='se')
        self.theme_box['values'] = ("cosmo", "flatly", "litera", "minty", "lumen", "sandstone", "yeti", "pulse", "united", "morph", "journal", "darkly", "superhero", "solar", "cyborg", "vapor", "simplex", "cerculean")
        self.theme_box.bind('<<ComboboxSelected>>', self.set_theme)
        # varlists manager

        # Label with varlists section title 
        varlists_label = ttk.Label(frame_mlc,  text='Lists:')
        varlists_label.grid(row=0, column=0, padx=5, pady=0, sticky='w')

        # Button for create varlist from currently installed packages
        varlist_create_button = ttk.Button(frame_mlc, text="New from installed", width= 20, command=lambda level=1 : self.create_varlist_from_level(level))
        varlist_create_button.grid(row=0, column=0, padx=4, pady=0, sticky='e')

        # Widget Listbox for varlists view
        self.varlists_listbox = tk.Listbox(frame_mlc, width=32)
        self.varlists_listbox.grid(row=1, column=0, padx=4, pady=4)

        vsb_varlists = ttk.Scrollbar(frame_mlc, orient="vertical", command=self.varlists_listbox.yview)
        self.varlists_listbox.configure(yscroll=vsb_varlists.set)
        vsb_varlists.grid(row=1, column=1, sticky='ns')
        
        # Keys for view list, open with OS' default editor
        self.varlists_listbox.bind('<Double-1>', self.view_varlist)
        self.varlists_listbox.bind('<Return>', self.view_varlist)

        # Buttons for load and delete varlists
        add_button = ttk.Button(frame_mlc, text="Install List's files", width= 20, command=self.add_varlist_files)
        add_button.grid(row=2, column=0, padx=4, pady=4)
        remove_button = ttk.Button(frame_mlc, text="Uninstall List's files", width= 20, command=self.remove_varlist_files)
        remove_button.grid(row=3, column=0, padx=4, pady=4)
        delete_button = ttk.Button(frame_mlc, text="Delete List", width= 20, command=self.delete_varlist)
        delete_button.grid(row=4, column=0, padx=4, pady=4)
        from_autoi_button = ttk.Button(frame_mlc, text='AutoIns->List', width=12, command=lambda level=1 : self.create_varlist_from_level(level, True))
        from_autoi_button.grid(row=5, column=0, padx=0, pady=4, sticky='w')
        autoi_button = ttk.Button(frame_mlc, text='List->AutoIns', width=12, command=self.varlist_to_autoinstall)
        autoi_button.grid(row=5, column=0, padx=0, pady=4, sticky='e')

        uninst_button = ttk.Button(frame_mld, text="Uninstall all packages", width=20, command=self.uninstall_all_packages)
        uninst_button.grid(row=0, column=0, pady=0, padx=2, sticky='n')
        config_ini_button = ttk.Button(frame_mld, text="Edit config.ini", width=15, command=self.edit_config_ini)
        config_ini_button.grid(row=1, column=0, pady=32, padx=2, sticky='n')
        vam_button1 = ttk.Button(frame_mld2, text='VaM!', width=15, command= lambda a='' : self.load_vam(a))
        vam_button1.grid(row=0, column=0, padx=16, pady=8, sticky='wes')
        vam_button2 = ttk.Button(frame_mld2, text='VaM! (openvr)', width=15, command= lambda a='-vrmode openvr' : self.load_vam(a))
        vam_button2.grid(row=1, column=0, padx=16, pady=8, sticky='wes')
        vam_button3 = ttk.Button(frame_mld2, text='VaM! (desktop)', width=15, command= lambda a='-vrmode None' : self.load_vam(a))
        vam_button3.grid(row=2, column=0, padx=16, pady=8, sticky='wes')


        self.root.bind('<Escape>', self.clear_filters)
        self.root.bind("<Control-r>", lambda event: self.root.after(300, self.update_button.invoke))
        self.root.bind("<Control-f>", lambda event: self.filter_entries_p['var_name'].focus())
        self.root.bind("<Control-u>", self.find_unpacked_vars_wrapper)
        # self.root.bind('<FocusIn>', self.update_focus_label)  # debug inspect widgets focus


    # Methods for PackagesApp class

    def set_theme(self, *event):
        ttk.Style(self.theme_var.get())

    def delete_from_database(self, *event):
        items = self.tree_p.selection()
        # remember self.columns_p = ('var_name', 'var_tags', 'var_date', 'var_deps', 'var_info', 'var_instr', 'var_thumbs', 'level', 'autoi', 'unp')
        if items:
            for item in items:
                values = self.tree_p.item(item, 'values')
                var_name = values[0]  # var_name

                # Delete item from Treeview
                self.tree_p.delete(item)

                # Delete record from SQLite table
                db_delete_record(self.db, "FROM packages WHERE var_name = ?", (var_name,))


    def packages_context_menu(self, event):
        # Get item under cursor
        item = self.tree_p.identify_row(event.y)

        if item:
            # If element not selected, add it to selection
            if not self.tree_p.selection():
                self.tree_p.selection_add(item)

            # Show context menu on clicked position 
            self.menu_p.post(event.x_root, event.y_root)


    def dependencies_context_menu(self, event):
        # Get item under cursor
        item = self.tree_d.identify_row(event.y)

        if item:
            # If element not selected, add it to selection
            if not self.tree_d.selection():
                self.tree_d.selection_add(item)

            # Show context menu on clicked position 
            self.menu_d.post(event.x_root, event.y_root)


    # Enable treeview' selection when on focus
    def on_focusin_treeview(self, event):
        tree = event.widget
        if tree.get_children():
            # If there is a selection, focus on it
            items = self.tree_p.selection()
            if items: tree.focus(items[0])
            else:
                # Focus on first element
                first_item = tree.get_children()[0]
                tree.focus(first_item)
                tree.selection_set(first_item)


    # def update_focus_label(self, event):
        # focused_widget = self.root.focus_get()
        # print(f"Current focus: {focused_widget}")


    # Load VaM with given args
    def load_vam(self, args):
        start_vam(self.VAM_dir, args)


    # Find unpacked vars in VaM's Custom directory, specifically Clothing and Hair
    def find_unpacked_vars_wrapper(self, event):
        # Try to identify some previously unpacked items (Clothing, Hair) in Custom directory and set them as Unpacked in database (and dummyvars, optionally)
        var_list = find_unpacked_vars(self.packages_dict, self.VAM_dir, self.VAM_addonpackages, self.set_status, self.dummyvars)
        if var_list:
            for var_name in var_list:
                db_update_record(self.db, 'packages SET unp = ? WHERE var_name = ?', ('1', var_name))
            self.refresh(all=False)
            # Update Treeview
            self.load_filtered_data()


    # Process unpackable items (Clothing, Hair). It unpacks packages or deletes unpacked files by matching them against zip namelists on each package
    def process_unpackable_packages(self, *event):
        # Which kind of packages are allowed to unpack in Custom directory (and install as dummy packages to avoid VaM's missing deps messages)?
        unpackable_tags = ('Clothing', 'Hair', 'Clothing, Hair', 'Assets, Clothing, Hair', 'Assets, Clothing', 'Assets, Clothing, Scripts' )
        items = self.tree_p.selection()
        # remember self.columns_p = ('var_name', 'var_tags', 'var_date', 'var_deps', 'var_info', 'var_instr', 'var_thumbs', 'level', 'autoi', 'unp')
        if items:
            var_list = []
            for item in items:
                values = self.tree_p.item(item, 'values')
                var_name = values[0]  # var_name
                tags = values[1]  # tags
                level = values[7]  # level
                unp = values[9] # unp
                if tags in unpackable_tags: var_list.append(var_name)

            # If there are packages to unpack/delete unpacked files, process them
            if var_list:
                if not unp == '1' and level == '3':  # External var unpack in Custom directory (and dummy var creation)
                        self.refresh(all=True)
                        unpack_files(self.packages_dict, self.VAM_dir, self.VAM_addonpackages, var_list, self.set_status, self.dummyvars)
                        for var_name in var_list:
                            db_update_record(self.db, 'packages SET unp = ? WHERE var_name = ?', ('1', var_name)) 
                elif unp == '1': # Delete unpacked files (and related dummy vars)
                        delete_unpacked_vars(self.packages_dict, self.VAM_dir, self.VAM_addonpackages, var_list, self.set_status, self.dummyvars)
                        for var_name in var_list:
                            db_update_record(self.db, 'packages SET unp = ?, level = ? WHERE var_name = ?', ('0', '3', var_name))
                else: return
                # Update Treeview
                self.load_filtered_data()
                
    # def toggle_favorite_packages(self, event):
    #     items = self.tree_p.selection()
    #     # remember self.columns_p = ('var_name', 'var_tags', 'var_date', 'var_deps', 'var_info', 'var_instr', 'var_thumbs', 'level', 'autoi', 'unp')
    #     if items:
    #         for item in items:
    #             values = self.tree_p.item(item, 'values')
    #             var_name = values[0]
    #             fav = bool(int(values[11]))
    #             print(fav)
    #             if fav: 
    #                 self.fav_set.remove(var_name)
    #                 word = 'removed from'
    #             else: 
    #                 word = 'added to'
    #                 self.fav_set.add(var_name)
    #             self.set_status(f'{var_name} {word} Varbrowser Favorites')
    #             db_update_record(self.db, 'packages SET fav = ?  WHERE var_name = ?', (int(not fav), var_name))
    #             self.is_fav_changed = True
    #         # Update Treeview
    #         self.load_filtered_data()



    # Method for installing available packages (moves) and external packages (copy), or uninstall installed packages turning them available (move, no delete)
    def process_installable_packages(self, f):  # currently processing input keys <a>, <i>, <u>, <Control-d>
        items = self.tree_p.selection()
        # remember self.columns_p = ('var_name', 'var_tags', 'var_date', 'var_deps', 'var_info', 'var_instr', 'var_thumbs', 'level', 'autoi', 'unp')
        if items:
            var_list = []
            pre_refresh = False
            for item in items:
                values = self.tree_p.item(item, 'values')
                var_name = values[0]  # var_name
                level = int(values[7])  # level
                if level == 3: # if any external package enable refresh all
                    pre_refresh = True
                if level == 1 or level == 2 or level == 3:
                    var_list.append((var_name, level))

            # Refresh VaM and external paths and get full paths for package copy
            if pre_refresh: self.refresh(all=True) # Make sure external paths are scanned
            full_path_list = find_var_paths([tup[0] for tup in var_list], self.packages_dict, self.set_status)

            # If there are packages to toggle autoinstalled/installed/availble, process them
            if full_path_list:
                # <I> key installs/uninstall (move between internal paths / copy from external paths / copy dummyvars and unpack )
                if f in ('i') or f in ('p'):
                    move_files(self.VAM_addonpackages, self.VAM_allpackages, [path for path, level in full_path_list if level == 1 ], self.set_status)
                    move_files(self.VAM_allpackages, self.VAM_addonpackages, [path for path, level in full_path_list if level == 2 ], self.set_status)
                    copy_files(self.common_dir, self.VAM_addonpackages, [path for path, level in full_path_list if level == 3 ], self.set_status)
                # <L> key installs by copying a symlinked file
                elif f in ('l'):
                    if not create_symlinks(self.common_dir, self.VAM_addonpackages, [path for path, level in full_path_list if level == 3 ], self.set_status):
                        return
                # <A> key toggles autoinstall setting
                elif f in ('a'):
                    for var_name, var_level in var_list:
                        if var_level == 1:
                            if var_name in self.autoi_set:
                                self.autoi_set.remove(var_name)
                                db_update_record(self.db, 'packages SET autoi = ? WHERE var_name = ?', ('0', var_name))
                            else:
                                self.autoi_set.add(var_name)
                                db_update_record(self.db, 'packages SET autoi = ? WHERE var_name = ?', ('1', var_name))
                        else: return
                    # self.is_autoi_changed = True  # used by refresh() so it can rebuild autoinstall.txt
                    save_varbrowser_dict(self.autoinstall_path, 'Names', self.autoi_set, self.set_status)  # Now it does it directly
                    full_path_list = [] # clear list to avoid refresh(), just update treeviews
                else: return
                
                # Refresh only when files were relocated
                if full_path_list: 
                    self.refresh(all=False)

                    if f in ('p'):
                        self.autoinstall_deps()
                        return
                    
                # Update all Treeviews
                self.load_filtered_data()


    def autoinstall_deps(self) -> bool:
        while True:
            var_dep_list = []
            self.filter_level_wrapper(('Installed', [1]))
            for item in self.tree_d.get_children():
                # Get values from columns of current item
                level_name, var_dep = self.tree_d.item(item, 'values')
                # Verify for level names 'Available' or 'External'
                if level_name == 'Available':
                    var_dep_list.append(var_dep)
#            print(var_dep_list)
            full_path_list = find_var_paths(var_dep_list, self.packages_dict, self.set_status)
            # If there are packages to install, process them
            if full_path_list:
                move_files(self.VAM_allpackages, self.VAM_addonpackages, [path for path, level in full_path_list if level == 2], self.set_status)

                # Refresh file locations
                self.refresh(all=False)

                # Update all Treeviews
                self.load_filtered_data()
            else:
                self.filter_level_wrapper(('All', []))
                break


    def install_all_deps_wrapper(self, level):
        var_dep_list = []
        external = False
        for item in self.tree_d.get_children():
            # Get values from columns of current item
            level_name, var_dep = self.tree_d.item(item, 'values')
            # Verify for level names 'Available' or 'External'
            if level_name == 'Available' or level_name == 'External':
                var_dep_list.append(var_dep)
                if level_name == 'External': external = True

        # Make sure external is refreshed so dictionary is filled with that data
        if external: self.refresh(all=True)

        full_path_list = find_var_paths(var_dep_list, self.packages_dict, self.set_status)
        # If there are packages to install, process them
        if full_path_list:
            if self.var_filter.get() != 'Installed':
                if not messagebox.askokcancel(f"Dependencies from non-Installed packages", f"Are you sure to install all these related dependencies? \nIf unsure, cancel and check filters on Packages (and filter Dependencies to see how many they are)"):
                    return
            move_files(self.VAM_allpackages, self.VAM_addonpackages, [path for path, level in full_path_list if level == 2], self.set_status)
            if level_name == 'External':
                copy_files(self.common_dir, self.VAM_addonpackages, [path for path, level in full_path_list if level == 3], self.set_status)

            # Refresh file locations
            self.refresh(all=False)
            # Update all Treeviews
            self.load_filtered_data()


    # Method - Update status bar messages
    def set_status(self, message):
        self.var_status.set(message)
        self.root.update() # to make sure it appears when loops block the interface


    # Method - Loads the image with OS default viewer 
    def os_show_image(self, event):
        path=os.getcwd()+'/'+self.image_path
        os.startfile(path)


    # Method - clear all the filters
    def clear_filters(self, event):
        for col in self.fcolumns_p:
            self.filter_entries_p[col].delete(0,tk.END)
        self.apply_filters()
        # First filter takes cursor focus
        self.filter_entries_p['var_name'].focus()


    def dependency_usage(self, *event): # it does not use event anyway
        if self.tree_d.selection():
            # If no empty panel and something selected, get first element
            selected_item = self.tree_d.selection()[0]
            var_dep = self.tree_d.item(selected_item)['values'][1]  # Get selected 'var_dep'
            self.filter_entries_p['var_deps'].delete(0,tk.END)
            self.filter_entries_p['var_deps'].insert(0,var_dep)
            self.apply_filters()


    def dependency_usage_packages(self, *event): # it does not use event anyway
        if self.tree_p.selection():
            # If no empty panel and something selected, get first element
            selected_item = self.tree_p.selection()[0]
            var_name = self.tree_p.item(selected_item)['values'][0].rsplit('.',1)[0]  # Get selected 'var_name' but not exact version
            self.filter_entries_p['var_deps'].delete(0,tk.END)
            self.filter_entries_p['var_deps'].insert(0,var_name)
            self.apply_filters()


    def dependencies_from_selected(self, *event):
        var_deps = ''
        for item in self.tree_p.selection():
            var_deps += self.tree_p.item(item)['values'][3]  # Get selected 'var_deps'
            # View dependencies (if any) from current record
            self.dependencies = var_deps.split(', ')
            self.filter_dependencies_and_ready(self.level_d_values) # filter on current (same) level and load deps data
            # self.load_deps_data(self.tree_d, self.dependencies)


    def dependencies_from_saved_scenes(self):
            self.set_status("Scanning saved scenes. This could take minutes! Please wait...")
            # Clear existing data in the Treeview (it will not be used this time)
            for item in self.tree_p.get_children():
                self.tree_p.delete(item)

            # If no empty panel and something selected, get first element
            self.dependencies = find_packages_in_json(os.path.join(self.VAM_dir, 'Saves/scene'), self.set_status, self.max_threads)
            self.filter_dependencies_and_ready([])


    # Method for displaying package information, instructions and image
    def view_info(self, event):
        # print(event.widget)
        if self.tree_p.selection():
            # If no empty panel and something selected, get first element
            selected_item = self.tree_p.selection()[0]
            # remember self.columns_p = ('var_name', 'var_tags', 'var_date', 'var_deps', 'var_info', 'var_instr', 'var_thumbs', 'level', 'autoi', 'unp')
            var_deps = self.tree_p.item(selected_item)['values'][3]  # Get selected 'var_deps'
            var_info = self.tree_p.item(selected_item)['values'][4]  # Get selected 'var_info'
            var_instr = self.tree_p.item(selected_item)['values'][5]  # Get selected 'var_instr'
            var_thumbs = self.tree_p.item(selected_item)['values'][6]  # Get selected 'var_thumbs'

            # Get information and instructions
            # update information and instructions texts
            self.text_info.delete(1.0, tk.END)
            self.text_info.insert(tk.END, str(var_info or '')) #var_info   - warning: or '' fixes None imports, already fixed in var_functions. Remove here when DB is rebuild
            self.text_instr.delete(1.0, tk.END)
            self.text_instr.insert(tk.END, str(var_instr or '')) #var_instr  -warning: idem

            # Get image path from current record
            if var_thumbs:
                self.image_path = var_thumbs   # also needed for loading image with OS' default app
                if os.path.exists(self.image_path):   # Verify file exists
                    img = Image.open(self.image_path)
                    img.thumbnail((self.thumbnail_size, self.thumbnail_size))  # Resize image 
                    photo = ImageTk.PhotoImage(img)
                    
                    self.image_label.config(image=photo)
                    self.image_label.image = photo  # Keep reference to avoid deletion
                else:
                    messagebox.showwarning("Warning", f"Somethings is wrong. Image path not found: {self.image_path}")
            else:
                self.image_label.config(image='')
            
            # View dependencies (if any) from current record
            package_dependencies = var_deps.split(', ')
            self.load_deps_data(self.tree_pd, package_dependencies)


    # Method for displaying known dependencies information, instruction and image
    def view_dep_info(self, event):
        tree = event.widget
        # Items in the dependencies treeviews have to be found in the packages treeview
        if tree.selection():
            item = tree.selection()[0]
            dep_name, version = tree.item(item, 'values')[1].rsplit('.',1)
            # Bult the search key with the exact number, or non-exact (min,latest)
            if not version.startswith('l' or 'm'): # Just the first char is ok in here
                dep_name = dep_name+version

            # Clear package level filter
            self.filter_level_wrapper(('All', []))
            # Clear columns filters
            for col in self.fcolumns_p:
                self.filter_entries_p[col].delete(0,tk.END)

            # Iterate on the packages view
            if self.tree_p.get_children(): # if it comes from dependencies from saved scenes, packages treeview may be empty
                for item in self.tree_p.get_children():
                    var_name = self.tree_p.item(item, 'values')[0]
                    # On first match, select and focus, show info and break loop
                    if dep_name in var_name:
                        self.tree_p.focus(item)
                        self.tree_p.selection_set(item)
                        self.view_info(event)
                        break

            # var_deps = self.tree_p.item(selected_item)['values'][3]  # Get selected 'var_deps'
            # var_info = self.tree_p.item(selected_item)['values'][4]  # Get selected 'var_info'
            # var_instr = self.tree_p.item(selected_item)['values'][5]  # Get selected 'var_instr'
            # var_thumbs = self.tree_p.item(selected_item)['values'][6]  # Get selected 'var_thumbs'

    def dep_info(self, event): # it does not use event anyway
        tree = event.widget
        # Items in the dependencies treeviews have to be found in the packages treeview
        if tree.selection():
            item = tree.selection()[0]
            dep_name, version = tree.item(item, 'values')[1].rsplit('.',1)
            # Bult the search key with the exact number, or non-exact (min,latest)
            if not version.startswith('l' or 'm'): # Just the first char is ok in here
                dep_name = dep_name+version
            self.clear_filters()
            self.filter_entries_p['var_name'].delete(0,tk.END)
            self.filter_entries_p['var_name'].insert(0,dep_name)
            self.apply_filters()
            self.tree_p.focus()


    def sort_column(self, tree: ttk.Treeview, col, sort_order=None, restore=False):
        if restore:
            # If there is a saved sorted column, apply saved order
            if sort_order:
                col = next(iter(sort_order))  # Get first key (column) from dictionary
            else: return
        else:
            # Updates sort order before applying
            if col in sort_order:
                sort_order[col] = not sort_order[col]
            else:
                sort_order[col] = True  # Default asc order
        
        reverse = not sort_order[col]

        # Retrieve current data from the treeview
        # items = [(tree.set(k, col), k) for k in tree.get_children('')]
        # Retrieve current data from the treeview, converting to int if needed
        items = []
        for k in tree.get_children(''):
            value = tree.set(k, col)
            if col == 'var_morphn': # numeric column better to sort as int
                if not value: value = 0 # empty are '', so set to zero
                value = int(value) # convert to int
            items.append((value, k))

        # Sort based on the column and the sorting order
        # items.sort(reverse=reverse, key=lambda t: (t[0] == '', t[0].lower()))
        items.sort(reverse=reverse, key=lambda t: (t[0] == '', t[0].lower() if isinstance(t[0], str) else t[0]))

        for index, (_, k) in enumerate(items):
            tree.move(k, '', index)

    def level_to_str(self, level):
        match int(level):
            case -1: return "Deleted"
            case 0: return "Missing"
            case 1: return "Installed"
            case 2: return "Available"
            case 3: return "External"


    def load_deps_data(self, tree: ttk.Treeview, deplist):

        # delete current items from treeview
        for item in tree.get_children():
            tree.delete(item)

       # integrate level filters (buttons) with field filters (entries)
        select_query ='var_dep, level FROM dependencies'
        params = []
        if self.level_d_values:
            if len(self.level_d_values) == 1:
                select_query += ' WHERE level = ?'
                params = self.level_d_values
         # db query for updating dependencies treeviews
        deps_dict = dict (db_select_all(self.db, select_query, params))  # receives unique values from db
        self.num_deps = 0
        for dep in deplist:
            if dep in deps_dict: # var_dep
                self.num_deps += 1
                color = deps_dict[dep] # level
                tree.insert('', tk.END, values=(self.level_to_str(deps_dict[dep]), dep), tags=(color,))
        

    def load_filtered_data(self):
        self.set_status("Updating window data")
        # Clear existing data in the Treeview
        for item in self.tree_p.get_children():
            self.tree_p.delete(item)
        
        # Construct SQL query based on current filters
        columns = ', '.join(self.columns_p)
        query = f"{columns} FROM packages"
        conditions = []
        params = []

        # integrate level filters (buttons) with field filters (entries)
        if self.level_values:
            if len(self.level_values) == 1:
                if self.level_values[0] == 11:
                    conditions.append("autoi = 1")
                elif self.level_values[0] == 12:
                    conditions.append("fav = 1")
                elif self.level_values[0] == 31:
                    conditions.append("unp = 1")
                else:
                    conditions.append("level = ?")
                    params.extend(self.level_values)
            else:
                placeholders = ", ".join(["?"] * len(self.level_values))
                conditions.append(f"level IN ({placeholders})")
                params.extend(self.level_values)

        for col, entry in self.filter_entries_p.items():
            value = entry.get().strip()
            if value:
                words = value.split()  # Split the input into words
                sub_conditions = []
                for word in words:
                    if col == 'selected':
                        # Assuming 'selected' is stored as 0 or 1
                        try:
                            word = int(word)
                            if word not in (0, 1):
                                continue
                        except ValueError:
                            continue
                    sub_conditions.append(f"{col} LIKE ?")
                    params.append(f"%{word}%")
                if sub_conditions:
                    conditions.append("(" + " AND ".join(sub_conditions) + ")")

        if conditions:
            query += " WHERE " + " AND ".join(conditions)

        # Execute the db query and fetch results
        rows = db_select_all(self.db, query, params)

        # Prepare unique deps for deps tree
        self.dependencies = set()

        # Insert data into the packages Treeview
        # remember from self.columns_p = ('var_name', 'var_tags', 'var_date', 'var_deps', 'var_info', 'var_instr', 'var_thumbs', 'level', 'autoi', 'unp')
        for row in rows:
            color=row[7] # level
            bgcolor='w'
            if row[8]: # autoi
                color='a'
            elif row[9]: # unp (unpacked)
                color='u'
            if bool(row[11]) == True: # fav (varbr's favorite)
                bgcolor='f'
            self.tree_p.insert('', tk.END, values=row, tags=(color,bgcolor))

            # Code warning: this part is the slowest. Try alternative way with a 'dependency by package' table in database, maybe
            if row[3]:  # 3rd row var_deps
                deps = row[3].split(', ')
                self.dependencies.update(deps)

        self.sort_column(self.tree_p, 'var_name', self.sort_order_p, restore=True)

        self.root.title(f'Vambropy : {len(rows)} packages')

        # Update deps TreeView with current filters
        self.filter_dependencies_and_ready(self.level_d_values)

        # List of .txt files in varlists directory
        self.txt_files = [f for f in os.listdir(self.varlists_dir) if f.endswith('.txt')]
        # Load varlists list in the Listbox
        self.varlists_listbox.delete(0, tk.END)
        for file in self.txt_files:
            self.varlists_listbox.insert(tk.END, file)


    def apply_filters(self):
        # Reload data based on current filters
        self.load_filtered_data()


    def filter_level_wrapper(self, filter):
        # set filter on level value
        self.level_values = filter[1]
        self.load_filtered_data()
        # print(self.var_filter.get())


    def filter_dependencies_and_ready(self, level_values:list):
        # set filter on level value
        self.level_d_values = level_values
        self.load_deps_data(self.tree_d, self.dependencies)
        # Sort deps column
        self.sort_column(self.tree_d, 'var_dep', self.sort_order_d, restore=True)
        # Update number of deps
        self.button_frame_d_label.config(text=f"Related dependencies: ({str(self.num_deps)})")
        # Update status to Ready and give help on some keys
        self.set_status('Ready.       -- Hints: press <Esc> to clear all multiword filters')


    def refresh_wrapper(self): # Triggers full refresh from button
        self.set_status("Updating autoinstall.txt and database...")
        try:
            # if self.is_autoi_changed: # do not save if not needed
                # save_varbrowser_dict(self.autoinstall_path, 'Names', self.autoi_set, self.set_status)
                # self.is_autoi_changed = False # saved & reset
            self.refresh(all=True)

            self.set_status("Database updated successfully")

        except Exception as e:
            self.set_status(f"Error updating database: {str(e)}")
        # Update Treeview
        self.load_filtered_data()


    def nav_autoviewable(self, event):
        if self.var_autoview.get():
            # Rewriting of move up and down to allow auto-view
            selected_item = self.tree_p.selection()
            if selected_item:
                item_index = self.tree_p.index(selected_item[0])

                if event.keysym in ('Up'):
                    # Move to previous (if not the first)
                    if item_index > 0:
                        new_item_id = self.tree_p.get_children()[item_index - 1]
                        self.tree_p.selection_set(new_item_id)
                elif event.keysym in ('Down'):
                    # Move to next (if not the last)
                    children = self.tree_p.get_children()
                    if item_index < len(children) - 1:
                        new_item_id = self.tree_p.get_children()[item_index + 1]
                        self.tree_p.selection_set(new_item_id)
                self.view_info(event)  # Call function when selected item changes


    def validate_paths(self):
        # Validate paths and show warning if needed
        paths_to_validate = {
            "VAM_dir": self.VAM_dir,
            "Repo1": self.Repo1,
            "Repo2": self.Repo2,
            "Repo3": self.Repo3,
            "Repo4": self.Repo4,
            "Repo5": self.Repo5
        }
        for name, path in paths_to_validate.items():
            if path:
                if name == 'VAM_dir':
                    if not os.path.exists(path):
                        messagebox.showwarning(f"Error", f"{name} path {path} not found. Please verify VAM_dir is correct in config.ini")
                        os.startfile('config.ini')
                        exit()

                if not os.path.exists(path):
                    messagebox.showwarning(f"Warning", f"{name}: Path {path} not found. Installing from external paths may fail")
                    # exit()
                if name != 'VAM_dir' and self.common_dir not in path:
                    messagebox.showerror(f"Error: Not common base directory in Repos", f"{self.common_dir} not found in {path}. Please fix repo_name or verify Repos, and restart.")
                    os.startfile('config.ini')
                    exit()

        # Update autoinstall and favorites path to full path
        self.autoinstall_path = os.path.join(self.VAM_dir,self.autoinstall_path)
        self.favorites_path = os.path.join(self.VAM_dir,self.favorites_path)
        # Create sfishere directory if not exists
        directory = os.path.dirname(self.autoinstall_path)
        if not os.path.exists(directory):
            os.makedirs(directory)

        # Check if file Autoinstall.txt exists
        if not os.path.exists(self.autoinstall_path):
            with open(self.autoinstall_path, 'w') as f:
                save_varbrowser_dict(self.autoinstall_path, 'Names', {}, self.set_status)
        # self.is_autoi_changed = True # Allow to reset autoi in db

        # Check if file Favorites.txt exists
        if not os.path.exists(self.favorites_path):
            with open(self.favorites_path, 'w') as f:
                save_varbrowser_dict(self.favorites_path, 'FavoriteNames', {}, self.set_status)
        # self.is_fav_changed = True # Allow to reset fav in db


    def set_autoview(self, value):
        # Update autoview value
        try:
            self.autoview = bool(value)
        except ValueError:
            messagebox.showwarning("Invalid Value", "Autoview must be a boolean.")


    def load_config(self, config_file_path):
        # Load config from config.ini file
        if not os.path.exists(config_file_path):
            messagebox.showwarning("Config File", f"{config_file_path} file not found.")
            return

        with open(config_file_path, 'r', encoding='utf-8') as file:
            for line in file:
                # Delete whitespaces
                line = line.strip()
                # Ignore comments 
                if not line or line.startswith('#'):
                    continue
                key, value = line.strip().split('=', 1)
                if hasattr(self, key):
                    setattr(self, key, value)

        self.var_autoview.set(self.autoview)
        # self.var_external.set(self.external)

        self.validate_paths() # valid path or exit

        self.VAM_allpackages = os.path.join(self.VAM_dir, 'AllPackages')
        self.VAM_addonpackages = os.path.join(self.VAM_dir, 'AddonPackages')

        # Convert to integer
        self.thumbnail_size = int(self.thumbnail_size)
        self.max_threads = int(self.max_threads)
        
        # Check for thumbnails and lists directories, create them if not present
        if not os.path.isdir('thumbnails'):
            os.makedirs('thumbnails')
        if not os.path.isdir('lists'):
            os.makedirs('lists')

        self.tree_p.tag_configure('0', foreground=self.color_missing)
        self.tree_p.tag_configure('1', foreground=self.color_installed)
        self.tree_p.tag_configure('2', foreground=self.color_available)
        self.tree_p.tag_configure('3', foreground=self.color_external)
        self.tree_p.tag_configure('a', foreground=self.color_autoinst)
        self.tree_p.tag_configure('u', foreground=self.color_unpacked)
        self.tree_p.tag_configure('f', background=self.bgcolor_favorite)
        self.tree_p.tag_configure('w', background=self.bgcolor_default)
        self.tree_pd.tag_configure('0', foreground=self.color_missing)
        self.tree_pd.tag_configure('1', foreground=self.color_installed)
        self.tree_pd.tag_configure('2', foreground=self.color_available)
        self.tree_pd.tag_configure('3', foreground=self.color_external)
        self.tree_pd.tag_configure('a', foreground=self.color_autoinst)
        self.tree_pd.tag_configure('u', foreground=self.color_unpacked)
        self.tree_d.tag_configure('0', foreground=self.color_missing)
        self.tree_d.tag_configure('1', foreground=self.color_installed)
        self.tree_d.tag_configure('2', foreground=self.color_available)
        self.tree_d.tag_configure('a', foreground=self.color_autoinst)
        self.tree_d.tag_configure('u', foreground=self.color_unpacked)
        self.tree_d.tag_configure('3', foreground=self.color_external)

        self.theme_var.set(self.theme)
        self.set_theme()

    def dedup_dialog(self, dupes, packages_dict: dict):
    # def open_dedupe_dialog(self, dupe_n, dupe_len, file_name, all_paths):
        from .dedupe_tools import get_file_hash

        def on_closing():
            dialog.destroy()
            exit()

        if not dupes:
            dupes = find_duplicate_files(packages_dict)

        if dupes:
            # Get first dictionary key
            file_name, all_paths = next(iter(dupes.items()))
            # For info on remaining dialogs
            dupe_len = len(dupes)

            # Check if dupe is exact copy or name only
            file_hashes = {}
            for path in all_paths:
                file_hash = get_file_hash(path)
                if file_hash not in file_hashes:
                    file_hashes[file_hash] = []
                file_hashes[file_hash].append(path)

            if len(file_hashes) > 1:
                message = f"Files with name '{file_name}' have duplicate names but not exact copies. Please check them out before deciding which one to keep:"
            else:
                message = f"Files with name '{file_name}' are duplicates. Please choose which one to keep:"
 
            # Create a Toplevel window for each set of duplicates
            dialog = tk.Toplevel(self.root)
            # dialog.title(f"Duplicate {dupe_n} of {dupe_len}: {file_name}")
            dialog.title(f"Duplicates remaining: {dupe_len}")
            # Relate toplevel window with main window
            dialog.transient(self.root)
            # Capture all input events (avoids playing with main window)
            dialog.grab_set()
            # Allow to use close window button to exit program
            dialog.protocol("WM_DELETE_WINDOW", on_closing)    
    
            ttk.Label(dialog, text=message).pack(pady=10)

            var = tk.StringVar()
            var.set(all_paths[0])  # Set default value

            for path in all_paths:
                rb = ttk.Radiobutton(dialog, text=path, variable=var, value=path)
                rb.pack(anchor='w', padx=20)


            def delete_files():
                path_to_keep = var.get()
                for path in all_paths:
                    if path != path_to_keep:
                        try:
                            os.remove(path)
                        except OSError as e:
                            messagebox.showerror("Error", f"Could not remove {path}: {e}")
                dialog.destroy()
                dupes.pop(file_name)  # Delete file from dictionary
                self.dedup_dialog(dupes, packages_dict)

            def cancel():
                dialog.destroy()
                dupes.pop(file_name)  # Delete file from dictionary
                self.dedup_dialog(dupes, packages_dict)

            btn_frame = ttk.Frame(dialog)
            btn_frame.pack(pady=20)

            confirm_btn = ttk.Button(btn_frame, text='Keep Selected', command=delete_files)
            confirm_btn.grid(row=0, column=0, padx=10)

            cancel_btn = ttk.Button(btn_frame, text='Cancel', command=cancel)
            cancel_btn.grid(row=0, column=1, padx=10)

            self.root.wait_window(dialog)
            # dialog.wait_visibility()
            # dialog.grab_set()
            # dialog.lift()
            # dialog.focus_force()


    def refresh(self, all=False): #, done_event):

        wrong_dummyvars_dir = os.path.join(self.VAM_allpackages,'dummyvars')
        if os.path.exists(wrong_dummyvars_dir) and os.listdir(wrong_dummyvars_dir):  # if dir exists and not empty
            # right_dummyvars_dir = os.path.join(self.VAM_addonpackages,'dummyvars')
            restore_dummyvars_dir(wrong_dummyvars_dir, self.VAM_addonpackages) # it goes to AddonPackages always


        # Define a dictionary for package locations and list of bad ones.
        # It is temporary, no file location will be stored in database
        self.packages_dict= {}; bad_ones = []
        
        # Scan for packages, add all its paths and find bad ones
        add_var_paths(VBR, self.VAM_allpackages, self.packages_dict, bad_ones) # VarBrowser
        add_var_paths(VAM, self.VAM_addonpackages, self.packages_dict, bad_ones) # VaM
        
        # Do not continue until all dupes are removed within VAM+Varbrowser context
        dupes = find_duplicate_files(self.packages_dict)
        if dupes: self.dedup_dialog(dupes, self.packages_dict)

        if all:
        # if self.external:
            # External repos paths (optional, dupes ignored)
            if self.Repo1: add_var_paths(EXT, self.Repo1, self.packages_dict, bad_ones)
            if self.Repo2: add_var_paths(EXT, self.Repo2, self.packages_dict, bad_ones)
            if self.Repo3: add_var_paths(EXT, self.Repo3, self.packages_dict, bad_ones)
            if self.Repo4: add_var_paths(EXT, self.Repo4, self.packages_dict, bad_ones)
            if self.Repo5: add_var_paths(EXT, self.Repo5, self.packages_dict, bad_ones)

        # Warn about bad package names
        if bad_ones:
            bad_list = '\n'.join(bad_ones)
            messagebox.showwarning(title='Rejected package(s)', 
                                   message=f'Bad file name format:\n {bad_list}')

        # Grab in memory the set of packages from VarBrowser's autoinstall.txt
        self.autoi_set = set(read_varbr_file(self.autoinstall_path, self.set_status)['Names'])

        # Grab in memory the set of packages from VarBrowser's favorites.txt
        favorites = []
        fav_list = (read_varbr_file(self.favorites_path, self.set_status)['FavoriteNames'])
        if fav_list:
            for item in fav_list:
                favorites.append(item.rsplit(':',1)[0]+'.latest')
        self.fav_set = find_var_names(favorites, self.packages_dict, self.set_status)
        
        # If any autoinstall files not installed, move them (or let varbrowser do that)
        # move_files(self.VAM_allpackages, self.VAM_addonpackages, self.autoi_set, self.set_status)

        # Update database with packages dictionary and autoinstall ones
        db_update(self.db, self.packages_dict, self.autoi_set, self.fav_set, self.var_reveal_missing.get(), self.set_status, self.max_threads)
        

    # varlists (profiles) management

    def process_varlist(self, source, dest, autoinstall=False):
        selected_index = self.varlists_listbox.curselection()
        if not selected_index:
            messagebox.showwarning("Warning", "No list selected.")
            return
        selected_file = self.varlists_listbox.get(selected_index[0])

        # Process selected varlist
        try:
            file_path = os.path.join(self.varlists_dir, selected_file)
            # get set (prevent dupes) if in varbrowser file format 
            # package_set = set((read_varbr_file(file_path))['Names'], self.set_status)
        except Exception as e:
            messagebox.showerror("Error", f"Failed to process list: {str(e)}")

        # Read file and extract to a set (prevent dupes)
        try:
            with open(file_path, 'r', encoding='utf-8') as file:
                package_set = {line.strip() for line in file}
        except FileNotFoundError:
            self.set_status(f"File {file_path} not found.")
        except Exception as e:
            self.set_status(f"Error reading file: {e}")

        # If saving varlist as Autoinstall.txt, replace current and backup
        if autoinstall:
            if messagebox.askokcancel(f"New Autoinstall set", f"Are you sure to set list {selected_file} as Autoinstall?"):
                save_varbrowser_dict(self.autoinstall_path, 'Names', package_set, self.set_status)
            else: return
        else:
            # Do not uninstall or try to install autoinstall packages
            for package_name in self.autoi_set:
                if package_name in package_set:
                    package_set.remove(package_name)

        self.set_status('Moving packages...')
        full_path_list = find_var_paths(package_set, self.packages_dict, self.set_status)
        
        move_files(source, dest, [path for path, level in full_path_list if level == 2], self.set_status)

        # Update files and db
        self.refresh(all=False)
        # Update Treeview
        self.load_filtered_data()


    def varlist_to_autoinstall(self):
        self.process_varlist(self.VAM_allpackages, self.VAM_addonpackages, autoinstall=True)


    def add_varlist_files(self):
        self.process_varlist(self.VAM_allpackages, self.VAM_addonpackages)


    def remove_varlist_files(self):
        self.process_varlist(self.VAM_addonpackages, self.VAM_allpackages)


    def autoinstall_to_varlist(self):
        self.autoi_set

    def delete_varlist(self):
        selected_index = self.varlists_listbox.curselection()
        if not selected_index:
            messagebox.showwarning("Warning", "No list selected.")
            return

        selected_file = self.varlists_listbox.get(selected_index[0])
        # Get full path
        file_path = os.path.join(self.varlists_dir, selected_file)

        if messagebox.askokcancel(f"Delete list {selected_file}", "Are you sure to delete this list?"):
            try:
                os.remove(file_path)
                self.varlists_listbox.delete(selected_index)
            except Exception as e:
                messagebox.showerror("Error", f"Failed to delete list: {str(e)}")


    def view_varlist(self, event):
        selected_index = self.varlists_listbox.curselection()
        if not selected_index:
            messagebox.showwarning("Warning", "No list selected.")
            return
        selected_file = self.varlists_listbox.get(selected_index[0])
        # Process selected var list
        file_path = os.path.join(self.varlists_dir, selected_file)
        os.startfile(file_path)


    def uninstall_all_packages(self):
        package_list = [tup[0] for tup in db_select_all(self.db, 'var_name from packages WHERE level = 1 AND (autoi = 0 or autoi IS NULL) AND ( unp = 0 OR unp IS NULL)')]
        # Do not uninstall 'autoinstall' or 'dummy' vars
        for package_name in package_list:
            if package_name in self.autoi_set:
                package_list.remove(package_name)

        number = len(package_list)
        if messagebox.askokcancel(f"Uninstall all packages", f"Are you sure uninstall {number} packages? They will be set as available."):
            full_path_list = [tup[0] for tup in find_var_paths(package_list, self.packages_dict, self.set_status)]
            move_files(self.VAM_addonpackages, self.VAM_allpackages, full_path_list, self.set_status)
        else: return
        self.set_status('Uninstaling all packages...')

        # Update files and db
        self.refresh(all=False)
        # Update Treeview
        self.load_filtered_data()


    def edit_config_ini(self):
        try:
            os.startfile('config.ini')
        except Exception as e:
            self.set_status(f"Error reading file: {e}")


    # Function for creating a varlist from installed packages (level 1), also for creating a list of missing dependencies (level 0), also from creating from autoinstall
    def create_varlist_from_level(self, level, from_autoinstall=False):
        from tkinter import simpledialog

        if not from_autoinstall:
            # Call function to select all packages currently installed
            if level == 0:  # For missing packages
                packages_list = []
                for item in self.tree_d.get_children():
                    # Get values from columns of current item
                    label, var_dep = self.tree_d.item(item, 'values')
                    # Verify if 'level' is 2 (converted to "Available")
                    if label == 'Missing':
                        packages_list.append(var_dep)
                        # packages_list = [tup[0] for tup in db_select_all(self.db, 'var_dep FROM dependencies WHERE level = 0')]
            elif level == 1:  # For installed packages
                packages_list = [tup[0] for tup in db_select_all(self.db, 'var_name FROM packages WHERE level = 1 AND (unp = 0 OR unp IS NULL)')]
                # Do not include autoinstall or dummy var
                for package_name in list(packages_list): # Use a copied list safely remove items from it
                    if package_name in self.autoi_set:
                        packages_list.remove(package_name)

            packages_list.sort()

            if not packages_list:
                messagebox.showwarning("Warning", "No packages found in VAM packages directory.")
                return
            
            file_name = ''
            if level == 0:
                file_name = 'Missing_dependencies'
            elif level == 1:
                # Ask user for file name
                file_name = simpledialog.askstring("List from installed packages", "Please enter the package list's name:", parent=self.root)

            if not file_name:
                messagebox.showwarning("Warning", "No list name entered")
                return

            # Make sure name ends with .txt
            if not file_name.endswith('.txt'):
                file_name += '.txt'

        else: # create autoinstall.txt from autoinstall
            file_name = 'Autoinstall.txt'
            packages_list = list(self.autoi_set)
            packages_list.sort()
            
        # Path where file will be saved (code warning : varlist directory must exist))
        file_path = f'lists/{file_name}'

        if os.path.exists(file_path):
            if not messagebox.askokcancel(f"{file_name} exists", "Overwrite existing list?"):
                return

        try:
            with open(file_path, 'w', encoding='utf-8') as file:
                for package in packages_list:
                    file.write(f"{package}\n")  # Each package is a tuple
            # messagebox.showinfo("Success", "List created successfully.")
        except IOError as e:
            messagebox.showerror("Error", f"File could not be written: {e}")

        self.load_filtered_data()

