import copy
import datetime
import h5py
import inspect  # to get the names and docstrings of the functions
import numpy as np
import pandas as pd
import pathlib
import sys
import traceback
import yaml
from typing import Union, get_args

from PyQt6 import QtCore
from PyQt6.uic import loadUi
from PyQt6.QtCore import Qt, QCoreApplication, QUrl, QItemSelection, QDir
from PyQt6.QtGui import QStandardItemModel, QAction, QStandardItem, QDesktopServices
from PyQt6.QtWidgets import (
    QApplication,
    QCheckBox,
    QComboBox,
    QDialog,
    QFileDialog,
    QGroupBox,
    QLabel,
    QListWidget,
    QMainWindow,
    QMenu,
    QMessageBox,
    QPushButton,
    QScrollArea,
    QSizePolicy,
    QStatusBar,
    QTableView,
    QTableWidget,
    QTableWidgetItem,
    QTabWidget,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)
if __name__ == '__main__':
    # The config file is also read in the CustomWidgets module so it has to be imported before that
    from utils.CreateEvaluixConfig import create_evaluix_config
    create_evaluix_config()

    # Selfmade modules. Some of these may need the config dictionary so it has to be updated before importing/calling them
    from GUIs.CustomWidgets import (
        ClickableMenu,
        ConsoleWidget,
        DragDropTableWidget,
        EvaluixConsole,
        ExportTableWidget,
        FitPointsTable,
        FitReportTable,
        FunctionViewer,
        HDF5PreviewDialog,
        InfoSettingsButton,
        InfoSettingsDialog,
        JumpSlider,
        MacroStackedWidgetContainer,
        ManualDataDialog,
        MyMplCanvas,
        ProfileMacrosDialog,
        ResultsTable,
    )
    from GUIs.Evaluix2_MainWindowLayout import Ui_MainWindow
    from utils.FileLoader import read_file, Dataset, Data, deepcopy_with_unit
    data = Data()
    from utils.EvaluationFunctions import *
else:
    # The config file is also read in the CustomWidgets module so it has to be imported before that
    from .utils.CreateEvaluixConfig import create_evaluix_config
    create_evaluix_config()

    # Selfmade modules. Some of these may need the config dictionary so it has to be updated before importing/calling them
    from .GUIs.CustomWidgets import (
        ClickableMenu,
        ConsoleWidget,
        DragDropTableWidget,
        EvaluixConsole,
        ExportTableWidget,
        FitPointsTable,
        FitReportTable,
        FunctionViewer,
        HDF5PreviewDialog,
        InfoSettingsButton,
        InfoSettingsDialog,
        JumpSlider,
        MacroStackedWidgetContainer,
        ManualDataDialog,
        MyMplCanvas,
        ProfileMacrosDialog,
        ResultsTable,
    )
    from .GUIs.Evaluix2_MainWindowLayout import Ui_MainWindow
    from .utils.FileLoader import read_file, Dataset, Data, deepcopy_with_unit
    data = Data()
    from .utils.EvaluationFunctions import *

#paths
root = pathlib.Path(__file__).resolve().parents[0]
print(root)

QDir.addSearchPath('icons', str(root / 'Icons'))


def load_config(path):
    with open(path, 'r') as file:
        return yaml.safe_load(file)

EvaluixConfig = load_config(root / 'utils' / 'EvaluixConfig.yaml')
ProfileConfig = load_config(root / 'utils' / 'DefaultProfile.yaml')


#TODO: Create just one update function for everything (table_metadata, table_datapkg, table_data, tabl
# e_loglist, canvas) and call it in the respective functions
# If programs slows down, check other solutions
#TODO: Write function so that the interaction with the gui is given by matching Widget names so that the only input is the main Tab/window name
# For example: locate_selected_data(tab) which returns the internal_id, data_pkg, data_cols, chosen_xdata or a part of this
# Maybe this function could also check if the selection changes and then update the respective widgets so that the update function is only called if necessary or desired

#Both of these TODO's could clean up the code a lot, make it more readable and easier to maintain and mainly extandable to other tabs which are designed in the same way

