#  """
#    Copyright (c) 2016- 2024, Wiliot Ltd. All rights reserved.
#
#    Redistribution and use of the Software in source and binary forms, with or without modification,
#     are permitted provided that the following conditions are met:
#
#       1. Redistributions of source code must retain the above copyright notice,
#       this list of conditions and the following disclaimer.
#
#       2. Redistributions in binary form, except as used in conjunction with
#       Wiliot's Pixel in a product or a Software update for such product, must reproduce
#       the above copyright notice, this list of conditions and the following disclaimer in
#       the documentation and/or other materials provided with the distribution.
#
#       3. Neither the name nor logo of Wiliot, nor the names of the Software's contributors,
#       may be used to endorse or promote products or services derived from this Software,
#       without specific prior written permission.
#
#       4. This Software, with or without modification, must only be used in conjunction
#       with Wiliot's Pixel or with Wiliot's cloud service.
#
#       5. If any Software is provided in binary form under this license, you must not
#       do any of the following:
#       (a) modify, adapt, translate, or create a derivative work of the Software; or
#       (b) reverse engineer, decompile, disassemble, decrypt, or otherwise attempt to
#       discover the source code or non-literal aspects (such as the underlying structure,
#       sequence, organization, ideas, or algorithms) of the Software.
#
#       6. If you create a derivative work and/or improvement of any Software, you hereby
#       irrevocably grant each of Wiliot and its corporate affiliates a worldwide, non-exclusive,
#       royalty-free, fully paid-up, perpetual, irrevocable, assignable, sublicensable
#       right and license to reproduce, use, make, have made, import, distribute, sell,
#       offer for sale, create derivative works of, modify, translate, publicly perform
#       and display, and otherwise commercially exploit such derivative works and improvements
#       (as applicable) in conjunction with Wiliot's products and services.
#
#       7. You represent and warrant that you are not a resident of (and will not use the
#       Software in) a country that the U.S. government has embargoed for use of the Software,
#       nor are you named on the U.S. Treasury Department’s list of Specially Designated
#       Nationals or any other applicable trade sanctioning regulations of any jurisdiction.
#       You must not transfer, export, re-export, import, re-import or divert the Software
#       in violation of any export or re-export control laws and regulations (such as the
#       United States' ITAR, EAR, and OFAC regulations), as well as any applicable import
#       and use restrictions, all as then in effect
#
#     THIS SOFTWARE IS PROVIDED BY WILIOT "AS IS" AND "AS AVAILABLE", AND ANY EXPRESS
#     OR IMPLIED WARRANTIES OR CONDITIONS, INCLUDING, BUT NOT LIMITED TO, ANY IMPLIED
#     WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, NONINFRINGEMENT,
#     QUIET POSSESSION, FITNESS FOR A PARTICULAR PURPOSE, AND TITLE, ARE DISCLAIMED.
#     IN NO EVENT SHALL WILIOT, ANY OF ITS CORPORATE AFFILIATES OR LICENSORS, AND/OR
#     ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
#     OR CONSEQUENTIAL DAMAGES, FOR THE COST OF PROCURING SUBSTITUTE GOODS OR SERVICES,
#     FOR ANY LOSS OF USE OR DATA OR BUSINESS INTERRUPTION, AND/OR FOR ANY ECONOMIC LOSS
#     (SUCH AS LOST PROFITS, REVENUE, ANTICIPATED SAVINGS). THE FOREGOING SHALL APPLY:
#     (A) HOWEVER CAUSED AND REGARDLESS OF THE THEORY OR BASIS LIABILITY, WHETHER IN
#     CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE);
#     (B) EVEN IF ANYONE IS ADVISED OF THE POSSIBILITY OF ANY DAMAGES, LOSSES, OR COSTS; AND
#     (C) EVEN IF ANY REMEDY FAILS OF ITS ESSENTIAL PURPOSE.
#  """

import json
import os
import sys
import time
import tkinter as tk
from tkinter import ttk, filedialog
import re
from ttkbootstrap.widgets import Button
from ttkbootstrap import Style
from wiliot_tools.utils.wiliot_gui.theme.setup_theme import setup_theme
from PIL import Image, ImageTk

script_dir = os.path.dirname(os.path.abspath(__file__))
assets = os.path.join(script_dir, 'assets')
MAC_SCREEN_RATIO = 0.875

