# Imports
import os
import threading
from functools import partial
import json
import copy
import math
import webbrowser
import sys
from pathlib import Path
from tkinter import *
from tkinter import ttk
from tkinter import filedialog
from importlib.metadata import version
import traceback

# External libraries
import pandas as pd
import numpy as np
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg,  NavigationToolbar2Tk) 
import h5py
import customtkinter as ctk
from CTkToolTip import *
from CTkMessagebox import CTkMessagebox
from CTkColorPicker import *

# Import the MEA library
try:
    from .mea import *
except:
    from mea import *


class MainApp(ctk.CTk):
    """
    Control frame selection and hold 'global' variables.
    """
    def __init__(self):
        # Initialize GUI
        super().__init__()

        # Get icon - works for both normal and frozen
        relative_path="cureq_icon.ico"
        try:
            self.base_path = sys._MEIPASS
        except Exception:
            source_path = Path(__file__).resolve()
            self.base_path = source_path.parent
        self.icon_path=os.path.join(self.base_path, relative_path)
        try:
            self.iconbitmap(self.icon_path)
        except Exception as error:
            print("Could not load in icon")
            print(error)

        # 'Global' variables
        self.tooltipwraplength=200

        # Colors
        self.gray_1 = '#333333'
        self.gray_2 = '#2b2b2b'
        self.gray_3 = "#3f3f3f"
        self.gray_4 = "#212121"
        self.gray_5 = "#696969"
        self.gray_6 = "#292929"
        self.entry_gray = "#565b5e"

        self.text_color = '#dce4ee'

        # Set theme from json
        theme_path=os.path.join(self.base_path, "theme.json")
        
        with open(theme_path, "r") as json_file:
            self.theme = json.load(json_file)

        ctk.set_default_color_theme(theme_path)
        ctk.set_appearance_mode("dark")

        base_color = self.theme["CTkButton"]["fg_color"][1]
        self.primary_1 = self.mix_color(base_color, self.gray_6, factor=0.9)

        # Initialize main frame
        self.show_frame(main_window)

        # The parent holds all the analysis parameters in a dict, which are here initialized with the default values
        self.parameters = get_default_parameters()
        self.default_parameters = get_default_parameters()

        print("Successfully launched MEA GUI")

    # Handle frame switching
    def show_frame(self, frame_class, *args, **kwargs):
        for widget in self.winfo_children():
            widget.destroy()
        frame = frame_class(self, *args, **kwargs)
        frame.pack(expand=True, fill="both") 

    # Function to calculate the optimal grid
    def calculate_optimal_grid(self, num_items):
        min_difference = num_items
        optimal_width = num_items
        optimal_height = 1
        for width in range(1, int(math.sqrt(num_items)) + 1):
            if num_items % width == 0:
                height = num_items // width
                difference = abs(width - height)
                if difference < min_difference:
                    min_difference = difference
                    optimal_width = width
                    optimal_height = height
        return int(optimal_width), int(optimal_height)
    
    def adjust_color(self, hex_color, factor):
        hex_color = hex_color.lstrip('#')
        r = int(hex_color[:2], 16)
        g = int(hex_color[2:4], 16) 
        b = int(hex_color[4:], 16)
        
        r = int(min(255, max(0, r * factor)))
        g = int(min(255, max(0, g * factor)))
        b = int(min(255, max(0, b * factor)))
        
        return f'#{r:02x}{g:02x}{b:02x}'

    def mix_color(self, hex_color1, hex_color2, factor):
        # Convert hex colors to RGB
        hex_color1 = hex_color1.lstrip('#')
        hex_color2 = hex_color2.lstrip('#')
        
        # Original color RGB
        r1 = int(hex_color1[:2], 16)
        g1 = int(hex_color1[2:4], 16)
        b1 = int(hex_color1[4:], 16)
        
        # Gray color RGB
        r2 = int(hex_color2[:2], 16)
        g2 = int(hex_color2[2:4], 16)
        b2 = int(hex_color2[4:], 16)
        
        # Mix colors based on factor
        r = int(r1 * (1-factor) + r2 * factor)
        g = int(g1 * (1-factor) + g2 * factor)
        b = int(b1 * (1-factor) + b2 * factor)
        
        return f'#{r:02x}{g:02x}{b:02x}'

    def set_theme(self, base_color):
        theme_path=os.path.join(self.base_path, "theme.json")

        with open(theme_path, "r") as json_file:
            theme = json.load(json_file)
        
        # Edit all relevant widgets
        theme["CTkButton"]["fg_color"]=["#3a7ebf", base_color]
        theme["CTkButton"]["hover_color"]=["#325882", self.adjust_color(base_color, factor=0.6)]

        theme["CTkCheckBox"]["fg_color"]=["#3a7ebf", base_color]
        theme["CTkCheckBox"]["hover_color"]=["#325882", self.adjust_color(base_color, factor=0.6)]

        theme["CTkEntry"]["border_color"]=["#325882", self.mix_color(base_color, self.entry_gray, factor=0.5)]

        theme["CTkComboBox"]["border_color"]=["#325882", self.mix_color(base_color, self.entry_gray, factor=0.5)]
        theme["CTkComboBox"]["button_color"]=["#325882", base_color]
        theme["CTkComboBox"]["button_hover_color"]=["#325882", self.mix_color(base_color, self.entry_gray, factor=0.5)]

        theme["CTkOptionMenu"]["fg_color"]=["#325882", self.mix_color(base_color, self.entry_gray, factor=0.5)]
        theme["CTkOptionMenu"]["button_color"]=["#325882", base_color]
        theme["CTkOptionMenu"]["button_hover_color"]=["#325882", self.mix_color(base_color, self.entry_gray, factor=0.5)]
        
        theme["CTkSlider"]["button_color"]=[base_color, base_color]
        theme["CTkSlider"]["button_hover_color"]=[self.adjust_color(base_color, factor=0.6), self.adjust_color(base_color, factor=0.6)]

        # Tabview buttons
        theme["CTkSegmentedButton"]["selected_color"]=["#3a7ebf", base_color]
        theme["CTkSegmentedButton"]["selected_hover_color"]=["#325882", self.adjust_color(base_color, factor=0.6)]
        
        self.primary_1 = self.mix_color(base_color, self.gray_6, factor=0.9)

        with open(theme_path, 'w') as json_file:
            json.dump(theme, json_file, indent=4)
        ctk.set_default_color_theme(theme_path)

        self.theme=theme

        self.show_frame(main_window)

class main_window(ctk.CTkFrame):
    """
    Main window and landing page for the user.
    Allows the user to switch to different frames to perform different tasks.
    """
    def __init__(self, parent):
        super().__init__(parent)

        self.parent=parent

        parent.title(f"CureQ MEA analysis tool - Version: {version('CureQ')}")

        # Weights
        self.grid_columnconfigure(0, weight=1)
        self.grid_rowconfigure(0, weight=1)

        # Frame for the sidebar buttons
        sidebarframe=ctk.CTkFrame(self)
        sidebarframe.grid(row=0, column=1, padx=5, pady=5, sticky='nesw')

        # Switch themes
        theme_switch = ctk.CTkButton(sidebarframe, text="Theme", command=self.colorpicker)
        theme_switch.grid(row=0, column=0, sticky='nesw', pady=10, padx=10)
        self.selected_color=parent.theme["CTkButton"]["fg_color"][1]

        cureq_button=ctk.CTkButton(master=sidebarframe, text="CureQ", command=lambda: webbrowser.open_new("https://cureq.nl/"))
        cureq_button.grid(row=1, column=0, sticky='nesw', pady=10, padx=10)

        pypi_button=ctk.CTkButton(master=sidebarframe, text="Library", command=lambda: webbrowser.open_new("https://pypi.org/project/CureQ/"))
        pypi_button.grid(row=2, column=0, sticky='nesw', pady=10, padx=10)

        github_button=ctk.CTkButton(master=sidebarframe, text="Github", command=lambda: webbrowser.open_new("https://github.com/CureQ"))
        github_button.grid(row=3, column=0, sticky='nesw', pady=10, padx=10)

        # Main button frame
        main_buttons_frame=ctk.CTkFrame(self)
        main_buttons_frame.grid(row=0, column=0, padx=5, pady=5, sticky='nesw')
        main_buttons_frame.grid_columnconfigure(0, weight=1)
        main_buttons_frame.grid_columnconfigure(1, weight=1)
        main_buttons_frame.grid_rowconfigure(0, weight=1)
        main_buttons_frame.grid_rowconfigure(1, weight=1)

        # Go to parameter_frame
        to_parameters_button=ctk.CTkButton(master=main_buttons_frame, text="Set Parameters", command=lambda: parent.show_frame(parameter_frame), height=90, width=160)
        to_parameters_button.grid(row=0, column=0, sticky='nesw', pady=10, padx=10)

        # View results
        view_results_button=ctk.CTkButton(master=main_buttons_frame, text="View Results", command=lambda: parent.show_frame(select_folder_frame), height=90, width=160)
        view_results_button.grid(row=1, column=0, sticky='nesw', pady=10, padx=10)

        # Batch processing
        batch_processing_button=ctk.CTkButton(master=main_buttons_frame, text="Batch Processing", command=lambda: parent.show_frame(batch_processing), height=90, width=160)
        batch_processing_button.grid(row=0, column=1, sticky='nesw', pady=10, padx=10)

        # single file processing
        process_file_button=ctk.CTkButton(master=main_buttons_frame, text="Process single file", command=lambda: parent.show_frame(process_file_frame), height=90, width=160)
        process_file_button.grid(row=1, column=1, sticky='nesw', pady=10, padx=10)

        # Utility/plotting buttons
        util_plot_button_frame=ctk.CTkFrame(master=self)
        util_plot_button_frame.grid(row=1, column=0, columnspan=2, sticky='nesw', pady=5, padx=5)

        compression_button=ctk.CTkButton(master=util_plot_button_frame, text="Compress/Rechunk Files", command=lambda: parent.show_frame(compress_files))
        compression_button.grid(row=0, column=0, sticky='nesw', padx=10, pady=10)
        
        features_over_time_button=ctk.CTkButton(master=util_plot_button_frame, text="Plotting", command=lambda: parent.show_frame(plotting_window))
        features_over_time_button.grid(row=0, column=1, sticky='nesw', padx=10, pady=10)

        for i in range(3):
            util_plot_button_frame.grid_rowconfigure(i, weight=1)


    def colorpicker(self):
        popup=ctk.CTkToplevel(self)
        popup.title('Theme Selector')

        try:
            popup.after(250, lambda: popup.iconbitmap(os.path.join(self.parent.icon_path)))
        except Exception as error:
            print(error)
        
        def set_theme():
            self.parent.set_theme(self.selected_color)
            popup.destroy()
            self.parent.show_frame(main_window)

        def set_color(color):
            self.selected_color=color

        popup.grid_columnconfigure(0, weight=1)
        popup.grid_rowconfigure(0, weight=1)
        popup.grid_rowconfigure(1, weight=1)
        colorpicker = CTkColorPicker(popup, width=350, command=lambda e: set_color(e), initial_color=self.selected_color)
        colorpicker.grid(row=0, column=0, sticky='nesw', padx=5, pady=5)
        confirm_button=ctk.CTkButton(master=popup, text="Confirm", command=set_theme)
        confirm_button.grid(row=1, column=0, sticky='nesw', padx=5, pady=5)
    

