"""
CrimsonCalc (v1.0.0)
© 2025  Miha Virant
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 3.

CrimsonCalc is developed at the Jožef Stefan Institute, Extreme Conditions Chemistry Laboratory,
Jamova cesta 39, 1000 Ljubljana, Slovenia. For more information, visit https://eccl.ijs.si/

Part of this script was developed with the assistance of ChatGPT (GPT-4 by OpenAI).
"""

import os
import sys
import glob
import re
import gc
import time
import tempfile
import tkinter as tk
import numpy as np
import matplotlib.pyplot as plt
from pybaselines import misc, classification
from brukeropus import read_opus
from brukeropus.file import get_param_label
from lmfit.models import PseudoVoigtModel
from scipy.signal import find_peaks
from tkinter import ttk, filedialog, messagebox, PhotoImage
from tkinterdnd2 import DND_FILES, TkinterDnD
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg


ruby_results = []

def categorize_files(file_paths):
    """
    Takes a list or set of selected file paths and returns four sorted lists:
    ruby_ref_files, opus_ref_files, ruby_files, opus_files
    
    Used in Tab2 to determine the files to process. The lists are then processed by process_opus_files or process_ruby_file.
    """
    ruby_ref_files = []
    ruby_files = []
    opus_ref_files = []
    opus_files = []

    for f in file_paths:
        base = os.path.basename(f).lower()
        if f.lower().endswith('.txt'):
            if 'reference' in base:
                ruby_ref_files.append(f)
            else:
                ruby_files.append(f)
        elif re.search(r"\.\d$", f):
            if 'reference' in base:
                opus_ref_files.append(f)
            else:
                opus_files.append(f)

    return (
        sorted(ruby_ref_files),
        sorted(opus_ref_files),
        sorted(ruby_files),
        sorted(opus_files)
    )

def process_opus_files(
    opus_files,
    auto_refresh=True,
    lambda_ref_tab2_value=694.28,
    model_name="FABc",
    tab2_temp_corr_var=False,
    tab2_tref_entry=298,
    tab2_tsample_entry=298
):
    """
    Process the opus files from the list using brukeropus package. Initially check
    for detector overflow, which is set to 62500 as default.
    """
    for file in opus_files:
        try:
            data = read_opus(file)
            number_scans = int(data.params['nss'])
            det_sat = 62500 * number_scans

            if 'e' in data.all_data_keys:
                spectral_data = getattr(data, 'e')
                if hasattr(spectral_data, 'x') and hasattr(spectral_data, 'y'):
                    x_values = np.round(spectral_data.x, 6)
                    y_len = spectral_data.y
                    num_spectra = len(y_len)
                    y_values = np.round(spectral_data.y.reshape(num_spectra, len(x_values)), 6)

                    base_name = os.path.splitext(file)[0]
                    result_file = f"{base_name}_result.txt"
                    overflow_file = f"{base_name}_overflow.txt"

                    with open(result_file, 'w', encoding='utf-8') as f:
                        f.write(f"{base_name}\n\n")
                        f.write("Spectrum_ID\tλ1\tλ1_SE\tR²\tSNR\tFWHM_1\tσ1\tη1\tλ2\tλ2_SE\tFWHM_2\tσ2\tη2\n")

                    output_folder = base_name
                    os.makedirs(output_folder, exist_ok=True)

                    for i in range(num_spectra):
                        spectrum_number = i + 1
                        output_file = os.path.join(output_folder, f"{os.path.basename(base_name)}_spectrum{spectrum_number}.txt")
                        max_intensity = np.max(y_values[i])
                        overflow = False

                        with open(output_file, 'w') as f:
                            f.write(f"{base_name}_spectrum{spectrum_number}\n")
                            f.write("wavelength (nm) | intensity\n")
                            np.savetxt(f, np.column_stack((x_values, y_values[i])), fmt="%.6f", delimiter='\t')
                            if max_intensity >= det_sat:
                                f.write("WARNING: Detector overflow detected!")
                                overflow = True
                                with open(overflow_file, "a") as overflow_log:
                                    overflow_log.write(f"{output_file}\n")

                        if not overflow:
                            spectrum_id = f"{os.path.basename(base_name)}_spectrum{spectrum_number}"
                            baseline_correction(
                                (x_values, y_values[i]),
                                result_file,
                                spectrum_id,
                                model_name=model_name,
                                output_folder=output_folder
                            )

            process_result_file(
                result_file,
                auto_refresh,
                lambda_ref_tab2_value,
                tab2_temp_corr_var,
                tab2_tref_entry,
                tab2_tsample_entry
            )
            gc.collect()

        except Exception as e:
            print(f"Error processing file {file}: {e}")


def process_ruby_file(
    ruby_files,
    auto_refresh=True,
    lambda_ref_tab2_value=694.28,
    model_name="FABc",
    tab2_temp_corr_var=False,
    tab2_tref_entry=298,
    tab2_tsample_entry=298
):
    """
    Process the .txt files, i.e. spectral files in text format with formatted extension to easily
    distinguish them from undesired .txt files.
    """
    for file in ruby_files:
        try:
            base_name = os.path.splitext(file)[0]
            output_folder = base_name
            os.makedirs(output_folder, exist_ok=True)

            result_file = f"{os.path.basename(base_name)}_result.txt"
            global ruby_results
            ruby_results.append(os.path.abspath(result_file))


            with open(result_file, 'w', encoding='utf-8') as f:
                f.write(f"{os.path.basename(base_name)}\n\n")
                f.write("Spectrum_ID\tλ1\tλ1_SE\tR²\tSNR\tFWHM_1\tσ1\tη1\tλ2\tλ2_SE\tFWHM_2\tσ2\tη2\n")

            x_values, y_values = readin_spectral_data(file)

            spectrum_number = f"{os.path.basename(base_name)}"

            output_file = os.path.join(output_folder, f"{os.path.basename(base_name)}.txt")
            with open(output_file, 'w') as f:
                f.write(f"{os.path.basename(base_name)}\n")
                f.write("wavelength (nm) | intensity\n")
                np.savetxt(f, np.column_stack((x_values, y_values)), fmt="%.6f")

            baseline_correction(
                (x_values, y_values),
                result_file,
                os.path.basename(base_name),
                model_name=model_name,
                output_folder=output_folder
            )

            process_result_file(
                result_file,
                auto_refresh,
                lambda_ref_tab2_value,
                tab2_temp_corr_var,
                tab2_tref_entry,
                tab2_tsample_entry
            )
            
        except Exception as e:
            print(f"Skipping file '{file}': {e}")

        finally:
            gc.collect()

    if ruby_results: 
        try:
            with open("Ruby_Report.txt", "w", encoding="utf-8") as report:
                report.write(
                    "Spectrum_ID\tPressure\tλ1\tλ1_SE\tR²\tSNR\tFWHM_1\tσ1\tη1\t"
                    "λ2\tλ2_SE\tFWHM_2\tσ2\tη2\t"
                    "λ3\tλ3_SE\tFWHM_3\tσ3\tη3\n"
                )
                for result_path in ruby_results:
                    try:
                        with open(result_path, "r", encoding="utf-8") as rf:
                            lines = rf.readlines()
                            if len(lines) >= 6:  # Ensure lines[6] exists
                                if tab2_temp_corr_var:
                                    report.write(lines[6])  # 7th line
                                else:
                                    report.write(lines[5])  # 6th line
                    except Exception as e:
                        print(f"Failed to read {result_path}: {e}")
            print("Ruby_Report.txt created.")
        except Exception as e:
            print(f"Error creating Ruby_Report.txt: {e}")
        ruby_results.clear()



def readin_spectral_data(source):
    """
    Accepts either a file path or raw string (e.g. clipboard content), and returns (x, y) NumPy arrays.
    Only parses lines with two valid numbers.
    """
    x_values = []
    y_values = []

    if isinstance(source, str) and os.path.isfile(source):
        with open(source, "r", encoding="utf-8") as f:
            lines = f.readlines()
    elif isinstance(source, str):
        lines = source.strip().splitlines()
    elif isinstance(source, list):
        lines = source
    else:
        raise ValueError("Unsupported source type.")

    for line in lines:
        match = re.match(
            r"^\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)[,\s;]+([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)\s*$",
            line
        )
        if match:
            x, y = map(float, match.groups())
            x_values.append(x)
            y_values.append(y)

    print('Data imported.')
    return np.array(x_values), np.array(y_values)


def baseline_correction(data_input, result_file, spectrum_id, model_name="FABc", output_folder="."):
    """
    Routine to substract the baseline using a selection of pybaseline models
    """
    if isinstance(data_input, tuple) and len(data_input) == 2:
        wavelength, intensity = data_input
        header = f"Spectrum {spectrum_id}"
    else:
        wavelength, intensity = readin_spectral_data(data_input)
        header = os.path.basename(data_input).replace(".txt", "")

    
    if model_name == "None":
        intensity = intensity
    elif model_name == "BEADS":
        for _ in range(1):
            baseline_model = misc.beads(
                intensity, 
                freq_cutoff=0.005,  # 0.005 (float)
                lam_0=1.0,  # 1 (float)
                lam_1=1.0,  # 1 (float)
                lam_2=1.0,  # 1 (float)
                asymmetry=6.0,  # 6.0 (float)
                filter_type=1,  # 1, order=2*filter_type (int)
                cost_function=2,    # 2, {2, 1, "l1_v1", "l1_v2"}, optional
                max_iter=50,    # 50 (int)
                tol=0.01,   # 1e-2 (float)
                eps_0=1e-06,    # 1e-6 (float)
                eps_1=1e-06,    # 1e-6 (float)
                fit_parabola=True, # False (bool)
                smooth_half_window=None,    # None, optional (int)
                x_data=wavelength
            )
            baseline = baseline_model[0]
            intensity = intensity - baseline

    elif model_name == "Rubberband":
        for _ in range(1):
            baseline_model = classification.rubberband(
                intensity, 
                segments=1,
                lam=3,
                diff_order=3,
                weights=None,
                smooth_half_window=2
            )
            baseline = baseline_model[0]
            intensity = intensity - baseline

    elif model_name == "FABc":
        for _ in range(1):
            baseline_model = classification.fabc(
                intensity, 
                lam=1e6,    # 1e6 (float)
                scale=None, # None = half of the value fomr optimize_window(), optional
                num_std=5.0,    # 3.0 (float)
                diff_order=3,   # 2 (int)
                min_length=2,   # 2 (int)
                weights=None,   # None, shape (N,), optional (array_like)
                weights_as_mask=False,  #False (bool)
                x_data=np.array(wavelength)
            )
            baseline = baseline_model[0]
            intensity = intensity - baseline

    # Generate output filename
    output_filename = os.path.join(output_folder, f"{spectrum_id}.procBC.txt")

    # Save baseline-corrected spectrum
    np.savetxt(output_filename, np.column_stack((wavelength, intensity)), fmt="%.6f", delimiter="\t", header=header, comments='')
    print(f"Baseline OK.")

    # After baseline correction, run fitting procedure on this file
    result, filtered_wavelength, filtered_intensity, r_squared, snr  = fitting(
        wavelength, intensity,
        result_file,
        spectrum_id,
        base_name=os.path.join(output_folder, spectrum_id)
    )


    gc.collect()
    return result, filtered_wavelength, filtered_intensity, r_squared, snr