FONT_TXT = ("Gudea", 12)
FONT_TXT_WARNING = ("Gudea", 12, "bold")
FONT_BUTTONS = ("Gudea", 16, "bold")


def popup_message(msg, logger=None, title='Error', font=("Helvetica", 14, 'bold'), log='info', bg=None, tk_frame=None):
    """
    Displays a popup message to the user.

    Args:
        msg (str): The message to be displayed in the popup.
        logger (object, optional): A logger object to log the message. Defaults to None.
        title (str, optional): The title of the popup window. Defaults to 'Error'.
        font (tuple, optional): The font to be used in the popup message. Defaults to ("Helvetica", 14, 'bold').
        log (str, optional): The log level to use when logging the message. Defaults to 'info'.
        bg (str, optional): The background color of the popup window. Defaults to None.
        tk_frame (object, optional): The parent Tkinter frame to use for the popup. Defaults to None.

    Returns:
        None :(
    """

    if tk_frame and tk_frame.winfo_exists():
        popup = tk.Toplevel(tk_frame)
    else:
        popup = tk.Tk()
    popup.wm_title(title)
    if bg is not None:
        popup.configure(bg=bg)
    if logger:
        getattr(logger, log)(f'{title} - {msg}')

    def popup_exit():
        popup.quit()
        popup.destroy()

    label = tk.Label(popup, text=msg, font=font)
    label.pack(side="top", fill="x", padx=10, pady=10)
    b1 = Button(popup, text="Okay", command=popup_exit)
    b1.pack(padx=10, pady=10)
    popup.update_idletasks()
    width = popup.winfo_width()
    height = popup.winfo_height()
    x = int((popup.winfo_screenwidth() / 2) - (width / 2))
    y = int((popup.winfo_screenheight() / 2) - (height / 2))
    popup.geometry(f"{width}x{height}+{x}+{y}")
    popup.mainloop()