class parameter_frame(ctk.CTkFrame):
    """
    Allows the user to set the different parameters for the analysis.
    """
    def __init__(self, parent):
        super().__init__(parent)

        self.parent=parent
        self.tooltipwraplength=parent.tooltipwraplength

        # Weights
        self.grid_columnconfigure(0, weight=1)
        self.grid_columnconfigure(1, weight=1)
        self.grid_columnconfigure(2, weight=1)
        self.grid_rowconfigure(0, weight=1)
        self.grid_rowconfigure(1, weight=1)

        """Filter parameters"""
        # Filter parameters frame
        filterparameters=ctk.CTkFrame(self)
        filterparameters.grid(row=0, column=0, padx=10, pady=10, sticky='nesw')
        filterparameters_label=ctk.CTkLabel(filterparameters, text="Filter Parameters", font=ctk.CTkFont(size=25))
        filterparameters_label.grid(row=0, column=0, padx=10, pady=10, sticky='w', columnspan=2)

        # Low cutoff
        lowcutofflabel=ctk.CTkLabel(master=filterparameters, text="Low cutoff:")
        lowcutofflabel.grid(row=1, column=0, padx=10, pady=10, sticky='w')
        lowcutofftooltip = CTkToolTip(lowcutofflabel, message='Define the low cutoff value for the butterworth bandpass filter. Values should be given in hertz')
        lowcutoffinput=ctk.CTkEntry(master=filterparameters)
        lowcutoffinput.grid(row=1, column=1, padx=10, pady=10, sticky='e')

        # High cutoff
        highcutofflabel=ctk.CTkLabel(master=filterparameters, text="High cutoff:")
        highcutofflabel.grid(row=2, column=0, padx=10, pady=10, sticky='w')
        highcutofftooltip = CTkToolTip(highcutofflabel, message='Define the high cutoff value for the butterworth bandpass filter. Values should be given in hertz')
        highcutoffinput=ctk.CTkEntry(master=filterparameters)
        highcutoffinput.grid(row=2, column=1, padx=10, pady=10, sticky='e')

        # Filter order
        orderlabel=ctk.CTkLabel(master=filterparameters, text="Filter order:")
        orderlabel.grid(row=3, column=0, padx=10, pady=10, sticky='w')
        ordertooltip = CTkToolTip(orderlabel, message='The filter order for the butterworth filter')
        orderinput=ctk.CTkEntry(master=filterparameters)
        orderinput.grid(row=3, column=1, padx=10, pady=10, sticky='e')

        """Spike detection parameters"""
        # Set up all the spike detection parameters
        spikedetectionparameters=ctk.CTkFrame(self)
        spikedetectionparameters.grid(row=0, column=1, padx=10, pady=10, sticky='nsew', rowspan=2)

        spikedetectionparameters.grid_columnconfigure(0, weight=1)
        spikedetectionparameters.grid_rowconfigure(1, weight=1)
        spikedetectionparameters.grid_rowconfigure(2, weight=1)

        filterparameters_label=ctk.CTkLabel(spikedetectionparameters, text="Spike Detection Parameters", font=ctk.CTkFont(size=25))
        filterparameters_label.grid(row=0, column=0, padx=10, pady=10, sticky='w')

        # Threshold parameters
        thresholdparameters=ctk.CTkFrame(spikedetectionparameters)
        thresholdparameters.grid(row=1, column=0, padx=10, pady=10, sticky='nsew')
        thresholdparameters_label=ctk.CTkLabel(thresholdparameters, text="Threshold Parameters", font=ctk.CTkFont(size=15))
        thresholdparameters_label.grid(row=0, column=0, padx=10, pady=10, sticky='w')

        # Threshold portion
        thresholdportionlabel=ctk.CTkLabel(master=thresholdparameters, text="Threshold portion:")
        thresholdportionlabel.grid(row=1, column=0, padx=10, pady=10, sticky='w')
        thresholdportiontooltip = CTkToolTip(thresholdportionlabel, message='Define the portion of the electrode data that is used for determining the threshold. A higher values will give a better estimate of the background noise, but will take longer to compute Ranges from 0 to 1.', wraplength=self.tooltipwraplength)
        thresholdportioninput=ctk.CTkEntry(master=thresholdparameters)
        thresholdportioninput.grid(row=1, column=1, padx=10, pady=10, sticky='w')

        # stdevmultiplier
        stdevmultiplierlabel=ctk.CTkLabel(master=thresholdparameters, text="Standard Deviation Multiplier:")
        stdevmultiplierlabel.grid(row=2, column=0, padx=10, pady=10, sticky='w')
        stdevmultipliertooltip = CTkToolTip(stdevmultiplierlabel, message='Define when beyond which point values are seen as outliers (spikes) when identifying spike-free noise\nA higher value will identify more data as noise', wraplength=self.tooltipwraplength)
        stdevmultiplierinput=ctk.CTkEntry(master=thresholdparameters)
        stdevmultiplierinput.grid(row=2, column=1, padx=10, pady=10, sticky='w')

        # RMSmultiplier
        RMSmultiplierlabel=ctk.CTkLabel(master=thresholdparameters, text="RMS Multiplier:")
        RMSmultiplierlabel.grid(row=3, column=0, padx=10, pady=10, sticky='w')
        RMSmultipliertooltip = CTkToolTip(RMSmultiplierlabel, 'Define the multiplication factor of the root mean square (RMS) of the background noise\nA higher number will lead to a higher threshold', wraplength=self.tooltipwraplength)
        RMSmultiplierinput=ctk.CTkEntry(master=thresholdparameters)
        RMSmultiplierinput.grid(row=3, column=1, padx=10, pady=10, sticky='w')

        # Spike validation parameters
        validationparameters=ctk.CTkFrame(spikedetectionparameters)
        validationparameters.grid(row=2, column=0, padx=10, pady=10, sticky='nsew')
        validationparameters_label=ctk.CTkLabel(thresholdparameters, text="Threshold Parameters", font=ctk.CTkFont(size=15))
        validationparameters_label.grid(row=0, column=0, padx=10, pady=10, sticky='w')

        # Refractory period
        refractoryperiodlabel=ctk.CTkLabel(master=validationparameters, text="Refractory Period:")
        refractoryperiodlabel.grid(row=1, column=0, padx=10, pady=10, sticky='w')
        refractoryperiodtooltip = CTkToolTip(refractoryperiodlabel, message='Define the refractory period in the spike detection In this period after a spike, no other spike can be detected Value should be given in seconds, so 2 ms = 0.002 s', wraplength=self.tooltipwraplength)
        refractoryperiodinput=ctk.CTkEntry(master=validationparameters)
        refractoryperiodinput.grid(row=1, column=1, padx=10, pady=10, sticky='e')

        # Dropdown menu where the user selects the validation method
        def option_selected(choice):
            validation_method = choice
            if validation_method=='DMP_noisebased':
                exittimeinput.configure(state="normal")
                maxheightinput.configure(state="normal")
                amplitudedropinput.configure(state="normal")
            else:
                exittimeinput.configure(state="disabled")
                maxheightinput.configure(state="disabled")
                amplitudedropinput.configure(state="disabled")
            
        
        options = ['DMP_noisebased', 'none']    
        dropdown_var = ctk.StringVar(value=options[0])
        dropdownlabel = ctk.CTkLabel(master=validationparameters, text="Spike validation method:")
        dropdownlabel.grid(row=2, column=0, padx=10, pady=10, sticky='w')
        dropdown_menu = ctk.CTkComboBox(validationparameters, variable=dropdown_var, values=options, command=option_selected)
        dropdown_menu.grid(row=2, column=1, padx=10, pady=10, sticky='nes')

        exittimelabel=ctk.CTkLabel(master=validationparameters, text="Exit time:")
        exittimelabel.grid(row=3, column=0, padx=10, pady=10, sticky='w')
        exittimetooltip = CTkToolTip(exittimelabel, 'Define the time a spike gets to drop a certain value.\nStarts from the peak of the spike. Value should be given in seconds, so 0.24 ms is 0.00024s', wraplength=self.tooltipwraplength)
        exittimeinput=ctk.CTkEntry(master=validationparameters)
        exittimeinput.grid(row=3, column=1, padx=10, pady=10, sticky='e')

        amplitudedroplabel=ctk.CTkLabel(master=validationparameters, text="Drop amplitude:")
        amplitudedroplabel.grid(row=4, column=0, padx=10, pady=10, sticky='w')
        amplitudedroptooltip = CTkToolTip(amplitudedroplabel, 'Multiplied with the standard deviation of the surrounding noise. This is the height the spike will have to drop in\na certain amount of time to be registered', wraplength=self.tooltipwraplength)
        amplitudedropinput=ctk.CTkEntry(master=validationparameters)
        amplitudedropinput.grid(row=4, column=1, padx=10, pady=10, sticky='e')

        maxheightlabel=ctk.CTkLabel(master=validationparameters, text="Max drop:")
        maxheightlabel.grid(row=5, column=0, padx=10, pady=10, sticky='w')
        maxheighttooltip = CTkToolTip(maxheightlabel, 'Multiplied with the threshold. The maximum height a spike can be required to drop in amplitude in the set timeframe', wraplength=self.tooltipwraplength)
        maxheightinput=ctk.CTkEntry(master=validationparameters)
        maxheightinput.grid(row=5, column=1, padx=10, pady=10, sticky='e')

        """Burst detection parameters"""
        # Set up all the burst detection parameters
        burstdetectionparameters=ctk.CTkFrame(self)
        burstdetectionparameters.grid(row=0, column=2, padx=10, pady=10, sticky='nesw')

        burstdetectionparameters_label=ctk.CTkLabel(burstdetectionparameters, text="Burst Detection Parameters", font=ctk.CTkFont(size=25))
        burstdetectionparameters_label.grid(row=0, column=0, padx=10, pady=10, sticky='w', columnspan=4)
        burstdetectionparameters.grid_columnconfigure(0, weight=1)

        # Setup up the minimal amount of spikes for a burst
        minspikeslabel=ctk.CTkLabel(master=burstdetectionparameters, text="Minimal amount of spikes:")
        minspikeslabel.grid(row=1, column=0, padx=10, pady=10, sticky='w')
        minspikestooltip = CTkToolTip(minspikeslabel, message='Define the minimal amount of spikes a burst should have before being considered as one', wraplength=self.tooltipwraplength)
        minspikesinput=ctk.CTkEntry(master=burstdetectionparameters)
        minspikesinput.grid(row=1, column=1, padx=10, pady=10, sticky='w')

        # Setup up the default threshold
        defaultthlabel=ctk.CTkLabel(master=burstdetectionparameters, text="Default interval threshold:")
        defaultthlabel.grid(row=2, column=0, padx=10, pady=10, sticky='w')
        defaultthtooltip = CTkToolTip(defaultthlabel, 'Define the default inter-spike interval threshold\nthat is used for burst detection\nValue should be given in miliseconds', wraplength=self.tooltipwraplength)
        defaultthinput=ctk.CTkEntry(master=burstdetectionparameters)
        defaultthinput.grid(row=2, column=1, padx=10, pady=10, sticky='w')

        # Setup up the max threshold
        maxisilabel=ctk.CTkLabel(master=burstdetectionparameters, text="Max interval threshold:")
        maxisilabel.grid(row=3, column=0, padx=10, pady=10, sticky='w')
        maxisitooltip = CTkToolTip(maxisilabel, message='Define the maximum value the inter-spike interval threshold can be when 2 peaks have been detected in the ISI graph\nValue should be given in miliseconds', wraplength=self.tooltipwraplength)
        maxisiinput=ctk.CTkEntry(master=burstdetectionparameters)
        maxisiinput.grid(row=3, column=1, padx=10, pady=10, sticky='w')

        # Setup the KDE bandwidth
        isikdebwlabel=ctk.CTkLabel(master=burstdetectionparameters, text="KDE bandwidth:")
        isikdebwlabel.grid(row=4, column=0, padx=10, pady=10, sticky='w')
        isikdebwtooltip = CTkToolTip(isikdebwlabel, message='Define the bandwidth that is used when calculating the\nkernel density estimate of the inter-spike intervals', wraplength=self.tooltipwraplength)
        isikdebwinput=ctk.CTkEntry(master=burstdetectionparameters)
        isikdebwinput.grid(row=4, column=1, padx=10, pady=10, sticky='w')

        """Network burst detection parameters"""
        networkburstdetectionparameters=ctk.CTkFrame(self)
        networkburstdetectionparameters.grid(row=1, column=2, padx=10, pady=10, sticky='nesw')
        networkburstdetectionparameters_label=ctk.CTkLabel(networkburstdetectionparameters, text="Network Burst Detection Parameters", font=ctk.CTkFont(size=25))
        networkburstdetectionparameters_label.grid(row=0, column=0, padx=10, pady=10, sticky='w', columnspan=2)
        networkburstdetectionparameters.grid_columnconfigure(0, weight=1)

        # Setup the minimum amount of channels participating
        minchannelslabel=ctk.CTkLabel(master=networkburstdetectionparameters, text="Min channels:")
        minchannelslabel.grid(row=1, column=0, padx=10, pady=10, sticky='w')
        minchannelstooltip = CTkToolTip(minchannelslabel, 'Define the minimal percentage of channels that should be active in a network burst, values ranges from 0 to 1', wraplength=self.tooltipwraplength)
        minchannelsinput=ctk.CTkEntry(master=networkburstdetectionparameters)
        minchannelsinput.grid(row=1, column=1, padx=10, pady=10, sticky='w')

        # Setup the thresholding method
        nwthoptions = ['Yen', 'Otsu', 'Li', 'Isodata', 'Mean', 'Minimum', 'Triangle']
        networkth_var = ctk.StringVar(value=nwthoptions[0])
        dropdownlabel = ctk.CTkLabel(master=networkburstdetectionparameters, text="Thresholding method:")
        dropdownlabel.grid(row=2, column=0, padx=10, pady=10, sticky='w')
        dropdown_menu = ctk.CTkComboBox(networkburstdetectionparameters, variable=networkth_var, values=nwthoptions)
        dropdown_menu.grid(row=2, column=1, padx=10, pady=10, sticky='w')

        # Setup the network burst detection KDE bandwidth
        nbd_kde_bandwidth_label=ctk.CTkLabel(master=networkburstdetectionparameters, text="KDE Bandwidth:")
        nbd_kde_bandwidth_label.grid(row=3, column=0, padx=10, pady=10, sticky='w')
        nbd_kde_bandwidth_tooltip = CTkToolTip(nbd_kde_bandwidth_label, 'Define the bandwidth value that should be used when creating the kernel density estimate for the network burst detection', wraplength=self.tooltipwraplength)
        nbd_kde_bandwidth_input=ctk.CTkEntry(master=networkburstdetectionparameters)
        nbd_kde_bandwidth_input.grid(row=3, column=1, padx=10, pady=10, sticky='w')

        """Other parameters"""
        otherparameters=ctk.CTkFrame(self)
        otherparameters.grid(row=1, column=0, padx=10, pady=10, sticky='nsew')
        otherparameters_label=ctk.CTkLabel(otherparameters, text="Other Parameters", font=ctk.CTkFont(size=25))
        otherparameters_label.grid(row=0, column=0, padx=10, pady=10, sticky='w', columnspan=2)

        def removeinactivefunc():
            if removeinactivevar.get():
                activitythinput.configure(state='normal')
            else:
                activitythinput.configure(state='disabled')

        # Use multiprocessing
        multiprocessinglabel=ctk.CTkLabel(otherparameters, text="Use multiprocessing:")
        multiprocessinglabel.grid(row=1, column=0, padx=10, pady=10, sticky='w')
        multiprocessingtooltip = CTkToolTip(multiprocessinglabel, message='Using multiprocessing means the electrodes will be analyzed in parallel, generally speeding up the analysis. Multiprocessing might not work properly if the device you\'re using does not have sufficient RAM/CPU-cores', wraplength=self.tooltipwraplength)
        multiprocessingvar=ctk.BooleanVar()
        multiprocessinginput=ctk.CTkCheckBox(otherparameters, onvalue=True, offvalue=False, variable=multiprocessingvar, text='')
        multiprocessinginput.grid(row=1, column=1, padx=10, pady=10, sticky='w')

        # Remove inactive electrodes
        removeinactivelabel=ctk.CTkLabel(otherparameters, text="Remove inactive electrodes:")
        removeinactivelabel.grid(row=2, column=0, padx=10, pady=10, sticky='w')
        removeinactivetooltip = CTkToolTip(removeinactivelabel, message='Should inactive electrodes be used when calculating well features?', wraplength=self.tooltipwraplength)
        removeinactivevar=ctk.IntVar()
        removeinactiveinput=ctk.CTkCheckBox(otherparameters, onvalue=True, offvalue=False, variable=removeinactivevar, command=removeinactivefunc, text='')
        removeinactiveinput.grid(row=2, column=1, padx=10, pady=10, sticky='w')

        # Setup the activity threshold
        activitythlabel=ctk.CTkLabel(master=otherparameters, text="Activity threshold:")
        activitythlabel.grid(row=3, column=0, padx=10, pady=10, sticky='w')
        activitythtooltip = CTkToolTip(activitythlabel, message='Define the minimal activity a channel has to have, to be used in calculating well features. Value should be given in hertz, so a value of 0.1 would mean any channel with less that 1 spike per 10 seconds will be removed', wraplength=self.tooltipwraplength)
        activitythinput=ctk.CTkEntry(otherparameters)
        activitythinput.grid(row=3, column=1, padx=10, pady=10, sticky='w')

        """Buttons and functions for saving, resetting and importing parameters"""
        def set_parameters(parameters):

            # Make sure every entry is set to 'normal'
            dropdown_var.set("DMP_noisebased")
            removeinactivevar.set(True)
            removeinactivefunc()
            option_selected(dropdown_var.get())

            lowcutoffinput.delete(0, END)
            lowcutoffinput.insert(0, parameters["low cutoff"])
            highcutoffinput.delete(0, END)
            highcutoffinput.insert(0, parameters["high cutoff"])
            orderinput.delete(0, END)
            orderinput.insert(0, parameters["order"])
            thresholdportioninput.delete(0, END)
            thresholdportioninput.insert(0, parameters["threshold portion"])
            stdevmultiplierinput.delete(0, END)
            stdevmultiplierinput.insert(0, parameters["standard deviation multiplier"])
            RMSmultiplierinput.delete(0, END)
            RMSmultiplierinput.insert(0, parameters["rms multiplier"])
            refractoryperiodinput.delete(0, END)
            refractoryperiodinput.insert(0, parameters["refractory period"])
            dropdown_var.set(parameters["spike validation method"])
            exittimeinput.delete(0, END)
            exittimeinput.insert(0, parameters["exit time"])
            amplitudedropinput.delete(0, END)
            amplitudedropinput.insert(0, parameters["drop amplitude"])
            maxheightinput.delete(0, END)
            maxheightinput.insert(0, parameters["max drop"])
            minspikesinput.delete(0, END)
            minspikesinput.insert(0, parameters["minimal amount of spikes"])
            defaultthinput.delete(0, END)
            defaultthinput.insert(0, parameters["default interval threshold"])
            maxisiinput.delete(0, END)
            maxisiinput.insert(0, parameters["max interval threshold"])
            isikdebwinput.delete(0, END)
            isikdebwinput.insert(0, parameters["burst detection kde bandwidth"])
            minchannelsinput.delete(0, END)
            minchannelsinput.insert(0, parameters["min channels"])
            networkth_var.set(parameters["thresholding method"])
            removeinactivevar.set(bool(parameters["remove inactive electrodes"]))
            activitythinput.delete(0, END)
            activitythinput.insert(0, parameters["activity threshold"])
            multiprocessingvar.set(bool(parameters["use multiprocessing"]))
            nbd_kde_bandwidth_input.delete(0, END)
            nbd_kde_bandwidth_input.insert(0, parameters["nbd kde bandwidth"])

            # Update other parameter availability
            removeinactivefunc()
            option_selected(dropdown_var.get())

        def import_parameters():
            parametersfile = filedialog.askopenfilename(filetypes=[("Parameter file", "*.json")])
            if parametersfile == '':
                return
            parameters=json.load(open(parametersfile))
            
            set_parameters(parameters)

        def save_parameters():
            try:
                self.parent.parameters['low cutoff']=int(lowcutoffinput.get())
                self.parent.parameters['high cutoff']=int(highcutoffinput.get())
                self.parent.parameters['order']=int(orderinput.get())
                self.parent.parameters['refractory period']=float(refractoryperiodinput.get())
                self.parent.parameters['exit time']=float(exittimeinput.get())
                self.parent.parameters['burst detection kde bandwidth']=float(isikdebwinput.get())
                self.parent.parameters['minimal amount of spikes']=int(minspikesinput.get())
                self.parent.parameters['max interval threshold']=float(maxisiinput.get())
                self.parent.parameters['default interval threshold']=float(defaultthinput.get())
                self.parent.parameters['max drop']=float(maxheightinput.get())
                self.parent.parameters['drop amplitude']=float(amplitudedropinput.get())
                self.parent.parameters['standard deviation multiplier']=float(stdevmultiplierinput.get())
                self.parent.parameters['rms multiplier']=float(RMSmultiplierinput.get())
                self.parent.parameters['min channels']=float(minchannelsinput.get())
                self.parent.parameters['thresholding method']=networkth_var.get()
                self.parent.parameters['nbd kde bandwidth']=float(nbd_kde_bandwidth_input.get())
                self.parent.parameters['remove inactive electrodes']=bool(removeinactivevar.get())
                self.parent.parameters['activity threshold']=float(activitythinput.get())
                self.parent.parameters['threshold portion']=float(thresholdportioninput.get())
                self.parent.parameters['spike validation method']=str(dropdown_var.get())
                self.parent.parameters['use multiprocessing']=bool(multiprocessingvar.get())
                self.parent.show_frame(main_window)

            except Exception as error:
                traceback.print_exc()
                CTkMessagebox(title="Error",
                              message='Certain parameters could not be converted to the correct datatype (e.g. int or float). Please check if every parameter has the correct values',
                              icon="cancel",
                              wraplength=400)
        
        set_parameters(self.parent.parameters)

        def set_default_parameters():
            set_parameters(self.parent.default_parameters)

        default_parameters=ctk.CTkButton(master=self, text="Restore default parameters", command=set_default_parameters)
        default_parameters.grid(row=3, column=1, padx=10, pady=10, sticky='nsew')

        import_parameters_button=ctk.CTkButton(master=self, text="Import parameters", command=import_parameters)
        import_parameters_button.grid(row=3, column=2, padx=10, pady=10, sticky='nsew')

        save_parameters_button=ctk.CTkButton(master=self, text="Save parameters and return", command=save_parameters)
        save_parameters_button.grid(row=3, column=0, padx=10, pady=10, sticky='nsew')