def fitting(wavelength, intensity, result_file, spectrum_number, base_name):
    """
    Fitting procedure using scipy and lmfit package. First a crude determination of peaks is performed, which are then used for 
    fitting 2 or 3 pseudo-Voigt functions to the baseline-corrected data.
    """
    peaks, properties = find_peaks(intensity, height=max(intensity) * 0.1, distance=10)
    if len(peaks) < 2:
        print(f"Warning: Less than 2 significant peaks found in {base_name}")
        return

    peak_heights = properties['peak_heights']
    strongest_peak_index = np.argmax(peak_heights)
    strongest_peak = wavelength[peaks[strongest_peak_index]]
    lower_x_peak = min(wavelength[peaks])

    peak1_center = strongest_peak
    peak2_center = lower_x_peak

    model = PseudoVoigtModel(prefix='peak1_') + PseudoVoigtModel(prefix='peak2_')
    params = model.make_params(
        peak1_amplitude=np.max(intensity) * 0.9,
        peak2_amplitude=np.max(intensity) * 0.6,
        peak1_center=peak1_center,
        peak2_center=peak2_center,
        peak1_sigma=0.6,
        peak2_sigma=0.6,
        peak1_fraction=1,
        peak2_fraction=0.75
    )

    constraints = {
        'peak1_sigma': {'min': 0, 'max': 15},
        'peak2_sigma': {'min': 0, 'max': 15},
        'peak1_fraction': {'min': 0, 'max': 1},
        'peak2_fraction': {'min': 0, 'max': 1}
    }
    for name, lim in constraints.items():
        params[name].set(**lim)

    mask = (wavelength >= (peak2_center - 5)) & (wavelength <= (peak1_center + 5))
    filtered_wavelength = wavelength[mask]
    filtered_intensity = intensity[mask]
    
    # Weighing scheme: threshold-based selection with nonlinear scaling of higher intensities
    threshold = 0.2 * np.max(filtered_intensity)
    weights = np.where(
        filtered_intensity >= threshold,
        (filtered_intensity / np.max(filtered_intensity)) ** 2,
        0.15
    )


    result = model.fit(filtered_intensity, params, x=filtered_wavelength, weights=weights, method='leastsq')
    fitted_values = {name: param.value for name, param in result.params.items()}
    uncertainties = {name: param.stderr for name, param in result.params.items()}

    residuals = filtered_intensity - result.best_fit
    snr = np.mean(filtered_intensity) / np.std(residuals, ddof=1)


    # Optional: Add third peak if SNR is low
    if snr < 10:
        print("SNR < 10 detected; adding a third peak.")
        model = (
            PseudoVoigtModel(prefix='peak1_') +
            PseudoVoigtModel(prefix='peak2_') +
            PseudoVoigtModel(prefix='peak3_')
        )
        third_center = fitted_values['peak1_center'] + 5
        params = model.make_params(
            **{f'peak{i}_{p}': fitted_values[f'peak{i}_{p}'] for i in (1, 2) for p in ['amplitude', 'center', 'sigma', 'fraction']},
            peak3_amplitude=np.max(filtered_intensity) * 0.3,
            peak3_center=third_center,
            peak3_sigma=0.7,
            peak3_fraction=0.5
        )

        third_constraints = {
            'peak3_center': {'min': fitted_values['peak1_center'] + 1, 'max': fitted_values['peak1_center'] + 15},
            'peak3_amplitude': {'min': 0, 'max': fitted_values['peak1_amplitude'] * 0.3},
            'peak3_sigma': {'min': 0, 'max': 15},
            'peak3_fraction': {'min': 0, 'max': 1}
        }
        for name, lim in {**constraints, **third_constraints}.items():
            params[name].set(**lim)

        result = model.fit(filtered_intensity, params, x=filtered_wavelength, weights=weights, method='leastsq')
        fitted_values = {name: param.value for name, param in result.params.items()}
        uncertainties = {name: param.stderr for name, param in result.params.items()}

    # Compute final fit statistics
    residuals = filtered_intensity - result.best_fit
    snr = np.mean(filtered_intensity) / np.std(residuals, ddof=1)
    r_squared = result.rsquared

    # Write results
    with open(result_file, 'a', encoding='utf-8') as f:
        line = f"{spectrum_number.split('_spectrum')[-1]}\t"
        line += (
            f"{safe_fmt(fitted_values['peak1_center'])}\t{safe_fmt(uncertainties['peak1_center'])}\t"
            f"{safe_fmt(r_squared)}\t{safe_fmt(snr)}\t"
            f"{safe_fmt(fitted_values['peak1_fwhm'])}\t{safe_fmt(fitted_values['peak1_sigma'])}\t"
            f"{safe_fmt(fitted_values['peak1_fraction'])}\t"
            f"{safe_fmt(fitted_values['peak2_center'])}\t{safe_fmt(uncertainties['peak2_center'])}\t"
            f"{safe_fmt(fitted_values['peak2_fwhm'])}\t{safe_fmt(fitted_values['peak2_sigma'])}\t"
            f"{safe_fmt(fitted_values['peak2_fraction'])}"
        )
        if snr < 10:
            line += (
                f"\t{safe_fmt(fitted_values.get('peak3_center'))}\t{safe_fmt(uncertainties.get('peak3_center'))}"
                f"\t{safe_fmt(fitted_values.get('peak3_fwhm'))}\t{safe_fmt(fitted_values.get('peak3_sigma'))}\t"
                f"{safe_fmt(fitted_values.get('peak3_fraction'))}"
            )
        f.write(line + "\n")


    # Plotting
    max_peak_width = max(fitted_values['peak1_fwhm'], fitted_values['peak2_fwhm'])
    min_x = min(fitted_values['peak1_center'], fitted_values['peak2_center']) - 3 * max_peak_width
    max_x = max(fitted_values['peak1_center'], fitted_values['peak2_center']) + 3 * max_peak_width
    dense_x = np.linspace(min_x, max_x, 5000)
    fitted_function = result.eval(x=dense_x)

    plt.figure(figsize=(12, 7), dpi=300)
    plt.plot(filtered_wavelength, filtered_intensity, 'k.', label='Experimental Data')
    plt.plot(dense_x, fitted_function, 'r-', linewidth=1, label='Fitted Function')
    plt.xlim(min_x, max_x)
    plt.ylim(0, max(filtered_intensity) * 1.1)
    plt.xlabel('Wavelength (nm)')
    plt.ylabel('Intensity')
    plt.title(f'{base_name}')
    plt.legend()
    plt.grid(False)
    lambda1_str = f"λ(R1): {fitted_values['peak1_center']:.4f} ± {uncertainties['peak1_center']:.4f} nm" if uncertainties['peak1_center'] else f"λ(R1): {fitted_values['peak1_center']:.4f} nm"
    lambda2_str = f"λ(R2): {fitted_values['peak2_center']:.4f} ± {uncertainties['peak2_center']:.4f} nm" if uncertainties['peak2_center'] else f"λ(R2): {fitted_values['peak2_center']:.4f} nm"
    
    r2_str = f"R²: {r_squared:.3f}"
    snr_str = f"SNR: {snr:.1f}"

    plt.text(
        0.05, 0.95,
        f"{lambda1_str}\n{lambda2_str}\n{r2_str}\n{snr_str}",
        transform=plt.gca().transAxes,
        fontsize=10,
        verticalalignment='top',
        bbox=dict(facecolor='white', edgecolor='gray', boxstyle='round,pad=0.3')
    )

    plt.savefig(f"{base_name}.png", bbox_inches='tight')  # raster
    plt.savefig(f"{base_name}.svg", format='svg', bbox_inches='tight')  # vector
    plt.close('all')
    gc.collect()

    return result, filtered_wavelength, filtered_intensity, r_squared, snr


def ruby2020_pressure_with_correlation(lambda_sample, lambda_ref,
                                       sigma_sample_fit, sigma_ref_fit,
                                       sigma_instr=0.005,
                                       A=1.870, sigma_A=0.01,
                                       B=5.63, sigma_B=0.03,
                                       rho=0):
    """
    Calculates pressure and uncertainty from Ruby2020 scale with optional correlation.

    Parameters:
    - lambda_sample: fitted peak center (nm)
    - lambda_ref: reference wavelength (nm)
    - sigma_sample_fit: peak fitting uncertainty for sample (nm)
    - sigma_ref_fit: peak fitting uncertainty for ref (nm)
    - sigma_instr: instrumental calibration uncertainty (default 0.005 nm)
    - rho: correlation coefficient between λ_sample and λ_ref [-1, 1]
    """

    # Combine fitting and instrument errors
    sigma_sample = np.sqrt(sigma_instr**2 + sigma_sample_fit**2)
    sigma_ref = np.sqrt(sigma_instr**2 + sigma_ref_fit**2)

    delta_lambda = lambda_sample - lambda_ref
    x = delta_lambda / lambda_ref

    P = A * 1000 * x * (1 + B * x)

    dP_dA = 1000 * x * (1 + B * x)
    dP_dB = A * 1000 * x**2
    dP_dls = A * 1000 / lambda_ref * (1 + 2 * B * x)
    dP_dlr = A * 1000 * (-lambda_sample / lambda_ref**2) * (1 + 2 * B * x)

    # Add correlation-aware covariance term
    covariance_term = 2 * dP_dls * dP_dlr * rho * sigma_sample * sigma_ref

    sigma_P_squared = (dP_dA * sigma_A)**2 + \
                      (dP_dB * sigma_B)**2 + \
                      (dP_dls * sigma_sample)**2 + \
                      (dP_dlr * sigma_ref)**2 + \
                      covariance_term

    sigma_P = np.sqrt(sigma_P_squared)
    return P, sigma_P


def safe_fmt(val):
    return f"{val:.6f}" if val is not None else "NaN"


def lambda_temp_corr(T):
    if T < 50:
        return -0.887
    elif 50 <= T < 296:
        delta_T = T - 296  # relative to room temperature
        return (
            0.00664 * delta_T
            + 6.76e-6 * (delta_T ** 2)
            - 2.33e-8 * (delta_T ** 3)
        )
    else:  # T >= 296
        delta_T = T - 296
        return (
            0.00746 * delta_T
            - 3.01e-6 * (delta_T ** 2)
            + 8.76e-9 * (delta_T ** 3)
        )


def process_result_file(
    result_file,
    auto_refresh=True,
    lambda_ref_tab2_value=694.28,
    tab2_temp_corr_var=False,
    tab2_tref_entry=298,
    tab2_tsample_entry=298
):
    with open(result_file, 'r', encoding='utf-8') as f:
        lines = f.readlines()

    # Determine processing mode based on file content
    is_reference = any('reference' in line.lower() for line in lines)
    is_ruby = any('ruby' in line.lower() for line in lines)


    # Extract data from line 4 onwards (tab-separated)
    data = [line.strip().split('\t') for line in lines[3:]]
    
    # Check if any line has more than 11 columns (i.e., 3rd peak was added)
    third_peak_present = any(len(row) > 11 for row in data)

    if is_reference:
        # Reference mode (original logic)
        # Extract second column values (convert to float)
        second_column_values = [float(row[1]) for row in data if len(row) > 1]
        # Calculate average and standard deviation
        avg_value = np.mean(second_column_values)
        std_dev = np.std(second_column_values, ddof=1)

        if third_peak_present:
            lines[2] = (
                "Spectrum_ID\tλ1\tλ1_SE\tR²\tSNR\tFWHM_1\tσ1\tη1\t"
                "λ2\tλ2_SE\tFWHM_2\tσ2\tη2\t"
                "λ3\tλ3_SE\tFWHM_3\tσ3\tη3\n"
            )

        # Insert average and standard deviation in the second and third lines
        lines.insert(1, f"{avg_value:.6f}\n")
        lines.insert(2, f"± {std_dev:.6f}\n")

        # Rewrite the file with updated content
        with open(result_file, 'w', encoding='utf-8') as f:
            f.writelines(lines)

        # Change the file extension to `.ini`
        new_result_file = result_file.replace('_result.txt', '.ini')
        os.rename(result_file, new_result_file)
        print(f"Reference .ini file created.")

    else:
        # Ruby mode - Pressure calculation
        pressure_values = []
        lambda_ref, ini_file = get_lambda_ref(auto_refresh, lambda_ref_tab2_value)

        for row in data:
            if len(row) > 1:
                peak_center = float(row[1])

                tab2_lambda_ref_temp = lambda_ref
                tab2_peak_center_temp = peak_center


                # Apply temperature correction if enabled
                if tab2_temp_corr_var:
                    try:
                        tab2_T_ref = float(tab2_tref_entry)
                        tab2_delta_lambda_T_ref = lambda_temp_corr(tab2_T_ref)

                        tab2_T_sample = float(tab2_tsample_entry)
                        tab2_delta_lambda_T_sample = lambda_temp_corr(tab2_T_sample)
                        
                        tab2_lambda_ref_temp = lambda_ref - tab2_delta_lambda_T_ref
                        tab2_peak_center_temp = peak_center - tab2_delta_lambda_T_sample
                    except Exception as e:
                        print("Tab2 temperature correction error:", e)

                pressure = 1870 * ((tab2_peak_center_temp - tab2_lambda_ref_temp) / tab2_lambda_ref_temp) * (1 + 5.63 * (tab2_peak_center_temp - tab2_lambda_ref_temp) / tab2_lambda_ref_temp)
                row.insert(1, f"{pressure:.6f}")  # Insert pressure value
                pressure_values.append(pressure)

        # Calculate average pressure and standard deviation
        avg_pressure = np.mean(pressure_values)
        std_pressure = np.std(pressure_values, ddof=1)

        # Insert average pressure and standard deviation into result file
        lines.insert(1, f"Pressure: {avg_pressure:.6f} ± {std_pressure:.6f}\nλ_ref = {lambda_ref:.6f} nm\n\n")

        lines.insert(1,
            f"Pressure: {avg_pressure:.6f} ± {std_pressure:.6f}\n"
            f"λ_ref = {lambda_ref:.6f} nm"

            + (
                f"\t\t({os.path.splitext(ini_file)[0]})"
                if ini_file else ""
            )
            + (
                f"\nT_ref = {tab2_T_ref:.2f} K, T_sample = {tab2_T_sample:.2f} K"
                if tab2_temp_corr_var else ""
            )
            + "\n\n"
        )



        # Update header for Ruby pressure mode
        if third_peak_present:
            lines[2] = (
                "Spectrum_ID\tPressure\tλ1\tλ1_SE\tR²\tSNR\tFWHM_1\tσ1\tη1\t"
                "λ2\tλ2_SE\tFWHM_2\tσ2\tη2\t"
                "λ3\tλ3_SE\tFWHM_3\tσ3\tη3\n"
            )
        else:
            lines[2] = "Spectrum_ID\tPressure\tλ1\tλ1_SE\tR²\tSNR\tFWHM_1\tσ1\tη1\tλ2\tλ2_SE\tFWHM_2\tσ2\tη2\n"

        # Rewrite data section with inserted pressure values
        updated_data = ["\t".join(row) + "\n" for row in data]
        lines[3:] = updated_data

        # Rewrite the file with updated content
        with open(result_file, 'w', encoding='utf-8') as f:
            f.writelines(lines)

    print(f"Result file complete.")