class MainWindow(QMainWindow):

    ###########################################
    # setup the main window
    ###########################################

    def __init__(self):
        super().__init__()

        # Load the python converted .ui file
        self.ui = Ui_MainWindow()  # Instantiate the generated class
        self.ui.setupUi(self)  # Set up the UI

        #######################################
        # General
        #######################################

        ############ Menu Bar ############
        
        # Load data
        self.load_afm_data = self.findChild(QAction, 'load_afm_data')
        self.load_afm_data.triggered.connect(
            lambda: self.safe_execute(
                lambda: (
                    read_file(data, file_or_dir=self.open_file_dialog(), dataformat='AFM'),
                    self.update_table_metadata(self.table_view_afm, type='AFM'),
                    self.save_progress() if ProfileConfig['Autosave_status'] else None,
                )
            )
        )
        
        self.load_lmoke_data = self.findChild(QAction, 'load_lmoke_data')
        self.load_lmoke_data.triggered.connect(
            lambda: self.safe_execute(
                lambda: (
                    read_file(data, file_or_dir=self.open_file_dialog(), dataformat='MOKE'),
                    self.update_table_metadata(self.hys_table_metadata, type='Hyst'),
                    self.save_progress() if ProfileConfig['Autosave_status'] else None,
                )
            )
        )
        
        self.load_vmoke_data = self.findChild(QAction, 'load_vmoke_data')
        self.load_vmoke_data.triggered.connect(
            lambda: self.safe_execute(
                lambda: (
                    read_file(data, file_or_dir=self.open_file_dialog(), dataformat='MOKE'),
                    self.update_table_metadata(self.hys_table_metadata, type='Hyst'),
                    self.save_progress() if ProfileConfig['Autosave_status'] else None,
                )
            )
        )

        self.load_vsm_data = self.findChild(QAction, 'load_vsm_data')
        self.load_vsm_data.triggered.connect(
            lambda: self.safe_execute(
                lambda: (
                    read_file(data, file_or_dir=self.open_file_dialog(), dataformat='VSM'),
                    self.update_table_metadata(self.hys_table_metadata, type='Hyst'),
                    self.save_progress() if ProfileConfig['Autosave_status'] else None,
                )
            )
        )
        
        self.load_kerr_imgs = self.findChild(QAction, 'load_kerr_imgs')
        self.load_kerr_imgs.triggered.connect(
            lambda: self.safe_execute(
                lambda: (
                    read_file(data, file_or_dir=self.open_file_dialog(), dataformat='Kerr_imgs'),
                    self.update_table_metadata(self.hys_table_metadata, type='Hyst'),
                    self.save_progress() if ProfileConfig['Autosave_status'] else None,
                )
            )
        )
        
        self.load_kerr_hys = self.findChild(QAction, 'load_kerr_hys')
        self.load_kerr_hys.triggered.connect(
            lambda: self.safe_execute(
                lambda: (
                    read_file(data, file_or_dir=self.open_file_dialog(), dataformat='Kerr'),
                    self.update_table_metadata(self.hys_table_metadata, type='Hyst'),
                    self.save_progress() if ProfileConfig['Autosave_status'] else None,
                )
            )
        )
        
        # Save evaluation progress
        self.save_path = None

        self.Save_progress_as_hdf5 = self.findChild(QAction, 'Save_progress_as_hdf5')
        self.Save_progress_as_hdf5.triggered.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.save_progress(
                        save_path=self.open_file_dialog(intention='save', filter='HDF5 files (*.h5)'),
                        ids=None),
                )
            )
        )

        self.Toggle_Autosave_Progress = self.findChild(QAction, 'Toggle_Autosave_Progress')
        self.Toggle_Autosave_Progress.triggered.connect(self.toggle_autosave_status)


        # Load evaluation progress
        self.load_progress_with_preview = self.findChild(QAction, 'load_progress_with_preview')
        self.load_progress_with_preview.triggered.connect(
            lambda: self.safe_execute(
                lambda: (self.load_progress(preview=True),
                        self.hys_btn_update_all.click(),
                )
            )
        )
        
        self.load_progress_directly = self.findChild(QAction, 'load_progress_directly')
        self.load_progress_directly.triggered.connect(
            lambda: self.safe_execute(
                lambda: (self.load_progress(preview=False),
                        self.hys_btn_update_all.click(),
                )
            )
        )
        
        self.menuFunctions = self.findChild(ClickableMenu, 'menuFunctions')
        self.menuFunctions.clicked.connect(
            self.open_FunctionViewer
        )
        
        # Insane Clown Posse
        self.actionActualHelp = self.findChild(QAction, 'actionActualHelp')
        
        # Connect the QAction to a slot that opens the YouTube link
        self.actionActualHelp.triggered.connect(self.ICP)

        ############ Status Bar ############

        self.statusbar = self.findChild(QStatusBar, 'statusbar')
        self.setStatusBar(self.statusbar)

        # Create labels for the status bar
        self.status_dataset_label = QLabel()
        self.status_autosave_label = QLabel()
        self.status_save_label = QLabel()
        self.status_message_label = QLabel()

        # Add the labels to the status bar
        self.statusbar.addWidget(self.status_dataset_label)
        self.statusbar.addWidget(self.status_autosave_label)
        self.statusbar.addWidget(self.status_save_label)
        self.statusbar.addWidget(self.status_message_label)

        #######################################
        # General about the TabWidget
        #######################################
        self.tab_main = self.findChild(QTabWidget, 'tab_main')
        self.tab_main.currentChanged.connect(self.update_all_tab)

        #######################################
        # AFM Tab
        # get all widgets of the AFM tab
        #######################################

        # # QTableView which vizualizes the metadata of data with type Img (currently just AFM)
        # self.table_view_afm = self.findChild(QTabWidget, 'table_view_afm')
        # # initialize the table_view_afm
        # self.update_table_metadata(self.table_view_afm, type='Img')

        # # QPushButton which triggers the update of the table_view_afm
        # self.btn_update_table_view_afm = self.findChild(QPushButton, 'btn_update_table_view_afm')
        # self.btn_update_table_view_afm.clicked.connect(
        #     lambda: self.safe_execute(
            # lambda: self.update_table_metadata(self.table_view_afm, type='AFM')
        #     )
        #   )

        #######################################
        # Hysteresis Tab
        #######################################
        self.initialize_hys_tab()

        #######################################
        # Kerr Tab
        #######################################
        self.initialize_kerr_tab()

        #######################################
        # AFM Tab
        #######################################
        self.initialize_afm_tab()
        # self.afm_btn_update_all.click()

        #######################################
        # Console Tab
        #######################################
        self.initialize_console_tab()

        #######################################
        # Datamanager Tab
        #######################################
        self.initialize_datamanager_tab()

        # software starts with the Hysteresis tab
        self.tab_main.setCurrentIndex(0)
        self.hys_btn_update_all.click()

    def initialize_console_tab(self):
        self.console_widget = self.findChild(EvaluixConsole, 'console_widget')
        # self.console_widget.console.execute("\n".join([line.lstrip() for line in """
        #     import numpy as np
        #     import scipy
        #     import pandas as pd
        #     import matplotlib.pyplot as plt
        #     from FileLoader import data, read_file, Dataset
        #     from EvaluationFunctions import *
        #     """.split('\n')]))

        #TODO: Check if this is going to be supported in PyQt6
        # # Find the QTextEdit widget and its layout
        # self.console_text_placeholder = self.findChild(QTextEdit, 'console_text_placeholder')
        # layout = self.console_text_placeholder.parent().layout()

        # # Create a new QsciScintilla widget
        # self.editor = QsciScintilla()

        # # Add a few features to the editor
        # self.lexer = QsciLexerPython()
        # self.editor.setLexer(lexer)
        # self.editor.setAutoIndent(True)

        # # Replace the QTextEdit widget with the QsciScintilla widget
        # layout.replaceWidget(self.console_text_placeholder, self.editor)

    def initialize_datamanager_tab(self):
        #######################################
        # Datamanager Tab
        #######################################

        ############ Metadata table ############
        self.dm_table_metadata = self.findChild(DragDropTableWidget, 'dm_table_metadata')
        
        self.dm_table_metadata.selectionModel().selectionChanged.connect(
            lambda: self.safe_execute(
                lambda: self.dm_btn_update_all.click()
                )
            )
        
        self.dm_table_metadata.fileDropped.connect(
            lambda file_path: self.safe_execute(
                lambda: (
                    read_file(data, file_or_dir=file_path, dataformat=self.dm_load_dataformat.currentText()),
                    self.dm_btn_update_all.click(),
                    self.save_progress() if ProfileConfig['Autosave_status'] else None,
                    )
                )
            )
        # make the metadata editable
        self.dm_table_metadata.setEditTriggers(QTableView.EditTrigger.DoubleClicked)
        self.dm_table_metadata.itemChanged.connect(
            lambda item: self.safe_execute(
                lambda: (
                    self.change_metadata(item),
                    self.dm_btn_update_all.click()
                )
            )
        )
        
        # QPushButton which triggers the update of all dm tables
        self.dm_btn_update_all = self.findChild(QPushButton, 'dm_btn_update_all')
        self.dm_btn_update_all.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.update_all(type='All'),
                    )
                )
            )

        # QComboBox to select the type of data to be loaded
        self.dm_load_dataformat = self.findChild(QComboBox, 'dm_load_dataformat')

        self.dm_btn_load_data = self.findChild(QPushButton, 'dm_btn_load_data')
        self.dm_btn_load_data.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    read_file(data, file_or_dir=self.open_file_dialog(), dataformat=self.dm_load_dataformat.currentText()),
                    self.dm_btn_update_all.click(),
                    self.save_progress() if ProfileConfig['Autosave_status'] else None,
                    )
                )
            )

        self.dm_del_dataset = self.findChild(QPushButton, 'dm_del_dataset')
        self.dm_del_dataset.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.del_dataset(),
                    self.dm_btn_update_all.click(),
                    )
                )
            )
        
        self.dm_add_data_manually = self.findChild(QPushButton, 'dm_add_data_manually')
        self.dm_add_data_manually.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    print('add data manually'),
                    self.add_data_manually(),
                    self.dm_btn_update_all.click(),
                    )
                )
            )

    def initialize_hys_tab(self):
        #######################################
        # Hysteresis Tab
        #######################################

        ############ Metadata table ############
        # QTableView which vizualizes the metadata for all loaded data with type Hyst
        self.hys_table_metadata = self.findChild(DragDropTableWidget, 'hys_table_metadata')

        # connect the current selection of the hys_table_metadata to the function visualize_selected_data
        self.hys_table_metadata.selectionModel().selectionChanged.connect(
            lambda: self.safe_execute(
                lambda: self.hys_btn_update_all.click()
                )
            )
        self.hys_table_metadata.fileDropped.connect(
            lambda file_path: self.safe_execute(
                lambda: (
                    read_file(data, file_or_dir=file_path, dataformat=self.hys_load_dataformat.currentText()),
                    self.hys_btn_update_all.click(),
                    self.save_progress() if ProfileConfig['Autosave_status'] else None,
                )
            )
        )

        # QPushButton which triggers the update of all hys tables
        self.hys_btn_update_all = self.findChild(QPushButton, 'hys_btn_update_all')
        self.hys_btn_update_all.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.update_all(type='Hyst')
                    )
                )
            )

        # QComboBox to select the type of data to be loaded
        self.hys_load_dataformat = self.findChild(QComboBox, 'hys_load_dataformat')

        # QPushButton which loads new data
        self.hys_btn_load_data = self.findChild(QPushButton, 'hys_btn_load_data')
        self.hys_btn_load_data.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    read_file(data, file_or_dir=self.open_file_dialog(), dataformat=self.hys_load_dataformat.currentText()),
                    self.update_table_metadata(self.hys_table_metadata, type='Hyst'),
                    self.save_progress() if ProfileConfig['Autosave_status'] else None,
                )
            )
        )

        # QPushButton which triggers the deletion of the selected dataset
        self.hys_del_dataset = self.findChild(QPushButton, 'hys_del_dataset')
        self.hys_del_dataset.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.del_dataset(),
                    self.hys_btn_update_all.click(),
                    )
                )
            )

        ############ Data Panel ############

        # QGroupBox parent which is the container for the canvas and hys_table_datapkg and hys_table_data
        self.hys_data_panel = self.findChild(QGroupBox, 'hys_data_panel')

        # Canvas, toolbar and table
        # QWidget which is the placeholder for the canvas
        self.hys_canvas_placeholder = self.findChild(QWidget, 'hys_canvas_placeholder')
        self.hys_data_panel.canvas = MyMplCanvas(self.hys_canvas_placeholder, Purpose="Hysteresis")
        self.hys_data_panel.canvas.toolbar.setStyleSheet("""
            QToolBar { background-color : rgb(45, 55, 65); color : white; }
            QToolButton { background-color : white; color : black; }
        """)

        # Override the default layout of the placeholder, insert the canvas and the toolbar
        layout = QVBoxLayout()
        layout.addWidget(self.hys_data_panel.canvas.toolbar)
        layout.addWidget(self.hys_data_panel.canvas)
        self.hys_canvas_placeholder.setLayout(layout)
        self.hys_data_panel.canvas.figure.tight_layout()

        # Add the QTableWidget to the hys_data_panel
        self.hys_data_panel.table = self.findChild(QTableWidget, 'hys_table_plot_content')

        # QPushButton to delete the selected data from preserved data packages
        self.hys_del_preserved_content = self.findChild(QPushButton, 'hys_del_preserved_content')
        self.hys_del_preserved_content.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.del_preserved_content(self.hys_data_panel),
                    self.hys_btn_update_all.click(),
                    )
                )
            )

        # QComboBox to select the x-data for the data table
        self.hys_data_xdata = self.findChild(QComboBox, 'hys_data_xdata')
        #self.update_choose_xdata(self.hys_data_xdata)
        self.hys_data_xdata.currentIndexChanged.connect(
            lambda: self.safe_execute(
                lambda: self.visualize_selected_data(self.hys_data_panel)
                )
            )

        # QPushButton to preserve the currently plotted data packages
        self.hys_preserve_current_datapgk = self.findChild(QPushButton, 'hys_preserve_current_datapgk')
        self.hys_preserve_current_datapgk.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.preserve_current_datapgk(self.hys_data_panel),
                    self.hys_btn_update_all.click(),
                    )
                )
            )

        # hys_table_data
        # QTableView to vizualize the data of a selected data in a data package
        self.hys_table_data = self.findChild(QTableWidget, 'hys_table_data')
        self.hys_table_data.prev_selection = []
        #self.update_table_data(self.hys_table_data)
        self.hys_table_data.selectionModel().selectionChanged.connect(
            lambda: (
                self.safe_execute(
                    lambda: self.check_fit_selection(self.hys_table_data)
                ),
                self.safe_execute(
                    lambda: self.visualize_selected_data(self.hys_data_panel)
                ),
            )
        )

        ############ Control Panel ############

        # QPush which triggers the deletion of outliers in the selected data
        self.btn_hys_del_outliers = self.findChild(InfoSettingsButton, 'btn_hys_del_outliers')
        self.btn_hys_del_outliers.buttonClicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.data_manipulation(del_outliers, add_xdata=False),
                    self.hys_btn_update_all.click())
                )
            )
        self.btn_hys_del_outliers.infoClicked.connect(
            lambda: self.safe_execute(
                lambda: self.info_settings(del_outliers)
                )
            )

        # QPushButton which triggers the opening removal of the selected data
        self.btn_hys_rmv_opening = self.findChild(InfoSettingsButton, 'btn_hys_rmv_opening')
        self.btn_hys_rmv_opening.buttonClicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.data_manipulation(rmv_opening, add_xdata=False),
                    self.hys_btn_update_all.click())
                )
            )
        self.btn_hys_rmv_opening.infoClicked.connect(
            lambda: self.safe_execute(
                lambda: self.info_settings(rmv_opening)
                )
            )

        # QPushButton which triggers the slope correction of the selected data
        self.btn_hys_slope = self.findChild(InfoSettingsButton, 'btn_hys_slope')
        self.btn_hys_slope.buttonClicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.data_manipulation(slope_correction, add_xdata=True),
                    self.hys_btn_update_all.click())
                )
            )
        self.btn_hys_slope.infoClicked.connect(
            lambda: self.safe_execute(
                lambda: self.info_settings(slope_correction)
                )
            )

        # QPushButton which triggers the normalization of the selected data
        self.btn_hys_norm = self.findChild(InfoSettingsButton, 'btn_hys_norm')
        self.btn_hys_norm.buttonClicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.data_manipulation(hys_norm, add_xdata=True),
                    self.hys_btn_update_all.click())
            )
            )
        self.btn_hys_norm.infoClicked.connect(
            lambda: self.safe_execute(
                lambda: self.info_settings(hys_norm)
                )
            )
        
        # QPushButton which triggers the centering of the selected data
        self.btn_hys_center = self.findChild(InfoSettingsButton, 'btn_hys_center_signal')
        self.btn_hys_center.buttonClicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.data_manipulation(hys_center, add_xdata=True),
                    self.hys_btn_update_all.click())
            )
            )
        self.btn_hys_center.infoClicked.connect(
            lambda: self.safe_execute(
                lambda: self.info_settings(hys_center)
                )
            )

        # QPushButton which triggers the normalization of the selected data
        self.btn_hys_smooth = self.findChild(InfoSettingsButton, 'btn_hys_smooth')
        self.btn_hys_smooth.buttonClicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.data_manipulation(smoothing1d, add_xdata=False),
                    self.hys_btn_update_all.click())
                )
            )
        self.btn_hys_smooth.infoClicked.connect(
            lambda: self.safe_execute(
                lambda: self.info_settings(smoothing1d)
                )
            )

        # QPushButton which triggers the numerical differentiation of the selected data
        self.btn_hys_num_deriv = self.findChild(InfoSettingsButton, 'btn_hys_num_deriv')
        self.btn_hys_num_deriv.buttonClicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.data_manipulation(num_derivative, add_xdata=True),
                    self.hys_btn_update_all.click())
                )
            )
        self.btn_hys_num_deriv.infoClicked.connect(
            lambda: self.safe_execute(
                lambda: self.info_settings(num_derivative)
                )
            )

        # QPushButton which triggers the numerical integration of the selected data
        self.btn_hys_integrate = self.findChild(InfoSettingsButton, 'btn_hys_integrate')
        self.btn_hys_integrate.buttonClicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.data_manipulation(num_integral, add_xdata=True),
                    self.hys_btn_update_all.click())
                )
            )
        self.btn_hys_integrate.infoClicked.connect(
            lambda: self.safe_execute(
                lambda: self.info_settings(num_integral)
                )
            )

        # QPushButton which triggers the numerical integration of the selected data
        self.btn_hys_num_integ = self.findChild(InfoSettingsButton, 'btn_hys_num_integ')


        # QPushButton which triggers the linear fit of the selected data
        self.btn_hys_lin_fit = self.findChild(InfoSettingsButton, 'btn_hys_lin_fit')
        self.btn_hys_lin_fit.buttonClicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.data_evaluation(lin_hyseval, add_xdata=True),
                    self.check_fit_selection(self.hys_table_data, fit=True),
                    self.hys_btn_update_all.click())
                )
            )
        self.btn_hys_lin_fit.infoClicked.connect(
            lambda: self.safe_execute(
                lambda: self.info_settings(lin_hyseval)
                )
            )

        # QPushButton which triggers the arctan fit of the selected data
        self.btn_hys_arctan_fit = self.findChild(InfoSettingsButton, 'btn_hys_arctan_fit')
        self.btn_hys_arctan_fit.buttonClicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.data_evaluation(tan_hyseval, add_xdata=True),
                    self.check_fit_selection(self.hys_table_data, fit=True),
                    self.hys_btn_update_all.click())
                )
            )
        self.btn_hys_arctan_fit.infoClicked.connect(
            lambda: self.safe_execute(
                lambda: self.info_settings(tan_hyseval)
                )
            )

        # QPushButton which triggers the double arctan fit of the selected data
        self.btn_hys_darctan_fit = self.findChild(InfoSettingsButton, 'btn_hys_darctan_fit')
        self.btn_hys_darctan_fit.buttonClicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.data_evaluation(double_tan_hyseval, add_xdata=True),
                    self.check_fit_selection(self.hys_table_data, fit=True),
                    self.hys_btn_update_all.click())
                )
            )
        self.btn_hys_darctan_fit.infoClicked.connect(
            lambda: self.safe_execute(
                lambda: self.info_settings(double_tan_hyseval)
                )
            )
        
        self.hys_macro_widget = self.findChild(MacroStackedWidgetContainer, 'hys_macro_widget')
        self.hys_macro_widget.editButton.clicked.connect(self.profile_macros)
        #self.hys_macro_widget.predefined_macros()
        
        # QListWidget which contains the loglist of the selected data package
        self.hys_loglist = self.findChild(QListWidget, 'hys_loglist')
        #self.update_loglist(self.hys_loglist)


        ############ Datapkg sub-Panel ############

        # hys_table_datapkg
        # QTableWidget to vizualize an overview about the data of the currently selected data package
        self.hys_table_datapkg = self.findChild(QTableWidget, 'hys_table_datapkg')
        #self.update_table_datapkg(self.hys_table_datapkg)
        self.hys_table_datapkg.selectionModel().selectionChanged.connect(
            lambda: self.safe_execute(
                lambda: self.hys_btn_update_all.click()
                )
            )


        # QCheckBox to trigger the creation of a new data package
        self.hys_new_data_flag = self.findChild(QCheckBox, 'hys_new_data_flag')

        # QPushButton which triggers the deletion of the selected data package
        self.hys_del_mod_data = self.findChild(QPushButton, 'hys_del_mod_data')
        self.hys_del_mod_data.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.del_mod_data(),
                    self.hys_btn_update_all.click(),
                    )
                )
            )

        # button to copy the selected data package
        self.hys_copy_datapkg = self.findChild(QPushButton, 'hys_copy_datapkg')
        self.hys_copy_datapkg.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.copy_mod_data(),
                    self.hys_btn_update_all.click(),
                    )
                )
            )

        # button to copy the selected data package column
        self.hys_copy_datacol = self.findChild(QPushButton, 'hys_copy_datacol')
        self.hys_copy_datacol.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.copy_datacol(),
                    self.hys_btn_update_all.click(),
                    )
                )
            )


        ############ Results sub-Panel ############
        self.hys_results_tab = self.findChild(QTabWidget, 'hys_results_tab')

        self.hys_choose_evalmodel = self.findChild(QComboBox, 'hys_choose_evalmodel')
        self.hys_choose_evalmodel.currentIndexChanged.connect(
            lambda: self.safe_execute(
                lambda: self.update_results_tab(self.hys_results_tab)
                )
            )
        

        self.update_results_tab(self.hys_results_tab)

        # # At the end of the initialization, update/initialize all widgets
        # self.hys_btn_update_all.click()

    def initialize_kerr_tab(self):

        ############ Metadata table ############
        # QTableView which vizualizes the metadata for all loaded data with type Hyst
        self.kerr_table_metadata = self.findChild(DragDropTableWidget, 'kerr_table_metadata')

        # connect the current selection of the hys_table_metadata to the function visualize_selected_data
        self.kerr_table_metadata.selectionModel().selectionChanged.connect(
            lambda: self.safe_execute(
                lambda: self.kerr_btn_update_all.click()
                )
            )
        self.kerr_table_metadata.fileDropped.connect(
            lambda file_path: self.safe_execute(
                lambda: (
                    read_file(data, file_or_dir=file_path, dataformat=self.kerr_load_dataformat.currentText()),
                    self.kerr_btn_update_all.click(),
                    self.save_progress() if ProfileConfig['Autosave_status'] else None,
                )
            )
        )

        # QPushButton which triggers the update of all hys tables
        self.kerr_btn_update_all = self.findChild(QPushButton, 'kerr_btn_update_all')
        self.kerr_btn_update_all.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.update_all(type='Img')
                    )
                )
            )

        # QComboBox to select the type of data to be loaded
        self.kerr_load_dataformat = self.findChild(QComboBox, 'kerr_load_dataformat')

        # QPushButton which loads new data
        self.kerr_btn_load_data = self.findChild(QPushButton, 'kerr_btn_load_data')
        self.kerr_btn_load_data.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    read_file(data, file_or_dir=self.open_file_dialog(), dataformat=self.kerr_load_dataformat.currentText()),
                    self.update_table_metadata(self.kerr_table_metadata, type='Img'),
                    self.save_progress() if ProfileConfig['Autosave_status'] else None,
                )
            )
        )

        # QPushButton which triggers the deletion of the selected dataset
        self.kerr_del_dataset = self.findChild(QPushButton, 'kerr_del_dataset')
        self.kerr_del_dataset.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.del_dataset(),
                    self.kerr_btn_update_all.click(),
                    )
                )
            )

        ############ Data Panel ############

        # QGroupBox parent which is the container for the canvas and hys_table_datapkg and hys_table_data
        self.kerr_data_panel = self.findChild(QGroupBox, 'kerr_data_panel')

        # Canvas, toolbar and table
        # QWidget which is the placeholder for the canvas
        self.kerr_canvas_placeholder = self.findChild(QWidget, 'kerr_canvas_placeholder')
        self.kerr_data_panel.canvas = MyMplCanvas(self.hys_canvas_placeholder, Purpose="Image")
        self.kerr_data_panel.canvas.toolbar.setStyleSheet("""
            QToolBar { background-color : rgb(45, 55, 65); color : white; }
            QToolButton { background-color : white; color : black; }
        """)

        # Override the default layout of the placeholder, insert the canvas and the toolbar
        layout = QVBoxLayout()
        layout.addWidget(self.kerr_data_panel.canvas.toolbar)
        layout.addWidget(self.kerr_data_panel.canvas)
        self.kerr_canvas_placeholder.setLayout(layout)
        self.kerr_data_panel.canvas.figure.tight_layout()

        # QSlider and QLabel to select the image to be displayed
        self.kerr_data_slider = self.findChild(JumpSlider, 'kerr_data_slider')
        self.kerr_data_slider.valueChanged.connect(
            lambda: self.safe_execute(
                lambda: (self.update_sliderlabels(),
                self.visualize_selected_imgs(self.kerr_data_panel))
                )
            )
        
        # # Add the QTableWidget to the hys_data_panel
        # self.hys_data_panel.table = self.findChild(QTableWidget, 'hys_table_plot_content')

        # # QPushButton to delete the selected data from preserved data packages
        # self.hys_del_preserved_content = self.findChild(QPushButton, 'hys_del_preserved_content')
        # self.hys_del_preserved_content.clicked.connect(
        #     lambda: self.safe_execute(
        #         lambda: (
        #             self.del_preserved_content(self.hys_data_panel),
        #             self.hys_btn_update_all.click(),
        #             )
        #         )
        #     )

        # # QComboBox to select the x-data for the data table
        # self.hys_data_xdata = self.findChild(QComboBox, 'hys_data_xdata')
        # #self.update_choose_xdata(self.hys_data_xdata)
        # self.hys_data_xdata.currentIndexChanged.connect(
        #     lambda: self.safe_execute(
        #         lambda: self.visualize_selected_data(self.hys_data_panel)
        #         )
        #     )

        # # QPushButton to preserve the currently plotted data packages
        # self.hys_preserve_current_datapgk = self.findChild(QPushButton, 'hys_preserve_current_datapgk')
        # self.hys_preserve_current_datapgk.clicked.connect(
        #     lambda: self.safe_execute(
        #         lambda: (
        #             self.preserve_current_datapgk(self.hys_data_panel),
        #             self.hys_btn_update_all.click(),
        #             )
        #         )
        #     )

        ############ Datapkg sub-Panel ############

        # QTableWidget to vizualize an overview about the data of the currently selected data package
        self.kerr_table_datapkg = self.findChild(QTableWidget, 'kerr_table_datapkg')
        self.kerr_table_datapkg.selectionModel().selectionChanged.connect(
            lambda: self.safe_execute(
                lambda: self.kerr_btn_update_all.click()
                )
            )

        # hys_table_data
        # QTableView to vizualize the data of a selected data in a data package
        self.kerr_table_data = self.findChild(QTableWidget, 'kerr_table_data')
        self.kerr_table_data.selectionModel().selectionChanged.connect(
            lambda: self.safe_execute(
                lambda: self.visualize_selected_imgs(self.kerr_data_panel)
                )
            )
        
        # connect the data_slider (initialized above) and the data_table
        self.kerr_selection_changed = False # flag to prevent the slider_to_table function to be called when the table is updated, i.e. selection cascades
        # Connect the signals and slots
        self.kerr_data_slider.valueChanged.connect(lambda value: self.slider_to_table(self.kerr_table_data, value))
        self.kerr_table_data.itemSelectionChanged.connect(lambda: self.table_to_slider(self.kerr_data_slider, self.kerr_table_data.currentRow()))


        # QCheckBox to trigger the creation of a new data package
        self.kerr_new_data_flag = self.findChild(QCheckBox, 'kerr_new_data_flag')

        # QPushButton which triggers the deletion of the selected data package
        self.kerr_del_mod_data = self.findChild(QPushButton, 'kerr_del_mod_data')
        self.kerr_del_mod_data.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.del_mod_data(),
                    self.kerr_btn_update_all.click(),
                    )
                )
            )

        # button to copy the selected data package
        self.kerr_copy_datapkg = self.findChild(QPushButton, 'kerr_copy_datapkg')
        self.kerr_copy_datapkg.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.copy_mod_data(),
                    self.kerr_btn_update_all.click(),
                    )
                )
            )

        # button to copy the selected data package column
        self.kerr_copy_datacol = self.findChild(QPushButton, 'kerr_copy_datacol')
        self.kerr_copy_datacol.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.copy_datacol(),
                    self.kerr_btn_update_all.click(),
                    )
                )
            )

    def initialize_afm_tab(self):
        #######################################
        # AFM Tab
        #######################################

        ############ Metadata table ############
        # QTableView which vizualizes the metadata for all loaded data with type AFM
        self.afm_table_metadata = self.findChild(DragDropTableWidget, 'afm_table_metadata')

        # connect the current selection of the hys_table_metadata to the function visualize_selected_data
        self.afm_table_metadata.selectionModel().selectionChanged.connect(
            lambda: self.safe_execute(
                lambda: self.afm_btn_update_all.click()
                )
            )
        self.afm_table_metadata.fileDropped.connect(
            lambda file_path: self.safe_execute(
                lambda: (
                    read_file(data, file_or_dir=file_path, dataformat=self.afm_load_dataformat.currentText()),
                    self.afm_btn_update_all.click(),
                    self.save_progress() if ProfileConfig['Autosave_status'] else None,
                )
            )
        )

        # QPushButton which triggers the update of all hys tables
        self.afm_btn_update_all = self.findChild(QPushButton, 'afm_btn_update_all')
        self.afm_btn_update_all.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.update_all(type='AFM')
                    )
                )
            )

        # QComboBox to select the type of data to be loaded
        self.afm_load_dataformat = self.findChild(QComboBox, 'afm_load_dataformat')

        # QPushButton which loads new data
        self.afm_btn_load_data = self.findChild(QPushButton, 'afm_btn_load_data')
        self.afm_btn_load_data.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    read_file(data, file_or_dir=self.open_file_dialog(), dataformat=self.afm_load_dataformat.currentText()),
                    self.afm_btn_update_all.click(),
                    self.save_progress() if ProfileConfig['Autosave_status'] else None,
                )
            )
        )

        # QPushButton which triggers the deletion of the selected dataset
        self.afm_del_dataset = self.findChild(QPushButton, 'afm_del_dataset')
        self.afm_del_dataset.clicked.connect(
            lambda: self.safe_execute(
                lambda: (
                    self.del_dataset(),
                    self.afm_btn_update_all.click(),
                    )
                )
            )

        ############ Data Panel ############

        # QGroupBox parent which is the container for the canvas
        self.afm_data_panel = self.findChild(QGroupBox, 'afm_data_panel')

        # Canvas, toolbar and table
        # QWidget which is the placeholder for the canvas
        self.afm_canvas_placeholder = self.findChild(QWidget, 'afm_canvas_placeholder')
        self.afm_data_panel.canvas = MyMplCanvas(self.afm_canvas_placeholder, Purpose="AFM")
        self.afm_data_panel.canvas.toolbar.setStyleSheet("""
            QToolBar { background-color : rgb(45, 55, 65); color : white; }
            QToolButton { background-color : white; color : black; }
        """)

        # Override the default layout of the placeholder, insert the canvas and the toolbar
        layout = QVBoxLayout()
        layout.addWidget(self.afm_data_panel.canvas.toolbar)
        layout.addWidget(self.afm_data_panel.canvas)
        self.afm_canvas_placeholder.setLayout(layout)
        self.afm_data_panel.canvas.figure.tight_layout()


        # at the end of the initialization, update/initialize all widgets
        self.afm_btn_update_all.click()

    ###########################################
    # main functions for the main window
    ###########################################
    # 0. Safe execution of functions
    # 1. Load data window
    # 2. Update and connect the widgets and status bar (order: metadata, datapkg, data, xdata, loglist, statusbar)
    # 3. get and delete selected data
    # 4. Data visualization, manipulation and evaluation

    # 0. Safe execution of functions
    def safe_execute(self, func):
        try:
            #result = func()
            func()
        except Exception as e:
            error_message = str(e) + "\n\n" + traceback.format_exc()

            msgBox = QMessageBox(self)
            msgBox.setIcon(QMessageBox.Icon.Critical)
            msgBox.setText("Error")
            msgBox.setInformativeText(str(e))

            # Set the detailed text
            msgBox.setDetailedText(error_message)

            # Add a "Copy to Clipboard" button
            copyButton = msgBox.addButton("Copy details to Clipboard", QMessageBox.ButtonRole.ActionRole)

            # Disconnect the button's signals
            try:
                copyButton.clicked.disconnect()
            except TypeError:
                pass  # No signals were connected
            copyButton.clicked.connect(lambda: self.copy_to_clipboard(error_message))

            # Add a "OK" button
            okButton = msgBox.addButton(QMessageBox.StandardButton.Ok)

            msgBox.exec()

        # return result

    def copy_to_clipboard(self, text):
        clipboard = QApplication.clipboard()
        clipboard.setText(text)

    # 1. Load data window
    def open_file_dialog(self, intention='load', filter='All Files (*)'):
        if intention == 'load':
            # Open a file dialog to choose a file/directory
            file_name, _ = QFileDialog.getOpenFileName(self, "Open File", "", filter)
            if file_name:
                return file_name
            return None
        elif intention == 'save':
            # Open a file dialog to choose a file/directoryconda install -c conda-forge opencv
            file_name, _ = QFileDialog.getSaveFileName(self, "Save File", "", filter)
            if file_name:
                return file_name
            return None
        else:
            self.update_status_bar("Invalid intention for loading or saving files. Please choose 'load' or 'save' for open_file_dialog().")

    # 2. Update and connect the widgets and status bar
    def update_table_metadata(self, table_metadata, type):
        # Check if the data object is empty
        if not data.dataset:
            self.update_status_bar("No data available. Please load a dataset.")
            return
        else:
            current_status = self.statusBar().currentMessage()
            if current_status == "No data available. Please load a dataset.":
                self.update_status_bar(current_status)

        key_importance = {
            'internal_id': 0,
            'sample': 1,
            'type': 2,
            'device': 3,
            'datetime': 4,
            'comment': 5,
            'uuid': 6,
        }

        # List to store the horizontal and vertical headers
        horizontal_headers = []
        vertical_headers = []

        # Prepare a list to store the table data
        table_content = []

        if type == 'All':
            # Iterate over the items in the data dictionary
            for id, dataset in data.dataset.items():
                # Reading out the metadata
                metadata = dataset.metadata

                # Exclude 'internal_id' from the keys and add to horizontal_headers
                for key in metadata.keys():
                    if key not in horizontal_headers:
                        horizontal_headers.append(str(key))

                # Sort the keys based on their importance, if the key is not in the dictionary, it will be sorted to the end
                horizontal_headers.sort(key=lambda k: key_importance.get(k, float('inf')))

                # Prepare a row data
                row_data = [str(metadata.get(key, '-')) for key in horizontal_headers]
                table_content.append(row_data)

        else:
            # Iterate over the items in the data dictionary
            for id, dataset in data.dataset.items():
                # Check if the metadata contains the type
                if type in dataset.metadata.values():
                    # Reading out the metadata
                    metadata = dataset.metadata

                    # Exclude 'internal_id' from the keys and add to horizontal_headers
                    for key in metadata.keys():
                        if key not in horizontal_headers:
                            horizontal_headers.append(str(key))

                    # Sort the keys based on their importance, if the key is not in the dictionary, it will be sorted to the end
                    horizontal_headers.sort(key=lambda k: key_importance.get(k, float('inf')))

                    # Prepare a row data
                    row_data = [str(metadata.get(key, '-')) for key in horizontal_headers]
                    table_content.append(row_data)

        # Set the number of rows and columns
        table_metadata.setRowCount(len(table_content))
        table_metadata.setColumnCount(len(horizontal_headers))
        vertical_headers = [str(i) for i in range(len(table_content))]
        
        # Add data to the table_metadata
        for i, row_data in enumerate(table_content):
            for j, cell_data in enumerate(row_data):
                # try:
                #     num = float(cell_data)
                #     str_num = "{:.5f}".format(num)
                #     item = NumericTableWidgetItem(str_num.rstrip('0').rstrip('.') if '.' in str_num else str_num)
                # except:
                item = QTableWidgetItem(cell_data)
                table_metadata.setItem(i, j, item)

        # substitute the 'internal_id' with "ID" in the horizontal_headers, just for better readability
        try:
            horizontal_headers[horizontal_headers.index('internal_id')] = 'ID'
        except ValueError:
            pass

        # Set horizontal and vertical headers
        table_metadata.setHorizontalHeaderLabels(horizontal_headers)
        table_metadata.setVerticalHeaderLabels(vertical_headers)

        # make sure the table is updated
        table_metadata.resizeColumnsToContents()
        table_metadata.update()

    def update_table_datapkg(self, table_datapkg):
        # Prepare a list to store the table data
        table_content = []

        # get the internal_id
        selected_widgets, selected_data = self.get_selection()
        _id, _pkg, _cols, _x = selected_data
        if _id is not None:

            # # check if an update is necessary
            # try:
            #     if table_datapkg.internal_id == _id:
            #         return
            # except:
            #     table_datapkg.internal_id = _id

            data_dict = vars(data.dataset[_id]) # Convert selected dataclass to a dictionary

            # Filter out 'metadata' and 'loglist'
            horizontal_headers = [str(key) for key in data_dict.keys() if not key in ['metadata', 'loglist'] and isinstance(data_dict[key], pd.DataFrame)]

            row_data = [f"Cols: {data_dict[key].shape[1]}, Rows: {data_dict[key].shape[0]}" for key in horizontal_headers]
            table_content.append(row_data)

            # Check if current selection would be outside of new table_content
            selection = table_datapkg.selectionModel().selectedIndexes()
            if selection:
                if selection[0].row() >= len(table_content):
                    table_datapkg.clearSelection()
                elif selection[0].column() >= len(horizontal_headers):
                    table_datapkg.clearSelection()

            # Set the number of rows and columns
            table_datapkg.setRowCount(len(table_content))
            table_datapkg.setColumnCount(len(horizontal_headers))

            for i, row_data in enumerate(table_content):
                for j, cell_data in enumerate(row_data):
                    item = QTableWidgetItem(cell_data)
                    table_datapkg.setItem(i, j, item)

            # Check if anything is already selected
            if not table_datapkg.selectionModel().selectedIndexes():
                # Select the "raw_data" column
                if "raw_data" in horizontal_headers:
                    table_datapkg.selectColumn(horizontal_headers.index("raw_data"))

        else:
            # Add a dummy row of data to make the table visible and selectable
            table_datapkg.setRowCount(1)
            table_datapkg.setColumnCount(1)
            horizontal_headers = ['No data selected']
            item = QTableWidgetItem('Cols: 0, Rows: 0')
            table_datapkg.setItem(0, 0, item)

        # Set horizontal headers and update the table
        table_datapkg.setHorizontalHeaderLabels(horizontal_headers)
        table_datapkg.resizeColumnsToContents()
        table_datapkg.update()

    def update_table_data(self, table_data):

        # get the internal_id and the data package
        selected_widgets, selected_data = self.get_selection()
        _id, _pkg, _, _ = selected_data
        _, _, _, _, data_xdata = selected_widgets
        if _id is not None and _pkg:
            # Control the header of the (first) selected column
            if _pkg == 'No data selected':
                return

            _data = getattr(data.dataset[_id], _pkg)
            horizontal_headers = list(_data.keys())

            if isinstance(_data, pd.DataFrame):
                # Set the number of rows and columns
                table_data.setRowCount(_data.shape[0] + 1)
                table_data.setColumnCount(_data.shape[1])

                # Add the attribute "unit" to the first row
                for j in range(_data.shape[1]):
                    header = horizontal_headers[j]
                    # Attempt to get the unit attribute
                    if hasattr(_data[header], 'unit'):
                        unit = getattr(_data[header], 'unit', None)
                        quantity_type = None
                        for key in EvaluixConfig['conversion_factors']:
                            if str(unit) in EvaluixConfig['conversion_factors'][key].keys():
                                quantity_type = key
                                break
                        
                    else:
                        unit = 'unknown unit'  # or unit = ''
                        quantity_type = None
                    
                    # Create a QComboBox for unit selection
                    unit_selector = QComboBox()
                    if quantity_type is not None:
                        available_units = EvaluixConfig['conversion_factors'][quantity_type]
                    else:
                        available_units = [unit]
                    unit_selector.addItems(available_units)
                    unit_selector.setCurrentText(unit)
                    unit_selector.currentTextChanged.connect(lambda new_unit, header=header: 
                        self.update_unit(new_unit, 
                                         _data,
                                         header, 
                                         table_data,
                                         data_xdata))

                    table_data.setCellWidget(0, j, unit_selector)

                for i in range(_data.shape[0]):
                    for j in range(_data.shape[1]):
                        # check if header is 'FlattendImage'
                        if horizontal_headers[j] == 'FlattendImage':
                            item = QTableWidgetItem("Image")
                        elif isinstance(_data.iloc[i, j], (int, float)):
                            item = QTableWidgetItem("{:.5f}".format(_data.iloc[i, j]))
                        else:
                            item = QTableWidgetItem(str(_data.iloc[i, j]))
                        table_data.setItem(i+1, j, item) # i is the row, j is the column

            # Set horizontal headers and update the table
            table_data.setHorizontalHeaderLabels(horizontal_headers)
            
            vertical_headers = ['Unit'] + [str(i) for i in range(_data.shape[0])]
            table_data.setVerticalHeaderLabels(vertical_headers)

            if not table_data.selectedIndexes() or len(table_data.selectedIndexes()) != table_data.rowCount() * len(set(index.column() for index in table_data.selectedIndexes())):
                # Try to select column 1, if it exists
                if table_data.columnCount() > 1:
                    table_data.selectColumn(1)
                # If column 1 doesn't exist, select column 0
                elif table_data.columnCount() > 0:
                    table_data.selectColumn(0)

        else:
            # Add a dummy row of data to make the table visible and selectable
            table_data.setRowCount(1)
            table_data.setColumnCount(1)
            horizontal_headers = ['No data selected']
            item = QTableWidgetItem('Cols: 0, Rows: 0')
            table_data.setItem(0, 0, item)
            # Set horizontal headers
            table_data.setHorizontalHeaderLabels(horizontal_headers)

        # Update the table
        table_data.resizeColumnsToContents()
        table_data.update()
        
    def update_unit(self, new_unit, data, header, table_data, data_xdata):
        # Copy units from the original series
        units = {key: (data[key].unit if key != header else new_unit) for key in data.keys()}
        
        # Get the currently selected columns
        data[header] = unit_converter(data[header], EvaluixConfig['conversion_factors'], new_unit)
        
        # Reassign the units to the series in data
        for key, unit in units.items():
            data[key].unit = unit
        
        # Refresh the table data
        self.update_table_data(table_data)
        self.update_choose_xdata(data_xdata)
        
        # Emit selectionChanged signal for table_data
        selection_model = table_data.selectionModel()
        if selection_model.hasSelection():
            selected_indexes = selection_model.selectedIndexes()
            selection = QItemSelection()
            for index in selected_indexes:
                selection.select(index, index)
            selection_model.selectionChanged.emit(selection, selection)
        

    def check_fit_selection(self, table_data, fit=False):
        # Temporarily disconnect the signals
        table_data.blockSignals(True)

        # delete the memory of the previous selection so that all columns are selected in case of a fit
        if fit:
            table_data.prev_selection = []
            print("Fit selected, resetting selection")

        # Get the currently selected columns
        selected_columns = table_data.selectionModel().selectedColumns()

        # List to hold all columns that need to be selected
        columns_to_select = []

        # For each selected column, check if there are other columns with headers that contain the selected header
        for column in selected_columns:
            if column not in table_data.prev_selection:
                header = table_data.horizontalHeaderItem(column.column()).text()

                # Get all column headers
                all_headers = [table_data.horizontalHeaderItem(i).text() for i in range(table_data.columnCount())]

                # Find headers that contain the selected header
                matching_headers = [h for h in all_headers if header.strip() in h.strip() and '_fit' in h.strip()]

                # Add the matching columns to the list
                for h in matching_headers:
                    index = all_headers.index(h)
                    columns_to_select.append(index)

        # Initialize a selection object
        selection = QtCore.QItemSelection()

        # Select all the columns in the list
        for column in columns_to_select:
            # Get the top and bottom index of the column, i.e. the whole column
            top = table_data.model().index(0, column)
            bottom = table_data.model().index(table_data.rowCount() - 1, column)
            # Add the selection to the selection object
            selection.select(top, bottom)

        # Select the columns in the list
        table_data.selectionModel().select(selection, QtCore.QItemSelectionModel.SelectionFlag.Select)

        # Update the table
        table_data.update()

        # Get the currently selected columns after the check
        selected_columns = table_data.selectionModel().selectedColumns()

        # Save the current selection
        table_data.prev_selection = selected_columns

        # Reconnect the signals
        table_data.blockSignals(False)

    def update_choose_xdata(self, combobox):
        # get the internal_id and the data package
        selected_widgets, selected_data = self.get_selection()
        _id, _pkg, _cols, _x = selected_data
        if _id is not None and _pkg:

            if _pkg == 'No data selected': # if no data is selected
                combobox.clear()
                combobox.addItem("No data selected")
                combobox.setCurrentIndex(0)
                return

            # retrieve the selected dataframe or more precisely the keys of the dataframe
            _data = getattr(data.dataset[_id], _pkg)
            horizontal_headers = list(_data.keys())

            if isinstance(_data, pd.DataFrame):
                # save old data and clear the combobox
                old_xdata = combobox.currentText()
                combobox.clear()

                combobox.addItem("Point number")
                items = ["Point number"]
                for header in horizontal_headers:
                    try:
                        item = header + " [" + _data[header].unit + "]"
                    except AttributeError:
                        item = header
                    items.append(item)
                    combobox.addItem(item)

                # set the old data as current text
                if old_xdata in horizontal_headers:
                    index = combobox.findText(old_xdata)
                else:
                    for item in items:
                        if item.startswith(("H", "field", "Field")):
                            index = combobox.findText(item)
                            break
                        else:
                            index = combobox.findText("Point number")

                combobox.setCurrentIndex(index)
        else:
            # Add a dummy row of data to make the table visible and selectable
            combobox.clear()
            combobox.addItem("No data selected")
            combobox.setCurrentIndex(0)

    def update_loglist(self, table_loglist):
        # get the internal_id and the data package
        selected_widgets, selected_data = self.get_selection()
        _id, _pkg, _cols, _x = selected_data
        if _id is not None and _pkg:

            if _pkg == 'No data selected' or _pkg == 'raw_data':
                table_loglist.clear()
                return

            key = _pkg.split("_")[-1]
            loglist = getattr(data.dataset[_id], "loglist_" + key)

            #input the loglist into the QListWidget
            table_loglist.clear()
            for log in loglist:
                table_loglist.addItem(log)

    def update_results_tab(self, results_tab=None):
        tab_main = self.findChild(QTabWidget, 'tab_main')

        # Get the currently open tab
        index = tab_main.currentIndex()
        tab_name = tab_main.widget(index).objectName()

        # Identify tabs function by slicing the tab_name
        _name = tab_name.split("_")[-1] + "_"  # the "_" is important due to current naming convention

        if results_tab is None:
            results_tab = self.findChild(QTabWidget, _name + 'results_tab')

        # Get the important widgets of the hys_results_tab
        choose_evalmodel = self.findChild(QComboBox, _name + 'choose_evalmodel')
        fit_report = results_tab.findChild(FitReportTable, _name + 'fit_report')
        results_report = results_tab.findChild(ResultsTable, _name + 'results_report')
        fit_points = results_tab.findChild(FitPointsTable, _name + 'fit_points_table')

        # Get the internal_id and the data package
        selected_widgets, selected_data = self.get_selection()
        _id, _pkg, _cols, _x = selected_data
        if _id is not None and _pkg:
            if _pkg == 'No data selected' or _pkg == 'raw_data':
                choose_evalmodel.clear()
                fit_report.clear()
                results_report.clear()
                fit_points.clear()
                return

            key = _pkg.split("_")[-1]
            results = getattr(data.dataset[_id], "results_" + key)

            # Check if the results are empty, i.e. {}
            if results == {}:
                choose_evalmodel.clear()
                fit_report.clear()
                results_report.clear()
                fit_points.clear()
                return

            else:
                choose_evalmodel.blockSignals(True)
                # Fill the QComboBox with the keys of the results
                old_evalmodel = choose_evalmodel.currentText()
                choose_evalmodel.clear()
                choose_evalmodel.addItems(list(results.keys()))
                if old_evalmodel in results.keys():
                    index = choose_evalmodel.findText(old_evalmodel)
                else:
                    index = 0
                choose_evalmodel.setCurrentIndex(index)
                # Calculate the width of the longest item
                longest_item = max(results.keys(), key=len)
                font_metrics = choose_evalmodel.fontMetrics()
                width = font_metrics.horizontalAdvance(longest_item) + 20  # Add some padding

                # Set the width of the QComboBox
                choose_evalmodel.setMinimumWidth(width)
                choose_evalmodel.blockSignals(False)
                
                # Process pending events
                QApplication.processEvents()

            if choose_evalmodel.currentText() in results.keys():
                # Fill the QTextEdit with the fit_report and results_report
                try:
                    fit_report.clear()
                    fit_report.fill_widget(results[choose_evalmodel.currentText()]["result"])
                except KeyError:
                    fit_report.clear()
                try:
                    results_report.clear()
                    results_report.fill_widget(results[choose_evalmodel.currentText()]["params"])
                except KeyError:
                    results_report.clear()

                try:
                    fit_points.clear()
                    fit_points.fill_widget(results[choose_evalmodel.currentText()]["fitted_data"])
                except KeyError:
                    fit_points.clear()
                    
                # Process pending events
                QApplication.processEvents()

    def update_fit_data(self, groupbox, evalmodel):
        # get the internal_id and the data package
        selected_widgets, selected_data = self.get_selection()
        _id, _pkg, _, _ = selected_data
        _, _, _, _, data_xdata = selected_widgets
        if _id is not None and _pkg:
            # Control the header of the (first) selected column
            if _pkg == 'No data selected':
                return
            
            # Create a new ExportTableWidget
            table_fitdata = ExportTableWidget()
            
            # get fitted data in data.dataset[_id].results[_pkg][evalmodel]
            fitted_data = getattr(data.dataset[_id].results[_pkg][evalmodel], 'fitted_data')
            
            # fill widget with fitted data
            horizontal_headers = list(fitted_data.keys())

            if isinstance(fitted_data, pd.DataFrame):
                # Set the number of rows and columns
                table_fitdata.setRowCount(fitted_data.shape[0] + 1)
                table_fitdata.setColumnCount(fitted_data.shape[1])

                # Add the attribute "unit" to the first row
                for j in range(fitted_data.shape[1]):
                    header = horizontal_headers[j]
                    # Attempt to get the unit attribute
                    if hasattr(fitted_data[header], 'unit'):
                        unit = getattr(fitted_data[header], 'unit', None)
                        quantity_type = None
                        for key in EvaluixConfig['conversion_factors']:
                            if str(unit) in EvaluixConfig['conversion_factors'][key].keys():
                                quantity_type = key
                                break
                        
                    else:
                        unit = 'unknown unit'  # or unit = ''
                        quantity_type = None
                    
                    # Create a QComboBox for unit selection
                    unit_selector = QComboBox()
                    if quantity_type is not None:
                        available_units = EvaluixConfig['conversion_factors'][quantity_type]
                    else:
                        available_units = [unit]
                    unit_selector.addItems(available_units)
                    unit_selector.setCurrentText(unit)
                    unit_selector.currentTextChanged.connect(lambda new_unit, header=header: 
                        self.update_unit(new_unit, 
                                         fitted_data,
                                         header, 
                                         table_fitdata,
                                         data_xdata))

                    table_fitdata.setCellWidget(0, j, unit_selector)

                for i in range(fitted_data.shape[0]):
                    for j in range(fitted_data.shape[1]):
                        # check if header is 'FlattendImage'
                        if horizontal_headers[j] == 'FlattendImage':
                            item = QTableWidgetItem("Image")
                        elif isinstance(fitted_data.iloc[i, j], (int, float)):
                            item = QTableWidgetItem("{:.5f}".format(fitted_data.iloc[i, j]))
                        else:
                            item = QTableWidgetItem(str(fitted_data.iloc[i, j]))
                        table_fitdata.setItem(i+1, j, item) # i is the row, j is the column

            # Set horizontal headers and update the table
            table_fitdata.setHorizontalHeaderLabels(horizontal_headers)
            
            vertical_headers = ['Unit'] + [str(i) for i in range(fitted_data.shape[0])]
            table_fitdata.setVerticalHeaderLabels(vertical_headers)

            if not table_fitdata.selectedIndexes() or len(table_fitdata.selectedIndexes()) != table_fitdata.rowCount() * len(set(index.column() for index in table_fitdata.selectedIndexes())):
                # Try to select column 1, if it exists
                if table_fitdata.columnCount() > 1:
                    table_fitdata.selectColumn(1)
                # If column 1 doesn't exist, select column 0
                elif table_fitdata.columnCount() > 0:
                    table_fitdata.selectColumn(0)

        else:
            # Add a dummy row of data to make the table visible and selectable
            table_fitdata.setRowCount(1)
            table_fitdata.setColumnCount(1)
            horizontal_headers = ['No data selected']
            item = QTableWidgetItem('Cols: 0, Rows: 0')
            table_fitdata.setItem(0, 0, item)
            # Set horizontal headers
            table_fitdata.setHorizontalHeaderLabels(horizontal_headers)

        # Update the table
        table_fitdata.resizeColumnsToContents()
        table_fitdata.update()

    def update_status_bar(self, message=None, saved_message=None):
        # Update the dataset label
        data_amount = len(data.dataset)
        self.status_dataset_label.setText(f"Loaded datasets: {data_amount} ")

        # show the Autosave_status from the ProfileConfig file
        if ProfileConfig['Autosave_status']:
            self.status_autosave_label.setText("Autosave: ON ")
            self.status_autosave_label.setStyleSheet("color: #ADD8E6")
            self.status_autosave_label.setToolTip(f"Autosave on filepath {self.save_path}")
            self.status_autosave_label.setToolTip(f""" <p style='background-color: rgb(45, 55, 65); border: 1px solid black;'>
        Autosave on filepath {self.save_path}
        </p>
        """)
        else:
            self.status_autosave_label.setText("Autosave: OFF ")
            self.status_autosave_label.setStyleSheet("color: yellow")
            self.status_autosave_label.setToolTip(f""" <p style='background-color: rgb(45, 55, 65); border: 1px solid black;'>
        Autosave is disabled
        </p>
        """)
            #self.status_autosave_label.setToolTip("Autosave is disabled")

        # Update the message label
        if message:
            message = message.strip() + " "
            self.status_message_label.setText(message)
        elif message == "" or message == 'clear':
            # If no message is provided, clear the message label
            self.message_label.clear()
        else:
            # If the message is None, do nothing beside checking if the current message "No data available. Please load a dataset."
            # is still valid which is not the case if data_amount > 0
            if self.status_message_label.text().strip() == "No data available. Please load a dataset." and data_amount > 0:
                self.status_message_label.clear()
            pass

        if saved_message:
            self.status_save_label.setText(saved_message)
        elif saved_message == "" or saved_message == 'clear':
            self.status_save_label.clear()
        else:
            pass

    def update_data_slider(self, data_slider):
        # get the internal_id and the data package
        selected_widgets, selected_data = self.get_selection()
        _id, _pkg, _cols, _x = selected_data
        if _id is not None and _pkg:
            if _pkg == 'No data selected':
                data_slider.clear()
                return

            else:
                _data = getattr(data.dataset[_id], _pkg)
                data_slider.setRange(0, _data.shape[0]-1)
                data_slider.setValue(0)

        else:
            data_slider.clear()
            data_slider.setRange(0, 0)
            data_slider.setValue(0)

    def update_sliderlabels(self):
        tab_main = self.findChild(QTabWidget, 'tab_main')

        # Get the currently open tab
        index = tab_main.currentIndex()
        tab_name = tab_main.widget(index).objectName()

        #identify tabs function by slicing the tab_name
        _name = tab_name.split("_")[-1] + "_" # the "_" is important due to current naming convention

        # get the field and img_nr labels
        field_label = self.findChild(QLabel, _name + 'field_label')
        img_nr_label = self.findChild(QLabel, _name + 'img_nr_label')

        # get the data_slider
        data_slider = self.findChild(JumpSlider, _name + 'data_slider')

        # put the values into the labels
        field_label.setText(f"Field: {data_slider.value()}")
        img_nr_label.setText(f"Image Nr: {data_slider.value()}")

    def slider_to_table(self, table, value):
        if not self.kerr_selection_changed:
            # Set the flag before changing the selection programmatically
            self.kerr_selection_changed = True
            table.selectRow(value)
            self.kerr_selection_changed = False

    def table_to_slider(self, slider, row):
        if not self.kerr_selection_changed:
            # Set the flag before changing the value programmatically
            self.kerr_selection_changed = True
            slider.setValue(row)
            self.kerr_selection_changed = False

    def update_all(self, type):
        tab_main = self.findChild(QTabWidget, 'tab_main')

        # Get the currently open tab
        index = tab_main.currentIndex()
        tab_name = tab_main.widget(index).objectName()

        #identify tabs function by slicing the tab_name
        _name = tab_name.split("_")[-1] + "_" # the "_" is important due to current naming convention
        # for example: hys_ or afm_
        
        # find the respective widgets
        table_metadata = self.findChild(DragDropTableWidget, _name + 'table_metadata')
        table_datapkg = self.findChild(QTableView, _name + 'table_datapkg')
        table_data = self.findChild(QTableView, _name + 'table_data')
        scroll_area = self.findChild(QScrollArea, _name + 'scroll_area')
        data_slider = self.findChild(JumpSlider, _name + 'data_slider')
        data_xdata = self.findChild(QComboBox, _name + 'data_xdata')
        loglist = self.findChild(QListWidget, _name + 'loglist')
        results_tab = self.findChild(QTabWidget, _name + 'results_tab')
        data_panel = self.findChild(QGroupBox, _name + 'data_panel')

        # block all connected signals to avoid update cascades
        # try/except because the following widgets are not always available
        try:
            table_metadata.blockSignals(True)
        except:
            pass
        try:
            table_datapkg.blockSignals(True)
        except:
            pass
        try:
            table_data.blockSignals(True)
        except:
            pass
        try:
            scroll_area.blockSignals(True)
        except:
            pass
        try:
            data_slider.blockSignals(True)
        except:
            pass
        try:
            data_xdata.blockSignals(True)
        except:
            pass

        # update the respective widgets
        #TODO: log the errors and find a better solution for this
        #print(f"An error occurred: {e}")
        try:
            self.update_table_metadata(table_metadata, type=type)
        except Exception as e:
            print(f"An error occurred: {e}." + traceback.format_exc())
            pass
        try:
            self.update_table_datapkg(table_datapkg)
        except Exception as e:
            pass
        try:
            self.update_table_data(table_data)
        except Exception as e:
            pass
        try:
            self.update_scroll_area(scroll_area) #TODO: implement this function
        except Exception as e:
            pass
        try:
            self.update_data_slider(data_slider)
        except Exception as e:
            pass
        try:
            self.update_choose_xdata(data_xdata)
        except Exception as e:
            pass
        try:
            self.update_loglist(loglist)
        except Exception as e:
            pass
        try:
            self.update_results_tab(results_tab)
        except Exception as e:
            pass
        try:
            if type == 'Hyst':
                self.visualize_selected_data(data_panel)
            elif type == 'AFM':
                self.visualize_selected_imgs(data_panel)
            elif type == 'Img':
                self.visualize_selected_imgs(data_panel)
        except Exception as e:
            pass
        try:
            self.update_status_bar(message=None)
        except Exception as e:
            pass

        # unblock all connected signals
        # try/except because the following widgets are not always available
        try:
            table_metadata.blockSignals(False)
        except:
            pass
        try:
            table_datapkg.blockSignals(False)
        except:
            pass
        try:
            table_data.blockSignals(False)
        except:
            pass
        try:
            scroll_area.blockSignals(False)
        except:
            pass
        try:
            data_slider.blockSignals(False)
        except:
            pass
        try:
            data_xdata.blockSignals(False)
        except:
            pass

    def update_all_tab(self):
        # This function just selects the correct type for the update_all function
        # depending on the current tab (order can be changed)

        index = self.tab_main.currentIndex()
        tab_name = self.tab_main.tabText(index)

        if tab_name == "Hysteresis":
            self.update_all(type='Hyst')
        elif tab_name == "AFM":
            self.update_all(type='Img')
        elif tab_name == "Kerr Microscope":
            self.update_all(type='Img')
        elif tab_name == "Datamanager":
            self.update_all(type='All')
        else:
            pass

    # 3. get, copy and delete selected data
    def get_selection(self):
        tab_main = self.findChild(QTabWidget, 'tab_main')

        # Get the currently open tab
        index = tab_main.currentIndex()
        tab_name = tab_main.widget(index).objectName()

        #identify tabs function by slicing the tab_name
        _name = tab_name.split("_")[-1] + "_" # the "_" is important due to current naming convention

        # find the respective widgets
        table_metadata = self.findChild(DragDropTableWidget, _name + 'table_metadata')
        table_datapkg = self.findChild(QTableView, _name + 'table_datapkg')
        table_data = self.findChild(QTableView, _name + 'table_data')
        data_xdata = self.findChild(QComboBox, _name + 'data_xdata')

        # initialize the variables and assign them to None
        _id = _pkg = _cols = _x = None

        # For each widget check if it exists and if it is selected. If this is correct, get the respective
        # selection and save it in the variable
        if table_metadata and table_metadata.selectionModel():
            internal_id = table_metadata.selectionModel().selectedRows()
            if internal_id:
                id_column = next((i for i in range(table_metadata.columnCount())
                                if table_metadata.horizontalHeaderItem(i).text() == 'ID'), None)
                if id_column is not None:
                    _id = int(table_metadata.item(internal_id[0].row(), id_column).text())

        if table_datapkg and table_datapkg.selectionModel():
            data_pkg = table_datapkg.selectionModel().selectedColumns()
            if data_pkg:
                _pkg = table_datapkg.model().headerData(data_pkg[0].column(), Qt.Orientation.Horizontal)

        if table_data and table_data.selectionModel():
            data_cols = table_data.selectionModel().selectedColumns()
            if data_cols:
                _cols = [table_data.model().headerData(col.column(), Qt.Orientation.Horizontal) for col in data_cols]

        if data_xdata and data_xdata.currentText() != "":
            _x = data_xdata.currentText()

        selected_widgets = tab_main, table_metadata, table_datapkg, table_data, data_xdata
        selected_data = _id, _pkg, _cols, _x

        return selected_widgets, selected_data

    def copy_dataset(self):
        # get the selected data
        selected_widgets, selected_data = self.get_selection()
        tab_main, table_metadata, table_datapkg, table_data, data_xdata = selected_widgets
        _id, _pkg, _cols, _x = selected_data

        # check if the selected data is valid
        if _id is not None and _pkg and _cols:
            if _id == 'No data selected':
                self.update_status_bar("No data selected. Please select a data package.")
                return
            else:
                # copy the data abd extend the dataset
                global data
                _metadata = copy.deepcopy(data.dataset[_id].metadata)
                _raw_data = deepcopy_with_unit(data.dataset[_id].raw_data)
                # _raw_data = copy.deepcopy(data.dataset[_id].raw_data)
                data.add_dataset(
                    Dataset(
                        _metadata,
                        _raw_data,
                    )
                )

                # get the new dataset (which has the highest internal_id)
                new_id = max(data.dataset.keys())

                used_keys = [int(key.split("_")[-1]) for key in data.dataset[_id].__dict__.keys() if "mod_data_" in key]
                # copy the mod_data_nr data packages
                for key in used_keys:
                    # check which mod_data_nr are available
                    if hasattr(data.dataset[_id], f'mod_data_{key}'):
                        # get the data
                        loglist = copy.deepcopy(getattr(data.dataset[_id], f'loglist_{key}'))
                        mod_data = deepcopy_with_unit(getattr(data.dataset[_id], f'mod_data_{key}'))
                        # mod_data = copy.deepcopy(getattr(data.dataset[_id], f'mod_data_{key}'))
                        results = copy.deepcopy(getattr(data.dataset[_id], f'results_{key}'))

                        # add that the data are copied to the loglist
                        timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                        loglist.append(f"copied with Evaluix2_v{EvaluixConfig['Version']} @ {timestamp}")

                        #add the data to the new dataset
                        data.dataset[new_id].add_log_mod_results(key = key, loglist = loglist, mod_data = mod_data, results = results)

                        # select the new dataset
                        self.update_table_metadata(table_metadata)
                        # find the column index for 'ID'
                        id_column = -1
                        for i in range(table_metadata.columnCount()):
                            if table_metadata.horizontalHeaderItem(i).text() == 'ID':
                                id_column = i
                                break

                        # if 'ID' column is found
                        if id_column != -1:
                            for i in range(table_metadata.rowCount()):
                                if table_metadata.item(i, id_column).text() == str(new_key):
                                    table_metadata.selectRow(i)
                                    selected_index = table_metadata.selectedIndexes()[0]
                                    table_metadata.scrollToItem(table_metadata.item(i, selected_index.column()))
                                    break

                # update the status bar
                self.update_status_bar(f"Copied dataset {_id} to dataset {new_id}")

            if ProfileConfig['Autosave_status']:
                self.save_progress(ids=[_id])

    def del_dataset(self):
        selected_widgets, selected_data = self.get_selection()
        _id, _pkg, _cols, _x = selected_data

        if _id is not None:
            if _id == 'No data selected':
                self.update_status_bar("No data selected. Please select a data package.")
                return
            else:
                reply = QMessageBox.question(self, 'Delete Dataset',
                             f"Are you sure you want to delete dataset {_id}?",
                             QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                             QMessageBox.StandardButton.No)
                if reply == QMessageBox.StandardButton.Yes:
                    # first, change selected dataset to not cause display problems
                    tab_main = self.findChild(QTabWidget, 'tab_main')

                    # Get the currently open tab
                    index = tab_main.currentIndex()
                    tab_name = tab_main.widget(index).objectName()

                    #identify tabs function by slicing the tab_name
                    _name = tab_name.split("_")[-1] + "_" # the "_" is important due to current naming convention
                    table_metadata = self.findChild(QTableView, _name + 'table_metadata')
                    table_metadata.selectRow(0)

                    self.update_status_bar(f"Deleted dataset {_id}")
                    data.del_dataset(_id)

            if ProfileConfig['Autosave_status']:
                self.save_progress()

    def copy_mod_data(self):
        selected_widgets, selected_data = self.get_selection()
        tab_main, table_metadata, table_datapkg, table_data, data_xdata = selected_widgets
        _id, _pkg, _cols, _x = selected_data

        if _id is not None and _pkg and _cols:
            if _pkg == 'No data selected':
                self.update_status_bar("No data selected. Please select a data package.")
                return

            elif "mod_data_" in _pkg:
                # extract the current mod_data_nr
                key = _pkg.split("_")[-1]
                loglist = copy.deepcopy(getattr(data.dataset[_id], "loglist_" + key))
                # mod_data = copy.deepcopy(getattr(data.dataset[_id], _pkg))
                mod_data = deepcopy_with_unit(getattr(data.dataset[_id], _pkg))
                results = copy.deepcopy(getattr(data.dataset[_id], "results_" + key))

            else:
                key = 0
                loglist = []
                # mod_data = copy.deepcopy(getattr(data.dataset[_id], _pkg))
                mod_data = deepcopy_with_unit(getattr(data.dataset[_id], _pkg))
                results = {}

            # add that the data are copied to the loglist
            timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")  # get the current timestamp
            loglist.append(f"copied with Evaluix2_v{EvaluixConfig['Version']} @ {timestamp}")

            # get the new mod_data_nr which should be the highest number + 1 or larger
            used_keys = [int(key.split("_")[-1]) for key in data.dataset[_id].__dict__.keys() if "mod_data_" in key]
            key = int(key)

            if len(used_keys) < 100:
                new_key = key + 1
                while new_key in used_keys and new_key < 100:
                    new_key += 1

                if new_key < 100:
                    # add the data to the new dataset
                    data.dataset[_id].add_log_mod_results(key = new_key, loglist = loglist, mod_data = mod_data, results = results)
                    self.update_status_bar(f"Copied data package {key} to data package {new_key} of dataset {_id}")

                    # select the new data package
                    self.update_table_datapkg(table_datapkg)
                    header = table_datapkg.horizontalHeader()
                    for i in range(header.count()):
                        if table_datapkg.horizontalHeaderItem(i).text() == 'mod_data_' + str(new_key):
                            table_datapkg.selectColumn(i)
                            selected_index = table_datapkg.selectedIndexes()[0]
                            table_datapkg.scrollToItem(table_datapkg.item(selected_index.row(), 0))
                            break

                    if ProfileConfig['Autosave_status']:
                        self.save_progress(ids=[_id])

                    return new_key

                elif key > 0:
                    new_key = key - 1
                    while new_key in used_keys and new_key > 0:
                        new_key -= 1

                    if new_key > 0:
                        # add the data to the new dataset
                        data.dataset[_id].add_log_mod_results(key = new_key, loglist = loglist, mod_data = mod_data, results = results)
                        self.update_status_bar(f"Copied data package {key} to data package {new_key} of dataset {_id}")

                        # select the new data package
                        self.update_table_datapkg(table_datapkg)
                        header = table_datapkg.horizontalHeader()
                        for i in range(header.count()):
                            if table_datapkg.horizontalHeaderItem(i).text() == 'mod_data_' + str(new_key):
                                table_datapkg.selectColumn(i)
                                selected_index = table_datapkg.selectedIndexes()[0]
                                table_datapkg.scrollToItem(table_datapkg.item(selected_index.row(), 0))
                                break

                        if ProfileConfig['Autosave_status']:
                            self.save_progress(ids=[_id])

                        return new_key

                else:
                    self.update_status_bar("No new data package could be created. Please delete some old data packages.")
                    return
            else:
                self.update_status_bar("No new data package could be created. Please delete some old data packages.")
                return

    def del_mod_data(self):
        selected_widgets, selected_data = self.get_selection()
        _id, _pkg, _cols, _x = selected_data

        if _id is not None and _pkg and _cols:
            if _pkg == 'No data selected':
                self.update_status_bar("No data selected. Please select a data package.")
                return
            elif _pkg == 'raw_data':
                self.update_status_bar("You can't delete the raw_data. Instead you have to delete the whole dataset.")
                return
            else:
                reply = QMessageBox.question(self, 'Delete Data Package',
                                         f"Are you sure you want to delete data package {_pkg} of dataset {_id}?",
                                         QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                                         QMessageBox.StandardButton.No)
                if reply == QMessageBox.StandardButton.Yes:
                    # first, change selected data package to "raw_data" as new displayed data package
                    tab_main = self.findChild(QTabWidget, 'tab_main')

                    # Get the currently open tab
                    index = tab_main.currentIndex()
                    tab_name = tab_main.widget(index).objectName()

                    #identify tabs function by slicing the tab_name
                    _name = tab_name.split("_")[-1] + "_" # the "_" is important due to current naming convention
                    table_datapkg = self.findChild(QTableView, _name + 'table_datapkg')
                    table_datapkg.selectColumn(0)

                    key = _pkg.split("_")[-1]
                    self.update_status_bar(f"Deleted data package {key} of dataset {_id}")
                    data.dataset[_id].del_log_mod_results(key)

            if ProfileConfig['Autosave_status']:
                self.save_progress(ids=[_id])

    def copy_datacol(self):
        selected_widgets, selected_data = self.get_selection()
        tab_main, table_metadata, table_datapkg, table_data, data_xdata = selected_widgets
        _id, _pkg, _cols, _x = selected_data

        if _id is not None and _pkg and _cols:
            if _pkg == 'No data selected':
                self.update_status_bar("No data selected. Please select a data package.")
                return

            # extract the data
            _data = getattr(data.dataset[_id], _pkg)
            if isinstance(_data, pd.DataFrame):
                # get the selected column
                # col = copy.deepcopy(_data[_cols[0]])
                col = deepcopy_with_unit(_data[_cols[0]])

                _data[_cols[0] + "_copy"] = col.copy()
                self.update_status_bar(f"Copied column {_cols[0]} to column {_cols[0] + '_copy'} of data package {_pkg} of dataset {_id}")

                # select the new data column
                self.update_table_data(table_data)
                header = table_data.horizontalHeader()
                for i in range(header.count()):
                    if table_data.horizontalHeaderItem(i).text() == _cols[0] + "_copy":
                        table_data.selectColumn(i)
                        selected_index = table_data.selectedIndexes()[0]
                        table_data.scrollToItem(table_data.item(selected_index.row(), 0))
                        break


            else:
                self.update_status_bar("No data selected. Please select a data package.")

            if ProfileConfig['Autosave_status']:
                self.save_progress(ids=[_id])

    def change_metadata(self, item):
        # get the current column
        column = item.column()

        # get the horizontal header of the current column
        metadata_key = self.dm_table_metadata.horizontalHeaderItem(column).text()
        
        if metadata_key == "ID":
            self.update_status_bar(message = "You can't change the ID of a dataset.")
            return

        # find the index of the "ID" column
        id_column = None
        for i in range(self.dm_table_metadata.columnCount()):
            if self.dm_table_metadata.horizontalHeaderItem(i).text() == "ID":
                id_column = i
                break

        # get the item in the "ID" column of the current row
        id_item = self.dm_table_metadata.item(item.row(), id_column) if id_column is not None else None

        # get the text of the id_item
        _id = int(id_item.text()) if id_item else None
        
        # get the current value and its type
        current_value = data.dataset[_id].metadata[metadata_key]
        current_type = type(current_value)

        # try to convert the new value to the current type
        try:
            new_value = current_type(item.text())
        except ValueError:
            raise ValueError(f"Cannot convert {item.text()} to {current_type}")
        
        self.update_status_bar(message = f"Changed metadata {metadata_key} of dataset {_id} to {new_value}")
        # set the new value
        data.dataset[_id].metadata[metadata_key] = new_value

    def add_data_manually(self):
        global data
        _id = data.id + 1
        while _id in data.dataset.keys():
            _id =+1
            if _id > 1000:
                raise ValueError("Too many datasets in the global dataset. No free ID available.")
        self.manual_data_input =  ManualDataDialog(self, _id)
        
        self.manual_data_input.applied.connect(
            lambda metadata, raw_data: 
            data.add_dataset(Dataset(
                metadata,
                pd.DataFrame(raw_data),
                )
            )
        )
        

    # Modify the keyPressEvent function to delete data packages with the delete key
    def keyPressEvent(self, event):
        if hasattr(event, 'key') and event.key() == Qt.Key.Key_Delete: # check if the key is actually a key and is the delete key

            self.map_groupbox_to_delbutton = {
                'hys_metadata_panel': self.hys_del_dataset,
                'hys_data_packages': self.hys_del_mod_data,
                'kerr_metadata_panel': self.kerr_del_dataset,
                'kerr_data_packages': self.kerr_del_mod_data,
                'afm_metadata_panel': self.afm_del_dataset,
                'afm_data_packages': self.afm_del_mod_data,
            }

            active_widget = QApplication.instance().focusWidget()
            while active_widget:
                if isinstance(active_widget, QGroupBox) and active_widget.objectName() in self.map_groupbox_to_delbutton:
                    button = self.map_groupbox_to_delbutton[active_widget.objectName()]
                    if hasattr(button, 'click'):
                        button.click()
                    break
                active_widget = active_widget.parent()

    # 4. Data visualization, manipulation and evaluation
    def visualize_selected_data(self, qgroupbox):
        # Get the selected dataset, data package, data columns and the chosen xdata
        selected_widgets, selected_data = self.get_selection()
        _id, _pkg, _cols, _x = selected_data

        # clear the canvas with the function clear() which is defined in the MyMplCanvas class
        qgroupbox.canvas.clear()

        if _id is not None and _pkg and _cols:
            try:
                _x_unitless = _x.split("[")[0].strip()
            except AttributeError:
                _x_unitless = _x
            plottable_data = [[_id, _pkg, _cols, _x_unitless]]

            # get the content of the table in which the preserved datasets are saved
            row_count = qgroupbox.table.rowCount()
            column_count = qgroupbox.table.columnCount()
            table_content = [[qgroupbox.table.item(i, j).text() for j in range(column_count)] for i in range(row_count)]

            # check if there are datasets in the table which have fitting data
            if table_content:
                for preserved_id, preserved_pkg in table_content:
                    preserved_id = int(preserved_id)
                    if not _id == preserved_id:#TODO: delete: and not _pkg == preserved_pkg:
                        if hasattr(data.dataset[preserved_id], preserved_pkg):
                            print(f"Preserved data package {preserved_pkg} of dataset {preserved_id} exists.")
                            # first check if the preserved data has a column fulfilling the requirement of _x
                            if not _x_unitless in getattr(data.dataset[preserved_id], preserved_pkg).columns and not _x_unitless == "Point number":
                                print(f"Preserved data package {preserved_pkg} of dataset {preserved_id} does not have a column {_x_unitless}.")
                                # if not, take next preserved dataset
                                continue

                            for col in getattr(data.dataset[preserved_id], preserved_pkg).columns:
                                print(f"Preserved data package {preserved_pkg} of dataset {preserved_id} has column {col}.")
                                if col in _cols:
                                    # preserve the data a second time to plot it in the same plot
                                    plottable_data.append([preserved_id, preserved_pkg, [col], _x_unitless])
                                    print([preserved_id, preserved_pkg, [col], _x_unitless])
                        else:
                            self.update_status_bar(f"Preserved data package {preserved_pkg} of dataset {preserved_id} does not exist.")

            x_min = x_max = y_min = y_max = None
            for _id, _pkg, _cols, _x_unitless in plottable_data:
                try:
                    try:
                        if _x_unitless == "Point number":
                            xdata = np.arange(len(getattr(data.dataset[_id], _pkg)[_cols[0]]))
                        else:
                            xdata = getattr(data.dataset[_id], _pkg)[_x_unitless]
                    except KeyError:
                        xdata = getattr(data.dataset[_id], _pkg).iloc[:, 0]

                    if x_min is None or np.min(xdata) < x_min:
                        x_min = np.min(xdata)
                    if x_max is None or np.max(xdata) > x_max:
                        x_max = np.max(xdata)

                    for col in _cols:
                        # Get the data
                        _data = getattr(data.dataset[_id], _pkg)[col]
                        # Check if qgroupbox.canvas.purpose is 'Hysteresis'
                        if qgroupbox.canvas.purpose == 'Hysteresis': # Try to plot both branches in slightly different colors given by qgroupbox.canvas prop_cycler
                            try:
                                # Get the current color from the color cycle
                                current_color = qgroupbox.canvas.axes._get_lines.get_next_color()
                                # Find the corresponding darker color from the color_list
                                darker_color = None
                                for color_pair in qgroupbox.canvas.color_list:
                                    if color_pair[0].lower() == current_color.lower():
                                        darker_color = color_pair[1]
                                        break
                            
                                # Plot the second branch with the corresponding darker color
                                if darker_color:
                                    # Slice xdata and ydata into two equally sized arrays, i.e. the branches
                                    xdata1, xdata2 = np.array_split(xdata, 2)
                                    _data1, _data2 = np.array_split(_data, 2)
                                    
                                    # Plot the first branch
                                    qgroupbox.canvas.axes.plot(xdata1, _data1, label=col, zorder=1, marker='o', markersize=4, lw=0.6, color=current_color)
                                    # Plot the second branch
                                    qgroupbox.canvas.axes.plot(xdata2, _data2, zorder=1, marker='o', markersize=4, lw=0.6, color=darker_color)
                                
                            except Exception as e:
                                print(f"An error occurred while plotting the hysteresis data: {e}")
                                # Just plot the whole hysteresis loop in one color
                                qgroupbox.canvas.axes.plot(xdata, _data, label=col, zorder=1, marker='o', markersize=4, lw=0.6)
                                    
                        else:
                            # Plot the data
                            qgroupbox.canvas.axes.plot(xdata, _data, label=col, zorder=1, marker='o', markersize=4, lw=0.6)
                        # Set the labels
                        try:
                            #first argument is the x-axis label, second argument is the y-axis label
                            qgroupbox.canvas.set_labels(xdata.name + " [" + xdata.unit + "]", _data.name + " [" + _data.unit + "]")
                        except AttributeError:
                            try:
                                qgroupbox.canvas.set_labels(_x, _data.name + " [" + _data.unit + "]")
                            except AttributeError:
                                qgroupbox.canvas.set_labels(_x, _data.name)

                        if y_min is None or np.min(_data) < y_min:
                            y_min = np.min(_data)
                        if y_max is None or np.max(_data) > y_max:
                            y_max = np.max(_data)
                except Exception as e:
                    self.update_status_bar(f"An error occurred while plotting the data: {e}")
                    print(f"An error occurred while plotting the data: {e}")

            # Automatically scale the view to the data
            if x_min is not None and x_max is not None and y_min is not None and y_max is not None:
                x_range = x_max - x_min
                y_range = y_max - y_min
                qgroupbox.canvas.axes.set_xlim([x_min - 0.05*x_range, x_max + 0.05*x_range])
                qgroupbox.canvas.axes.set_ylim([y_min - 0.05*y_range, y_max + 0.05*y_range])
            else:
                qgroupbox.canvas.axes.autoscale()

            qgroupbox.canvas.axes.legend()
            qgroupbox.canvas.figure.tight_layout()
            qgroupbox.canvas.draw()

    def preserve_current_datapgk(self, qgroupbox):
        # Get the selected dataset, data package, data columns and the chosen xdata
        selected_widgets, selected_data = self.get_selection()
        _id, _pkg, _cols, _x = selected_data

        # check if _id and _pkg are valid
        if _id is not None and _pkg and _cols:
            # get the QTableWidget (qgroupbox.table) which contains the plotted data and add the data to the table if it is not already there

            # get the HorizontalHeaderLabels
            horizontal_headers = [qgroupbox.table.horizontalHeaderItem(i).text() for i in range(qgroupbox.table.columnCount())]
            # check if table has been initialized
            if not horizontal_headers == ["Dataset id", "Data Package"]:
                horizontal_headers = ["Dataset id", "Data Package"]
                # set the horizontal headers
                qgroupbox.table.setColumnCount(len(horizontal_headers))
                qgroupbox.table.setHorizontalHeaderLabels(horizontal_headers)

            # get the number of rows and the content of the table
            row_count = qgroupbox.table.rowCount()
            column_count = qgroupbox.table.columnCount()
            table_content = [[qgroupbox.table.item(i, j).text() for j in range(column_count)] for i in range(row_count)]

            # check if the data is already in the table
            if [str(_id), _pkg] not in table_content:
                # add the data to the table
                qgroupbox.table.setRowCount(row_count + 1)
                for j, data in enumerate([_id, _pkg]):
                    item = QTableWidgetItem(str(data))
                    qgroupbox.table.setItem(row_count, j, item)
                qgroupbox.table.resizeColumnsToContents()
                qgroupbox.table.update()

            else:
                self.update_status_bar("The selected data package is already in the table.")

        else:
            self.update_status_bar("Invalid data selected. Please select a dataset and a data package.")

    def del_preserved_content(self, qgroupbox):
        # get the selection in the qgroupbox.table
        selection = qgroupbox.table.selectionModel().selectedRows()

        if selection:
            # get the row numbers in reverse order to not mess up the indices
            rows = sorted([index.row() for index in selection], reverse=True)

            # remove the rows from the table
            for row in rows:
                qgroupbox.table.removeRow(row)

            qgroupbox.table.resizeColumnsToContents()
            qgroupbox.table.update()

    def visualize_selected_imgs(self, qgroupbox):
        # Get the selected dataset, data package, data columns and the chosen xdata
        selected_widgets, selected_data = self.get_selection()
        tab_main, table_metadata, table_datapkg, table_data, data_xdata = selected_widgets
        _id, _pkg, _cols, _x = selected_data

        # # Check if the QGroupbox has a Qslider which is used to select the image to be plotted
        # data_slider = qgroupbox.findChild(JumpSlider, 'kerr_data_slider')
        # if data_slider is not None:
        #     # if yes, get the slider value which is the image number to be plotted
        #     try:
        #         img_nr = qgroupbox.kerr_data_slider.value()
        #     except:
        #         img_nr = 0

        # clear the canvas with the function clear() which is defined in the MyMplCanvas class
        qgroupbox.canvas.clear()
        
        if _id is not None and _pkg and table_data:
            
            # in this case, the rows of the table_data is important, not the cols
            # get the content of the table_data
            if table_data and table_data.selectionModel():
                data_rows = table_data.selectionModel().selectedRows()
            
            if data_rows:
                # get the row number of the selected data
                row = data_rows[0].row()
                # get the selected data
                flattendimg = np.asarray(getattr(data.dataset[_id], _pkg)['FlattendImage'][row])
                imgshape = getattr(data.dataset[_id], _pkg)['Shape (px)'][row]

                img = flattendimg.reshape(imgshape)
                qgroupbox.canvas.axes.imshow(img, cmap='gray')

                qgroupbox.canvas.figure.tight_layout()
                qgroupbox.canvas.draw()
            
            # # if img_nr is not defined, set it to 0
            # if not 'img_nr' in locals(): #TODO: work on this
            #     img_nr = 0

            # # for now as a test, just plot the first image of the dataset
            # flattendimg = np.asarray(getattr(data.dataset[_id], _pkg)['FlattendImage'][img_nr])
            # imgshape = getattr(data.dataset[_id], _pkg)['Shape (px)'][img_nr]

            # img = flattendimg.reshape(imgshape)
            # qgroupbox.canvas.axes.imshow(img, cmap='gray')

            # qgroupbox.canvas.figure.tight_layout()
            # qgroupbox.canvas.draw()

    def data_manipulation(self, func, add_xdata=False, **kwargs):
        # Get the selected dataset, data package, data columns and the chosen xdata
        selected_widgets, selected_data = self.get_selection()
        tab_main, table_metadata, table_datapkg, table_data, data_xdata = selected_widgets
        _id, _pkg, _cols, _x = selected_data

        if _id is not None and _pkg and _cols:

            # first check if new data has to be created due to
            # 1. the data package is "raw_data"
            # 2. the data package is not "raw_data" but the new_data_flag is checked
            # 3. the function requires another data length than the original data (currently only num_derivative)
            tab_main = self.findChild(QTabWidget, 'tab_main')
            # Get the currently open tab
            index = tab_main.currentIndex()
            tab_name = tab_main.widget(index).objectName()
            #identify tabs function by slicing the tab_name
            _name = tab_name.split("_")[-1] + "_" # the "_" is important due to current naming convention
            new_data_flag = self.findChild(QCheckBox, _name + 'new_data_flag')

            # functions which require a new data package
            funcs_with_new_data = [num_derivative, num_integral]

            if _pkg == "raw_data" or new_data_flag.isChecked() or func in funcs_with_new_data:
                key = self.copy_mod_data()
                _pkg = f"mod_data_{key}"

            for col in _cols:
                # Get the data
                _data = getattr(data.dataset[_id], _pkg)[col]
                
                # save the unit if there is any
                unit = _data.unit if hasattr(_data, 'unit') else None
                print(f"unit: {unit}")

                # extract the keyword arguments and their values from the yaml file as input for the function
                kwargs_yaml = copy.deepcopy(ProfileConfig['function_info'][func.__name__])

                # check if additional keyword arguments are given and
                if kwargs:
                    for key, value in kwargs.items():
                        kwargs_yaml[key]['value'] = value

                # create a dictionary with the keyword arguments and their default/input values
                type_mapping = {
                    'int': int,
                    'float': float,
                    'str': str,
                    'list': list,
                    'dict': dict,
                    'Union': Union,
                    'pd.DataFrame': pd.DataFrame,
                    'pd.Series': pd.Series,
                    'np.ndarray': np.ndarray,
                    'bool': str_to_bool,
                    # Add more types here if necessary
                }

                kwargs = {} # new dictionary to store the keyword arguments and their values
                for key in kwargs_yaml.keys():
                    if key in ['xdata', 'ydata', 'Category']:
                        continue

                    expected_type_str = ProfileConfig['function_info'][func.__name__][key]['type']
                    value = ProfileConfig['function_info'][func.__name__][key]['value']
                    if 'Union' in expected_type_str:
                        expected_types = [type_mapping[type_str.strip()] for type_str in expected_type_str.replace('Union[', '').replace(']', '').split(',')]
                        for expected_type in expected_types:
                            try:
                                kwargs[key] = expected_type(value)
                                break
                            except ValueError:
                                continue
                        else:
                            self.update_status_bar(f"The input {value} is not valid for the type {expected_type_str}.")
                            return
                    else:
                        expected_type = type_mapping[expected_type_str]
                        try:
                            kwargs[key] = expected_type(value)
                        except ValueError:
                            self.update_status_bar(f"The input {value} is not valid for the type {expected_type_str}.")
                            return


                # kwargs = {key: type_mapping[kwargs_yaml[key]['type']](kwargs_yaml[key]['default']) for key in kwargs_yaml.keys()}

                if add_xdata:
                    # Get the x-data
                    if _x == "Point number":
                        xdata = np.arange(len(_data))
                    else:
                        try:
                            xdata = getattr(data.dataset[_id], _pkg)[_x]
                        except KeyError:
                            xdata = getattr(data.dataset[_id], _pkg).iloc[:, 0]
                            self.update_status_bar("KeyError: The selected xdata is not in the data package. Using the first column instead.")

                    unitx = xdata.unit if hasattr(xdata, 'unit') else None
                    
                    # Manipulate the data
                    new_data = func(xdata, _data, **kwargs)
                else:
                    new_data = func(_data, **kwargs)

                if func in funcs_with_new_data:
                    # Create a dictionary where the keys are the column names and the values are the columns of new_data
                    data_dict = {f"{col}_{i}": new_data[i] for i in range(len(new_data))}
                    # Overwrite the data
                    try:
                        key = _pkg.split("_")[-1]
                        data.dataset[_id].update_mod_data(key = key, new_data = data_dict)
                        for col_key in data_dict.keys():
                            if col_endswith('_0'):
                                getattr(data.dataset[_id], _pkg)[col_key].unit = unitx
                            else:
                                getattr(data.dataset[_id], _pkg)[col_key].unit = unit
                        print('new data set')
                    except Exception as e:
                        print(f"An error occurred while setting the new data: {e}")
                        
                else:
                    # Overwrite the data
                    try:
                        if len(new_data) == 2 and add_xdata:
                            getattr(data.dataset[_id], _pkg)[col] = new_data[1]
                            getattr(data.dataset[_id], _pkg)[_x] = new_data[0] if not _x == "Point number" else np.arange(len(new_data[1]))
                            if unit:
                                if hasattr(getattr(data.dataset[_id], _pkg)[col], 'unit'):
                                    getattr(data.dataset[_id], _pkg)[col].unit = unit
                                if hasattr(getattr(data.dataset[_id], _pkg)[_x], 'unit'):
                                    getattr(data.dataset[_id], _pkg)[_x].unit = unitx
                        else:
                            getattr(data.dataset[_id], _pkg)[col] = new_data
                            if unit and hasattr(getattr(data.dataset[_id], _pkg)[col], 'unit'):
                                getattr(data.dataset[_id], _pkg)[col].unit = unit

                        print('new data set')
                    except Exception as e:
                        print('failed to set new data')
                        print(f"An error occurred while setting the new data: {e}")
                    

                # update the loglist with the function name and the arguments
                log_func = f"data manipulation via {func.__name__} at {col}"
                log_xdata = f" with xdata = {_x}" if add_xdata else ""
                if kwargs:
                    log_kwargs = ' with kwargs: ' + ', '.join(f"{k}={v}" for k, v in kwargs.items())
                else:
                    log_kwargs = ""

                logmessage = log_func + log_xdata + log_kwargs

                loglist = getattr(data.dataset[_id], "loglist_" + _pkg.split("_")[-1])
                loglist.append(logmessage)

            if ProfileConfig['Autosave_status']:
                self.save_progress(ids=[_id])

    def data_evaluation(self, func, add_xdata=False, **kwargs):
        #TODO: how to proceed with the results? They should be stored within the
        # results of the data package which is a dictionary. Name of the result should be the column name + the function name + fit if it is a fit.

        selected_widgets, selected_data = self.get_selection()
        _id, _pkg, _cols, _x = selected_data

        if _id is not None and _pkg and _cols:
            # first check if new data has to be created due to _pkg being "raw_data"
            if _pkg == "raw_data":
                key = self.copy_mod_data()
                _pkg = f"mod_data_{key}"

            for col in _cols:
                # Get the data
                _data = getattr(data.dataset[_id], _pkg)[col]

                # extract the keyword arguments and their values from the yaml file as input for the function
                kwargs_yaml = copy.deepcopy(ProfileConfig['function_info'][func.__name__])

                # check if additional keyword arguments are given and
                if kwargs:
                    for key, value in kwargs.items():
                        kwargs_yaml[key]['value'] = value

                # create a dictionary with the keyword arguments and their default/input values
                type_mapping = {
                    'int': int,
                    'float': float,
                    'str': str,
                    'list': list,
                    'dict': dict,
                    'Union': Union,
                    'pd.DataFrame': pd.DataFrame,
                    'pd.Series': pd.Series,
                    'np.ndarray': np.ndarray,
                    'bool': str_to_bool,
                    # Add more types here if necessary
                }

                kwargs = {} # new dictionary to store the keyword arguments and their values
                for key in kwargs_yaml.keys():
                    if key in ['xdata', 'ydata', 'Category']:
                        continue

                    expected_type_str = ProfileConfig['function_info'][func.__name__][key]['type']
                    value = ProfileConfig['function_info'][func.__name__][key]['value']
                    if 'Union' in expected_type_str:
                        expected_types = [type_mapping[type_str.strip()] for type_str in expected_type_str.replace('Union[', '').replace(']', '').split(',')]
                        for expected_type in expected_types:
                            try:
                                kwargs[key] = expected_type(value)
                                break
                            except ValueError:
                                continue
                        else:
                            self.update_status_bar(f"The input {value} is not valid for the type {expected_type_str}.")
                            return
                    else:
                        expected_type = type_mapping[expected_type_str]
                        try:
                            kwargs[key] = expected_type(value)
                        except ValueError:
                            self.update_status_bar(f"The input {value} is not valid for the type {expected_type_str}.")
                            return

                if add_xdata:
                    # Get the x-data
                    if _x == "Point number":
                        xdata = np.arange(len(_data))
                    else:
                        try:
                            xdata = getattr(data.dataset[_id], _pkg)[_x]
                        except KeyError:
                            xdata = getattr(data.dataset[_id], _pkg).iloc[:, 0]
                            self.update_status_bar("KeyError: The selected xdata is not in the data package. Using the first column instead.")

                    # Evaluate the data
                    eval_result = func(xdata, _data, **kwargs)
                else:
                    eval_result = func(_data, **kwargs)

                if len(eval_result) == 3:
                    fitted_data, params, result = eval_result
                    result_dict = {
                        'function': func.__name__,
                        'fitted_data': fitted_data,
                        'params': params, 
                        'result': result}
                elif len(eval_result) == 2:
                    fitted_data, result = eval_result
                    result_dict = {
                        'function': func.__name__,
                        'fitted_data': fitted_data,
                        'result': result}
                else:
                    params = eval_result
                    result_dict = {
                        'function': func.__name__,
                        'params': params}
                    
                # update the loglist with the function name and the arguments
                log_func = f"data evaluation via {func.__name__} at {col} "
                log_xdata = f" with xdata = {_x}" if add_xdata else ""
                if kwargs:
                    log_kwargs = 'with kwargs: ' + ', '.join(f"{k}={v}" for k, v in kwargs.items())
                else:
                    log_kwargs = ""

                logmessage = log_func + log_xdata + log_kwargs

                loglist = getattr(data.dataset[_id], "loglist_" + _pkg.split("_")[-1])
                loglist_pos = len(loglist)
                loglist.append(logmessage)
                result_dict['loglist_pos'] = loglist_pos

                results = getattr(data.dataset[_id], f"results_{_pkg.split('_')[-1]}")
                results_key = f'{func.__name__}' + f'_at_log_{loglist_pos}'
                results[results_key] = result_dict

            if ProfileConfig['Autosave_status']:
                self.save_progress(ids=[_id])

        self.update_status_bar(f"Data evaluation via {func.__name__} completed.")
        self.update_results_tab()

    # 5. Info, settings and configuration
    def info_settings(self, func, profile=False):
        if not profile:
            # Create a new instance of InfoSettingsDialog
            self.dialog = InfoSettingsDialog()
            self.dialog.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)

            ############ Info Tab ############
            # Find the QLabel in the Info tab
            self.info_funclabel = self.dialog.findChild(QLabel, 'info_funclabel')
            # Set the text of the QLabel to the function name
            func_name = f"Function name: {func.__name__}"
            self.info_funclabel.setText(func_name)
            # Find the QTextEdit in the Info tab
            self.info_text = self.dialog.findChild(QTextEdit, 'info_text')
            # Set the text of the QLabel to the docstring
            docstring = inspect.getdoc(func)
            self.info_text.setPlainText(docstring)

            ############ Settings Tab ############
            # Find the QLabel in the Settings tab
            self.settings_funclabel = self.dialog.findChild(QLabel, 'settings_funclabel')
            # Set the text of the QLabel to the function name
            self.settings_funclabel.setText(func_name)
            # Find the QTableWidget in the Settings tab
            self.settings_table = self.dialog.findChild(QTableWidget, 'settings_table')
            # Get the signature of the function
            parameters = {name: [param['type'], param['value'], param['default']] for name, param in ProfileConfig['function_info'][func.__name__].items() if name != 'Category'}
            parameters['Category'] = ['str', 'This settles the GUI logic.\nDo not change.', ProfileConfig['function_info'][func.__name__]['Category']]
            # Set the number of rows in the table to the number of parameters
            self.settings_table.setRowCount(len(parameters))
            # Set the number of columns in the table to 3 (for name, type, value and default value)
            self.settings_table.setColumnCount(4)
            # Set the headers of the table
            self.settings_table.setHorizontalHeaderLabels(['Name', 'Type', 'Value', 'Default'])
            # Populate the table with the parameters
            for i, (name, param) in enumerate(parameters.items()):
                # Create QTableWidgetItem objects for the name, type, and value
                name_item = QTableWidgetItem(name)
                if name_item.text() in ['xdata', 'ydata']:
                    type_item = QTableWidgetItem('pd.DataFrame')
                    value_item = QTableWidgetItem('Select in data table')
                    default_item = QTableWidgetItem('-')
                else:
                    type_item = QTableWidgetItem(str(param[0]))
                    value_item = QTableWidgetItem(str(param[1]) if param[1] != '-' else 'No value given')
                    default_item = QTableWidgetItem(str(param[2]) if param[2] != '-' else 'No default')

                # Make the name, type, and default items not editable
                name_item.setFlags(name_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
                type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
                default_item.setFlags(default_item.flags() & ~Qt.ItemFlag.ItemIsEditable)

                # Add the items to the table
                self.settings_table.setItem(i, 0, name_item)
                self.settings_table.setItem(i, 1, type_item)
                self.settings_table.setItem(i, 2, value_item)
                self.settings_table.setItem(i, 3, default_item)

            # Resize the columns to fit their contents
            self.settings_table.resizeColumnsToContents()

            # connect the settings_table to the update_yaml function
            self.settings_table.itemChanged.connect(lambda item: self.safe_execute(lambda: self.update_yaml(item, func=func)))

            ############ Display InfoSett ############
            self.dialog.show()
            
        else:
            # func is a string, extract the function object from the function name using getattr
            func_obj = getattr(sys.modules[__name__], func)
            
            # No new dialog or Labels needed, just filling the docstring and the settings table
            # Find the QTextEdit in the Info tab and fill it with the docstring
            self.info_text = self.dialog.findChild(QTextEdit, 'info_text')
            docstring = inspect.getdoc(func_obj)
            self.info_text.setPlainText(docstring)
            
            # Find the QTableWidget in the Settings tab and fill it with the ProfileConfig params
            self.funcsettings_table = self.dialog.findChild(QTableWidget, 'funcsettings_table')
            # Get the signature of the function in the ProfileConfig
            parameters = {name: [param['type'], param['value'], param['default']] for name, param in ProfileConfig['function_info'][func].items() if name != 'Category'}
            parameters['Category'] = ['str', 'This settles the GUI logic.\nDo not change.', ProfileConfig['function_info'][func]['Category']]
            # Set the number of rows in the table to the number of parameters
            self.funcsettings_table.setRowCount(len(parameters))
            # Set the number of columns in the table to 4 (for name, type, value and default value)
            self.funcsettings_table.setColumnCount(4)
            # Set the headers of the table
            self.funcsettings_table.setHorizontalHeaderLabels(['Name', 'Type', 'Value', 'Default'])
            
            #Block signals during initial population of the table
            self.funcsettings_table.blockSignals(True)
            
            # Populate the table with the parameters
            for i, (name, param) in enumerate(parameters.items()):
                # Create QTableWidgetItem objects for the name, type, and value
                name_item = QTableWidgetItem(name)
                if name_item.text() in ['xdata', 'ydata']:
                    type_item = QTableWidgetItem('pd.DataFrame')
                    value_item = QTableWidgetItem('Select in data table')
                    default_item = QTableWidgetItem('-')
                else:
                    type_item = QTableWidgetItem(str(param[0]))
                    value_item = QTableWidgetItem(str(param[1]) if param[1] != '-' else 'No value given')
                    default_item = QTableWidgetItem(str(param[2]) if param[2] != '-' else 'No default')

                # Make the name, type, and default items not editable
                name_item.setFlags(name_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
                type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
                default_item.setFlags(default_item.flags() & ~Qt.ItemFlag.ItemIsEditable)

                # Add the items to the table
                self.funcsettings_table.setItem(i, 0, name_item)
                self.funcsettings_table.setItem(i, 1, type_item)
                self.funcsettings_table.setItem(i, 2, value_item)
                self.funcsettings_table.setItem(i, 3, default_item)
            
            # Resize the columns to fit their contents
            self.funcsettings_table.resizeColumnsToContents()
            
            # Unblock signals after initial population of the table
            self.funcsettings_table.blockSignals(False)
            
            # Disconnect any existing connections to the itemChanged signal
            try:
                self.funcsettings_table.itemChanged.disconnect()
            except TypeError:
                pass  # No existing connections to disconnect

            # connect the funcsettings_table to the update_yaml function
            self.funcsettings_table.itemChanged.connect(lambda item: self.safe_execute(lambda: self.update_yaml(item, func=func_obj, table=self.funcsettings_table)))
            
        
    def profile_macros(self):
        self.dialog = ProfileMacrosDialog()
        self.dialog.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
        
        ############ Load profile ############
        # Find the Label current_profile_label
        self.current_profile_label = self.dialog.findChild(QLabel, 'current_profile_label')
        # Set the text of the QLabel to the current profile file in EvaluixConfig
        self.current_profile_label.setText('Current profile: ' + EvaluixConfig['ProfileConfig'])
        # Find the Button btn_load_profile
        self.btn_load_profile = self.dialog.findChild(QPushButton, 'btn_load_profile')
        # Connect the Button to the load_profile function
        self.btn_load_profile.clicked.connect(self.load_profile) #TODO: implement load_profile function
        
        ############ General settings ############
        # Display all information of ProfileConfig in a QTableWidget if the key is not "function_info" or "Macro"
        # Find the QTableWidget in the General settings tab
        self.general_settings_table = self.dialog.findChild(QTableWidget, 'general_settings_table')
        
        # Extract the general settings from ProfileConfig
        general_settings = {key: value for key, value in ProfileConfig.items() if key not in ['function_info', 'Macros']}
        # Set the number of rows in the table to the number of general settings
        self.general_settings_table.setRowCount(len(general_settings))
        # Set the number of columns in the table to 2 (for key and value)
        self.general_settings_table.setColumnCount(2)
        # Set the headers of the table
        self.general_settings_table.setHorizontalHeaderLabels(['Key', 'Value'])
        # Populate the table with the general settings
        for i, (key, value) in enumerate(general_settings.items()):
            # Create QTableWidgetItem objects for the key and value
            key_item = QTableWidgetItem(key)
            value_item = QTableWidgetItem(str(value))
            # Make the key item not editable
            key_item.setFlags(key_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
            # Add the items to the table
            self.general_settings_table.setItem(i, 0, key_item)
            self.general_settings_table.setItem(i, 1, value_item)

        # Resize the columns to fit their contents
        self.general_settings_table.resizeColumnsToContents()

        ############ Function info ############
        # This is basically a copy of the tabs of the InfoSettingsDialog but instead of a single function, the function can be chosen from a dropdown menu
        # Find the QComboBox in the Function info tab
        self.functions_comboBox = self.dialog.findChild(QComboBox, 'functions_comboBox')

        # Connect a selection change to the info_settings function
        self.functions_comboBox.currentTextChanged.connect(lambda: self.safe_execute(lambda: self.info_settings(self.functions_comboBox.currentText(), profile=True)))

        if self.functions_comboBox is None:
            raise ValueError("functions_comboBox not found in the dialog")

        # Populate the combobox with the function names in ProfileConfig['function_info']
        self.functions_comboBox.addItems(ProfileConfig['function_info'].keys())

        # Select the function "tan_hyseval" by default, or the first function if it is not available
        if 'tan_hyseval' in ProfileConfig['function_info']:
            self.functions_comboBox.setCurrentText('tan_hyseval')
        else:
            self.functions_comboBox.setCurrentText(list(ProfileConfig['function_info'].keys())[0])

        ############ Macros ############
        # TODO: Implement the Macros tab
        
        
        # Connect the general_settings_table to the update_yaml function
        self.general_settings_table.itemChanged.connect(lambda item: self.safe_execute(lambda: self.update_yaml(item, yaml_file=EvaluixConfig['ProfileConfig'], table=self.general_settings_table)))
        
        self.dialog.show()
        
    def load_profile(self):
        # Load the profile from the file dialog
        filename, _ = QFileDialog.getOpenFileName(self, "Load Profile", "", "YAML Files (*.yaml)")
        if filename:
            # Update EvaluixConfig with the new profile file
            EvaluixConfig['ProfileConfig'] = filename
            # Save the updated EvaluixConfig to the config file
            with open(root / 'utils'  / 'EvaluixConfig.yaml', 'w') as file:
                yaml.dump(EvaluixConfig, file)
                
            # Load the new profile file
            ProfileConfig = load_config(filename)
            self.update_status_bar(f"Profile {filename} loaded.")
            self.current_profile_label.setText('Current profile: ' + filename)
            
    def convert_value(self, value, expected_type):
        try:
            return expected_type(value)
        except ValueError:
            return None

    def update_yaml(self, item, func=None, key=None, yaml_file=EvaluixConfig['ProfileConfig'], table=None):
        if func:
            # Get the row and column of the changed item
            row = item.row()
            column = item.column()

            # Only update the YAML file if the 'Value' column was changed
            if column == 2:
                # Get the parameter name and new value
                if not table:
                    table = self.settings_table
                
                name_item = table.item(row, 0)
                value_item = table.item(row, 2)
                name = name_item.text()
                value = value_item.text()

                if name_item.text() in ['xdata', 'ydata', 'Category']:
                    self.update_status_bar(f"The parameter {name_item.text()} is not allowed to be changed in the YAML file.")

                    return

                # check if input is valid compared to the type of the parameter
                type_mapping = {
                    'int': int,
                    'float': float,
                    'str': str,
                    'list': list,
                    'dict': dict,
                    'Union': Union,
                    'pd.DataFrame': pd.DataFrame,
                    'pd.Series': pd.Series,
                    'np.ndarray': np.ndarray,
                    'bool': str_to_bool,
                    # Add more types here if necessary
                }

                expected_type_str = ProfileConfig['function_info'][func.__name__][name]['type']
                converted_value = None
                if 'Union' in expected_type_str:
                    expected_types = [type_mapping[type_str.strip()] for type_str in expected_type_str.replace('Union[', '').replace(']', '').split(',')]
                    for _expected_type in expected_types:
                        _value = self.convert_value(value, _expected_type)
                        if _value is not None:
                            converted_value = _value
                            break

                else:
                    expected_type = type_mapping.get(expected_type_str)
                    if expected_type is None:
                        self.update_status_bar(f"Unknown type {expected_type_str} for parameter {name}.")
                        return
                    _value = self.convert_value(value, expected_type)
                    if _value is None:
                        self.update_status_bar(f"The input {value} is not valid for the type {expected_type_str}.")

                    converted_value = _value

                # update the value in the table without emitting the signal (otherwise it would call this function again and be trapped in a loop)
                table.blockSignals(True) # Block signals
                if converted_value is None:
                    table.item(row, 2).setText(ProfileConfig['function_info'][func.__name__][name]['value']) # Reset the item
                    table.blockSignals(False) # Unblock signals
                    return
                else:
                    table.item(row, 2).setText(str(converted_value)) # Update the item
                    table.blockSignals(False) # Unblock signals

                # Update the value in the ProfileConfig dictionary
                ProfileConfig['function_info'][func.__name__][name]['value'] = str(value)
                print(f"Updated {name} to {value} in the ProfileConfig dictionary.")

                # Write the updated ProfileConfig dictionary back to the YAML file
                with open(yaml_file, 'w') as file:
                    # Check if file is available
                    if file:
                        print(f"File {yaml_file} found.")
                    yaml.dump(ProfileConfig, file)

        elif key:
            # in this case the item is directly the entry of the ProfileConfig dictionary
            # for example to change the Autosave_status
            ProfileConfig[key] = item

            with open(yaml_file, 'w') as file:
                yaml.dump(ProfileConfig, file)
        else:
            self.update_status_bar("To update the yaml file, either a key or a func has to be given.")
            return

    def toggle_autosave_status(self):
        ProfileConfig['Autosave_status'] = not ProfileConfig['Autosave_status']

        if ProfileConfig['Autosave_status']:
            if not self.save_path:
                self.save_path = self.open_file_dialog(intention='save', filter='HDF5 files (*.h5)')
                if not self.save_path:
                    self.update_status_bar(saved_message = "Autosave is set to True but no save path is selected. Please select a save path.")
                    return
                else:
                    self.save_progress()
            else:
                self.save_progress()

        self.update_yaml(ProfileConfig['Autosave_status'], key='Autosave_status')
        self.update_status_bar(message=None)

    def save_progress(self, save_path=None, ids=None):
        # Save the progress of the selected datasets to an HDF5 file
        # First check if a valid save path is set. TODO: Implement a "Save As" dialog to change the save path
        
        # This first "if not" checks whether a save path is given as an argument. If not, it checks whether the autosave is enabled and a save path is set.
        if not save_path:
            save_path = self.get_save_path()
            
        # This second "if not" checks whether a save path is now set. If not, it returns and does not save the progress.
        if not save_path:
            self.update_status_bar(saved_message="No progress saved. Please select a valid file.")
            return
        
        self.save_path = save_path # Set the save path (for autosave) to the selected path

        with h5py.File(save_path, 'a') as file:
            self.remove_deleted_datasets(file) # Remove datasets that are not in the global data object
            ids = ids or data.dataset.keys() # Save all datasets if no ids are given

            for i, dataset in enumerate(data): # Iterate over each dataset in the global data object
                if i in list(ids): # Save only the selected datasets (set to all above if no ids are given)
                    self.save_dataset(file, i, dataset) # Save the dataset to the HDF5 file

        self.update_status_bar(saved_message=f"Progress saved to {save_path} at {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}.")
        self.update_autosave_path(save_path)

    def get_save_path(self):
        # Check if autosave is enabled and a save path is set
        if ProfileConfig['Autosave_status'] and self.save_path:
            return self.save_path
        # Otherwise, open a file dialog to select a save path
        return self.open_file_dialog(intention='save', filter='HDF5 files (*.h5)')

    def remove_deleted_datasets(self, file):
        # Quickly compare if there are any datasets in the file which are not in the global data object and delete them
        # TODO: Think about warning the user that some datasets are not in the global data object and will be deleted before proceeding
        for key in file.keys():
            if int(key.split('_')[-1]) not in data.dataset.keys():
                del file[key]

    def save_dataset(self, file, i, dataset):
        try:
            # Check if the group already exists and delete it
            if f'dataset_{i}' in file:
                del file[f'dataset_{i}']

            # Create a new group for each dataset
            group = file.create_group(f'dataset_{i}')
            # Every attribute of the dataset is saved as an attribute in the group
            for key, value in dataset.items():
                subgroup = self.get_or_create_subgroup(group, key) # Create subgroups for metadata, raw_data and mod_data_n. In the later subgroup, loglist_n and results_n are also saved within mod_data_n.
                self.save_value(subgroup, key, value)
        except Exception as e:
            self.update_status_bar(f"An error occurred while saving the dataset {i}: {e}")

    def get_or_create_subgroup(self, group, key):
        # Check if the current attribute/key is part of the mod_data_n, loglist_n, or results_n data packages
        # These are saved together in the mod_data_n group
        if key.startswith(('mod_data', 'loglist', 'results')):
            n = key.split('_')[-1]
            # create a new mod_data_n group if it doesn't exist
            if f'data_pkg_{n}' not in group:
                return group.create_group(f'data_pkg_{n}')
            # else, choose the existing mod_data_n group
            return group[f'data_pkg_{n}']
        # for metadata, raw_data, and possibly other attributes, create a new subgroup
        return group.create_group(key)

    def save_value(self, subgroup, key, value):
        # This function basically maps the correct hdf5 saving method to the type of the value
        if isinstance(value, pd.DataFrame):
            self.save_dataframe(subgroup, key, value) # For raw_data and mod_data_n
        elif isinstance(value, dict):
            self.save_dict(subgroup, key, value) # For metadata and results_n
        elif isinstance(value, list):
            self.save_list(subgroup, key, value) # For loglist_n
        else:
            subgroup.attrs[key] = value # For all other attributes

    def save_dataframe(self, subgroup, key, value):
        # Save pandas DataFrames as HDF5 datasets in the group. This currently only works for pd.DataFrame objects in which numerically typed columns are stored. TODO: Implement a way to save other types of DataFrames (Kerr images).
        h5dataset = subgroup.create_dataset(key, data=value.to_numpy())
        h5dataset.attrs['columns'] = value.columns.tolist()

    def save_dict(self, subgroup, key, value):
        # There is no default implementation for saving dictionaries in HDF5 files.
        # Therefore, we have to decide how to save the dictionary based on its content/intention.
        if key == 'metadata':
            self.save_metadata(subgroup, value)
        elif 'results' in key: # results_n
            self.save_results(subgroup, key, value)

    def save_metadata(self, subgroup, value):
        # Save dictionaries as groups with attributes in the group
        for k, v in value.items():
            try:
                # If the value is a path and contains backslashes, replace them with forward slashes as backslashes are not allowed in HDF5 attributes
                if isinstance(v, pathlib.Path):
                    v = str(v).replace('\\', '/')
                subgroup.attrs[k] = v
            except TypeError:
                print(f"Could not save {k} with value {v} as attribute in the group metadata.")

    def save_results(self, subgroup, key, value):
        # Save results_n dictionaries as groups with attributes in the group
        # create a subgroup for the main key as every key (fit, analysis method) in the results_n dictionary is a subgroup
        results_subgroup = subgroup.create_group(key)
        for k, v in value.items():
            results_subsubgroup = results_subgroup.create_group(k)
            # Save the results as attributes in the subgroup
            if isinstance(v, dict):
                for kk, vv in v.items():
                    results_subsubgroup.attrs[kk] = vv
            else:
                print(f"Value {v} is not a dictionary. Could not save it as dataset in the group {key}.")

    def save_list(self, subgroup, key, value):
        # Save lists as HDF5 datasets in the group
        subgroup.create_dataset(key, data=value)

    def update_autosave_path(self, save_path):
        if ProfileConfig['Autosave_status'] and self.save_path != save_path:
            reply = QMessageBox.question(self, 'Save Progress',
                                f'Autosave path is currently set to {self.save_path}.\n' \
                                f"Would you like to change the autosave path to {save_path}?",
                                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                                QMessageBox.StandardButton.No)
            if reply == QMessageBox.StandardButton.Yes:
                self.save_path = str(save_path)

    def load_progress(self, preview=False):
        # Load a saved progress
        global data

        filename = self.open_file_dialog(intention='load', filter='HDF5 files (*.h5)')

        if filename and filename.endswith('.h5'):
            # Open the HDF5 file

            if preview:
                self.hdf5PreviewDialog = HDF5PreviewDialog(filename)
                result = self.hdf5PreviewDialog.exec()

                if result == QDialog.DialogCode.Rejected:
                    self.update_status_bar(f"Cancel button was pressed")
                    return
                
            if data.dataset:
                reply = QMessageBox.question(self, 'Load Progress',
                        "The current data object has datasets inside. " \
                        "Loading progress will delete these datasets. " \
                        "Do you want to continue?",
                        QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                        QMessageBox.StandardButton.No)
                if reply == QMessageBox.StandardButton.No:
                    return
            
            data.clear()
            
            try:
                with h5py.File(filename, 'r') as file:
                    for group_name in file:
                        if group_name.startswith('dataset_'):
                            group = file[group_name]
                            id = int(group_name.split('_')[-1])
                            
                            # Check if the 'raw_data' subgroup and 'columns' attribute exist
                            if 'raw_data' in group and 'columns' in group['raw_data']['raw_data'].attrs:
                                raw_data = pd.DataFrame(np.array(group['raw_data']['raw_data']), columns=list(group['raw_data']['raw_data'].attrs['columns']))
                            else:
                                self.update_status_bar(f"'raw_data' subgroup or 'columns' attribute does not exist in group {group_name}")
                                print(f"'raw_data' subgroup or 'columns' attribute does not exist in group {group_name}")
                                continue
                            
                            data.write_specific_dataset(
                                id,
                                Dataset(
                                    metadata = dict(group['metadata'].attrs),
                                    raw_data = raw_data,
                                )
                            )

                            for subgroup_name in group:
                                if subgroup_name.startswith('data_pkg_'):
                                    key = subgroup_name.split('_')[-1]
                                    subgroup = group[subgroup_name]
                                    
                                    results = {}
                                    for results_key in subgroup[f'results_{key}'].keys():
                                        results[results_key] = dict(subgroup[f'results_{key}'][results_key].attrs)
                                            
                                    data.dataset[id].add_log_mod_results(
                                        key = key,
                                        loglist = list(np.array(subgroup[f'loglist_{key}'])),
                                        mod_data = pd.DataFrame(np.array(subgroup[f'mod_data_{key}']), columns=list(subgroup[f'mod_data_{key}'].attrs['columns'])),
                                        #results = dict(subgroup[f'results_{key}'].attrs),
                                        results = results,
                                    )
                                    
            except Exception as e:
                self.update_status_bar(f"An error occurred while loading the progress: {e}")
                print(f"An error occurred while loading the progress: {e}")
                print(traceback.format_exc())

    def open_FunctionViewer(self):
        # Open the FunctionViewer
        self.functionViewer = FunctionViewer()
        self.functionViewer.applied.connect(self.apply_function)
        self.functionViewer.show()
        
    def apply_function(self, module_name, function_name, parameters):
        # check if module is imported
        if module_name not in sys.modules:
            importlib.import_module(module_name)
            
        # get the function from the module
        func = getattr(sys.modules[module_name], function_name)
        
        # apply the function
        try:
            print(f"Applying function {function_name} with parameters {parameters}")
            for k, v in parameters.items():
                match = re.match(r"data\.dataset\[(\d+)\]\['(.*?)'\]\['(.*?)'\]", v)
                if match:
                    _id, _pkg, _col = match.groups()
                    parameters[k] = data.dataset[int(id)][pkg][col]
                else:
                    parameters[k] = v
            
            results = func(**parameters)
            print(results)
        except Exception as e:
            print(f"An error occurred while applying the function: {e}")
            print(traceback.format_exc())
    
    # 7. Quit the application
    def closeEvent(self, event):
        # Add any cleanup code here if necessary
        # ...

        # Quit the application
        QCoreApplication.quit()
    
    # 8. Insane Clown Posse
    def ICP(self):
        QApplication.instance().setOverrideCursor(Qt.CursorShape.WaitCursor)
        QDesktopServices.openUrl(QUrl("https://www.youtube.com/watch?v=8GyVx28R9-s&t=112"))
        QApplication.instance().restoreOverrideCursor()
    
def str_to_bool(s):
    if s.lower() == 'true':
        return True
    elif s.lower() == 'false':
        return False
    else:
        raise ValueError(f"Cannot convert {s} to bool.")

def main():
    try:
        app = QApplication([])
        window = MainWindow()
        window.show()
        app.exec()
    except Exception as e:
        with open(root / 'logs' / 'log.txt', 'a') as f:
            f.write(str(e))
            f.write(traceback.format_exc())
        raise e

if __name__ == "__main__":
    main()