class select_folder_frame(ctk.CTkFrame):
    """
    Allows the user to select a dataset and outputfile to inspect the results.
    """
    def __init__(self, parent):
        super().__init__(parent)

        self.grid_columnconfigure(0, weight=1)
        self.grid_rowconfigure(0, weight=1)

        self.folder_path=''
        self.data_path=''

        datalocation=ctk.CTkFrame(self)
        datalocation.grid(row=0, column=0, padx=10, pady=10, sticky='nsew')
        datalocation.grid_columnconfigure(1, weight=1)

        folderlabel=ctk.CTkLabel(master=datalocation, text="Folder:")
        folderlabel.grid(row=0, column=0, padx=10, pady=10, sticky='w')
        self.btn_selectfolder=ctk.CTkButton(master=datalocation, text="Select a folder", command=self.openfolder)
        self.btn_selectfolder.grid(row=0, column=1, padx=10, pady=10, sticky='nesw')

        rawfilelabel=ctk.CTkLabel(master=datalocation, text="File:")
        rawfilelabel.grid(row=1, column=0, padx=10, pady=10, sticky='w')
        self.btn_selectrawfile=ctk.CTkButton(master=datalocation, text="Select a file", command=self.openrawfile)
        self.btn_selectrawfile.grid(row=1, column=1, padx=10, pady=10, sticky='nesw')

        view_results_button=ctk.CTkButton(self, text="View results", command= lambda: self.go_to_results(parent))
        view_results_button.grid(row=2, column=0, columnspan=2, padx=10, pady=10, sticky='nesw')

        return_button=ctk.CTkButton(self, text="Return", command= lambda: parent.show_frame(main_window))
        return_button.grid(row=3, column=0, columnspan=2, padx=10, pady=10, sticky='nesw')

    def openfolder(self):
            resultsfolder = filedialog.askdirectory()
            if resultsfolder != '':
                self.btn_selectfolder.configure(text=resultsfolder)
                self.folder_path=resultsfolder
    
    def openrawfile(self):
            selectedfile = filedialog.askopenfilename(filetypes=[("MEA data", "*.h5")])
            if len(selectedfile)!=0:
                self.btn_selectrawfile.configure(text=os.path.split(selectedfile)[1])
                self.data_path=selectedfile

    # Check if the correct folder/file has been selected
    def go_to_results(self, parent):
        if self.folder_path == '' or self.data_path == '':
            CTkMessagebox(title="Error", message='Please select an output folder and raw datafile', icon="cancel", wraplength=400)
            return
        # Check for subfolders
        if not(os.path.exists(f"{self.folder_path}/burst_values") and os.path.exists(f"{self.folder_path}/spike_values") and os.path.exists(f"{self.folder_path}/network_data")):
            CTkMessagebox(title="Error", message='Could not load in the results, please make sure you have selected the correct folder', icon="cancel", wraplength=400)
            return
        
        # Check for parameters json
        try:
            parameters=open(f"{self.folder_path}/parameters.json")
            parameters=json.load(parameters)
        except Exception as error:
            traceback.print_exc()
            CTkMessagebox(title='Error', message='Could not load in the results, please make sure you have selected the correct folder', icon='cancel', wraplength=400)
            return
        
        # Check the raw datafile
        try:
            with h5py.File(self.data_path, 'r') as hdf_file:
                datashape=hdf_file["Data/Recording_0/AnalogStream/Stream_0/ChannelData"].shape
        except Exception as error:
            traceback.print_exc()
            CTkMessagebox(title='Error', message='Could not load in the raw data, please make sure you have selected the correct file', icon='cancel', wraplength=400)
            return

        # If everything is good, go to the next frame
        parent.show_frame(view_results, folder=self.folder_path, rawfile=self.data_path)


class view_results(ctk.CTkFrame):
    """
    Allow the user to view the results of the MEA analysis
    """
    def __init__(self, parent, folder, rawfile):
        super().__init__(parent)

        self.folder=folder
        self.rawfile=rawfile
        self.parent=parent

        self.parent.title(f"CureQ MEA analysis tool - Version: {version('CureQ')} - {self.folder}")

        self.tab_frame=ctk.CTkTabview(self, anchor='nw')
        self.tab_frame.grid(column=0, row=0, sticky='nesw', pady=10, padx=10)
        self.grid_columnconfigure(0, weight=1)
        self.grid_rowconfigure(0, weight=1)

        self.tab_frame.add("Single Electrode View")
        self.tab_frame.tab("Single Electrode View").grid_columnconfigure(0, weight=1)
        self.tab_frame.tab("Single Electrode View").grid_rowconfigure(0, weight=1)

        self.tab_frame.add("Whole Well View")
        self.tab_frame.tab("Whole Well View").grid_columnconfigure(0, weight=1)
        self.tab_frame.tab("Whole Well View").grid_rowconfigure(0, weight=1)

        # sev = single electrode view
        # wwv = whole well view

        # Load files
        parameters=open(f"{folder}/parameters.json")
        parameters=json.load(parameters)
        with h5py.File(rawfile, 'r') as hdf_file:
            datashape=hdf_file["Data/Recording_0/AnalogStream/Stream_0/ChannelData"].shape
        well_amnt = datashape[0]/parameters["electrode amount"]

        """Single electrode view"""
        self.selected_well=1
        sev_well_button_frame=ctk.CTkFrame(self.tab_frame.tab("Single Electrode View"))
        sev_well_button_frame.grid(row=0, column=0, pady=10, padx=10, sticky='nesw')

        sev_electrode_button_frame=ctk.CTkFrame(self.tab_frame.tab("Single Electrode View"))
        sev_electrode_button_frame.grid(row=0, column=1, pady=10, padx=10, sticky='nesw')
        
        # Wellbuttons
        ywells, xwells=parent.calculate_optimal_grid(well_amnt)
        self.sev_wellbuttons=[]
        i=1

        for y in range(ywells):
            for x in range(xwells):
                well_btn=ctk.CTkButton(master=sev_well_button_frame, text=i, command=partial(self.set_selected_well, i), height=100, width=100, font=ctk.CTkFont(size=25))
                well_btn.grid(row=y, column=x, sticky='nesw')
                self.sev_wellbuttons.append(well_btn)
                i+=1

        # Electrode buttons
        electrode_amnt = parameters["electrode amount"]
        if electrode_amnt==12:
            # Preset for 12 electrode MEA
            electrode_layout=np.array([ [False, True, True, False],
                                        [True, True, True, True],
                                        [True, True, True, True],
                                        [False, True, True, False]])
        else:
            height, width = parent.calculate_optimal_grid(electrode_amnt)
            electrode_layout = np.ones((height, width), dtype=bool)
        i = 1
        electrodebuttons=[]
        for x in range(electrode_layout.shape[0]):
            for y in range(electrode_layout.shape[1]):
                if electrode_layout[x,y]:
                    electrode_btn=ctk.CTkButton(master=sev_electrode_button_frame, text=i, command=partial(self.open_sev_tab, i), height=100, width=100, font=ctk.CTkFont(size=25))
                    electrode_btn.grid(row=x, column=y, sticky='nesw')
                    electrodebuttons.append(electrode_btn)
                    i+=1

        """Whole well view"""
        wwv_well_button_frame=ctk.CTkFrame(self.tab_frame.tab("Whole Well View"))
        wwv_well_button_frame.grid(row=0, column=0, pady=10, padx=10, sticky='nesw')

        wwv_wellbuttons=[]
        i=1

        for y in range(ywells):
            for x in range(xwells):
                well_btn=ctk.CTkButton(master=wwv_well_button_frame, text=i, command=partial(self.open_wwv_tab, i), height=100, width=100, font=ctk.CTkFont(size=25))
                well_btn.grid(row=y, column=x)
                wwv_wellbuttons.append(well_btn)
                i+=1

        # Button to return to main menu
        return_to_main = ctk.CTkButton(master=self, text="Return to main menu", command=lambda: self.parent.show_frame(main_window), fg_color=parent.gray_1)
        return_to_main.grid(row=1, column=0, pady=10, padx=10)
    
    def set_selected_well(self, i):
        self.selected_well=i
        for j in range(len(self.sev_wellbuttons)):
            self.sev_wellbuttons[j].configure(fg_color=self.parent.theme["CTkButton"]["fg_color"][1])
        self.sev_wellbuttons[i-1].configure(fg_color=self.parent.theme["CTkButton"]["hover_color"][1])


    def open_sev_tab(self, electrode):
        single_electrode_view(self.parent, self.folder, self.rawfile, self.selected_well, electrode)

    def open_wwv_tab(self, well):
        whole_well_view(self.parent, self.folder, well)