class WiliotGui(object):
    class_tk = tk.Tk()
    class_style = Style()

    def __init__(self, params_dict=None, params_path=None, assets_path=assets, parent=None, do_gui_init=True,
                 do_button_config=True, exit_sys_upon_cancel=True, full_screen=False, theme='wiliot',
                 width_offset=100, height_offset=100, disable_all_children_windows=True, screen_ratio=None):

        """
        :param params_dict: dictionary, each key represent a different parameter,
                            each value is a dictionary in itself with the following format:
                            'value': the param default value, auto bool -> create checkbox, str -> create entry,
                            list/tuple -> create combobox,
                            'text': optional, if specified will be the widget label otherwise the key will be used
                            'widget_type': if specified will be the type of widget to create,
                            currently only supports file_input
        :type params_dict: dict
        :param params_path: json with the above param dict, see param_dict description
        :type params_path: str
        :param do_gui_init
        :type do_gui_init bool
        :param do_button_config
        :type do_button_config bool
        :param exit_sys_upon_cancel if True, the gui will call system exit to stop the app
        :type exit_sys_upon_cancel bool
        """
        # minimize the main tk which stays open the whole app runtime
        WiliotGui.class_tk.geometry('0x0')
        WiliotGui.class_tk.wm_state('iconic')
        self.img_ref = None  # do not delete this variable, uses as image pointer
        self.values = None
        self.cancel_event = False
        self.sys_cancel = exit_sys_upon_cancel
        self.parent = parent
        self.disable_all_children_windows = disable_all_children_windows
        if parent is not None and disable_all_children_windows:
            self.update_all_win_children_state(state='disable')

        if params_path:
            try:
                with open(params_path, 'r') as file:
                    self.params_dict = json.load(file)
            except FileNotFoundError:
                raise FileNotFoundError(f"Error: The file {params_path} was not found.")
            except json.JSONDecodeError:
                raise json.JSONDecodeError(f"Error: The file {params_path} could not be decoded as JSON.",
                                           doc=params_path, pos=0)
        elif params_dict:
            self.params_dict = params_dict
        else:
            self.params_dict = {}
        self.assets_path = assets_path
        self.layout = tk.Toplevel(WiliotGui.class_tk) if parent is None else tk.Toplevel(parent)
        self.last_resize_time = time.time()
        self.debounce_delay = 0.01
        self.widgets = {}
        self.widgets_vals = {}
        self.widgets_groups = {}
        self.widgets_tabs = {}
        self.style = WiliotGui.class_style
        self.logo_photo, self.logo_label = setup_theme(style=self.style, layout=self.layout, theme_name=theme)
        self.theme_name = theme
        self.width_offset = width_offset
        self.height_offset = height_offset

        if do_gui_init:
            self.initialize_gui(full_screen=full_screen, screen_ratio=screen_ratio)

        if do_button_config:
            self.button_configs()

    def update_geometry(self):
        screen_w = self.layout.winfo_screenwidth()
        screen_h = self.layout.winfo_screenheight()
        self.layout.update_idletasks()
        max_col, _ = self.layout.grid_size()
        self.logo_label.grid(columnspan=max_col)
        width = self.layout.winfo_width() + self.width_offset
        height = self.layout.winfo_height() + self.height_offset
        x = int((screen_w / 2) - (width / 2))
        y = int((screen_h / 2) - (height / 2))
        self.layout.geometry(f"{width}x{height}+{x}+{y}")

    def apply_screen_ratio(self, screen_ratio=1.0):
        screen_width = int(screen_ratio * self.layout.winfo_screenwidth())
        screen_height = int(screen_ratio * self.layout.winfo_screenheight())
        self.layout.geometry(f"{screen_width}x{screen_height}+0+0")

    def initialize_gui(self, full_screen=False, screen_ratio=None):
        """
        Initializes the GUI by setting up the window, logo, buttons and widgets.

        This function...functions as a simple way to set up the GUI by calling the `setup_window`, `setup_logo`,
        and `create_widgets` methods.
        It also binds the `resize_bg_image` method to the `<Configure>` event of the layout.

        Parameters:
            self : The instance of the WiliotGui class.
            full_screen: if True, opens the gui in full screen

        Returns:
            None
        """
        self.setup_window(full_screen=full_screen, screen_ratio=screen_ratio)
        self.layout.protocol("WM_DELETE_WINDOW", self.on_close)
        if self.parent is not None and self.disable_all_children_windows:
            self.layout.attributes('-topmost', True)  # pop window up
            self.layout.attributes('-topmost', False)  # allow user to move wind if needed

    def setup_window(self, full_screen=False, screen_ratio=None):
        """
        Sets up the window for the GUI based on the number of parameters in the `params_dict` attribute.

        Args:
            full_screen (bool): if True GUI will be full screen mode
        Returns:
            None
        """
        self.layout.title("Wiliot GUI")
        if full_screen:
            if sys.platform != 'darwin':
                self.layout.state('zoomed')
            else:
                self.apply_screen_ratio(screen_ratio=MAC_SCREEN_RATIO)
        elif screen_ratio:
            self.apply_screen_ratio(screen_ratio=screen_ratio)

        # Make the app responsive
        for index in range(20):
            self.layout.columnconfigure(index=index, weight=1)
            self.layout.rowconfigure(index=index, weight=1)

        if self.parent is None:
            font_txt = FONT_TXT_WARNING if self.style.theme_use() == 'warning' else FONT_TXT
            self.layout.option_add("*font", font_txt)
        self.create_widgets()
        self.update_geometry()

    def create_widgets(self):
        """
        A function that creates widgets based on the params_dict attribute.

        This function iterates over the key-value pairs in the given dictionary.
        For each key-value pair, it performs the following steps:

        1. Formats the key using the `format_label` method.
        2. Calculates the row and column for the widget based on the current row and column.
        3. Checks the type of the value:
            - If the value is a boolean, it adds a checkbox widget using the `add_widget` method.
            - If the value is a list, it adds a combobox widget using the `add_widget` method.
            - For any other type, it adds an entry widget using the `add_widget` method.
        4. Increments the current row and column based on the maximum number of items per column.

        The function uses the following parameters:
        - `self`: The instance of the class that contains the function.
        """
        start_row = 1  # Adjust this value to position widgets below the logo
        current_row = start_row
        current_column = 0

        new_keys = {}
        for key, widget_dict in self.params_dict.items():
            if isinstance(widget_dict, dict) and all(isinstance(v, dict) for v in widget_dict.values()):
                # If the value is a nested dictionary, place all elements in the same row
                for index, (sub_key, sub_widget_dict) in enumerate(widget_dict.items()):
                    text_label = sub_widget_dict['text'] if 'text' in sub_widget_dict.keys() else self.format_label(
                        sub_key)
                    widget_key = f"{key}_{sub_key}"  # Ensures unique keys for sub-widgets
                    self.add_widget(widget_type=sub_widget_dict.get('widget_type'),
                                    label_text=text_label,
                                    options=sub_widget_dict.get('options'),
                                    default_value=sub_widget_dict.get('value', ''),
                                    widget_key=widget_key,
                                    widget_group=sub_widget_dict.get('group'),
                                    widget_tab={'name': key, 'tab': sub_widget_dict.get('tab')},
                                    columnspan=sub_widget_dict.get('columnspan', 1),
                                    rowspan=sub_widget_dict.get('rowspan', 1),
                                    row=current_row, column=current_column)
                    current_row += 1
                    new_keys[widget_key] = {'widget_type': sub_widget_dict.get('widget_type')}

            elif isinstance(widget_dict, list):
                for widget_row_dict in widget_dict:
                    sub_key = list(widget_row_dict.keys())[0]
                    sub_widget_dict = widget_row_dict[sub_key]
                    text_label = sub_widget_dict['text'] if 'text' in sub_widget_dict.keys() else self.format_label(
                        sub_key)
                    widget_key = f"{key}_{sub_key}"  # Ensure unique keys for sub-widgets
                    self.add_widget(widget_type=sub_widget_dict.get('widget_type'),
                                    label_text=text_label,
                                    options=sub_widget_dict.get('options'),
                                    default_value=sub_widget_dict.get('value', ''),
                                    widget_key=widget_key,
                                    widget_group=sub_widget_dict.get('group'),
                                    columnspan=sub_widget_dict.get('columnspan', 1),
                                    rowspan=sub_widget_dict.get('rowspan', 1),
                                    row=current_row, column=current_column)
                    current_column += 1 * (2 if sub_widget_dict.get('widget_type') not in ['label', 'button'] else 1)

                    new_keys[widget_key] = {'widget_type': sub_widget_dict.get('widget_type')}

                # Reset column after processing the list
                current_column = 0
                current_row += 1

            else:
                text_label = widget_dict['text'] if 'text' in widget_dict.keys() else self.format_label(key)
                widget_key = key  # Ensure the correct key is used

                self.add_widget(widget_type=widget_dict.get('widget_type'),
                                label_text=text_label,
                                options=widget_dict.get('options'),
                                default_value=widget_dict.get('value', ''),
                                widget_key=widget_key,
                                widget_group=widget_dict.get('group'),
                                columnspan=widget_dict.get('columnspan', 1),
                                rowspan=widget_dict.get('rowspan', 1),
                                row=current_row, column=current_column)

                current_row += 1

        self.params_dict = {**self.params_dict, **new_keys}

    @staticmethod
    def format_label(label):
        return label.replace('_', ' ').capitalize()

    def continue_script(self):
        pass

    def on_submit(self):
        """
        Handles the event when the submit button is clicked.

        This function is called when the submit button is clicked in the GUI.
        It updates the `params_dict` attribute with the current values of the widgets
        and then destroys the layout of the GUI.

        Parameters:
            self (WiliotGui): The instance of the WiliotGui class.
        """

        print("Submitted")
        self.submit_event = True
        for key, widget_var in self.params_dict.items():
            widget = self.widgets.get(key)
            if widget:
                if isinstance(widget, tk.BooleanVar):
                    self.params_dict[key]['output'] = widget.get()
                elif isinstance(widget, tk.StringVar):
                    self.params_dict[key]['output'] = widget.get()
        self.layout.quit()
        self.layout.destroy()

    def on_close(self):
        print("Window closed by user")
        self.exit_gui()

    def exit_gui(self):
        self.cancel_event = True
        self.layout.quit()
        self.layout.destroy()
        if self.sys_cancel:
            sys.exit()

    def exit_app(self):
        if self.parent is not None:
            self.layout.destroy()
        WiliotGui.class_tk.destroy()

    def on_cancel(self):
        """
        Cancels the action and destroys the layout of the GUI.
        Parameters:
            self (WiliotGui): The instance of the WiliotGui class.
        """
        print("Action cancelled by user")
        self.exit_gui()

    def get_cancel_event(self):
        return self.cancel_event

    def set_button_command(self, button_key, command):
        """
        Sets the command to be executed when a button is clicked.

        Args:
            button_key (str): The key of the button widget.
            command (callable): The function to be called when the button is clicked.

        Returns:
            None
        """

        if button_key in self.widgets:
            button_widget = self.widgets[button_key]
            if isinstance(button_widget, Button):
                button_widget.config(command=command)

    def button_configs(self, submit_button_text="SUBMIT", submit_command=None,
                       cancel_button_text='CANCEL', cancel_command=None):
        """
        Configures the buttons for the GUI layout with the specified submit and cancel commands,
        font style, and font size.

        Parameters:
            self (WiliotGui): The instance of the WiliotGui class.
            submit_button_text (str): The string presented on the button
            submit_command (function): The command to execute on submit.
            cancel_button_text (str): The string presented on the button
            cancel_command (function): The command to execute on cancel.
        """
        if submit_command is None:
            submit_command = self.on_submit
        if cancel_command is None:
            cancel_command = self.on_cancel
        max_col, max_row = self.layout.grid_size()
        submit_button = Button(self.layout, text=submit_button_text, command=submit_command,
                               style="custom.Accent.TButton")
        submit_button.grid(row=max_row, column=0, padx=0, pady=0, sticky="w")

        cancel_button = Button(self.layout, text=cancel_button_text, command=cancel_command,
                               style="custom.Accent.TButton")
        cancel_button.grid(row=max_row, column=max_col, padx=0, pady=0, sticky="e")
        self.update_geometry()

    def run(self, save_path=''):
        self.layout.mainloop()
        if self.parent is not None and self.disable_all_children_windows:
            self.update_all_win_children_state(state='enable')
            self.parent.attributes('-topmost', True)  # pop window up
            self.parent.attributes('-topmost', False)  # allow user to move wind if needed
        values = self.get_all_values() if not self.cancel_event else None
        if values and save_path:
            with open(save_path, 'w') as f:
                json.dump(values, f)
        return values


    def add_widget(self, widget_type, label_text, options=None, default_value=None, row=0, column=0, padx=10, pady=5,
                   widget_key=None, widget_group=None, widget_tab=None, columnspan=1, rowspan=1):
        """
        Adds a widget to the layout based on the specified widget type.

        Parameters:
            widget_type (str): The type of widget to add. Valid options are:
                - 'checkbox': A checkbox widget that allows the user to select or deselect a value.
                - 'combobox': A combobox widget that displays a dropdown list of options and allows the user to
                              select one.
                - 'entry': A text entry widget that allows the user to enter text.
                - 'file_input': A file input widget that is essentially an entry widget with an additional
                                button to browse and select a file.
                - 'button': A button widget that can be clicked to perform an action.
                - 'label': A label widget that displays text.
                - 'canvas': A canvas widget that can be used to draw graphics.
            label_text (str): The text to display as the label for the widget.
            options (list, optional): A list of options for the widget. Only applicable for 'combobox' widget type.
            default_value (Any, optional): The default value for the widget.
            row (int, optional): The row position for the widget.
            column (int, optional): The column position for the widget.
            padx (int, optional): The horizontal padding for the widget.
            pady (int, optional): The vertical padding for the widget.
            widget_key (str, optional): The key to use for the widget in the self.widgets dictionary.
            widget_group (str, optional): The group to use for the widget. If specified, the widget will be placed
                                          inside a LabelFrame with the specified group name.
            widget_tab (dict, optional): The tab to use for the widget. If specified, the widget will be placed
                                         inside a tab with the specified tab name.
            columnspan (int, optional): The number of columns to stretch the widget upon.
            rowspan (int, optional): The number of rows to stretch the widget upon.

        Returns:
            The widget variable associated with the added widget.

        Raises:
            Exception: If an unsupported widget type is specified.
        """
        var = None
        if widget_type is None:
            if isinstance(default_value, bool):
                widget_type = 'checkbox'
            elif isinstance(default_value, list) or isinstance(default_value, tuple):
                widget_type = 'combobox'
            else:
                widget_type = 'entry'

        if widget_tab is not None:
            if widget_tab['name'] not in self.widgets_tabs.keys():
                notebook = ttk.Notebook(self.layout)
                notebook.grid(row=row, column=column, sticky="nsew", columnspan=3, padx=padx)

                new_tab = ttk.Frame(notebook)
                for index in range(10):
                    new_tab.columnconfigure(index=index, weight=1)
                    new_tab.rowconfigure(index=index, weight=1)
                notebook.add(new_tab, text=widget_tab['tab'])

                self.widgets_tabs[widget_tab['name']] = {'layout': notebook,
                                                         'tab': {
                                                             widget_tab['tab']: {'layout': new_tab,
                                                                                 'row': 0}}}
                row = 0
                layout = new_tab
            elif widget_tab['tab'] not in self.widgets_tabs[widget_tab['name']]['tab'].keys():
                notebook = self.widgets_tabs[widget_tab['name']]['layout']
                new_tab = ttk.Frame(notebook)
                for index in range(10):
                    new_tab.columnconfigure(index=index, weight=1)
                    new_tab.rowconfigure(index=index, weight=1)
                notebook.add(new_tab, text=widget_tab['tab'])
                self.widgets_tabs[widget_tab['name']]['tab'][widget_tab['tab']] = {'layout': new_tab, 'row': 0}
                row = 0
                layout = new_tab
            else:
                layout = self.widgets_tabs[widget_tab['name']]['tab'][widget_tab['tab']]['layout']
                row = self.widgets_tabs[widget_tab['name']]['tab'][widget_tab['tab']]['row']
                row += 1
                self.widgets_tabs[widget_tab['name']]['tab'][widget_tab['tab']]['row'] = row
        else:
            layout = self.layout

        if widget_group is not None:
            if widget_group not in self.widgets_groups.keys():
                layout = tk.LabelFrame(layout, text=widget_group)
                layout.grid(row=row, column=column, columnspan=2, padx=(20, 10), pady=(20, 10), sticky="nsew")
                self.widgets_groups[widget_group] = {'layout': layout, 'row': 0}
                row = 0
            else:
                layout = self.widgets_groups[widget_group]['layout']
                row = self.widgets_groups[widget_group]['row']
                row += 1
                self.widgets_groups[widget_group]['row'] = row
        else:
            layout = layout

        if widget_type not in ['button', 'label', 'image']:
            label = tk.Label(layout, text=label_text)
            label.grid(row=row, column=column, padx=padx, pady=pady, sticky="ew")
            column += 1  # Move to the next column for the widget

        if widget_type == 'checkbox':
            var = tk.BooleanVar(value=default_value)
            style_str = "success.Roundtoggle.TCheckbutton" \
                if self.theme_name == 'wiliot' else "warning.Roundtoggle.TCheckbutton"
            widget = ttk.Checkbutton(layout, variable=var, style=style_str)

        elif widget_type == 'combobox':
            if not isinstance(default_value, list) and not isinstance(default_value, tuple):
                default_value = [default_value]
            var = tk.StringVar(value=default_value[0])
            options = options if options is not None else tuple(default_value)
            widget = ttk.Combobox(layout, values=options, textvariable=var)
        elif widget_type == 'entry':
            var = tk.StringVar(value=default_value)
            widget = tk.Entry(layout, textvariable=var)
        elif widget_type == 'file_input':
            var = tk.StringVar(value=default_value)
            widget = tk.Entry(layout, textvariable=var)
            is_multiple = (options == 'multiple')
            is_folder = (options == 'folder')
            browse_button = Button(layout, text="Browse",
                                   command=lambda: self.browse_files(var=var,
                                                                     is_multiple=is_multiple, is_folder=is_folder))
            browse_button.grid(row=row, column=column + 1, padx=0, pady=pady, sticky="ew")
        elif widget_type == 'button':
            button_txt = default_value if default_value else label_text
            widget = Button(layout, text=button_txt)
            padx = padx + len(button_txt)
        elif widget_type == 'label':
            var = tk.StringVar(value=default_value)
            widget = tk.Label(layout, textvariable=var)
            if isinstance(options, dict):
                if 'font' in options:
                    widget.configure(font=options['font'])
                if 'fg' in options:
                    widget.configure(fg=options['fg'])
                if 'bg' in options:
                    widget.configure(bg=options['bg'])
        elif widget_type == 'canvas':
            widget = tk.Canvas(layout, width=150, height=300)
        elif widget_type == 'image':
            img_path = default_value
            if os.path.isfile(img_path):
                try:
                    width = options.get("width", 150) if options else 150
                    height = options.get("height", 150) if options else 150

                    img = Image.open(img_path)
                    img = img.resize((width, height), Image.LANCZOS)
                    self.img_ref = ImageTk.PhotoImage(img)
                    widget = tk.Label(layout, image=self.img_ref)
                except Exception as e:
                    print(f"Error loading image {img_path}: {e}")
                    widget = tk.Label(layout, text="Error loading image")
            else:
                widget = tk.Label(layout, text="Image not found")
        elif widget_type == 'spinbox':
            var = tk.StringVar(value=default_value)
            widget = tk.Spinbox(layout, from_=options[0], to=options[-1], textvariable=var)
        else:
            raise Exception('could not select the relevant widget type')

        widget.grid(row=row, column=column, padx=padx, pady=pady, sticky="ew", columnspan=columnspan, rowspan=rowspan)
        self.widgets_vals[widget_key] = var  # Use widget_key
        self.widgets[widget_key] = widget  # Use widget_key

    @staticmethod
    def browse_files(var, is_multiple=False, is_folder=False):
        if is_multiple:
            filenames = filedialog.askopenfilenames(title="Select Files", )
        elif is_folder:
            filenames = filedialog.askdirectory(title="Select a Folder", )
        else:
            filenames = filedialog.askopenfilename(title="Select a File", )
        var.set(filenames)

    def get_all_values(self):
        values = {}
        for key, param in self.params_dict.items():
            if isinstance(param, list) or (
                    isinstance(param, dict) and all(isinstance(v, dict) for v in param.values())):
                continue
            else:
                if param.get('widget_type') not in ['canvas', 'button']:
                    widget = self.widgets_vals.get(key)
                    if widget:
                        values[key] = widget.get()
                        if param.get('widget_type', '') == 'file_input':
                            values[key] = self.extract_file_names(values[key])
        return values

    @staticmethod
    def extract_file_names(str_in):
        if os.path.exists(str_in):
            return str_in
        match = re.findall(r"'(.*?)'", str_in)
        if match:
            files_out = [file.strip() for file in match]
        else:
            return str_in
        return files_out[0] if len(files_out) == 1 else files_out

    def update_widget(self, widget_key, new_value=None, color=None, disabled=None, options=None, button_style=None):
        widget_var = self.widgets_vals.get(widget_key)
        widget = self.widgets.get(widget_key)
        if new_value is not None:
            if isinstance(widget, tk.Label):
                widget_var.set(new_value)
            elif isinstance(widget, ttk.Combobox):
                widget_var.set(new_value)
            elif isinstance(widget, tk.Entry):
                widget.delete(0, tk.END)
                widget.insert(0, new_value)
            elif isinstance(widget, Button):
                widget.config(text=new_value)
            else:
                print(f'Unsupported widget type: {type(widget)}')

        if options is not None and isinstance(widget, ttk.Combobox):
            widget['values'] = options

        if color is not None:
            if isinstance(widget, Button):
                self.update_button_style(widget=widget, widget_key=widget_key, color=color, button_style=button_style)
            else:
                widget.config(fg=color)
        if disabled is not None:
            state = 'disabled' if disabled else 'normal'
            widget.config(state=state)

    def add_event(self, widget_key, command, event_type=None):
        """
        Adds an event to a widget.

        Args:
            widget_key (str): The key of the widget to add the event to.
            command (callable): The function to call when the event occurs.
            event_type (str, optional): The type of event to bind to. Defaults to '<FocusOut>'.

        Returns:
            bool: True if the event was added successfully, False otherwise.
        """

        widget = self.widgets.get(widget_key)

        if isinstance(widget, Button) or event_type == 'button':
            widget.configure(command=command)
            return True
        if event_type is None:
            if isinstance(widget, ttk.Combobox) or event_type == 'combobox':
                event_type = '<FocusIn>'
            elif isinstance(widget, tk.Entry) or event_type == 'entry':
                event_type = '<KeyRelease>'
            else:
                event_type = '<FocusOut>'

        widget.bind(event_type, command)

    def add_recurrent_function(self, cycle_ms, function):
        self.layout.after(ms=cycle_ms, func=self.wrapper_recurrent_func(cycle_ms=cycle_ms, function=function))

    def wrapper_recurrent_func(self, cycle_ms, function):
        def wrapper(*args, **kwargs):
            function(*args, **kwargs)
            self.layout.after(ms=cycle_ms, func=wrapper)

        return wrapper

    @staticmethod
    def update_button_style(widget, widget_key, color, button_style):
        style = Style()
        style_name = f"{widget_key}.TButton"
        if button_style:
            style.configure(style_name, **button_style)
        else:
            style.configure(style_name, background=color, foreground='white')
        widget.config(style=style_name)

    @staticmethod
    def update_all_win_children_state(win=None, state='disable'):
        def update_win(cur_win):
            for child in cur_win.winfo_children():
                if isinstance(child, tk.Tk) or isinstance(child, tk.Toplevel):
                    child.attributes('-disabled', disabled)
                    update_win(child)

        if win is None:
            win = WiliotGui.class_tk
        if 'disable' in state:
            disabled = True
            state = 'disabled'
        elif 'enable' in state or 'normal' in state:
            disabled = False
            state = 'normal'
        else:
            raise Exception('update_all_win_children_state: the supported states are disable, enable')
        update_win(win)