#Script that extracts the reference lambda value from the .ini file. If the file is not present, the default value will be used.
def get_lambda_ref(auto_refresh=True, lambda_ref_tab2_value=694.28):
    ini_file_used = None  # Default if no .ini file is used

    if auto_refresh:
        ini_files = glob.glob("*.ini")
        if ini_files:
            ini_file = ini_files[0]  # Assuming one .ini file in the folder
            ini_file_used = ini_file
            with open(ini_file, "r") as f:
                lines = f.readlines()
                if len(lines) >= 2:
                    try:
                        return float(lines[1].strip()), ini_file_used
                    except ValueError:
                        print("Invalid lambda_ref in .ini file. Using default.")
        return 694.28, ini_file_used  # Return default value with None or ini_file_used
    else:
        return float(lambda_ref_tab2_value), ini_file_used

def get_tab2_temperatures():
    """
    Checks if temperature correction is enabled in Tab 2 and returns T_ref and T_sample.
    Returns None if not enabled.
    """




# ----- Start of an App -----
class RealTimePlotApp:
    def __init__(self, root):
        self.root = root
        self.root.title("CrimsonCalc")

        # Create Tabs
        self.notebook = ttk.Notebook(root)
        self.tab1 = ttk.Frame(self.notebook)
        self.tab2 = ttk.Frame(self.notebook)

        self.notebook.add(self.tab1, text="  Single Spectrum Processing  ")
        self.notebook.add(self.tab2, text="  Batch Data Processing  ")
        self.notebook.pack(expand=True, fill="both")

        # Tab 1 - Single Spectrum Processing
        self.data = []
        self.filtered_wavelength = None
        self.filtered_intensity = None

        self.corrected_y = None
        self.create_plotting_tab()

        self.update_plot()

        # Tab 2 - Opus Data Processing
        self.create_opus_processor_tab()


        # Tab 3 - Pressure Calculator
        self.tab3 = ttk.Frame(self.notebook)
        self.notebook.add(self.tab3, text="  HP Calculator  ")
        self.create_pressure_calculator_tab()


        # Always present Quit button
        self.create_quit_button()

        
    # ----- TAB 1: Single Spectrum Processing -----

    def create_plotting_tab(self):
        self.tab1_frame = ttk.Frame(self.tab1)
        self.tab1_frame.grid(row=0, column=0, padx=10, pady=10, sticky="nw")
        ttk.Label(self.tab1_frame, text="Single Spectrum Plotting and Processing", anchor="w", font=("Arial", 14, "bold"), justify="left").grid(row=0, column=0, sticky="nw", padx=10, pady=5)

        # Data Input
        self.input_frame = ttk.LabelFrame(self.tab1_frame, text=" Data Input")
        self.input_frame.grid(row=1, column=0, sticky="nw", pady=(10, 5))

        # Experiment Name Field
        ttk.Label(self.input_frame, text="Experiment Name:").grid(row=1, column=0, sticky='w', pady=5, padx=(5,5))
        self.experiment_name = ttk.Entry(self.input_frame)
        self.experiment_name.grid(row=1, column=1, sticky='e', pady=5, padx=(5,5))

        # Paste Data from Clipboard Button
        ttk.Button(self.input_frame, text="Paste Data from Clipboard", command=self.paste_data).grid(row=2, column=1, columnspan=2, sticky="e", pady=5, padx=(5,5))

        # Table with data
        self.table_frame = ttk.LabelFrame(self.tab1_frame, text=" Experimental Data")
        self.table_frame.grid(row=3, column=0, columnspan=2, sticky="ew")
        
        # Scrollable Treeview
        tree_container = ttk.Frame(self.table_frame)
        tree_container.pack(fill="both", expand=True)

        tree_scrollbar = ttk.Scrollbar(tree_container, orient="vertical")
        self.tree = ttk.Treeview(tree_container, columns=("X", "Y"), show="headings", yscrollcommand=tree_scrollbar.set, style="Custom.Treeview")

        tree_scrollbar.config(command=self.tree.yview)
        tree_scrollbar.pack(side="right", fill="y")
        self.tree.pack(side="left", fill="both", expand=True)

        self.tree.heading("X", text="Wavelength (nm)")
        self.tree.heading("Y", text="Intensity")
        self.tree.column("X", anchor="center")
        self.tree.column("Y", anchor="center")

        # Delete Buttons in the centre of the same row
        delete_buttons_frame = ttk.Frame(self.table_frame)
        delete_buttons_frame.pack(pady=5)

        ttk.Button(delete_buttons_frame, text="Delete Selected", command=self.delete_selected).pack(side=tk.LEFT, padx=5)
        ttk.Button(delete_buttons_frame, text="Delete All", command=self.delete_all).pack(side=tk.LEFT, padx=5)

        # Data Processing Settings
        self.settings_frame = ttk.LabelFrame(self.tab1_frame, text=" Data Processing Settings")
        self.settings_frame.grid(row=6, columnspan=2, pady=10, sticky="w")

        ttk.Label(self.settings_frame, text="Baseline model").grid(row=1, column=0, sticky="w", pady=5, padx=(5,5))

        self.bc_selection = ttk.Combobox(self.settings_frame, values=["None", "Rubberband", "BEADS", "FABc"])
        self.bc_selection.grid(row=1, column=1, pady=5, padx=(5,10))
        self.bc_selection.set("BEADS")

        ttk.Button(self.settings_frame, text="Process Data", command=self.process_data).grid(row=2, column=1, sticky="e", pady=5, padx=(5,5))
        
        # Display Region (Radio Buttons)
        self.display_settings_frame = ttk.LabelFrame(self.tab1_frame, text=" Plot Settings")
        self.display_settings_frame.grid(row=7, column=0, columnspan=2, sticky='w', pady=10)

        self.display_region = tk.StringVar(value="full")  # default

        ttk.Radiobutton(self.display_settings_frame, text="Full Range", variable=self.display_region, value="full", command=self.update_plot).grid(row=0, column=0, sticky="w", padx=(5,5), pady=5)
        ttk.Radiobutton(self.display_settings_frame, text="Fitted Region", variable=self.display_region, value="fitted", command=self.update_plot).grid(row=0, column=1, sticky="w", padx=(5,5), pady=5)

        # Plot Component Visibility
        self.show_original = tk.BooleanVar(value=True)
        self.show_corrected = tk.BooleanVar(value=True)
        self.show_fit = tk.BooleanVar(value=True)

        ttk.Checkbutton(self.display_settings_frame, text="Original Data", variable=self.show_original, command=self.update_plot).grid(row=1, column=0, sticky="w", padx=(5,5), pady=5)
        ttk.Checkbutton(self.display_settings_frame, text="Corrected Data", variable=self.show_corrected, command=self.update_plot).grid(row=1, column=1, sticky="w", padx=(5,5), pady=5)
        ttk.Checkbutton(self.display_settings_frame, text="Fitted Model", variable=self.show_fit, command=self.update_plot).grid(row=1, column=2, sticky="w", padx=(5,5), pady=5)

        # Pressure Frame
        self.pressure_frame = ttk.LabelFrame(self.tab1_frame, text=" Pressure")
        self.pressure_frame.grid(row=8, column=0, columnspan=2, sticky='ew', pady=10)

        # Lambda Reference Entry
        ttk.Label(self.pressure_frame, text="λ reference (nm):").grid(row=1, column=0, sticky="w", pady=5, padx=(5,5))
        self.lambda_ref_entry = ttk.Entry(self.pressure_frame, width=15)
        self.lambda_ref_entry.insert(0, "694.28")  # Default value
        self.lambda_ref_entry.grid(row=1, column=1, sticky='w')
        self.lambda_ref_entry.bind("<KeyRelease>", self.calculate_pressure)

        # Sample Wavelength Display
        ttk.Label(self.pressure_frame, text="λ sample (nm):").grid(row=2, column=0, sticky="w", pady=5, padx=(5,5))
        self.sample_wavelength_label = ttk.Label(self.pressure_frame, text="N/A")
        self.sample_wavelength_label.grid(row=2, column=1, sticky="w")

        # Pressure Display
        self.pressure_value_label = ttk.Label(self.pressure_frame, text="Pressure: N/A", anchor="w", font=("Arial", 14, "bold"), justify="left")
        self.pressure_value_label.grid(row=1, column=2, rowspan=2, sticky="ew", padx=(10,10), pady=(10,10))


        # Temperature correction frame
        self.tab1_temperature_correction_frame = ttk.LabelFrame(self.tab1_frame, text="Temperature Correction")
        self.tab1_temperature_correction_frame.grid(row=9, column=0, columnspan=2, sticky="we", padx=5, pady=5)

        self.tab1_temp_corr_var = tk.BooleanVar()


        # Checkbox
        self.tab1_temp_corr_check = ttk.Checkbutton(
            self.tab1_temperature_correction_frame,
            text="",
            variable=self.tab1_temp_corr_var,
            command=self.toggle_temp_correction
        )
        self.tab1_temp_corr_check.grid(row=0, column=0, sticky="w")

        # Labels and Entry fields
        self.tref_label = ttk.Label(self.tab1_temperature_correction_frame, text="𝑇 reference (K):")
        self.tref_label.grid(row=0, column=1, sticky="e", pady=5, padx=5)

        self.tref_entry = ttk.Entry(self.tab1_temperature_correction_frame, width=7)
        self.tref_entry.grid(row=0, column=2, sticky="w", padx=5)
        self.tref_entry.insert(0, "298")
        self.tref_entry.config(state="disabled")

        self.tsample_label = ttk.Label(self.tab1_temperature_correction_frame, text="𝑇 sample (K):")
        self.tsample_label.grid(row=0, column=3, sticky="e", pady=5, padx=5)

        self.tsample_entry = ttk.Entry(self.tab1_temperature_correction_frame, width=7)
        self.tsample_entry.grid(row=0, column=4, sticky="w", pady=5, padx=5)
        self.tsample_entry.insert(0, "298")
        self.tsample_entry.config(state="disabled")

        self.tref_entry.bind("<KeyRelease>", self.on_temp_change)
        self.tsample_entry.bind("<KeyRelease>", self.on_temp_change)

        # Saving Options Frame
        self.save_frame = ttk.LabelFrame(self.tab1_frame, text=" Saving Options")
        self.save_frame.grid(row=10, columnspan=2, pady=5, sticky='w')

        ttk.Label(self.save_frame, text="Folder Path:").grid(row=1, column=0, sticky='w', padx=(5,10), pady=5)
        self.folder_path_entry = ttk.Entry(self.save_frame, width=30)
        self.folder_path_entry.grid(row=1, column=1, columnspan=2, sticky='w', padx=(5,10), pady=5)

        button_frame = ttk.Frame(self.save_frame)
        button_frame.grid(row=2, column=0, columnspan=3, pady=5)

        ttk.Button(button_frame, text="Browse", command=self.browse_save_folder).pack(side=tk.LEFT, padx=10)
        ttk.Button(button_frame, text="Save All", command=self.save_as).pack(side=tk.LEFT, padx=10)
        ttk.Button(button_frame, image=save_icon, command=self.save_tab1_to_desktop).pack(side=tk.LEFT, padx=10)


        #DND option for the Tab1
        self.tab1.drop_target_register(DND_FILES)
        self.tab1.dnd_bind("<<Drop>>", self.handle_drop_tab1)

        # Canvas to plot the data
        self.fig, self.ax = plt.subplots(figsize=(12,7)) # Size in inches
        self.canvas = FigureCanvasTkAgg(self.fig, master=self.tab1)
        self.canvas_widget = self.canvas.get_tk_widget()
        self.canvas_widget.grid(row=0, column=4, columnspan=2, sticky="w")


    def on_temp_change(self, event):
        self.calculate_pressure()


    def toggle_temp_correction(self):
        if self.tab1_temp_corr_var.get():
            self.tref_entry.config(state="normal")
            self.tsample_entry.config(state="normal")
        else:
            self.tref_entry.config(state="disabled")
            self.tsample_entry.config(state="disabled")
        self.on_temp_change(None)


    def handle_drop_tab1(self, event):
        self.delete_all()
        files = self.root.tk.splitlist(event.data)
        for file in files:
            if os.path.isfile(file):
                try:
                    with open(file, "r", encoding="utf-8") as f:
                        lines = f.readlines()
                    x_vals, y_vals = readin_spectral_data(lines)
                    for x, y in zip(x_vals, y_vals):
                        self.data.append((x, y))
                    self.update_table()
                except Exception as e:
                    messagebox.showerror("Error", f"Failed to read dropped file:\n{file}\n\n{e}")


    def paste_data(self):
        try:
            self.delete_all()
            clipboard_data = self.root.clipboard_get()
            x_vals, y_vals = readin_spectral_data(clipboard_data)
            for x, y in zip(x_vals, y_vals):
                self.data.append((x, y))
            self.update_table()
        except Exception as e:
            messagebox.showerror("Clipboard Error", f"Failed to parse clipboard data: {e}")


    def update_table(self):
        self.data.sort(key=lambda point: point[0])  # Sort data by x values
        for row in self.tree.get_children():
            self.tree.delete(row)
        for x, y in self.data:
            self.tree.insert("", "end", values=(x, y))
        self.update_plot()


    def update_plot(self):
        self.ax.clear()
        experiment_name = self.experiment_name.get().strip()
        plot_region = self.display_region.get()

        if self.data:
            x_vals, y_vals = zip(*self.data)

            # Choose the plotting range based on radio selection
            if plot_region == "fitted" and self.filtered_wavelength is not None:
                # Create masks for fitted region
                wavelength_array = np.array(x_vals)
                original_intensity_array = np.array(y_vals)

                mask = np.isin(np.round(wavelength_array, 6), np.round(self.filtered_wavelength, 6))

                x_plot = wavelength_array[mask]
                y_plot = original_intensity_array[mask]

                if self.corrected_y is not None:
                    corrected_plot = np.array(self.corrected_y)[mask]
                else:
                    corrected_plot = None
            else:
                x_plot = x_vals
                y_plot = y_vals
                corrected_plot = self.corrected_y


            # Plot: Original Data
            if self.show_original.get():
                self.ax.plot(x_plot, y_plot, 'b.', markersize=2, label="Original Data")

            # Plot: Corrected Data
            if corrected_plot is not None and self.show_corrected.get():
                self.ax.plot(x_plot, corrected_plot, 'k.', label="Corrected Data")

            # Plot: Fitted Model
            if self.show_fit.get() and hasattr(self, 'fitting_result') and self.fitting_result is not None:
                result = self.fitting_result
                max_peak_width = max(result.params['peak1_fwhm'].value, result.params['peak2_fwhm'].value)
                min_x = min(result.params['peak1_center'].value, result.params['peak2_center'].value) - 5
                max_x = max(result.params['peak1_center'].value, result.params['peak2_center'].value) + 5
                dense_x = np.linspace(min_x, max_x, 1000)
                fitted_function = result.eval(x=dense_x)
                self.ax.plot(dense_x, fitted_function, color='red', label="Fitted Model")

        self.ax.set_title(experiment_name if experiment_name else "")
        self.ax.set_xlabel("Wavelength (nm)")
        self.ax.set_ylabel("Intensity")

        # --- Show λ1 and λ2 with uncertainties inside plot ---
        if self.show_fit.get() and hasattr(self, 'fitting_result') and self.fitting_result is not None:
            result = self.fitting_result
            try:
                lambda1 = result.params['peak1_center'].value
                sigma1 = result.params['peak1_center'].stderr or 0
                lambda2 = result.params['peak2_center'].value
                sigma2 = result.params['peak2_center'].stderr or 0

                # Format with ± if stderr available
                lambda1_str = f"λ(R1): {lambda1:.4f} ± {sigma1:.4f} nm" if sigma1 else f"λ₁ (R1): {lambda1:.4f} nm"
                lambda2_str = f"λ(R2): {lambda2:.4f} ± {sigma2:.4f} nm" if sigma2 else f"λ₂ (R2): {lambda2:.4f} nm"

                r2_str = f"R²: {self.r_squared:.3f}"
                snr_str = f"SNR: {self.snr:.1f}"


                # Add text box inside plot
                self.ax.text(
                    0.05, 0.95,
                    f"{lambda1_str}\n{lambda2_str}\n{r2_str}\n{snr_str}",
                    transform=self.ax.transAxes,
                    fontsize=11,
                    verticalalignment='top',
                    horizontalalignment='left',
                    bbox=dict(facecolor='white', edgecolor='gray', boxstyle='round,pad=0.3')
                )
            except Exception as e:
                print(f"Failed to extract λ values for plot: {e}")

        handles, labels = self.ax.get_legend_handles_labels()
        if handles:
            self.ax.legend()

        self.canvas.draw()



    def process_data(self):
        self.filtered_wavelength = None
        self.filtered_intensity = None

        if not self.data:
            messagebox.showerror("Error", "No data available for processing.")
            return

        x_vals, y_vals = zip(*self.data)
        wavelength = np.round(np.array(x_vals), 6)
        intensity = np.round(np.array(y_vals), 6)

        # Get baseline model from dropdown
        bc_option = self.bc_selection.get()

        # Use experiment name or fallback to "Untitled"
        spectrum_id = self.experiment_name.get().strip() or "Untitled"

        # Use a temporary directory
        with tempfile.TemporaryDirectory() as output_folder:
            result_file = os.path.join(output_folder, f"{spectrum_id}_result.txt")

            # Call main baseline correction + fitting function
            result, filtered_wavelength, filtered_intensity, r_squared, snr = baseline_correction(
                (wavelength, intensity),
                result_file=result_file,
                spectrum_id=spectrum_id,
                model_name=bc_option,
                output_folder=output_folder
            )

            self.fitting_result = result
            self.filtered_wavelength = filtered_wavelength
            self.filtered_intensity = filtered_intensity
            self.r_squared = r_squared
            self.snr = snr

            self.peak1_center = self.fitting_result.params['peak1_center'].value
            self.sample_wavelength_label.config(text=f"{self.peak1_center:.4f}")


            self.calculate_pressure()

            # Load corrected data back in for display
            corrected_file = os.path.join(output_folder, f"{spectrum_id}.procBC.txt")
            if os.path.exists(corrected_file):
                try:
                    corrected_data = np.loadtxt(corrected_file, skiprows=1)
                    self.corrected_y = corrected_data[:, 1]
                    self.update_plot()
                except Exception as e:
                    messagebox.showerror("Error", f"Failed to load corrected data:\n{e}")
            else:
                messagebox.showerror("Error", "Baseline-corrected file not found.")


    def delete_selected(self):
        selected_items = self.tree.selection()
        for item in selected_items:
            values = self.tree.item(item, "values")
            self.data.remove((float(values[0]), float(values[1])))
            self.tree.delete(item)
        self.corrected_y = None
        self.fitting_result = None
        self.update_plot()

    def delete_all(self):
        self.data.clear()  # Clear the data list
        for row in self.tree.get_children():
            self.tree.delete(row)  # Clear all table entries
        self.corrected_y = None
        self.fitting_result = None
        self.update_plot()

    def save_tab1_to_desktop(self):
        try:
            lambda_ref = self.lambda_ref_entry.get()
            lambda_sample = self.sample_wavelength_label.cget("text")
            pressure = self.pressure_value_label.cget("text").replace("Pressure: ", "").replace(" GPa", "")
            experiment_name1 = self.experiment_name.get().strip()

            if not lambda_ref or "N/A" in lambda_sample or "N/A" in pressure:
                messagebox.showwarning("Warning", "Please ensure λ reference, sample, and pressure are valid.")
                return

            timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
            desktop_path = os.path.join(os.path.expanduser("~"), "Desktop")
            result_path = os.path.join(desktop_path, "CrimsonCalc_results.txt")

            with open(result_path, "a", encoding="utf-8") as f:
                line = f"{timestamp}\tSingle Spectrum Processing:\t{experiment_name1}\t{pressure}\t{lambda_sample}\t{lambda_ref}"
                if self.tab1_temp_corr_var.get():
                    tref = self.tref_entry.get()
                    tsample = self.tsample_entry.get()
                    line += f"\t{tref}\t{tsample}"

                line += "\n"
                f.write(line)

        except Exception as e:
            messagebox.showerror("Error", f"Failed to save Single Spectrum values:\n{e}")


    # ----- TAB 2: Opus Data Processing -----

    def create_opus_processor_tab(self):
        self.tab2_frame = ttk.Frame(self.tab2)
        self.tab2_frame.grid(row=0, column=0, padx=10, pady=10, sticky="nw")

        ttk.Label(self.tab2_frame, text="Batch Processing of Ruby Fluorescence Files", anchor="w", font=("Arial", 14, "bold"), justify="left").grid(row=0, column=0, sticky="nw", padx=10, pady=5)


        # ----- Folder Input frame -----
        self.batch_folder_frame = ttk.LabelFrame(self.tab2_frame, text=" Batch Processing of Folders")
        self.batch_folder_frame.grid(row=1, column=0, sticky="nw", pady=(10, 5))
        
        ttk.Label(self.batch_folder_frame, text="Select the folder with the ruby fluorescence spectral files to process:").grid(row=0, column=0, columnspan=3, sticky="w", pady=5, padx=(5, 5))
        self.folder_path = tk.StringVar()
        ttk.Entry(self.batch_folder_frame, textvariable=self.folder_path, width=40).grid(row=1, column=0, columnspan=2, sticky="w", pady=(5,5), padx=(5, 5))
        ttk.Button(self.batch_folder_frame, text="Browse", command=self.browse_folder).grid(row=1, column=2, columnspan=2, sticky="e", pady=(5,5), padx=(5, 5))


        # --- Drag and Drop Files Section ---
        self.dnd_frame = ttk.LabelFrame(self.tab2_frame, text=" Drag and Drop")
        self.dnd_frame.grid(row=4, column=0, sticky="nw", pady=(10, 5))
        
        ttk.Label(self.dnd_frame, text="Drag and drop files below:").grid(row=0, column=0, sticky="w", pady=5, padx=(5, 5))

        listbox_frame = ttk.Frame(self.dnd_frame)
        listbox_frame.grid(row=1, column=0, columnspan=6, pady=5, padx=5, sticky="w")

        self.dropped_files_listbox = tk.Listbox(listbox_frame, height=8, width=60)
        self.dropped_files_listbox.pack(side="left", fill="both", expand=True)

        scrollbar = ttk.Scrollbar(listbox_frame, orient="vertical", command=self.dropped_files_listbox.yview)
        scrollbar.pack(side="right", fill="y")

        self.dropped_files_listbox.config(yscrollcommand=scrollbar.set)

        # Remove Selected File Button
        ttk.Button(self.dnd_frame, text="Remove Selected File", command=self.remove_selected_dropped_file).grid(row=7, column=5, sticky="e", pady=(5,5), padx=(5, 5))

        self.dropped_files_listbox.drop_target_register(DND_FILES)
        self.dropped_files_listbox.dnd_bind("<<Drop>>", self.handle_drop)

        self.dropped_files = []


        # ----- Baseline Selection Frame -----
        self.bssel_tab2_frame = ttk.LabelFrame(self.tab2_frame, text=" Baseline Model Selection")
        self.bssel_tab2_frame.grid(row=8, column=0, sticky="nw", pady=(10, 5))

        ttk.Label(self.bssel_tab2_frame, text="Baseline model:").grid(row=0, column=0, sticky="w", padx=5, pady=5)
        self.tab2_bc_selection = ttk.Combobox(self.bssel_tab2_frame, values=["None", "Rubberband", "BEADS", "FABc"], width=15)
        self.tab2_bc_selection.grid(row=0, column=1, sticky="w", padx=5, pady=5)
        self.tab2_bc_selection.set("BEADS")  # Default


        # ----- Reference Selection & Process Frame -----
        self.refsel_frame = ttk.LabelFrame(self.tab2_frame, text=" Processing Options")
        self.refsel_frame.grid(row=9, column=0, sticky="nw", pady=(10, 5))

        ttk.Label(self.refsel_frame, text="λ reference used for pressure calculation:").grid(row=0, column=0, columnspan=2, sticky="w", pady=5, padx=(5, 5))
        self.lambda_ref_tab2 = ttk.Entry(self.refsel_frame, width=10)
        self.lambda_ref_tab2.insert(0, "694.28")  # Default value
        self.lambda_ref_tab2.grid(row=0, column=0, columnspan=2, sticky="e", pady=5, padx=(5, 5))


        # Checkbox for auto-refresh if .ini file is created
        self.tab2_auto_refresh_var = tk.BooleanVar()
        self.tab2_auto_refresh_check = ttk.Checkbutton(
            self.refsel_frame,
            text="Read the reference value from the corresponding file instead.",
            variable=self.tab2_auto_refresh_var
        )
        self.tab2_auto_refresh_check.grid(row=1, column=0, columnspan=2, sticky="w", pady=5, padx=(5, 5))


        self.tab2_temp_corr_var = tk.BooleanVar(value=False)
        self.tab2_temp_corr_check = ttk.Checkbutton(
            self.refsel_frame,
            text="Temperature correction",
            variable=self.tab2_temp_corr_var
        )
        self.tab2_temp_corr_check.grid(row=2, column=0, sticky='w', pady=5, padx=(5, 5))


        # Temperature correction frame
        self.tab2_temperature_correction_frame = ttk.Frame(self.refsel_frame)
        self.tab2_temperature_correction_frame.grid(row=3, column=0, columnspan=2, sticky="w")

        def tab2_toggle_temperature_entries():
            state = "normal" if self.tab2_temp_corr_var.get() else "disabled"
            self.tab2_tref_entry.config(state=state)
            self.tab2_tsample_entry.config(state=state)

        self.tab2_temp_corr_check.configure(command=tab2_toggle_temperature_entries)

        self.tab2_tref_label = ttk.Label(self.tab2_temperature_correction_frame, text="𝑇 reference (K):")
        self.tab2_tref_label.grid(row=0, column=0, sticky="e", pady=5, padx=5)
        self.tab2_tref_entry = ttk.Entry(self.tab2_temperature_correction_frame, width=10)
        self.tab2_tref_entry.grid(row=0, column=1, sticky="w", pady=5, padx=5)
        self.tab2_tref_entry.insert(0, "298")

        self.tab2_tsample_label = ttk.Label(self.tab2_temperature_correction_frame, text="𝑇 sample (K):")
        self.tab2_tsample_label.grid(row=0, column=2, sticky="e", pady=5, padx=5)
        self.tab2_tsample_entry = ttk.Entry(self.tab2_temperature_correction_frame, width=10)
        self.tab2_tsample_entry.grid(row=0, column=3, sticky="w", pady=5, padx=5)
        self.tab2_tsample_entry.insert(0, "298")

        # Ensure correct initial state (disabled if checkbox not checked)
        tab2_toggle_temperature_entries()


        # Process Button (Tab2)
        ttk.Button(self.refsel_frame, text="Process", command=self.process_files).grid(row=4, column=0, columnspan=2, sticky="e", pady=(5,5), padx=(5, 5))



    # ----- TAB 3: Pressure Calculator -----

    def create_pressure_calculator_tab(self):
        self.tab3_frame = ttk.Frame(self.tab3)
        self.tab3_frame.grid(row=0, column=0, padx=10, pady=10, sticky="nw")

        self.pressure_calculators_frame = ttk.Frame(self.tab3_frame)
        self.pressure_calculators_frame.grid(row=0, column=0, padx=10, pady=10, sticky="nw")

        ttk.Label(self.pressure_calculators_frame, text="Pressure Calculators", anchor="w", font=("Arial", 14, "bold"), justify="left").grid(row=0, column=0, sticky="nw", padx=10, pady=5)


        # ----- Ruby Fluorescence Frame -----
        self.ruby_fluorescence_frame = ttk.LabelFrame(self.pressure_calculators_frame, text=" Ruby Fluorescence")
        self.ruby_fluorescence_frame.grid(row=1, column=0, sticky="nw", pady=(10, 5), padx=10)
        
        ttk.Label(self.ruby_fluorescence_frame, text="λ reference (nm):").grid(row=1, column=0, sticky="w", padx=5, pady=5)
        self.lambda_ref_tab3 = ttk.Entry(self.ruby_fluorescence_frame, width=15)
        self.lambda_ref_tab3.insert(0, "694.28")
        self.lambda_ref_tab3.grid(row=1, column=1, padx=(5,5), pady=5)
        self.lambda_ref_tab3.bind("<KeyRelease>", self.update_ruby_pressure_tab3)

        ttk.Label(self.ruby_fluorescence_frame, text="λ sample (nm):").grid(row=2, column=0, sticky="w", padx=5, pady=5)
        self.peak1_center_tab3 = ttk.Entry(self.ruby_fluorescence_frame, width=15)
        self.peak1_center_tab3.grid(row=2, column=1, padx=5, pady=5)
        self.peak1_center_tab3.bind("<KeyRelease>", self.update_ruby_pressure_tab3)


        # --- Temperature Correction Frame for Ruby Fluorescence (Tab 3) ---
        self.ruby_temp_correction_frame = ttk.LabelFrame(self.ruby_fluorescence_frame, text="Temperature Correction")
        self.ruby_temp_correction_frame.grid(row=5, column=0, columnspan=2, sticky="we", padx=5, pady=5)

        self.tab3_temp_corr_var = tk.BooleanVar()


        def toggle_tab3_temp_entries():
            if self.tab3_temp_corr_var.get():
                self.tab3_tref_entry.config(state="normal")
                self.tab3_tsample_entry.config(state="normal")
            else:
                self.tab3_tref_entry.config(state="disabled")
                self.tab3_tsample_entry.config(state="disabled")

            on_tab3_temp_change(None)


        # Temperature correction checkbox
        self.tab3_temp_corr_check = ttk.Checkbutton(
            self.ruby_temp_correction_frame,
            text="Enable temperature correction",
            variable=self.tab3_temp_corr_var,
            command=toggle_tab3_temp_entries
        )
        self.tab3_temp_corr_check.grid(row=0, column=0, columnspan=2, sticky="w")

        # T reference label and entry (insert value first, then disable)
        self.tab3_tref_label = ttk.Label(self.ruby_temp_correction_frame, text="𝑇 reference (K):")
        self.tab3_tref_label.grid(row=1, column=0, sticky="w", padx=5)

        self.tab3_tref_entry = ttk.Entry(self.ruby_temp_correction_frame, width=10)
        self.tab3_tref_entry.grid(row=1, column=1, sticky="w", padx=5)
        self.tab3_tref_entry.insert(0, "298")
        self.tab3_tref_entry.config(state="disabled")

        # T sample label and entry (insert value first, then disable)
        self.tab3_tsample_label = ttk.Label(self.ruby_temp_correction_frame, text="𝑇 sample (K):")
        self.tab3_tsample_label.grid(row=2, column=0, sticky="w", padx=5)

        self.tab3_tsample_entry = ttk.Entry(self.ruby_temp_correction_frame, width=10)
        self.tab3_tsample_entry.grid(row=2, column=1, sticky="w", padx=5)
        self.tab3_tsample_entry.insert(0, "298")
        self.tab3_tsample_entry.config(state="disabled")


        # Optional: trigger recalculation on key press
        def on_tab3_temp_change(event):
            self.update_ruby_pressure_tab3()

        self.tab3_tref_entry.bind("<KeyRelease>", on_tab3_temp_change)
        self.tab3_tsample_entry.bind("<KeyRelease>", on_tab3_temp_change)

        self.pressure_result_tab3 = ttk.Label(self.ruby_fluorescence_frame, text="Pressure: N/A", font=("Arial", 12, "bold"))
        self.pressure_result_tab3.grid(row=4, column=0, columnspan=2, pady=10)

        ttk.Button(
            self.ruby_fluorescence_frame,
            image=copy_icon,
            command=lambda: self.copy_to_clipboard(self.pressure_result_tab3.cget("text").replace("Pressure: ", "").replace(" GPa", ""))
        ).grid(row=4, column=2, padx=5)

        ttk.Button(
            self.ruby_fluorescence_frame,
            image=save_icon,
            command=self.save_ruby_pressure_tab3
        ).grid(row=4, column=3, padx=5)


        # ----- Diamond Edge Frame -----
        self.diamond_edge_frame = ttk.LabelFrame(self.pressure_calculators_frame, text=" Diamond Anvil Raman Edge")
        self.diamond_edge_frame.grid(row=1, column=1, sticky="nw", pady=(10, 5), padx=10)

        ttk.Label(self.diamond_edge_frame, text="Wavenumber reference (cm⁻¹):").grid(row=4, column=0, sticky="w", padx=(5,5), pady=(5,5))
        self.diamond_edge_ref_entry = ttk.Entry(self.diamond_edge_frame, width=15)
        self.diamond_edge_ref_entry.insert(0, "1333")
        self.diamond_edge_ref_entry.grid(row=4, column=1, padx=(5,5), pady=(5,5))
        self.diamond_edge_ref_entry.bind("<KeyRelease>", self.update_diamond_pressure_tab3)

        ttk.Label(self.diamond_edge_frame, text="Wavenumber sample (cm⁻¹):").grid(row=5, column=0, sticky="w", padx=(5,5), pady=(5,5))
        self.diamond_edge_sample_entry = ttk.Entry(self.diamond_edge_frame, width=15)
        self.diamond_edge_sample_entry.grid(row=5, column=1, padx=(5,5), pady=(5,5))
        self.diamond_edge_sample_entry.bind("<KeyRelease>", self.update_diamond_pressure_tab3)

        self.diamond_pressure_result_label = ttk.Label(self.diamond_edge_frame, text="Pressure: N/A", font=("Arial", 12, "bold"))
        self.diamond_pressure_result_label.grid(row=6, column=0, columnspan=2, pady=10)

        ttk.Button(
            self.diamond_edge_frame,
            image=copy_icon,
            command=lambda: self.copy_to_clipboard(
                self.diamond_pressure_result_label.cget("text").replace("Pressure: ", "").replace(" GPa", "")
            )
        ).grid(row=6, column=2, padx=5)

        ttk.Button(
            self.diamond_edge_frame,
            image=save_icon,
            command=self.save_diamond_pressure_tab3
        ).grid(row=6, column=3, padx=5)


        # ----- Error Propagation Frame -----
        self.ruby_error_frame = ttk.LabelFrame(self.pressure_calculators_frame, text=" Error Propagation")
        self.ruby_error_frame.grid(row=1, column=2, sticky="nw", pady=(10, 5), padx=10)

        # Inputs
        ttk.Label(self.ruby_error_frame, text="λ sample (nm):").grid(row=0, column=0, sticky="w", pady=5, padx=(5,5))
        self.ruby_lambda_sample_entry = ttk.Entry(self.ruby_error_frame, width=15)
        self.ruby_lambda_sample_entry.grid(row=0, column=1, padx=(5,5))

        ttk.Label(self.ruby_error_frame, text="±λ sample (nm):").grid(row=1, column=0, sticky="w", pady=5, padx=(5,5))
        self.ruby_sigma_sample_entry = ttk.Entry(self.ruby_error_frame, width=15)
        self.ruby_sigma_sample_entry.grid(row=1, column=1, padx=(5,5))

        ttk.Label(self.ruby_error_frame, text="λ ref (nm):").grid(row=2, column=0, sticky="w", pady=5, padx=(5,5))
        self.ruby_lambda_ref_entry = ttk.Entry(self.ruby_error_frame, width=15)
        self.ruby_lambda_ref_entry.insert(0, "694.28")
        self.ruby_lambda_ref_entry.grid(row=2, column=1, padx=(5,5))

        ttk.Label(self.ruby_error_frame, text="±λ ref (nm):").grid(row=3, column=0, sticky="w", pady=5, padx=(5,5))
        self.ruby_sigma_ref_entry = ttk.Entry(self.ruby_error_frame, width=15)
        self.ruby_sigma_ref_entry.insert(0, "0.01")
        self.ruby_sigma_ref_entry.grid(row=3, column=1, padx=(5,5))

        ttk.Label(self.ruby_error_frame, text="Instrument Error (nm):").grid(row=4, column=0, sticky="w", padx=5, pady=5)
        self.ruby_instr_entry = ttk.Entry(self.ruby_error_frame, width=15)
        self.ruby_instr_entry.insert(0, "0.05")
        self.ruby_instr_entry.grid(row=4, column=1)

        # Correlation Slider
        ttk.Label(self.ruby_error_frame, text="Correlation ρ (λs, λr):").grid(row=5, column=0, padx=5, pady=5, sticky="w")
        self.rho_slider = tk.Scale(self.ruby_error_frame,
                                   from_=-1.0, to=1.0,
                                   resolution=0.1,
                                   orient="horizontal",
                                   length=80,
                                   command=self.update_ruby_error_propagation)  # <- Add this!

        self.rho_slider.set(0.00)  # Default: uncorrelated
        self.rho_slider.grid(row=5, column=1, pady=5)

        self.ruby_error_result_label = ttk.Label(self.ruby_error_frame, text="Pressure: N/A ± N/A", font=("Arial", 12, "bold"))
        self.ruby_error_result_label.grid(row=6, column=0, columnspan=2, pady=10, padx=(5,5))

        entries = [
            self.ruby_lambda_sample_entry,
            self.ruby_sigma_sample_entry,
            self.ruby_lambda_ref_entry,
            self.ruby_sigma_ref_entry,
            self.ruby_instr_entry
        ]
        for entry in entries:
            entry.bind("<KeyRelease>", self.update_ruby_error_propagation)


        ttk.Button(
            self.ruby_error_frame,
            image=save_icon,
            command=self.save_ruby_error_propagation_tab3
        ).grid(row=6, column=3, padx=5)



        # ----- DAC Calculators frame -----
        self.msc_calculators_frame = ttk.Frame(self.tab3_frame)
        self.msc_calculators_frame.grid(row=1, column=0, padx=10, pady=10, sticky="nw")

        ttk.Label(self.msc_calculators_frame, text="DAC Calculators", anchor="w", font=("Arial", 14, "bold"), justify="left").grid(row=2, column=0, sticky="nw", padx=10, pady=(10,5))

        # ----- Gasket Thickness Frame -----
        self.gasket_thickness_frame = ttk.LabelFrame(self.msc_calculators_frame, text=" Gasket Thickness")
        self.gasket_thickness_frame.grid(row=3, column=0, sticky="nw", pady=(10, 5), padx=10)

        ttk.Label(self.gasket_thickness_frame, text="Interference order:").grid(row=0, column=0, sticky="w", padx=5, pady=5)
        self.n_entry = ttk.Entry(self.gasket_thickness_frame, width=15)
        self.n_entry.grid(row=0, column=1, padx=5, pady=5)
        self.n_entry.bind("<KeyRelease>", self.calculate_gasket_thickness)

        ttk.Label(self.gasket_thickness_frame, text="Wavenumber start (cm⁻¹):").grid(row=1, column=0, sticky="w", padx=(5,5), pady=(5,5))
        self.gasket_peak1_entry = ttk.Entry(self.gasket_thickness_frame, width=15)
        self.gasket_peak1_entry.grid(row=1, column=1, padx=(5,5), pady=(5,5))
        self.gasket_peak1_entry.bind("<KeyRelease>", self.calculate_gasket_thickness)

        ttk.Label(self.gasket_thickness_frame, text="Wavenumber end (cm⁻¹):").grid(row=2, column=0, sticky="w", padx=(5,5), pady=(5,5))
        self.gasket_peak2_entry = ttk.Entry(self.gasket_thickness_frame, width=15)
        self.gasket_peak2_entry.grid(row=2, column=1, padx=(5,5), pady=(5,5))
        self.gasket_peak2_entry.bind("<KeyRelease>", self.calculate_gasket_thickness)

        self.thickness_result_label = ttk.Label(self.gasket_thickness_frame, text="Gasket thickness: N/A", font=("Arial", 12, "bold"))
        self.thickness_result_label.grid(row=3, column=0, columnspan=2, padx=5, pady=10)


        ttk.Button(
            self.gasket_thickness_frame,
            image=copy_icon,
            command=lambda: self.copy_to_clipboard(
                self.thickness_result_label.cget("text").replace("Gasket thickness: ", "").replace(" μm", "")
            )
        ).grid(row=3, column=2, padx=5)


        ttk.Button(
            self.gasket_thickness_frame,
            image=save_icon,
            command=self.save_gasket_thickness_tab3
        ).grid(row=3, column=3, padx=5)



        # ----- Upper Pressure Limit Frame -----
        self.upper_pressure_limit_frame = ttk.LabelFrame(self.msc_calculators_frame, text=" Upper Pressure Limit")
        self.upper_pressure_limit_frame.grid(row=3, column=1, sticky="nw", pady=(10, 5), padx=10)

        ttk.Label(self.upper_pressure_limit_frame, text="Culet diameter (μm):").grid(row=0, column=0, padx=(5,5), pady=(5,5))
        self.culet_diam_entry = ttk.Entry(self.upper_pressure_limit_frame, width=15)
        self.culet_diam_entry.grid(row=0, column=1, padx=(5,5), pady=(5,5))
        self.culet_diam_entry.bind("<KeyRelease>", self.update_upper_pressure_limit)

        self.dunstan_limit_label = ttk.Label(self.upper_pressure_limit_frame, text="Dunstan et al.: N/A", font=("Arial", 12, "bold"))
        self.dunstan_limit_label.grid(row=1, column=0, columnspan=2, padx=5, pady=(5,5))

        self.ruoff_limit_label = ttk.Label(self.upper_pressure_limit_frame, text="Ruoff et al.: N/A", font=("Arial", 12, "bold"))
        self.ruoff_limit_label.grid(row=2, column=0, columnspan=2, padx=5, pady=(5,5))

        self.obannon_limit_label = ttk.Label(self.upper_pressure_limit_frame, text="O'Bannon et al.: N/A", font=("Arial", 12, "bold"))
        self.obannon_limit_label.grid(row=3, column=0, columnspan=2, padx=5, pady=(5,10))


        ttk.Button(
            self.upper_pressure_limit_frame,
            image=save_icon,
            command=self.save_upper_pressure_limit_tab3
        ).grid(row=3, column=2, padx=5)



    def update_ruby_error_propagation(self, event=None):
        try:
            ls = float(self.ruby_lambda_sample_entry.get())
            sls = float(self.ruby_sigma_sample_entry.get())
            lr = float(self.ruby_lambda_ref_entry.get())
            slr = float(self.ruby_sigma_ref_entry.get())
            s_instr = float(self.ruby_instr_entry.get())
            rho = self.rho_slider.get()

            pressure, sigma_P = ruby2020_pressure_with_correlation(
                lambda_sample=ls,
                lambda_ref=lr,
                sigma_sample_fit=sls,
                sigma_ref_fit=slr,
                sigma_instr=s_instr,
                rho=rho
            )

            self.ruby_error_result_label.config(text=f"Pressure: {pressure:.2f} ± {sigma_P:.2f} GPa")
        except ValueError:
            self.ruby_error_result_label.config(text="Pressure: N/A ± N/A")


    # Methods to calculate the pressure in Tab3
    def update_ruby_pressure_tab3(self, event=None):
        try:
            lambda_ref = float(self.lambda_ref_tab3.get())
            peak1_center = float(self.peak1_center_tab3.get())

            lambda_ref_temp = lambda_ref
            peak1_center_temp = peak1_center

            # Apply temperature correction if enabled
            if self.tab3_temp_corr_var.get():
                try:
                    T_ref = float(self.tab3_tref_entry.get().strip())
                    delta_lambda_T_ref = lambda_temp_corr(T_ref)
                    lambda_ref_temp = lambda_ref_temp - delta_lambda_T_ref

                    T_sample = float(self.tab3_tsample_entry.get().strip())
                    delta_lambda_T_sample = lambda_temp_corr(T_sample)
                    peak1_center_temp = peak1_center_temp - delta_lambda_T_sample

                except Exception as e:
                    print("Tab 3 Temperature correction error:", e)

            # Check if λ_sample < λ_ref
            if peak1_center_temp < lambda_ref_temp:
                self.pressure_result_tab3.config(text="Pressure: N/A", font=("Arial", 12, "bold"))
                return

            # Calculate pressure
            pressure = 1870 * ((peak1_center_temp - lambda_ref_temp) / lambda_ref_temp) * (1 + 5.63 * (peak1_center_temp - lambda_ref_temp) / lambda_ref_temp)

            # Add asterisk if pressure > 150 GPa
            suffix = " *" if pressure > 150 else ""
            self.pressure_result_tab3.config(
                text=f"Pressure: {pressure:.3f} GPa{suffix}",
                font=("Arial", 12, "bold")
            )

        except ValueError:
            self.pressure_result_tab3.config(text="Pressure: N/A", font=("Arial", 12, "bold"))



    def update_diamond_pressure_tab3(self, event=None):
        try:
            d_ref = float(self.diamond_edge_ref_entry.get())
            d_sample = float(self.diamond_edge_sample_entry.get())

            # Check: if d_sample < d_ref, result is invalid
            if d_sample < d_ref:
                self.diamond_pressure_result_label.config(text="Pressure: N/A", font=("Arial", 12, "bold"))
                return

            # Calculate pressure using formula
            pressure = 517 * ((d_sample - d_ref) / d_ref) + 764 * (((d_sample - d_ref) / d_ref)**2)

            # Add asterisk if pressure > 310 GPa
            suffix = " *" if pressure > 500 else ""
            self.diamond_pressure_result_label.config(
                text=f"Pressure: {pressure:.3f} GPa{suffix}",
                font=("Arial", 12, "bold")
            )

        except ValueError:
            self.diamond_pressure_result_label.config(text="Pressure: N/A", font=("Arial", 12, "bold"))


    def calculate_gasket_thickness(self, event=None):
        try:
            n = float(self.n_entry.get())
            peak1 = float(self.gasket_peak1_entry.get())
            peak2 = float(self.gasket_peak2_entry.get())

            if peak1 == peak2:
                self.thickness_result_label.config(text="Peaks must be different.")
                return

            d = 5000 * n / (peak1 - peak2) if peak1 > peak2 else -5000 * n / (peak1 - peak2)

            if d > 10:
                self.thickness_result_label.config(text=f"Gasket thickness: {int(d)} μm")
            elif 1 <= d <= 10:
                self.thickness_result_label.config(text=f"Gasket thickness: {d:.1f} μm")
            else:
                self.thickness_result_label.config(text=f"Gasket thickness: {d:.2f} μm")
        except ValueError:
            self.thickness_result_label.config(text="Gasket thickness: N/A")


    def update_upper_pressure_limit(self, event=None):
        try:
            culet_diam = float(self.culet_diam_entry.get())
            p_dunstan = 12.5* (10**3) / (culet_diam)
            p_ruoff = 1856 * (culet_diam) ** (-0.5)
            p_obannon = 1727 * (culet_diam) ** (-0.54)
            self.dunstan_limit_label.config(text=f"Dunstan et al.: {int(p_dunstan)} GPa", font=("Arial", 12, "bold"))
            self.ruoff_limit_label.config(text=f"Ruoff et al.: {int(p_ruoff)} GPa", font=("Arial", 12, "bold"))
            self.obannon_limit_label.config(text=f"O'Bannon et al.: {int(p_obannon)} GPa", font=("Arial", 12, "bold"))
        except ValueError:
            self.dunstan_limit_label.config(text="Dunstan et al.: N/A", font=("Arial", 12, "bold"))
            self.ruoff_limit_label.config(text="Ruoff et al.: N/A", font=("Arial", 12, "bold"))
            self.obannon_limit_label.config(text="O'Bannon et al.: N/A", font=("Arial", 12, "bold"))


    #----- Other definitions -----


    def browse_folder(self):
        folder_selected = filedialog.askdirectory()
        short_path = os.path.join("...", *folder_selected.strip(os.sep).split(os.sep)[-3:])
        self.folder_path.set(short_path)
        self.full_folder_path = folder_selected


    def process_files(self):
        folder = getattr(self, 'full_folder_path', self.folder_path.get().strip())
        tab2_auto_refresh = self.tab2_auto_refresh_var.get()
        lambda_ref_value = self.lambda_ref_tab2.get()
        baseline_model = self.tab2_bc_selection.get()
        tab2_temp_corr_var = self.tab2_temp_corr_var.get()
        tab2_tref_entry = self.tab2_tref_entry.get()
        tab2_tsample_entry = self.tab2_tsample_entry.get()


        collected_files = set()

        # Gather files from folder
        if folder and os.path.isdir(folder):
            for file in os.listdir(folder):
                if file.lower().endswith(".txt") or re.search(r"\.\d$", file):
                    collected_files.add(os.path.abspath(os.path.join(folder, file)))

        # Add dropped files
        for path in self.dropped_files:
            if os.path.isfile(path):
                collected_files.add(os.path.abspath(path))

        if not collected_files:
            messagebox.showwarning("Warning", "No valid files found in folder or dropped list.")
            return

        # Categorize into four lists
        ruby_ref_files, opus_ref_files, ruby_files, opus_files = categorize_files(collected_files)

        # Change working dir
        os.chdir(os.path.dirname(list(collected_files)[0]))

        try:
            if ruby_ref_files:
                process_ruby_file(
                    ruby_ref_files,
                    auto_refresh=tab2_auto_refresh,
                    lambda_ref_tab2_value=lambda_ref_value,
                    model_name=baseline_model,
                    tab2_temp_corr_var=tab2_temp_corr_var,
                    tab2_tref_entry=tab2_tref_entry,
                    tab2_tsample_entry=tab2_tsample_entry
                )
            if opus_ref_files:
                process_opus_files(
                    opus_ref_files,
                    auto_refresh=tab2_auto_refresh,
                    lambda_ref_tab2_value=lambda_ref_value,
                    model_name=baseline_model,
                    tab2_temp_corr_var=tab2_temp_corr_var,
                    tab2_tref_entry=tab2_tref_entry,
                    tab2_tsample_entry=tab2_tsample_entry
                )
            if ruby_files:
                process_ruby_file(
                    ruby_files,
                    auto_refresh=tab2_auto_refresh,
                    lambda_ref_tab2_value=lambda_ref_value,
                    model_name=baseline_model,
                    tab2_temp_corr_var=tab2_temp_corr_var,
                    tab2_tref_entry=tab2_tref_entry,
                    tab2_tsample_entry=tab2_tsample_entry
                )
            if opus_files:
                process_opus_files(
                    opus_files,
                    auto_refresh=tab2_auto_refresh,
                    lambda_ref_tab2_value=lambda_ref_value,
                    model_name=baseline_model,
                    tab2_temp_corr_var=tab2_temp_corr_var,
                    tab2_tref_entry=tab2_tref_entry,
                    tab2_tsample_entry=tab2_tsample_entry
                )
            messagebox.showinfo("Success", "Processing complete.")

        except Exception as e:
            messagebox.showerror("Processing Error", f"Error during processing: {e}")


        # Clear drag-drop list and folder path
        self.dropped_files.clear()
        self.dropped_files_listbox.delete(0, tk.END)
        self.folder_path.set("")
        self.full_folder_path = ""


    def browse_save_folder(self):
        folder_selected = filedialog.askdirectory()
        if folder_selected:
            self.full_save_folder = folder_selected  # full path for saving
            short_path = os.path.join("...", *folder_selected.strip(os.sep).split(os.sep)[-3:])
            self.folder_path_entry.delete(0, tk.END)
            self.folder_path_entry.insert(0, short_path)


    def save_as(self):
        # Retrieve folder path from the entry field
        self.save_folder = getattr(self, 'full_save_folder', self.folder_path_entry.get().strip())

        # Check if a folder path exists
        if not self.save_folder:
            messagebox.showwarning("Warning", "Please select or enter a folder path.")
            return

        # Retrieve experiment name
        experiment_name = self.experiment_name.get().strip()
        if not experiment_name:
            messagebox.showwarning("Warning", "Please provide an experiment name.")
            return

        # Ensure filenames are unique
        timestamp = ""
        base_filename = os.path.join(self.save_folder, experiment_name)

        # Check if files already exist and add timestamp if necessary
        txt_filename = base_filename + ".txt"
        png_filename = base_filename + ".png"
        svg_filename = base_filename + ".svg"
        result_filename = base_filename  + "_result.txt"
        procBC_filename = base_filename + "_procBC.txt"
        if os.path.exists(txt_filename) or os.path.exists(png_filename) or os.path.exists(svg_filename) or os.path.exists(result_filename):
            timestamp = "_" + time.strftime("%Y%m%d_%H%M%S")

        txt_filename = base_filename + timestamp + ".txt"
        png_filename = base_filename + timestamp + ".png"
        svg_filename = base_filename + timestamp + ".svg"
        procBC_filename = base_filename + timestamp + "_procBC.txt"
        result_filename = base_filename + timestamp + "_result.txt"

        # Save table data to .txt file
        try:
            with open(txt_filename, "w") as file:
                file.write("Wavelength\tIntensity\n")
                for x, y in self.data:
                    file.write(f"{x}\t{y}\n")
        except Exception as e:
            messagebox.showerror("Error", f"Failed to save data file: {e}")
            return

        # Save processed baseline-corrected data to _procBC.txt if available
        if hasattr(self, 'corrected_y') and self.corrected_y is not None:
            try:
                with open(procBC_filename, "w") as file:
                    file.write(f"Baseline model: {self.bc_selection.get()}\n")
                    file.write("Wavelength\tCorrected Intensity\n")
                    x_vals, _ = zip(*self.data)
                    for x, cy in zip(x_vals, self.corrected_y):
                        file.write(f"{x}\t{cy}\n")
            except Exception as e:
                messagebox.showerror("Error", f"Failed to save processed baseline file: {e}")
                return

        # Save plot to .png file
        try:
            self.fig.savefig(png_filename)
            self.fig.savefig(svg_filename)
        except Exception as e:
            messagebox.showerror("Error", f"Failed to save plot file: {e}")

        # Save result file (_result.txt)
        if hasattr(self, 'fitting_result') and self.fitting_result is not None:
            try:
                with open(result_filename, "w", encoding="utf-8") as f:
                    spectrum_id = experiment_name + timestamp
                    f.write(f"{spectrum_id}\n")

                    params = self.fitting_result.params

                    lambda1 = params['peak1_center'].value
                    sigma1 = params['peak1_center'].stderr or 0
                    fwhm1 = params['peak1_fwhm'].value
                    sigma1_val = params['peak1_sigma'].value
                    eta1 = params['peak1_fraction'].value

                    lambda2 = params['peak2_center'].value
                    sigma2 = params['peak2_center'].stderr or 0
                    fwhm2 = params['peak2_fwhm'].value
                    sigma2_val = params['peak2_sigma'].value
                    eta2 = params['peak2_fraction'].value

                    lambda_ref = float(self.lambda_ref_entry.get())
                    lambda_ref_temp = lambda_ref
                    lambda1_temp = lambda1

                    # Apply temperature correction if enabled
                    if self.tab1_temp_corr_var.get():
                        try:
                            T_ref_tab1 = float(self.tref_entry.get())
                            delta_lambda_T_ref_tab1 = lambda_temp_corr(T_ref_tab1)

                            T_sample_tab1 = float(self.tsample_entry.get())
                            delta_lambda_T_sample_tab1 = lambda_temp_corr(T_sample_tab1)
                            
                            lambda_ref_temp = lambda_ref - delta_lambda_T_ref_tab1
                            lambda1_temp = lambda1 - delta_lambda_T_sample_tab1
                        except Exception as e:
                            print("Temperature correction error:", e)

                    pressure = 1870 * ((lambda1_temp - lambda_ref_temp) / lambda_ref_temp) * (1 + 5.63 * (lambda1_temp - lambda_ref_temp) / lambda_ref_temp)

                    line = f"Pressure: {pressure:.6f} GPa\nλ_ref = {lambda_ref:.6f} nm\n"

                    if self.tab1_temp_corr_var.get():
                        line += f"T ref: {T_ref_tab1} K\tT sample: {T_sample_tab1} K\n"

                    line += "\n"
                    f.write(line)


                    # Check if third peak was added
                    if 'peak3_center' in params:
                        lambda3 = params['peak3_center'].value
                        sigma3 = params['peak3_center'].stderr or 0
                        fwhm3 = params['peak3_fwhm'].value
                        sigma3_val = params['peak3_sigma'].value
                        eta3 = params['peak3_fraction'].value

                        f.write("Spectrum_ID\tPressure\tλ1\tλ1_SE\tR²\tSNR\tFWHM_1\tσ1\tη1\t"
                                "λ2\tλ2_SE\tFWHM_2\tσ2\tη2\t"
                                "λ3\tλ3_SE\tFWHM_3\tσ3\tη3\n")
                        f.write(
                            f"{spectrum_id}\t{pressure:.6f}\t"
                            f"{lambda1:.6f}\t{sigma1:.6f}\t{fwhm1:.6f}\t{sigma1_val:.6f}\t{eta1:.6f}\t"
                            f"{lambda2:.6f}\t{sigma2:.6f}\t{fwhm2:.6f}\t{sigma2_val:.6f}\t{eta2:.6f}\t"
                            f"{lambda3:.6f}\t{sigma3:.6f}\t{fwhm3:.6f}\t{sigma3_val:.6f}\t{eta3:.6f}\n"
                        )
                    else:
                        f.write("Spectrum_ID\tPressure\tλ1\tλ1_SE\tR²\tSNR\tFWHM_1\tσ1\tη1\tλ2\tλ2_SE\tFWHM_2\tσ2\tη2\n")
                        f.write(
                            f"{spectrum_id}\t{pressure:.6f}\t"
                            f"{lambda1:.6f}\t{sigma1:.6f}\t{fwhm1:.6f}\t{sigma1_val:.6f}\t{eta1:.6f}\t"
                            f"{lambda2:.6f}\t{sigma2:.6f}\t{fwhm2:.6f}\t{sigma2_val:.6f}\t{eta2:.6f}\n"
                        )

            except Exception as e:
                messagebox.showerror("Error", f"Failed to save result file: {e}")


    def create_quit_button(self):
        # Quit Button and About Button
        button_frame = ttk.Frame(self.root)
        button_frame.pack(side=tk.RIGHT, anchor='se', padx=10, pady=10)

        ttk.Button(button_frame, text="About", command=self.show_about_popup).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="Quit", command=self.root.quit).pack(side=tk.LEFT, padx=5)


    def calculate_pressure(self, event=None):
        try:
            if hasattr(self, 'peak1_center'):
                self.lambda_ref = float(self.lambda_ref_entry.get())

                self.lambda_ref_temp = self.lambda_ref
                self.peak1_center_temp = self.peak1_center

                # Apply temperature correction if enabled
                if self.tab1_temp_corr_var.get():
                    try:
                        self.T_ref = float(self.tref_entry.get())
                        self.delta_lambda_T_ref = lambda_temp_corr(self.T_ref)

                        self.T_sample = float(self.tsample_entry.get())
                        self.delta_lambda_T_sample = lambda_temp_corr(self.T_sample)
                        

                        self.lambda_ref_temp = self.lambda_ref - self.delta_lambda_T_ref
                        self.peak1_center_temp = self.peak1_center - self.delta_lambda_T_sample
                    except Exception as e:
                        print("Temperature correction error:", e)

                pressure = 1870 * ((self.peak1_center_temp - self.lambda_ref_temp) / self.lambda_ref_temp) * (1 + 5.63 * (self.peak1_center_temp - self.lambda_ref_temp) / self.lambda_ref_temp)
                self.pressure_value_label.config(text=f"Pressure: {pressure:.3f} GPa")
        except ValueError:
            self.pressure_value_label.config(text="Pressure: N/A")


    def handle_drop(self, event):
        dropped_paths = self.root.tk.splitlist(event.data)
        for path in dropped_paths:
            if os.path.isfile(path):
                if path.lower().endswith(".txt") or re.search(r"\.\d$", path):
                    abs_path = os.path.abspath(path)
                    if abs_path not in self.dropped_files:  # avoid duplicates
                        self.dropped_files.append(abs_path)
                        abbrev = os.path.join("...", os.path.basename(os.path.dirname(path)), os.path.basename(path))
                        self.dropped_files_listbox.insert(tk.END, abbrev)
                else:
                    messagebox.showwarning("Unsupported File", f"Skipped: {os.path.basename(path)}\nOnly .txt and .[0-9] files are allowed.")


    def remove_selected_dropped_file(self):
        selected_indices = self.dropped_files_listbox.curselection()
        for index in reversed(selected_indices):  # Remove from end to start to avoid index shifting
            self.dropped_files_listbox.delete(index)
            del self.dropped_files[index]


    # Copy Tab3 Ruby data
    def copy_to_clipboard(self, text):
        self.root.clipboard_clear()
        self.root.clipboard_append(text)
        self.root.update()  # Ensures clipboard retains content


    def save_ruby_pressure_tab3(self):
        try:
            lambda_ref = self.lambda_ref_tab3.get()
            peak1_center = self.peak1_center_tab3.get()
            pressure_text = self.pressure_result_tab3.cget("text").replace("Pressure: ", "").replace(" GPa", "")
            temp_info = ""

            if self.tab3_temp_corr_var.get():
                    T_sample = float(self.tab3_tsample_entry.get())
                    T_ref = float(self.tab3_tref_entry.get())
                    temp_info = f"\t(T sample: {T_sample} K, T ref: {T_ref} K)"

            if not lambda_ref or not peak1_center or "N/A" in pressure_text:
                messagebox.showwarning("Warning", "Please ensure all values are filled and valid.")
                return

            timestamp = time.strftime("%Y-%m-%d_%H:%M:%S")

            desktop_path = os.path.join(os.path.expanduser("~"), "Desktop")
            result_path = os.path.join(desktop_path, "CrimsonCalc_results.txt")

            with open(result_path, "a", encoding="utf-8") as f:
                f.write(f"{timestamp}\tRuby pressure:\t{pressure_text}\t{lambda_ref}\t{peak1_center}\t{temp_info}\n")
            
        except Exception as e:
            messagebox.showerror("Error", f"Failed to save values:\n{e}")


    def save_diamond_pressure_tab3(self):
        try:
            ref = self.diamond_edge_ref_entry.get()
            sample = self.diamond_edge_sample_entry.get()
            pressure_text = self.diamond_pressure_result_label.cget("text").replace("Pressure: ", "").replace(" GPa", "")

            if not ref or not sample or "N/A" in pressure_text:
                messagebox.showwarning("Warning", "Please ensure all values are filled and valid.")
                return

            timestamp = time.strftime("%Y-%m-%d %H:%M:%S")

            desktop_path = os.path.join(os.path.expanduser("~"), "Desktop")
            result_path = os.path.join(desktop_path, "CrimsonCalc_results.txt")

            with open(result_path, "a", encoding="utf-8") as f:
                f.write(f"{timestamp}\tDiamond Edge Pressure:\t{pressure_text}\t{ref}\t{sample}\n")

        except Exception as e:
            messagebox.showerror("Error", f"Failed to save diamond edge values:\n{e}")


    def save_ruby_error_propagation_tab3(self):
        try:
            ls = self.ruby_lambda_sample_entry.get()
            sls = self.ruby_sigma_sample_entry.get()
            lr = self.ruby_lambda_ref_entry.get()
            slr = self.ruby_sigma_ref_entry.get()
            s_instr = self.ruby_instr_entry.get()
            rho = self.rho_slider.get()
            result_text = self.ruby_error_result_label.cget("text").replace("Pressure: ", "").replace(" GPa", "")

            if not all([ls, sls, lr, slr, s_instr]) or "N/A" in result_text:
                messagebox.showwarning("Warning", "Please ensure all values are filled and valid.")
                return

            timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
            desktop_path = os.path.join(os.path.expanduser("~"), "Desktop")
            result_path = os.path.join(desktop_path, "CrimsonCalc_results.txt")

            with open(result_path, "a", encoding="utf-8") as f:
                f.write(f"{timestamp}\tError Propagation calculation:\t{result_text}\t{ls}\t{sls}\t{lr}\t{slr}\t{s_instr}\t{rho:.1f}\n")

        except Exception as e:
            messagebox.showerror("Error", f"Failed to save error propagation values:\n{e}")



    def save_gasket_thickness_tab3(self):
        try:
            n = self.n_entry.get()
            peak1 = self.gasket_peak1_entry.get()
            peak2 = self.gasket_peak2_entry.get()
            thickness_text = self.thickness_result_label.cget("text").replace("Gasket thickness: ", "").replace(" μm", "")

            if not n or not peak1 or not peak2 or "N/A" in thickness_text or "must be different" in thickness_text:
                messagebox.showwarning("Warning", "Please ensure all values are filled and valid.")
                return

            timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
            desktop_path = os.path.join(os.path.expanduser("~"), "Desktop")
            result_path = os.path.join(desktop_path, "CrimsonCalc_results.txt")

            with open(result_path, "a", encoding="utf-8") as f:
                f.write(f"{timestamp}\tGasket Thickness:\t{thickness_text}\t{n}\t{peak1}\t{peak2}\n")

        except Exception as e:
            messagebox.showerror("Error", f"Failed to save gasket thickness values:\n{e}")


    def save_upper_pressure_limit_tab3(self):
        try:
            culet_diam = self.culet_diam_entry.get()
            dunstan = self.dunstan_limit_label.cget("text").replace("Dunstan et al.: ", "").replace(" GPa", "")
            ruoff = self.ruoff_limit_label.cget("text").replace("Ruoff et al.: ", "").replace(" GPa", "")
            obannon = self.obannon_limit_label.cget("text").replace("O'Bannon et al.: ", "").replace(" GPa", "")

            if not culet_diam or "N/A" in (dunstan + ruoff + obannon):
                messagebox.showwarning("Warning", "Please ensure all values are filled and valid.")
                return

            timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
            desktop_path = os.path.join(os.path.expanduser("~"), "Desktop")
            result_path = os.path.join(desktop_path, "CrimsonCalc_results.txt")

            with open(result_path, "a", encoding="utf-8") as f:
                f.write(f"{timestamp}\tUpper Pressure Limit:\t{culet_diam}\t{dunstan}\t{ruoff}\t{obannon}\n")

        except Exception as e:
            messagebox.showerror("Error", f"Failed to save upper pressure limit values:\n{e}")


    def show_about_popup(self):
        about_text = (
            "© 2025  Miha Virant\n"
            "This program is free software; you can redistribute it and/or modify it under the terms of the "
            "GNU General Public License as published by the Free Software Foundation; version 3.\n\n"
            "CrimsonCalc is developed at the Jožef Stefan Institute, Extreme Conditions Chemistry Laboratory,\n"
            "Jamova cesta 39, 1000 Ljubljana, Slovenia. For more information, visit https://eccl.ijs.si/\n\n\n"
            "Scientific References:\n"
            "• Pressure from ruby fluorescence: Shen et al. (https://doi.org/10.1080/08957959.2020.1791107)\n"
            "• Pressure from diamond anvil Raman edge: Eremets et al. (https://doi.org/10.1038/s41467-023-36429-9)\n"
            "• Gasket thickness determination with interferometry: Kim et al. (https://doi.org/10.1038/s41598-021-84883-6)\n"
            "• Upper pressure limit based on diamond culet size by Dunstan et al. (https://doi.org/10.1088/0022-3735/22/11/004)\n"
            "• Upper pressure limit based on diamond culet size by Ruoff et al. (https://doi.org/10.1063/1.1141509)\n"
            "• Upper pressure limit based on diamond culet size by O'Bannon et al. (https://doi.org/10.1063/1.5049720)\n"
            "• Temperature dependence of ruby fluorescence peaks: Datchi et al. (https://doi.org/10.1080/08957950701659593)\n\n\n"

            "Python Packages Used:\n"
            "• NumPy – https://numpy.org (BSD License © 2005-2024, NumPy Developers)\n"
            "• SciPy – https://scipy.org (BSD License © 2001-2002 Enthought, Inc. 2003-2024, SciPy Developers)\n"
            "• Matplotlib – https://matplotlib.org; https://doi.ieeecomputersociety.org/10.1109/MCSE.2007.55 (Python Software Foundation License, © 2012 – 2024, The Matplotlib development team)\n"
            "• Tkinter – https://docs.python.org/3/library/tkinter.html (Python Software Foundation License Version 2, © 2001-2025, Python Software Foundation)\n"
            "• TkinterDnD2 – https://github.com/pmgagne/tkinterdnd2 (MIT License, © 2020, Philippe Gagné)\n"
            "• LMFit – https://lmfit.github.io/lmfit-py/ ; https://doi.org/10.5281/zenodo.15014437 (BSD-3 License, © 2025, LMFit Development Team) \n"
            "• pybaselines – https://github.com/derb12/pybaselines (BSD 3-Clause License, © 2021, Donald Erb)\n"
            "• brukeropus – https://github.com/joshduran/brukeropus (MIT License, © 2024, joshduran, Josh Duran)\n\n\n"
            "Part of this script was developed with the assistance of ChatGPT (GPT-4 by OpenAI, accessed 2025).\n"

        )

        popup = tk.Toplevel(self.root)
        popup.title("About CrimsonCalc")
        popup.geometry("800x500")
        popup.resizable(True, True)

        text_widget = tk.Text(popup, wrap="word")
        text_widget.insert("1.0", about_text)
        text_widget.config(state="normal")  # allow selection/copying
        text_widget.pack(expand=True, fill="both", padx=10, pady=10)

        ttk.Button(popup, text="Close", command=popup.destroy).pack(pady=5)


# Utility to support bundled resource paths
def resource_path(relative_path):
    """ Get absolute path to resource (works for dev and for PyInstaller bundle) """
    try:
        base_path = sys._MEIPASS  # PyInstaller temp folder
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)


def resource_path(relative_path):
    """ Get the path to the resource, whether in a bundle or a normal file system. """
    try:
        # PyInstaller stores data files in a temporary folder in bundle mode
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)


# Run the GUI
if __name__ == "__main__":
    root = TkinterDnD.Tk()  # <-- Correct for drag-and-drop
    icon_img = tk.PhotoImage(file=resource_path('crimsoncalc.png'))
    copy_icon = PhotoImage(file=resource_path('copy_icon.png'))
    save_icon = PhotoImage(file=resource_path('save_icon.png'))
    root.iconphoto(False, icon_img)
    app = RealTimePlotApp(root)
    root.mainloop()