class single_electrode_view(ctk.CTkToplevel):
    """
    Allows the user to inpect the spike and burst detection on a single electrode.
    """
    def __init__(self, parent, folder, rawfile, well, electrode):
        super().__init__(parent)
        self.title(f"Well: {well}, Electrode: {electrode}")

        self.tab_frame=ctk.CTkTabview(self, anchor='nw')
        self.tab_frame.pack(fill='both', expand=True, pady=10, padx=10)

        self.grid_rowconfigure(0, weight=1)

        self.tab_frame.add("Spike Detection")
        self.tab_frame.tab("Spike Detection").grid_columnconfigure(0, weight=1)
        self.tab_frame.tab("Spike Detection").grid_rowconfigure(0, weight=1)

        self.tab_frame.add("Burst Detection")
        self.tab_frame.tab("Burst Detection").grid_columnconfigure(0, weight=1)
        self.tab_frame.tab("Burst Detection").grid_rowconfigure(0, weight=1)

        # Set the icon with a little delay, otherwise it does not work
        try:
            self.after(250, lambda: self.iconbitmap(os.path.join(parent.icon_path)))
        except Exception as error:
            print(error)

        self.parameters=open(f"{folder}/parameters.json")
        self.parameters=json.load(self.parameters)
        self.electrode_nr=(well-1)*self.parameters["electrode amount"]+electrode-1

        self.rawfile=rawfile
        self.folder=folder
        
        """Spike detection"""
        # Create a frame for the plots
        self.electrode_plot_frame=ctk.CTkFrame(master=self.tab_frame.tab("Spike Detection"))
        self.electrode_plot_frame.grid(row=0, column=0, sticky='nesw')
        self.electrode_plot_frame.grid_columnconfigure(0, weight=1)
        self.electrode_plot_frame.grid_rowconfigure(0, weight=1)

        # Create a frame for the settings
        electrode_settings_frame=ctk.CTkFrame(master=self.tab_frame.tab("Spike Detection"))
        electrode_settings_frame.grid(row=1, column=0)
        electrode_settings_frame.grid_columnconfigure(0, weight=1)
        electrode_settings_frame.grid_columnconfigure(1, weight=1)
        electrode_settings_frame.grid_columnconfigure(2, weight=1)

        self.tab_frame.tab("Spike Detection").grid_columnconfigure(0, weight=1)
        self.tab_frame.tab("Spike Detection").grid_rowconfigure(0, weight=1)

        # Bandpass options
        bp_options_ew_frame = ctk.CTkFrame(master=electrode_settings_frame)
        bp_options_ew_frame.grid(row=0, column=0, pady=10, padx=10)
        bandpass_options_label=ctk.CTkLabel(master=bp_options_ew_frame, text='Bandpass Parameters', font=ctk.CTkFont(size=25)).grid(row=0, column=0, pady=10, padx=10, sticky='w', columnspan=2)

        lowcut_label=ctk.CTkLabel(master=bp_options_ew_frame, text='Low cutoff').grid(row=1, column=0, pady=10, padx=10, sticky='w')
        self.lowcut_ew_entry=ctk.CTkEntry(master=bp_options_ew_frame)
        self.lowcut_ew_entry.grid(row=1, column=1, pady=10, padx=10, sticky='w')

        highcut_label=ctk.CTkLabel(master=bp_options_ew_frame, text='High cutoff').grid(row=2, column=0, sticky='w', pady=10, padx=10)
        self.highcut_ew_entry=ctk.CTkEntry(master=bp_options_ew_frame)
        self.highcut_ew_entry.grid(row=2, column=1, pady=10, padx=10, sticky='w')

        order_label=ctk.CTkLabel(master=bp_options_ew_frame, text='Order').grid(row=3, column=0, sticky='w', pady=10, padx=10)
        self.order_ew_entry=ctk.CTkEntry(master=bp_options_ew_frame)
        self.order_ew_entry.grid(row=3, column=1, pady=10, padx=10, sticky='w')

        # Threshold options
        th_options_ew_frame = ctk.CTkFrame(master=electrode_settings_frame)
        th_options_ew_frame.grid(row=0, column=1, pady=10, padx=10)
        threshold_options_label=ctk.CTkLabel(master=th_options_ew_frame, text='Threshold Parameters', font=ctk.CTkFont(size=25)).grid(row=0, column=0, pady=10, padx=10, sticky='w', columnspan=2)
        
        stdevmultiplier_label=ctk.CTkLabel(master=th_options_ew_frame, text='Standard deviation multiplier').grid(row=1, column=0, pady=10, padx=10, sticky='w')
        self.stdevmultiplier_ew_entry=ctk.CTkEntry(master=th_options_ew_frame)
        self.stdevmultiplier_ew_entry.grid(row=1, column=1, pady=10, padx=10, sticky='w')

        RMSmultiplier_label=ctk.CTkLabel(master=th_options_ew_frame, text='RMS multiplier').grid(row=2, column=0, sticky='w', pady=10, padx=10)
        self.RMSmultiplier_ew_entry=ctk.CTkEntry(master=th_options_ew_frame)
        self.RMSmultiplier_ew_entry.grid(row=2, column=1, pady=10, padx=10, sticky='w')

        thpn_label=ctk.CTkLabel(master=th_options_ew_frame, text='Threshold portion').grid(row=3, column=0, sticky='w', pady=10, padx=10)
        self.thpn_ew_entry=ctk.CTkEntry(master=th_options_ew_frame)
        self.thpn_ew_entry.grid(row=3, column=1, pady=10, padx=10, sticky='w')

        # Spike validation options
        val_options_ew_frame = ctk.CTkFrame(master=electrode_settings_frame)
        val_options_ew_frame.grid(row=0, column=2, pady=10, padx=10)
        spike_val_options_label=ctk.CTkLabel(master=val_options_ew_frame, text='Spike Detection Parameters', font=ctk.CTkFont(size=25)).grid(row=0, column=0, pady=10, padx=10, sticky='w', columnspan=4)

        def ew_option_selected(event):
            self.set_states()
                
        validation_options = ['DMP_noisebased', 'none']
        self.validation_method_var = ctk.StringVar(value=validation_options[0])
        validation_method_label = ctk.CTkLabel(master=val_options_ew_frame, text="Spike validation method:")
        validation_method_label.grid(row=1, column=0, padx=10, pady=10, sticky='w')
        self.validation_method_entry = ctk.CTkOptionMenu(val_options_ew_frame, variable=self.validation_method_var, values=validation_options, command=ew_option_selected)
        self.validation_method_entry.grid(row=1, column=1, padx=10, pady=10, sticky='nesw')

        rfpd_label=ctk.CTkLabel(master=val_options_ew_frame, text='Refractory period').grid(row=2, column=0, pady=10, padx=10, sticky='w')
        self.rfpd_ew_entry=ctk.CTkEntry(master=val_options_ew_frame)
        self.rfpd_ew_entry.grid(row=2, column=1, pady=10, padx=10, sticky='w')

        exittime_label=ctk.CTkLabel(master=val_options_ew_frame, text='Exit time').grid(row=3, column=0, pady=10, padx=10, sticky='w')
        self.exittime_ew_entry=ctk.CTkEntry(master=val_options_ew_frame)
        self.exittime_ew_entry.grid(row=3, column=1, pady=10, padx=10, sticky='w')

        dropamplitude_label=ctk.CTkLabel(master=val_options_ew_frame, text='Drop amplitude').grid(row=1, column=2, pady=10, padx=10, sticky='w')
        self.dropamplitude_ew_entry=ctk.CTkEntry(master=val_options_ew_frame)
        self.dropamplitude_ew_entry.grid(row=1, column=3, pady=10, padx=10, sticky='w')

        maxdrop_label=ctk.CTkLabel(master=val_options_ew_frame, text='Max drop').grid(row=2, column=2, pady=10, padx=10, sticky='w')
        self.maxdrop_ew_entry=ctk.CTkEntry(master=val_options_ew_frame)
        self.maxdrop_ew_entry.grid(row=2, column=3, pady=10, padx=10, sticky='w')

        plot_rectangle_label=ctk.CTkLabel(master=val_options_ew_frame, text='Plot validation')
        plot_rectangle_label.grid(row=3, column=2, pady=10, padx=10, sticky='w')
        plot_rectangle_tooltip = CTkToolTip(plot_rectangle_label, message='Display the rectangles that have been used to validate the spikes. Warning: Plotting these is computationally expensive and might take a while', wraplength=parent.tooltipwraplength)
        self.plot_rectangle=ctk.BooleanVar(value=False)
        self.plot_rectangle.set(False)
        self.plot_rectangle_ew_entry=ctk.CTkCheckBox(master=val_options_ew_frame, variable=self.plot_rectangle, text='')
        self.plot_rectangle_ew_entry.grid(row=3, column=3, pady=10, padx=10, sticky='w')

        # Set values and create initial plot
        self.reset(parent)

        # Buttons
        update_plot_button=ctk.CTkButton(master=electrode_settings_frame, text='Update plot', command=partial(self.update_plot, parent))
        update_plot_button.grid(row=1, column=0, pady=10, padx=10, sticky='nesw')

        reset_button = ctk.CTkButton(master=electrode_settings_frame, text='Reset', command=partial(self.reset, parent))
        reset_button.grid(row=1, column=1, pady=10, padx=10, sticky='nesw')

        
        """Burst Detection"""
        # Create the initial burst plot
        self.burstplotsframe=ctk.CTkFrame(master=self.tab_frame.tab("Burst Detection"))
        self.burstplotsframe.grid(row=0, column=0, sticky='nesw')

        burstsettingsframe=ctk.CTkFrame(master=self.tab_frame.tab("Burst Detection"), fg_color=parent.gray_6)
        burstsettingsframe.grid(row=1, column=0, pady=10, padx=10)

        # Burst detection settings
        burst_options_label=ctk.CTkLabel(master=burstsettingsframe, text='Burst Detection Parameters', font=ctk.CTkFont(size=25)).grid(row=0, column=0, pady=10, padx=10, sticky='w', columnspan=4)
        minspikes_bw_label=ctk.CTkLabel(master=burstsettingsframe, text='Minimal amount of spikes').grid(row=1, column=0, pady=10, padx=10, sticky='w')
        self.minspikes_bw_entry=ctk.CTkEntry(master=burstsettingsframe)
        self.minspikes_bw_entry.grid(row=1, column=1, pady=10, padx=10, sticky='w')
        def_iv_bw_label=ctk.CTkLabel(master=burstsettingsframe, text='Default interval threshold').grid(row=2, column=0, pady=10, padx=10, sticky='w')
        self.def_iv_bw_entry=ctk.CTkEntry(master=burstsettingsframe)
        self.def_iv_bw_entry.grid(row=2, column=1, pady=10, padx=10, sticky='w')
        max_iv_bw_label=ctk.CTkLabel(master=burstsettingsframe, text='Max interval threshold').grid(row=1, column=2, pady=10, padx=10, sticky='w')
        self.max_iv_bw_entry=ctk.CTkEntry(master=burstsettingsframe)
        self.max_iv_bw_entry.grid(row=1, column=3, pady=10, padx=10, sticky='w')
        kde_bw_bw_label=ctk.CTkLabel(master=burstsettingsframe, text='KDE bandwidth').grid(row=2, column=2, pady=10, padx=10, sticky='w')
        self.kde_bw_bw_entry=ctk.CTkEntry(master=burstsettingsframe)
        self.kde_bw_bw_entry.grid(row=2, column=3, pady=10, padx=10, sticky='w')

        # Burst buttons
        update_burst_plot_button=ctk.CTkButton(master=burstsettingsframe, text='Update plot', command=partial(self.update_burst_plot, parent))
        update_burst_plot_button.grid(row=3, column=0, pady=10, padx=10, sticky='nesw', columnspan=2)

        reset_burst_button = ctk.CTkButton(master=burstsettingsframe, text='Reset', command=partial(self.burst_reset, parent))
        reset_burst_button.grid(row=3, column=2, pady=10, padx=10, sticky='nesw', columnspan=2)

        # Create first plot
        self.burst_reset(parent)

    def set_states(self):
        validation_method = self.validation_method_var.get()
        if validation_method=='DMP_noisebased':
            self.exittime_ew_entry.configure(state="normal")
            self.maxdrop_ew_entry.configure(state="normal")
            self.dropamplitude_ew_entry.configure(state="normal")
            self.plot_rectangle_ew_entry.configure(state="normal")
        else:
            self.exittime_ew_entry.configure(state="disabled")
            self.maxdrop_ew_entry.configure(state="disabled")
            self.dropamplitude_ew_entry.configure(state="disabled")
            self.plot_rectangle.set(False)
            self.plot_rectangle_ew_entry.configure(state="disabled")

    def plot_single_electrode(self, parent, parameters):
        with h5py.File(self.rawfile, 'r') as hdf_file:
            dataset=hdf_file["Data/Recording_0/AnalogStream/Stream_0/ChannelData"]
            raw_data=dataset[self.electrode_nr]
        electrode_data=butter_bandpass_filter(raw_data, parameters)
        threshold=fast_threshold(electrode_data, parameters)
        fig=spike_validation(data=electrode_data, electrode=self.electrode_nr, threshold=threshold, parameters=parameters, plot_electrodes=True, savedata=False, plot_rectangles=self.plot_rectangle.get())
        
        # Check which colorscheme we have to use
        axiscolour=parent.text_color
        bgcolor=parent.gray_4

        # Set the plot background
        fig.set_facecolor(bgcolor)
        ax=fig.axes[0]
        ax.set_facecolor(bgcolor)

        # Change the other colours
        ax.xaxis.label.set_color(axiscolour)
        ax.yaxis.label.set_color(axiscolour)
        for side in ['top', 'bottom', 'left', 'right']:
            ax.spines[side].set_color(axiscolour)
        ax.tick_params(axis='x', colors=axiscolour)
        ax.tick_params(axis='y', colors=axiscolour)
        ax.set_title(label=ax.get_title(),color=axiscolour)

        plot_canvas = FigureCanvasTkAgg(fig, master=self.electrode_plot_frame)  
        plot_canvas.get_tk_widget().grid(row=0, column=0, sticky='nsew')
        toolbarframe=ctk.CTkFrame(master=self.electrode_plot_frame, fg_color=parent.primary_1)
        toolbarframe.grid(row=1, column=0, sticky='s')
        toolbar = NavigationToolbar2Tk(plot_canvas, toolbarframe)
        toolbar.config(background=parent.primary_1)
        toolbar._message_label.config(background=parent.primary_1)
        for button in toolbar.winfo_children():
            button.config(background=parent.primary_1)
        toolbar.update()
        plot_canvas.draw()

    def update_plot(self, parent):
        # Get the new parameters from the entry widgets
        # Create a temporary dict we will give to the plot electrode function
        temp_parameters=copy.deepcopy(self.parameters)
        temp_parameters['low cutoff']=int(self.lowcut_ew_entry.get())
        temp_parameters['high cutoff']=int(self.highcut_ew_entry.get())
        temp_parameters['order']=int(self.order_ew_entry.get())
        temp_parameters['standard deviation multiplier']=float(self.stdevmultiplier_ew_entry.get())
        temp_parameters['rms multiplier']=float(self.RMSmultiplier_ew_entry.get())
        temp_parameters['threshold portion']=float(self.thpn_ew_entry.get())
        temp_parameters['refractory period']=float(self.rfpd_ew_entry.get())
        if str(self.validation_method_var.get())=='none':
            temp_parameters['drop amplitude']=0
        else:
            temp_parameters['exit time']=float(self.exittime_ew_entry.get())
            temp_parameters['drop amplitude']=float(self.dropamplitude_ew_entry.get())
            temp_parameters['max drop']=float(self.maxdrop_ew_entry.get())
        
        # Update the output folder path, as this might have changed since the original analysis
        temp_parameters['output path']=self.folder

        # Plot the electrode with the new parameters
        self.plot_single_electrode(parent, temp_parameters)

    def default_values(self):
        # Bandpass
        self.lowcut_ew_entry.delete(0,END)
        self.lowcut_ew_entry.insert(0,self.parameters["low cutoff"])
        self.highcut_ew_entry.delete(0,END)
        self.highcut_ew_entry.insert(0,self.parameters["high cutoff"])
        self.order_ew_entry.delete(0,END)
        self.order_ew_entry.insert(0,self.parameters["order"])
        # Threshold
        self.stdevmultiplier_ew_entry.delete(0,END)
        self.stdevmultiplier_ew_entry.insert(0,self.parameters["standard deviation multiplier"])
        self.RMSmultiplier_ew_entry.delete(0,END)
        self.RMSmultiplier_ew_entry.insert(0,self.parameters["rms multiplier"])
        self.thpn_ew_entry.delete(0,END)
        self.thpn_ew_entry.insert(0,self.parameters["threshold portion"])
        # Spike validation
        self.rfpd_ew_entry.delete(0,END)
        self.rfpd_ew_entry.insert(0,self.parameters["refractory period"])
        self.exittime_ew_entry.delete(0,END)
        self.exittime_ew_entry.insert(0,self.parameters["exit time"])
        self.dropamplitude_ew_entry.delete(0,END)
        self.dropamplitude_ew_entry.insert(0,self.parameters["drop amplitude"])
        self.maxdrop_ew_entry.delete(0,END)
        self.maxdrop_ew_entry.insert(0,self.parameters["max drop"])
        self.plot_rectangle.set(False)
        self.validation_method_var.set(self.parameters['spike validation method'])
        self.set_states()
    
    def reset(self, parent):
        # First, enable all the possibly disabled entries so we can alter the values
        self.exittime_ew_entry.configure(state="normal")
        self.maxdrop_ew_entry.configure(state="normal")
        self.dropamplitude_ew_entry.configure(state="normal")
        self.plot_rectangle_ew_entry.configure(state="normal")
        # Insert the new values
        self.default_values()
        # Update the availability of certain entries
        self.set_states()
        # Update the plot
        self.update_plot(parent)

    def plot_burst_detection(self, parent, parameters):
        with h5py.File(self.rawfile, 'r') as hdf_file:
            dataset=hdf_file["Data/Recording_0/AnalogStream/Stream_0/ChannelData"]
            raw_data=dataset[self.electrode_nr]
        electrode_data=butter_bandpass_filter(raw_data, parameters)
        KDE_fig, burst_fig = burst_detection(data=electrode_data, electrode=self.electrode_nr, parameters=parameters, plot_electrodes=True, savedata=False)
        
        # Check which colorscheme we have to use
        axiscolour=parent.text_color
        bgcolor=parent.gray_4

        for fig in [KDE_fig, burst_fig]:
            fig.set_facecolor(bgcolor)
            ax=fig.axes[0]
            ax.set_facecolor(bgcolor)
            # Change the other colours
            ax.xaxis.label.set_color(axiscolour)
            ax.yaxis.label.set_color(axiscolour)
            for side in ['top', 'bottom', 'left', 'right']:
                ax.spines[side].set_color(axiscolour)
            ax.tick_params(axis='x', colors=axiscolour)
            ax.tick_params(axis='y', colors=axiscolour)
            ax.set_title(label=ax.get_title(),color=axiscolour)
        
        # Plot the raw burst plot
        burst_canvas = FigureCanvasTkAgg(burst_fig, master=self.burstplotsframe)  
        burst_canvas.get_tk_widget().grid(row=0, column=0, sticky='nsew')
        toolbarframe=ctk.CTkFrame(master=self.burstplotsframe)
        toolbarframe.grid(row=1, column=0, sticky='s', columnspan=2)
        toolbar = NavigationToolbar2Tk(burst_canvas, toolbarframe)
        toolbar.config(background=parent.primary_1)
        toolbar._message_label.config(background=parent.primary_1)
        for button in toolbar.winfo_children():
            button.config(background=parent.primary_1)
        toolbar.update()
        burst_canvas.draw()

        # Plot the KDE plot
        KDE_canvas = FigureCanvasTkAgg(KDE_fig, master=self.burstplotsframe)  
        KDE_canvas.get_tk_widget().grid(row=0, column=1, sticky='nsew')

        self.burstplotsframe.grid_columnconfigure(0, weight=3)
        self.burstplotsframe.grid_columnconfigure(1, weight=1)
        self.burstplotsframe.grid_rowconfigure(0, weight=1)
        KDE_canvas.draw()

    def update_burst_plot(self, parent):
        # Get the new parameters from the entry widgets
        temp_parameters=copy.deepcopy(self.parameters)
        temp_parameters["minimal amount of spikes"]=int(self.minspikes_bw_entry.get())
        temp_parameters["default interval threshold"]=float(self.def_iv_bw_entry.get())
        temp_parameters["max interval threshold"]=float(self.max_iv_bw_entry.get())
        temp_parameters["burst detection kde bandwidth"]=float(self.kde_bw_bw_entry.get())

        # Update the output folder path, as this might have changed since the original analysis
        temp_parameters['output path']=self.folder

        self.plot_burst_detection(parent, temp_parameters)

    def default_values_burst(self):
        # Reset the values to the ones in the JSON file
        self.minspikes_bw_entry.delete(0,END)
        self.minspikes_bw_entry.insert(0,self.parameters["minimal amount of spikes"])
        self.def_iv_bw_entry.delete(0,END)
        self.def_iv_bw_entry.insert(0,self.parameters["default interval threshold"])
        self.max_iv_bw_entry.delete(0,END)
        self.max_iv_bw_entry.insert(0,self.parameters["max interval threshold"])
        self.kde_bw_bw_entry.delete(0,END)
        self.kde_bw_bw_entry.insert(0,self.parameters["burst detection kde bandwidth"])

    def burst_reset(self, parent):
        self.default_values_burst()
        self.update_burst_plot(parent)