if __name__ == "__main__":

    num = 0
    t_i = time.time()


    def disable_widget():
        values = app.get_all_values()
        return not values['enable_disable_widget']


    def update_str():
        values = app.get_all_values()
        return values['IMAGE']


    def increment_num():
        global num
        num += 1
        return num


    def my_recurrent_function():
        global t_i
        t_now = time.time() - t_i
        elapsed_str = f'elapsed time: {round(t_now, 2)}'
        app.update_widget('clock', elapsed_str)
        print(elapsed_str)


    my_params_dict = {}

    my_params_dict["DEBUG_UID"] = '0xd3d300000003'
    my_params_dict["NUM_ITERATIONS"] = str(int(1))
    my_params_dict["IMAGE"] = ["MainFlow.hex", "D3FSM_435.hex", "D4FSM_508.hex"]  # Choose image from generic flows

    my_params_dict["enable_disable_widget"] = True
    my_params_dict["RF_TESTS_EN"] = False
    my_params_dict["NUM_ITERATIONS"] = str(int(1))
    my_params_dict["IMAGE"] = ["MainFlow.hex", "D3FSM_435.hex", "D4FSM_508.hex"]  # Choose image from generic flows
    my_params_dict = {k: {'value': v} for k, v in my_params_dict.items()}

    my_params_dict['group_hi'] = {'value': True, 'group': 'my checkbox'}
    my_params_dict['group_bye'] = {'value': False, 'group': 'my checkbox'}

    my_params_dict['file1'] = {'value': '', 'widget_type': 'file_input'}
    my_params_dict['file2'] = {'value': '', 'widget_type': 'file_input'}
    my_params_dict['temperature_tab'] = {'min': {'value': 0, 'tab': 'tab1'},
                                         'max': {'value': 100, 'tab': 'tab2'}}
    my_params_dict['temperature_rows'] = [{'min': {'value': 0}}, {'max': {'value': 100}}]
    my_params_dict['file'] = {'value': '', 'widget_type': 'file_input', 'options': 'multiple'}

    my_params_dict['button'] = {'value': 'Hi', 'widget_type': 'button', 'columnspan': 2}
    my_params_dict['update_label'] = {'value': 'n press: 0', 'widget_type': 'label'}
    my_params_dict['clock'] = {'value': '', 'widget_type': 'label'}

    app = WiliotGui(my_params_dict, full_screen=True, theme='wiliot')
    app.add_event(widget_key='button',
                  event_type='button',
                  command=lambda: app.update_widget(widget_key='update_label', new_value=increment_num()))
    app.add_event(widget_key='IMAGE',
                  event_type='<<ComboboxSelected>>',
                  command=lambda args: app.update_widget(widget_key='update_label', new_value=update_str()))
    app.add_event(widget_key='enable_disable_widget',
                  event_type='button',
                  command=lambda: app.update_widget(widget_key='IMAGE', disabled=disable_widget()))
    app.add_recurrent_function(cycle_ms=1000, function=my_recurrent_function)
    values_out = app.run()
    print(values_out)

    app = WiliotGui(my_params_dict, full_screen=False, theme='warning', exit_sys_upon_cancel=False)
    app.add_event(widget_key='button',
                  event_type='button',
                  command=lambda: app.update_widget(widget_key='update_label', new_value=increment_num()))
    app.add_event(widget_key='IMAGE',
                  event_type='<<ComboboxSelected>>',
                  command=lambda args: app.update_widget(widget_key='update_label', new_value=update_str()))
    app.add_event(widget_key='enable_disable_widget',
                  event_type='button',
                  command=lambda: app.update_widget(widget_key='IMAGE', disabled=disable_widget()))
    app.add_recurrent_function(cycle_ms=1000, function=my_recurrent_function)
    values_out = app.run()
    print(values_out)

    i = 0
    while i < 5:
        print('check if all user GUI closed successfully')
        time.sleep(1)
        i += 1
    app.exit_app()
    i = 0
    while i < 5:
        print('check if main GUI closed successfully')
        time.sleep(1)
        i += 1
    print('done')