class whole_well_view(ctk.CTkToplevel):
    """
    Allows the user to inspect the network burst detection of a single well.
    """
    def __init__(self, parent, folder, well):
        super().__init__(parent)
        self.title(f"Well: {well}")

        self.tab_frame=ctk.CTkTabview(self, anchor='nw')
        self.tab_frame.pack(fill='both', expand=True, pady=10, padx=10)

        self.grid_rowconfigure(0, weight=1)

        self.tab_frame.add("Network Burst Detection")
        self.tab_frame.tab("Network Burst Detection").grid_columnconfigure(0, weight=1)
        self.tab_frame.tab("Network Burst Detection").grid_rowconfigure(0, weight=1)

        self.tab_frame.add("Well Activity")
        self.tab_frame.tab("Well Activity").grid_columnconfigure(0, weight=1)
        self.tab_frame.tab("Well Activity").grid_rowconfigure(0, weight=1)

        # Set the icon with a little delay, otherwise it does not work
        try:
            self.after(250, lambda: self.iconbitmap(os.path.join(parent.icon_path)))
        except Exception as error:
            print(error)

        self.parameters=open(f"{folder}/parameters.json")
        self.parameters=json.load(self.parameters)

        self.folder=folder
        self.well=well
        self.parent=parent

        """Network burst detection plot"""
        # Create a frame for the plots
        self.nbd_plot_frame=ctk.CTkFrame(master=self.tab_frame.tab("Network Burst Detection"))
        self.nbd_plot_frame.grid(row=0, column=0, sticky='nesw')
        self.nbd_plot_frame.grid_columnconfigure(0, weight=1)
        self.nbd_plot_frame.grid_rowconfigure(0, weight=1)

        # Create a frame for the settings
        nbd_settings_frame=ctk.CTkFrame(master=self.tab_frame.tab("Network Burst Detection"), fg_color=parent.gray_6)
        nbd_settings_frame.grid(row=1, column=0)

        # Network burst detection settings
        # NB settings
        burst_options_label=ctk.CTkLabel(master=nbd_settings_frame, text='Network Burst Detection Parameters', font=ctk.CTkFont(size=25)).grid(row=0, column=0, pady=10, padx=10, sticky='w', columnspan=2)
        min_channels_nb_label=ctk.CTkLabel(master=nbd_settings_frame, text="Min channels (%)").grid(row=1, column=0, sticky='w', padx=10, pady=10)
        self.min_channels_nb_entry=ctk.CTkEntry(master=nbd_settings_frame)
        self.min_channels_nb_entry.grid(row=1, column=1, sticky='w', padx=10, pady=10)
        
        self.th_method_nb_var = ctk.StringVar(value=nbd_settings_frame)
        self.nwthoptions_nb = ['Yen', 'Otsu', 'Li', 'Isodata', 'Mean', 'Minimum', 'Triangle']
        th_method_nb = ctk.CTkLabel(master=nbd_settings_frame, text="Thresholding method:")
        th_method_nb.grid(row=2, column=0, padx=10, pady=10, sticky='w')
        self.th_method_dropdown_nb = ctk.CTkOptionMenu(nbd_settings_frame, variable=self.th_method_nb_var, values=self.nwthoptions_nb)
        self.th_method_dropdown_nb.grid(row=2, column=1, padx=10, pady=10, sticky='w')

        nbd_kde_bandwidth_nb_label=ctk.CTkLabel(master=nbd_settings_frame, text="KDE Bandwidth:").grid(row=3, column=0, sticky='w', padx=10, pady=10)
        self.nbd_kde_bandwidth_nb_entry=ctk.CTkEntry(master=nbd_settings_frame)
        self.nbd_kde_bandwidth_nb_entry.grid(row=3, column=1, sticky='w', padx=10, pady=10)

        # Buttons
        nb_update_plot_button=ctk.CTkButton(master=nbd_settings_frame, text="Update plot", command=self.update_plot)
        nb_update_plot_button.grid(row=4, column=0, sticky='nesw', padx=10, pady=10)

        nb_reset_button=ctk.CTkButton(master=nbd_settings_frame, text="Reset", command=self.reset)
        nb_reset_button.grid(row=4, column=1, sticky='nesw', padx=10, pady=10)

        # Create initial plot
        self.reset()

        '''Electrode activity'''
        self.electrode_activity_plot_frame=ctk.CTkFrame(master=self.tab_frame.tab("Well Activity"))
        self.electrode_activity_plot_frame.grid(row=0, column=0, sticky='nsew')
        self.electrode_activity_plot_frame.grid_columnconfigure(0, weight=1)
        self.electrode_activity_plot_frame.grid_rowconfigure(0, weight=1)
        electrode_activity_settings=ctk.CTkFrame(master=self.tab_frame.tab("Well Activity"), fg_color=parent.gray_6)
        electrode_activity_settings.grid(row=1, column=0)

        self.def_bw_value=0.1

        el_act_bw_label=ctk.CTkLabel(master=electrode_activity_settings, text="KDE bandwidth")
        el_act_bw_label.grid(row=0, column=0, sticky='w', padx=10, pady=10)
        self.el_act_bw_entry=ctk.CTkEntry(master=electrode_activity_settings)
        self.el_act_bw_entry.grid(row=0, column=1, sticky='nesw', pady=10, padx=10)
        self.el_act_bw_entry.insert(0, self.def_bw_value)

        # Buttons
        el_act_update_plot=ctk.CTkButton(master=electrode_activity_settings, text='Update plot', command=self.well_activity_update_plot)
        el_act_update_plot.grid(row=1, column=0, sticky='nesw', padx=10, pady=10)

        el_act_reset=ctk.CTkButton(master=electrode_activity_settings, text='Reset', command=self.reset_electrode_activity)
        el_act_reset.grid(row=1, column=1, sticky='nesw', padx=10, pady=10)

        # Create initial plot
        self.reset_electrode_activity()

    def plot_network_bursts(self, parameters):
        fig=network_burst_detection(wells=[self.well], parameters=parameters, plot_electrodes=True, savedata=False, save_figures=False)
        
        # Check which colorscheme we have to use
        axiscolour=self.parent.text_color
        bgcolor=self.parent.gray_4

        fig.set_facecolor(bgcolor)
        for ax in fig.axes:
            ax.set_facecolor(bgcolor)
            # Change the other colours
            ax.xaxis.label.set_color(axiscolour)
            ax.yaxis.label.set_color(axiscolour)
            for side in ['top', 'bottom', 'left', 'right']:
                ax.spines[side].set_color(axiscolour)
            ax.tick_params(axis='x', colors=axiscolour)
            ax.tick_params(axis='y', colors=axiscolour)
            ax.set_title(label=ax.get_title(),color=axiscolour)

        plot_canvas = FigureCanvasTkAgg(fig, master=self.nbd_plot_frame)  
        plot_canvas.get_tk_widget().grid(row=0, column=0, sticky='nsew')
        toolbarframe=ttk.Frame(master=self.nbd_plot_frame)
        toolbarframe.grid(row=1, column=0, sticky='s')
        toolbar = NavigationToolbar2Tk(plot_canvas, toolbarframe)
        toolbar.config(background=self.parent.primary_1)
        toolbar._message_label.config(background=self.parent.primary_1)
        for button in toolbar.winfo_children():
            button.config(background=self.parent.primary_1)
        toolbar.update()
        plot_canvas.draw()

    def default_values(self):
        self.th_method_nb_var.set(self.parameters["thresholding method"])
        self.min_channels_nb_entry.delete(0,END)
        self.min_channels_nb_entry.insert(0,self.parameters["min channels"])
        self.nbd_kde_bandwidth_nb_entry.delete(0, END)
        self.nbd_kde_bandwidth_nb_entry.insert(0, self.parameters["nbd kde bandwidth"])

    def update_plot(self):
        temp_parameters=copy.deepcopy(self.parameters)
        temp_parameters["min channels"]=float(self.min_channels_nb_entry.get())
        temp_parameters["thresholding method"]=str(self.th_method_nb_var.get())
        temp_parameters["nbd kde bandwidth"]=float(self.nbd_kde_bandwidth_nb_entry.get())
        
        # Update the output folder path, as this might have changed since the original analysis
        temp_parameters['output path']=self.folder

        self.plot_network_bursts(parameters=temp_parameters)

    def reset(self):
        self.default_values()
        self.update_plot()

    def plot_well_activity(self):
        fig=well_electrodes_kde(outputpath=self.folder, well=self.well, electrode_amnt=self.parameters["electrode amount"], measurements=self.parameters["measurements"], hertz=self.parameters["sampling rate"], bandwidth=float(self.el_act_bw_entry.get()))
        
        # Check which colorscheme we have to use
        axiscolour="#586d97"
        bgcolor=self.parent.gray_4
        
        fig.set_facecolor(bgcolor)
        for ax in fig.axes:
            ax.set_facecolor(bgcolor)
            # Change the other colours
            ax.xaxis.label.set_color(axiscolour)
            ax.yaxis.label.set_color(axiscolour)
            for side in ['top', 'bottom', 'left', 'right']:
                ax.spines[side].set_color(axiscolour)
            ax.tick_params(axis='x', colors=axiscolour)
            ax.tick_params(axis='y', colors=axiscolour)
            ax.set_title(label=ax.get_title(),color=axiscolour)

        plot_canvas = FigureCanvasTkAgg(fig, master=self.electrode_activity_plot_frame)  
        plot_canvas.get_tk_widget().grid(row=0, column=0, sticky='nsew')
        toolbarframe=ttk.Frame(master=self.electrode_activity_plot_frame)
        toolbarframe.grid(row=1, column=0)
        toolbar = NavigationToolbar2Tk(plot_canvas, toolbarframe)
        toolbar.config(background=self.parent.primary_1)
        toolbar._message_label.config(background=self.parent.primary_1)
        for button in toolbar.winfo_children():
            button.config(background=self.parent.primary_1)
        toolbar.update()
        plot_canvas.draw()

    def well_activity_update_plot(self):
        self.plot_well_activity()

    def reset_electrode_activity(self):
        self.el_act_bw_entry.delete(0, END)
        self.el_act_bw_entry.insert(0, self.def_bw_value)
        self.well_activity_update_plot()


class process_file_frame(ctk.CTkFrame):
    """
    Allows the user to perform analysis on a single MEA experiment.
    """
    def __init__(self, parent):
        super().__init__(parent)  # Initialize the parent class

        self.parent=parent

        self.crashed=False
        self.progressfile=''

        self.grid_columnconfigure(0, weight=1)

        # Values
        self.selected_file=''

        self.select_file_button = ctk.CTkButton(master=self, text="Select a file", command=self.select_file)
        self.select_file_button.grid(row=0, column=0, pady=5, padx=5, sticky='nesw')

        parametersframe = ctk.CTkFrame(master=self)
        parametersframe.grid(row=1, column=0, padx=5, pady=5, sticky='nesw')

        parametersframe.grid_columnconfigure(0, weight=1)
        parametersframe.grid_columnconfigure(1, weight=1)

        sampling_rate_label = ctk.CTkLabel(master=parametersframe, text="Sampling Rate:")
        sampling_rate_label.grid(row=0, column=0, pady=10, padx=10, sticky='w')

        self.sampling_rate_entry = ctk.CTkEntry(master=parametersframe)
        self.sampling_rate_entry.grid(row=0, column=1, sticky='nesw', pady=10, padx=10)

        electrode_amnt_label = ctk.CTkLabel(master=parametersframe, text="Electrode amount:")
        electrode_amnt_label.grid(row=1, column=0, pady=10, padx=10, sticky='w')

        self.electrode_amnt_entry = ctk.CTkEntry(master=parametersframe)
        self.electrode_amnt_entry.grid(row=1, column=1, sticky='nesw', pady=10, padx=10)

        self.start_button = ctk.CTkButton(master=self, text="Start Analysis", command=lambda: threading.Thread(target=self.start_analysis).start())
        self.start_button.grid(row=2, column=0, pady=5, padx=5, sticky='nesw')

        self.return_button = ctk.CTkButton(master=self, text="Return to Main Menu", command=lambda: parent.show_frame(main_window), fg_color=parent.gray_1)
        self.return_button.grid(row=3, column=0, padx=5, pady=5, sticky='nesw')

    def select_file(self):
        selectedfile = filedialog.askopenfilename(filetypes=[("MEA data", "*.h5")])
        if len(selectedfile)!=0:
            self.selected_file=selectedfile
            self.select_file_button.configure(text=selectedfile)

    def call_library(self):
        try:
            electrode_amnt=int(self.electrode_amnt_entry.get())
            sampling_rate=int(self.sampling_rate_entry.get())
            analyse_wells(self.selected_file, electrode_amnt=electrode_amnt, sampling_rate=sampling_rate, parameters=self.parent.parameters)
        except Exception as error:
            CTkMessagebox(title="Error", message=f'Something went wrong during the analysis:\n{error}', icon="cancel", wraplength=400)
            traceback.print_exc()
            self.crashed=True
            self.start_button.configure(state='normal')
            self.return_button.configure(state='normal')

    def abort_analysis(self):
        np.save(self.progressfile, ["abort"])
        print("Aborting analysis, please wait...")

    def start_analysis(self):
        # Check things
        if self.selected_file=='':
            CTkMessagebox(title="Error", message='Please select a file', icon="cancel", wraplength=400)
            return
        try:
            sampling_rate=int(self.sampling_rate_entry.get())
            electrode_amnt=int(self.electrode_amnt_entry.get())
        except:
            CTkMessagebox(title="Error", message='Please fill in the sampling rate and electrode amount.', icon="cancel", wraplength=400)
            return

        self.start_button.configure(state='disabled')
        self.return_button.configure(state='disabled')

        # Create a popup for the progress
        popup=ctk.CTkToplevel(self)
        popup.title('Progress')

        try:
            popup.after(250, lambda: popup.iconbitmap(os.path.join(self.parent.icon_path)))
        except Exception as error:
            print(error)

        popup.grid_columnconfigure(0, weight=1)

        popup.protocol("WM_DELETE_WINDOW", lambda: self.abort_analysis())

        progress_label = ctk.CTkLabel(master=popup, text="The MEA data is being processed, please wait for the application to finish.")
        progress_label.grid(row=0, column=0, pady=10, padx=10, sticky='nesw')

        progressbar=ctk.CTkProgressBar(master=popup, orientation='horizontal', mode='determinate', progress_color="#239b56", width=400)
        progressbar.grid(row=1, column=0, pady=10, padx=10, sticky='nesw')
        progressbar.set(0)

        progress_info = ctk.CTkLabel(master=popup, text='')
        progress_info.grid(row=2, column=0, pady=10, padx=10, sticky='nesw')

        # Start the analysis in a new thread
        process=threading.Thread(target=self.call_library)
        process.start()

        # And keep track of the progress
        start=time.time()
        path=os.path.split(self.selected_file)[0]
        self.progressfile=f"{path}/progress.npy"
        # Remove potential pre-existing progressfile
        try: 
            os.remove(self.progressfile)
        except:
            pass
        while True:
            currenttime=time.time()
            elapsed=round(currenttime-start,1)
            try:
                progress=np.load(self.progressfile)
            except:
                progress=['starting']
            if self.crashed:
                popup.destroy()
                os.remove(self.progressfile)
                self.start_button.configure(state='normal')
                self.return_button.configure(state='normal')
                return
            if progress[0]=='done':
                break
            elif progress[0]=='starting':
                progress_info.configure(text=f'Loading raw data, time elapsed: {elapsed} seconds')
            elif progress[0]=='rechunking':
                progress_info.configure(text=f'Rechunking data to new chunk shape, time elapsed: {elapsed} seconds')
            elif progress[0]=='abort':
                progress_info.configure(text=f'Aborting analysis, please wait...')
            elif progress[0]=='stopped':
                popup.destroy()
                os.remove(self.progressfile)
                self.start_button.configure(state='normal')
                self.return_button.configure(state='normal')
                return
            else:
                progressbar.set(progress[0]/progress[1])
                progress_info.configure(text=f"Analyzing data, channel: {progress[0]}/{progress[1]}, time elapsed: {elapsed} seconds")
            time.sleep(0.01)
        currenttime=time.time()
        elapsed=round(currenttime-start,2)
        os.remove(self.progressfile)
        progress_label.configure(text='')
        progress_info.configure(text=f"Analysis finished in {elapsed} seconds.")
        popup.protocol("WM_DELETE_WINDOW", popup.destroy)
        self.start_button.configure(state='normal')
        self.return_button.configure(state='normal')


class batch_processing(ctk.CTkFrame):
    """
    Allows the user to process multiple MEA experiments.
    """
    def __init__(self, parent):
        super().__init__(parent)  # Initialize the parent class

        self.parent=parent

        # Configure grid layout
        self.grid_columnconfigure(0, weight=1)
        self.grid_rowconfigure(0, weight=1)

        # Different main frames
        self.file_selection_frame=ctk.CTkFrame(self)
        self.file_selection_frame.grid(row=0, column=0, sticky='nsew')
        self.file_selection_frame.grid_columnconfigure(0, weight=1)
        self.file_selection_frame.grid_rowconfigure(0, weight=0)
        self.file_selection_frame.grid_rowconfigure(1, weight=1)
        self.file_selection_frame.grid_rowconfigure(2, weight=0)

        # List to store selected file paths
        self.selected_files = []

        """ Folder selection frame """
        self.folder_frame = ctk.CTkFrame(self.file_selection_frame, fg_color=parent.gray_3)
        self.folder_frame.grid(row=0, column=0, padx=20, pady=10, sticky="ew")
        
        # Select folder button
        self.select_folder_btn = ctk.CTkButton(
            self.folder_frame, 
            text="Select Folder", 
            command=self.select_folder
        )
        self.select_folder_btn.pack(pady=10, padx=10, fill="x")

        # Treeview frame
        self.tree_frame = ctk.CTkFrame(self.file_selection_frame)
        self.tree_frame.grid(row=1, column=0, padx=20, pady=10, sticky="nsew")
        self.tree_frame.grid_columnconfigure(0, weight=1)
        self.tree_frame.grid_rowconfigure(0, weight=1)

        # Vertical Scrollbar
        self._vertical_scrollbar = ctk.CTkScrollbar(
            self.tree_frame, 
            orientation="vertical"
        )
        self._vertical_scrollbar.grid(row=0, column=1, padx=(0, 5), pady=5, sticky='ns')
        
        # Horizontal Scrollbar
        self._horizontal_scrollbar = ctk.CTkScrollbar(
            self.tree_frame, 
            orientation="horizontal"
        )
        self._horizontal_scrollbar.grid(row=1, column=0, padx=5, pady=(0, 5), sticky='ew')

        # Treeview Customisation
        bg_color = self.tree_frame.cget("fg_color")[1]
        text_color = parent._apply_appearance_mode(ctk.ThemeManager.theme["CTkLabel"]["text_color"])
        selected_color = parent._apply_appearance_mode(ctk.ThemeManager.theme["CTkButton"]["fg_color"])
        treestyle = ttk.Style()
        treestyle.theme_use('default')
        treestyle.configure("Treeview", background=bg_color, foreground=text_color, fieldbackground=bg_color, borderwidth=0)
        treestyle.map('Treeview', background=[('selected', bg_color)], foreground=[('selected', selected_color)])
        parent.bind("<<TreeviewSelect>>", lambda event: parent.focus_set())

        # Treeview with scrollbars
        self.tree = ttk.Treeview(
            self.tree_frame, 
            columns=("fullpath", "type"), 
            selectmode="extended",
            yscrollcommand=self._vertical_scrollbar.set,
            xscrollcommand=self._horizontal_scrollbar.set,
            show='tree'
        )
        self.tree.grid(row=0, column=0, sticky="nsew", pady=(5,0), padx=(5,0))

        # Link scrollbars to Treeview
        self._vertical_scrollbar.configure(command=self.tree.yview)
        self._horizontal_scrollbar.configure(command=self.tree.xview)

        # Configure treeview columns
        self.tree.heading("#0", text="Directory Structure")
        self.tree.column("#0", width=600, stretch=True)

        # Selected tag
        self.tree.tag_configure('selected', background='#627747')

        # Print selected files button
        self.print_files_btn = ctk.CTkButton(
            self.file_selection_frame, 
            text="Confirm Selection", 
            command=partial(self.confirm_selection, parent)
        )
        self.print_files_btn.grid(row=2, column=0, padx=10, pady=10, sticky='nesw')

        to_main_menu=ctk.CTkButton(self.file_selection_frame, 
            text="Return to Main Menu", 
            command=lambda: parent.show_frame(main_window)
            )
        to_main_menu.grid(row=3, column=0, padx=10, pady=10, sticky='news')

        # Bind events
        self.tree.bind("<<TreeviewOpen>>", self.open_folder)
        self.tree.bind("<<TreeviewSelect>>", self.item_selected)

        """ Process files frame """
        self.selected_files_labels = []
        self.progressbars = []
        self.analysis_crashed=False     # Flag to communicate if analysis has crashed with a file

        # Frames
        self.process_files_frame=ctk.CTkFrame(self)
        self.selected_files_frame = ctk.CTkFrame(self.process_files_frame, fg_color=parent.gray_3)
        self.selected_files_frame.grid(row=0, column=0, pady=5, padx=5, sticky='nesw')
        self.selected_files_frame.grid_columnconfigure(0, weight=1)
        self.selected_files_frame.grid_rowconfigure(0, weight=1)

        self.process_files_frame.grid_columnconfigure(0, weight=2)
        self.process_files_frame.grid_rowconfigure(0, weight=1)
        self.process_files_frame.grid_columnconfigure(1, weight=1)

        self.settings_and_progress_frame=ctk.CTkFrame(self.process_files_frame, fg_color="transparent")
        self.settings_and_progress_frame.grid(row=0, column=1, sticky='nesw')
        self.settings_and_progress_frame.grid_rowconfigure(1, weight=1)
        self.settings_and_progress_frame.grid_columnconfigure(0, weight=1)

        self.analysis_settings_frame=ctk.CTkFrame(self.settings_and_progress_frame, fg_color=parent.gray_3)
        self.analysis_settings_frame.grid(row=0, column=0, sticky='nesw', pady=5, padx=5)

        self.analysis_progress_frame=ctk.CTkFrame(self.settings_and_progress_frame, fg_color=parent.gray_3)
        self.analysis_progress_frame.grid(row=1, column=0, sticky='nesw', pady=5, padx=5)
        self.analysis_progress_frame.grid_columnconfigure(0, weight=1)
        
        # Analysis settings
        self.sampling_rate_label = ctk.CTkLabel(self.analysis_settings_frame, text="Sampling rate:")
        self.sampling_rate_label.grid(row=0, column=0, pady=5, padx=5, sticky='w')
        self.sampling_rate_input = ctk.CTkEntry(self.analysis_settings_frame)
        self.sampling_rate_input.grid(row=0, column=1, pady=5, padx=5, sticky='w')

        self.electrode_amnt_label = ctk.CTkLabel(self.analysis_settings_frame, text="Electrode amount:")
        self.electrode_amnt_label.grid(row=1, column=0, pady=5, padx=5, sticky='w')
        self.electrode_amnt_input = ctk.CTkEntry(self.analysis_settings_frame)
        self.electrode_amnt_input.grid(row=1, column=1, pady=5, padx=5, sticky='w')

        # Analysis and progress
        self.start_analysis_button = ctk.CTkButton(self.analysis_progress_frame, text="Initiate Analysis", command=lambda: threading.Thread(target=self.initiate_analysis).start())
        self.start_analysis_button.grid(row=0, column=0, sticky='nesw', padx=5, pady=5)

        self.progressbar = ctk.CTkProgressBar(self.analysis_progress_frame, orientation='horizontal', mode='determinate', progress_color="#239b56")
        self.progressbar.grid(row=1, column=0, sticky='nesw', padx=5, pady=20)
        self.progressbar.set(0)

        self.abort_analysis_bool=False
        self.abort_analysis_button = ctk.CTkButton(self.analysis_progress_frame, text="Abort Analysis", command=self.abort_analysis_func)
        self.abort_analysis_button.grid(row=2, column=0, sticky='nesw', padx=5, pady=5)
        self.abort_analysis_button.configure(state='disabled')

        self.back_button = ctk.CTkButton(self.analysis_progress_frame, text="Return", command=lambda: parent.show_frame(main_window))
        self.back_button.grid(row=3, column=0, sticky='nesw', padx=5, pady=5)


    # Select parent folder
    def select_folder(self):
        # Clear existing tree
        for i in self.tree.get_children():
            self.tree.delete(i)
        self.selected_files=[]

        # Open folder selection dialog
        folder_path = filedialog.askdirectory()
        if folder_path:
            # Populate tree with root folder
            root_id = self.tree.insert("", "end", text=os.path.basename(folder_path), 
                                       values=(folder_path, "Folder"), open=True)
            self.populate_tree(root_id, folder_path)

    def populate_tree(self, parent, path):
        try:
            for name in sorted(os.listdir(path)):
                full_path = os.path.join(path, name)
                if os.path.isdir(full_path):
                    # Add folder
                    folder_id = self.tree.insert(parent, "end", text=name, 
                                                 values=(full_path, "Folder"))
                    # Add a dummy child to enable expansion
                    self.tree.insert(folder_id, "end", text="placeholder")
                else:
                    # Add file
                    if name.endswith(".h5"):
                        self.tree.insert(parent, "end", text=name, 
                                        values=(full_path, "File"))
        except PermissionError:
            print(f"Permission denied for {path}")

    def open_folder(self, event):
        # Remove placeholder and populate actual contents when folder is opened
        item = self.tree.focus()
        if self.tree.get_children(item) and \
           self.tree.item(self.tree.get_children(item)[0])['text'] == 'placeholder':
            # Remove placeholder
            self.tree.delete(self.tree.get_children(item)[0])
            # Populate actual contents
            self.populate_tree(item, self.tree.item(item, "values")[0])

    def deselect_all(self):
        for item in self.tree.selection():
            self.tree.selection_remove(item)

    def add_selection(self, item):
        if len(self.tree.item(item)['tags'])!=0 and self.tree.item(item)['tags'][0]=="selected":
            self.tree.item(item, tags=())
            self.selected_files.remove(item)
        else:
            self.tree.item(item, tags="selected")
            self.selected_files.append(item)

    def item_selected(self, event):
        for selected_item in self.tree.selection():
            values = self.tree.item(selected_item, "values")
            # If the selected item is a file
            if values[1]=="File":
                self.add_selection(selected_item)
            # If the selected item is a folder
            elif values[1]=="Folder":
                children = self.tree.get_children(selected_item)
                for child in children:
                    if self.tree.item(child, "values")[1] == "File" and self.tree.item(child, "values")[0].endswith(".h5"):
                        self.add_selection(child) 
        self.deselect_all()

    def confirm_selection(self, parent):
        self.selected_files_table = ctk.CTkScrollableFrame(self.selected_files_frame)
        self.selected_files_table.grid(row=0, column=0, padx=10, pady=10, sticky='nesw')
        self.selected_files_table.columnconfigure(0, weight=1)
        self.selected_files_table.columnconfigure(1, weight=1)

        for index, file in enumerate(self.selected_files):
            label=ctk.CTkLabel(self.selected_files_table, text=self.tree.item(file, "values")[0], corner_radius=20, width=300)
            label.grid(row=index, column=0, sticky='w')
            self.selected_files_labels.append(label)
            progressbar=ctk.CTkProgressBar(self.selected_files_table, orientation='horizontal', mode='determinate', progress_color="#239b56")
            progressbar.grid(row=index, column=1, sticky='w')
            progressbar.set(0)
            self.progressbars.append(progressbar)

        self.process_files_frame.grid(row=0, column=0, sticky='nsew')
        self.file_selection_frame.grid_forget()

    def call_library(self, file, index):
        try:
            electrode_amnt=int(self.electrode_amnt_input.get())
            sampling_rate=int(self.sampling_rate_input.get())
            analyse_wells(file, electrode_amnt=electrode_amnt, sampling_rate=sampling_rate, parameters=self.parent.parameters)
        except:
            traceback.print_exc()
            self.progressbars[index].configure(progress_color='#922b21')
            self.progressbars[index].set(1)
            self.analysis_crashed=True

    def initiate_analysis(self):
        self.start_analysis_button.configure(state='disabled')
        self.back_button.configure(state='disabled')
        self.abort_analysis_button.configure(state='normal')

        for index, file in enumerate(self.selected_files):
            if self.abort_analysis_bool:
                self.abort_analysis_button.configure(text="Aborted Analysis")
                break
            # Start the analysis in another thread

            file=self.tree.item(file, "values")[0]

            process=threading.Thread(target=self.call_library, args=(file, index))
            process.start()

            # Read out the progress
            progressfile=f'{os.path.split(file)[0]}/progress.npy'

            # Make sure there is no pre-existing progressfile
            try: os.remove(progressfile)
            except: pass

            while True:
                while True: 
                    if self.analysis_crashed:
                        break
                    try:
                        progress=np.load(progressfile)
                        break
                    except: pass
                try:
                    if progress[0] == 'done':
                        break
                except: pass
                if self.analysis_crashed:
                    self.analysis_crashed=False
                    break
                try:
                    currentprogress=(progress[0]/progress[1])
                    self.progressbars[index].set(currentprogress)
                except: pass
            self.progressbar.set((index+1)/len(self.selected_files_labels))
        
        self.abort_analysis_button.configure(state="disabled")
        self.back_button.configure(state='normal')

    def abort_analysis_func(self):
        self.abort_analysis_bool=True
        self.abort_analysis_button.configure(text="Aborting analysis after current file")
        self.abort_analysis_button.configure(state="disabled")



class compress_files(ctk.CTkFrame):
    """
    Allows the user to compress/rechunk multiple files.
    """
    def __init__(self, parent):
        super().__init__(parent)

        self.parent=parent

        self.grid_columnconfigure(0, weight=1)
        self.grid_columnconfigure(1, weight=1)

        self.select_file_button=ctk.CTkButton(master=self, text="Select a file", command=self.openfiles)
        self.select_file_button.grid(row=0, column=0, columnspan=2, padx=10, pady=10, sticky='nesw')

        self.to_main_frame_button=ctk.CTkButton(master=self, text="Return to main menu", command=lambda: parent.show_frame(main_window))
        self.to_main_frame_button.grid(row=6, column=0, columnspan=2, padx=10, pady=10, sticky='nesw')

        gzip_level_text=ctk.CTkLabel(master=self, text="GZIP compression level: 1")
        gzip_level_text.grid(row=2, column=0, padx=10, pady=10, sticky='nesw', columnspan=2)

        def show_value(value):
            gzip_level_text.configure(text=f"GZIP compression level: {int(value)}")

        self.slider_value = ctk.IntVar(value=1)
        self.gzip_level_slider=ctk.CTkSlider(master=self, from_=1, to=9, orientation='horizontal', variable=self.slider_value, width=200, number_of_steps=8, command=show_value)
        self.gzip_level_slider.grid(row=3, column=0, padx=10, pady=(0, 10), columnspan=2, sticky='nesw')
        self.gzip_level_slider.configure(state='disabled')

        def lzf_selected():
            self.gzip_var.set(False)
            self.gzip_level_slider.configure(state='disabled')
            self.compression_method='lzf'

        def gzip_selected():
            self.lzf_var.set(False)
            self.gzip_level_slider.configure(state='normal')
            self.compression_method='gzip'

        self.lzf_var=ctk.IntVar()
        lzf_button=ctk.CTkCheckBox(self, text='LZF', onvalue=True, offvalue=False, variable=self.lzf_var, command=lzf_selected)
        lzf_button.grid(row=1, column=0, padx=10, pady=10, sticky='nesw')

        self.gzip_var=ctk.IntVar()
        gzip_button=ctk.CTkCheckBox(self, text='GZIP', onvalue=True, offvalue=False, variable=self.gzip_var, command=gzip_selected)
        gzip_button.grid(row=1, column=1, padx=10, pady=10, sticky='nesw')

        self.compress_all=ctk.BooleanVar()
        compress_all_button=ctk.CTkCheckBox(self, text='Compress all files in folder', onvalue=True, offvalue=False, variable=self.compress_all)
        compress_all_button.grid(row=4, column=0, padx=10, pady=10, sticky='nesw', columnspan=2)
        self.compress_all.set(False)

        # Default values
        self.lzf_var.set(True)
        self.selected_file=''
        self.compression_method='lzf'
        self.compression_level=1

        self.compress_files_button=ctk.CTkButton(master=self, text="Start compression", command=lambda: threading.Thread(target=self.compress_files_function).start())
        self.compress_files_button.grid(row=5, column=0, columnspan=2, padx=10, pady=10, sticky='nesw')

        self.abort_flag=False

    def openfiles(self):
        selectedfile = filedialog.askopenfilename(filetypes=[("MEA data", "*.h5")])
        if len(selectedfile)!=0:
            self.selected_file=selectedfile
            self.select_file_button.configure(text=selectedfile)

    def compress_files_function(self):
        if self.selected_file=='':
            return
        if self.compress_all.get():
            folder=os.path.dirname(self.selected_file)
            mea_files=[os.path.join(folder, f) for f in os.listdir(folder) if f.endswith(".h5")]
        else:
            mea_files=[self.selected_file]

        popup=ctk.CTkToplevel(self)
        popup.title('File Compression')
        try:
            popup.after(250, lambda: popup.iconbitmap(os.path.join(self.parent.icon_path)))
        except Exception as error:
            print(error)
        
        progressinfo=ctk.CTkLabel(master=popup, text=f'Compressing {len(mea_files)} file{"s" if len(mea_files) != 1 else ""}')
        progressinfo.grid(row=0, column=0, pady=10, padx=20)
        progressbarlength=300
        progressbar=ctk.CTkProgressBar(master=popup, orientation='horizontal', width=progressbarlength, mode='determinate', progress_color="#239b56")
        progressbar.grid(row=1, column=0, pady=10, padx=20)
        progressbar.set(0)
        info=ctk.CTkLabel(master=popup, text='')
        info.grid(row=2, column=0, pady=10, padx=20)
        finishedfiles=ctk.CTkLabel(master=popup, text='')
        finishedfiles.grid(row=3, column=0, pady=10, padx=20)

        # Aborting compression
        def abort_compression():
            self.abort_flag=True
            print("Aborting compression for further files...")
            abort_button.configure(text="Aborting compression for further files...")
            abort_button.configure(state='disabled')

        abort_button=ctk.CTkButton(master=popup, text="Abort compression", command=abort_compression)
        abort_button.grid(row=4, column=0, columnspan=2, padx=10, pady=10, sticky='nesw')
        popup.protocol("WM_DELETE_WINDOW", abort_compression)

        succesfiles=[]
        failedfiles=[]
    

        for file in range(len(mea_files)):
            if self.abort_flag:
                self.abort_flag=False
                popup.destroy()
                return
            info.configure(text=f"Compressing file {file+1} out of {len(mea_files)}")
            try:
                print(f"Compressing {mea_files[file]}")
                rechunk_dataset(fileadress=mea_files[file], compression_method=self.compression_method, compression_level=self.compression_level, always_compress_files=True)
                succesfiles.append(mea_files[file])
            except Exception as error:
                print(f"Could not compress {mea_files[file]}")
                traceback.print_exc()
                failedfiles.append(mea_files[file])
            currentprogress=((file+1)/len(mea_files))
            progressbar.set(currentprogress)
            finishedfiles_text=""
            if len(succesfiles) != 0:
                finishedfiles_text+="Compressed files:\n"
                for i in range(len(succesfiles)):
                    finishedfiles_text+=f"{succesfiles[i]}\n"
            if len(failedfiles) != 0:
                finishedfiles_text+="Failed files:\n"
                for i in range(len(failedfiles)):
                    finishedfiles_text+=f"{failedfiles[i]}\n"
            finishedfiles.configure(text=finishedfiles_text)
        
        abort_button.configure(state='disabled')
        info.configure(text="Finished compression")
        popup.protocol("WM_DELETE_WINDOW", popup.destroy)


class plotting_window(ctk.CTkFrame):
    """
    Allows the user to combine multiple MEA experiments to generate plots and discern between healthy and diseased cultures.
    """
    def __init__(self, parent):
        super().__init__(parent)
        
        self.parent=parent

        # Weights
        self.grid_columnconfigure(1, weight=1)
        self.grid_rowconfigure(1, weight=1)

        # Plot frames
        plotting_frame = ctk.CTkFrame(master=self, fg_color='transparent')
        plotting_frame.grid(row=1, column=0, sticky='nesw')

        select_file_frame = ctk.CTkFrame(master=self, fg_color=self.parent.gray_1)
        select_file_frame.grid(row=0, column=0, padx=5, pady=5, sticky='nesw', columnspan=4)
        select_file_frame.grid_columnconfigure(0, weight=1)

        features_over_time_frame = ctk.CTkFrame(master=plotting_frame, fg_color=self.parent.gray_1)
        features_over_time_frame.grid(row=1, column=0, padx=5, pady=5, sticky='nesw')
        features_over_time_frame.grid_columnconfigure(0, weight=1)
        features_over_time_frame.grid_columnconfigure(1, weight=1)

        boxplot_frame = ctk.CTkFrame(master=plotting_frame, fg_color=self.parent.gray_1)
        boxplot_frame.grid(row=2, column=0, sticky='nesw', padx=5, pady=5)
        boxplot_frame.grid_columnconfigure(0, weight=1)
        boxplot_frame.grid_columnconfigure(1, weight=1)


        # Selected files frame
        self.selected_files_frame = ctk.CTkScrollableFrame(master=self)
        self.selected_files_frame.grid(row=1, column=1, padx=5, pady=5, sticky='nesw')

        # Label frames
        self.assign_labels_frame = ctk.CTkFrame(master=self)
        self.assign_labels_frame.grid(row=1, column=2, padx=5, pady=5, sticky='new')

        label_main_frame=ctk.CTkFrame(master=self, fg_color='transparent')
        label_main_frame.grid(row=1, column=3, sticky='nesw')

        create_labels_frame = ctk.CTkFrame(master=label_main_frame, fg_color=self.parent.gray_1)
        create_labels_frame.grid(row=0, column=0, padx=5, pady=5, sticky='nesw')
        create_labels_frame.grid_columnconfigure(0, weight=1)

        label_settings_frame=ctk.CTkFrame(label_main_frame, fg_color=self.parent.gray_1)
        label_settings_frame.grid(row=1, column=0, padx=5, pady=5, sticky='nesw')
        label_settings_frame.grid_columnconfigure(0, weight=1)

        self.labels_frame = ctk.CTkFrame(master=label_main_frame, fg_color=self.parent.gray_1)
        self.labels_frame.grid(row=2, column=0, padx=5, pady=5, sticky='nesw')
        self.labels_frame.grid_columnconfigure(0, weight=1)

        label_main_frame.grid_rowconfigure(0, weight=0)
        label_main_frame.grid_rowconfigure(1, weight=0)
        label_main_frame.grid_rowconfigure(2, weight=0)

        # Values
        self.selected_folder = ''
        self.well_buttons=[]
        self.label_buttons=[]
        self.assigned_labels={}
        self.selected_label=[]
        self.default_colors=get_defaultcolors()
        self.well_amnt=None

        self.select_folder_button=ctk.CTkButton(master=select_file_frame, text='Select a folder', command=self.create_well_buttons)
        self.select_folder_button.grid(row=0, column=0, columnspan=2, pady=10, padx=10, sticky='nesw')

        # Features over time
        fot_label=ctk.CTkLabel(master=features_over_time_frame, text="Features over time", font=ctk.CTkFont(size=15))
        fot_label.grid(row=0, column=0, columnspan=2, padx=10, pady=10, sticky='w')

        prefix_label=ctk.CTkLabel(master=features_over_time_frame, text="DIV Prefix:")
        prefix_label.grid(row=1, column=0, padx=10, pady=10, sticky='w')

        self.prefix_entry=ctk.CTkEntry(master=features_over_time_frame)
        self.prefix_entry.grid(row=1, column=1, padx=10, pady=10, sticky='nesw')

        show_datapoints_label=ctk.CTkLabel(master=features_over_time_frame, text='Show datapoints:')
        show_datapoints_label.grid(row=2, column=0, padx=10, pady=10, sticky='w')

        self.show_datapoints_entry=ctk.CTkCheckBox(master=features_over_time_frame, text='')
        self.show_datapoints_entry.grid(row=2, column=1, padx=10, pady=10, sticky='nesw')

        create_plot_button=ctk.CTkButton(text="Plot Features over time", master=features_over_time_frame, command=self.create_plots)
        create_plot_button.grid(row=3, column=0, pady=10, padx=10, sticky='nesw', columnspan=2)

        # Combine measurements
        bp_label=ctk.CTkLabel(master=boxplot_frame, text="Boxplots", font=ctk.CTkFont(size=15))
        bp_label.grid(row=0, column=0, columnspan=2, padx=10, pady=10, sticky='w')

        bp_show_datapoints_label=ctk.CTkLabel(master=boxplot_frame, text='Show datapoints:')
        bp_show_datapoints_label.grid(row=1, column=0, padx=10, pady=10, sticky='w')

        self.bp_show_datapoints_entry=ctk.CTkCheckBox(master=boxplot_frame, text='')
        self.bp_show_datapoints_entry.grid(row=1, column=1, padx=10, pady=10, sticky='nesw')

        discern_wells_label=ctk.CTkLabel(master=boxplot_frame, text='Color wells:')
        discern_wells_label.grid(row=2, column=0, padx=10, pady=10, sticky='w')

        self.discern_wells_entry=ctk.CTkCheckBox(master=boxplot_frame, text='')
        self.discern_wells_entry.grid(row=2, column=1, padx=10, pady=10, sticky='nesw')

        create_plot_button=ctk.CTkButton(text="Create Boxplots", master=boxplot_frame, command=self.create_boxplots)
        create_plot_button.grid(row=3, column=0, pady=10, padx=10, sticky='nesw', columnspan=2)


        return_to_main = ctk.CTkButton(master=plotting_frame, text="Return to main menu", command=lambda: self.parent.show_frame(main_window), fg_color=parent.gray_1)
        return_to_main.grid(row=3, column=0, columnspan=2, padx=5, pady=5, sticky='nesw')

        # Create labels
        self.new_label_entry=ctk.CTkEntry(master=create_labels_frame, placeholder_text="Create a label; e.g.: \'control\'")
        self.new_label_entry.grid(row=0, column=0, pady=10, padx=10, sticky='nesw')

        new_label_button=ctk.CTkButton(master=create_labels_frame, text='Add Label', command=self.new_label)
        new_label_button.grid(row=1, column=0, padx=10, pady=5, sticky='nesw')

        save_label_button=ctk.CTkButton(master=label_settings_frame, text="Save Labels", command=self.save_labels)
        save_label_button.grid(row=2, column=0, padx=10, pady=(10,5), sticky='nesw')

        save_label_button=ctk.CTkButton(master=label_settings_frame, text="Import Labels", command=self.import_labels)
        save_label_button.grid(row=3, column=0, padx=10, pady=5, sticky='nesw')

        save_label_button=ctk.CTkButton(master=label_settings_frame, text="Reset Labels", command=self.reset_labels)
        save_label_button.grid(row=4, column=0, padx=10, pady=(5,10), sticky='nesw')

    def save_labels(self):
        file_path = filedialog.asksaveasfilename(
        defaultextension=".json",
        filetypes=[("JSON files", "*.json"), ("All files", "*.*")],
        title="Save JSON File"
        )

        if not file_path:
            return
        
        with open(file_path, "w") as json_file:
            json.dump(self.assigned_labels, json_file, indent=4)

        CTkMessagebox(message=f"Labels succesfully saved at {file_path}", icon="check", option_1="Ok", title="Saved Labels")

    def import_labels(self):
        file_path = filedialog.askopenfilename(
            filetypes=[("JSON files", "*.json"), ("All files", "*.*")],  # File type filters
            title="Open JSON File")

        if not file_path:
            return

        with open(file_path, "r") as json_file:
            imported_labels = json.load(json_file)
        self.set_labels(imported_labels)

    def set_labels(self, labels):
        # Reset labels
        for label_button in self.label_buttons:
            label_button.destroy()
        self.label_buttons=[]

        for label in labels.keys():
            self.create_label_button(label)
        self.assigned_labels=labels

        self.update_button_colours()

    def reset_labels(self):
        self.set_labels({})

    def create_plots(self):
        # Perform checks
        valid_groups=False
        for label in self.assigned_labels.keys():
            if len(self.assigned_labels[label]) > 0:
                valid_groups=True
        if not valid_groups:
            CTkMessagebox(title="Error",
                              message='Please create at least one label, and assign at least one well to it.',
                              icon="cancel",
                              wraplength=400)
            return
        if str(self.prefix_entry.get()) == '':
            CTkMessagebox(title="Error",
                              message='Please define the prefix that is used to indicate the age of the neurons, e.g. DIV, t, day',
                              icon="cancel",
                              wraplength=400)
            return

        # Call library
        try:
            file_path = filedialog.asksaveasfilename(
            defaultextension=".pdf",
            filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")],
            title="Save PDF File"
            )

            if not file_path:
                return

            pdf_path=features_over_time(folder=self.selected_folder, labels=copy.deepcopy(self.assigned_labels), div_prefix=str(self.prefix_entry.get()), output_fileadress=file_path, colors=self.default_colors, show_datapoints=bool(self.show_datapoints_entry.get()))
            CTkMessagebox(message=f"Figures succesfully saved at {file_path}", icon="check", option_1="Ok", title="Saved Figures")
            webbrowser.open(f"file://{pdf_path}")
        except Exception as error:
            CTkMessagebox(title="Error",
                              message='Something went wrong while creating the plots',
                              icon="cancel",
                              wraplength=400)
            
            traceback.print_exc()

    def create_boxplots(self):
        # check if there are groups that contain wells
        valid_groups=False
        for label in self.assigned_labels.keys():
            if len(self.assigned_labels[label]) > 0:
                valid_groups=True
        if not valid_groups:
            CTkMessagebox(title="Error",
                              message='Please create at least one label, and assign at least one well to it.',
                              icon="cancel",
                              wraplength=400)
            return
        
        try:
            file_path = filedialog.asksaveasfilename(
            defaultextension=".pdf",
            filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")],
            title="Save PDF File"
            )

            if not file_path:
                return
            
            pdf_path=combined_feature_boxplots(folder=self.selected_folder, labels=copy.deepcopy(self.assigned_labels), output_fileadress=file_path, colors=self.default_colors, show_datapoints=bool(self.bp_show_datapoints_entry.get()), discern_wells=bool(self.discern_wells_entry.get()), well_amnt=self.well_amnt)
            webbrowser.open(f"file://{pdf_path}")
            CTkMessagebox(message=f"Figures succesfully saved at {file_path}", icon="check", option_1="Ok", title="Saved Figures")
        except Exception as error:
            CTkMessagebox(title="Error",
                              message='Something went wrong while creating the boxplots',
                              icon="cancel",
                              wraplength=400)
            traceback.print_exc()



    def set_selected_label(self, label):
        self.selected_label=label

    def create_label_button(self, label):
        label_button=ctk.CTkButton(master=self.labels_frame, text=label, command=partial(self.set_selected_label, label), fg_color=self.default_colors[len(self.label_buttons)])
        label_button.grid(row=len(self.label_buttons), column=0, pady=5, padx=10, sticky='nesw')
        self.label_buttons.append(label_button)
        self.assigned_labels[label]=[]
        self.new_label_entry.delete(0, END)

    def new_label(self):
        label = self.new_label_entry.get()
        if (label == '') or (label in self.assigned_labels.keys()):
            return
        self.create_label_button(label)

    def update_button_colours(self):
        for well_button in self.well_buttons:
            well_button.configure(fg_color=self.parent.theme["CTkButton"]["fg_color"][1])
        for index, key in enumerate(self.assigned_labels.keys()):
            for well in self.assigned_labels[key]:
                self.well_buttons[well-1].configure(fg_color=self.default_colors[index])

    def well_button_func(self, button):
        # If the label was already selected, remove the selection
        if button in self.assigned_labels[self.selected_label]:
            self.assigned_labels[self.selected_label].remove(button)
        else:
            # First remove well from all labels
            for key in self.assigned_labels.keys():
                if button in self.assigned_labels[key]:
                    self.assigned_labels[key].remove(button)
            self.assigned_labels[self.selected_label].append(button)
        print(self.assigned_labels)
        self.update_button_colours()

    def create_well_buttons(self):
        folder=filedialog.askdirectory()
        if folder == '':
            return
        well_amnts=[]
        file_names=[]
        for root, dirs, files in os.walk(folder):
            for file in files:
                if file.endswith("Features.csv"):
                    data=pd.read_csv(os.path.join(root, file))
                    well_amnts.append(len(data))
                    file_names.append(Path(os.path.join(root, file)).stem)
        if len(well_amnts) < 1:
            CTkMessagebox(title="Error",
                              message='Not enough features-files found. Minimum amount is 1',
                              icon="cancel",
                              wraplength=400)
            return
        if not(np.min(well_amnts) == np.max(well_amnts)):
            CTkMessagebox(title="Error",
                              message='Not all experiments have the same amount of wells, please remove the exceptions from the folder.',
                              icon="cancel",
                              wraplength=400)
            return
        
        self.well_amnt=int(np.mean(well_amnts))
        self.selected_folder=folder
        self.select_folder_button.configure(text=self.selected_folder)
        width, height = self.parent.calculate_optimal_grid(np.mean(well_amnts))
        counter=1

        for w in range(width):
            for h in range(height):
                well_button=ctk.CTkButton(master=self.assign_labels_frame, text=counter, command=partial(self.well_button_func, counter), height=100, width=100, font=ctk.CTkFont(size=25))
                well_button.grid(row=w, column=h, sticky='nesw')
                self.well_buttons.append(well_button)
                counter+=1

        for i, file in enumerate(file_names):
            file_label=ctk.CTkLabel(master=self.selected_files_frame, text=file)
            file_label.grid(row=i, column=0, sticky='w', padx=10, pady=2)

        self.select_folder_button.configure(state='disabled')

def MEA_GUI():
    """
    Launches the graphical user interface (GUI) of the CureQ MEA analysis tool.

    Always launch the function with an "if __name__ == '__main__':" guard as follows:
        if __name__ == "__main__":
            MEA_GUI()
    """

    app = MainApp()
    app.mainloop()


if __name__ == "__main__":
    MEA_GUI()