import json
import logging
import os
import shutil
import sys
import traceback

import numpy as np
import pyqtgraph as pg
from pyqtgraph.parametertree import Parameter
from pyqtgraph.Qt import QtCore
from PySide6 import QtCore, QtWidgets
from PySide6.QtCore import qInstallMessageHandler

from .viewer_kernel import ViewerKernel
from .viewer_ui import Ui_mainWindow as Ui

format = logging.Formatter("%(asctime)s %(message)s")
home_dir = os.path.join(os.path.expanduser("~"), ".pyxpcsviewer")
if not os.path.isdir(home_dir):
    os.mkdir(home_dir)

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)-24s: %(message)s")

logger = logging.getLogger(__name__)


def exception_hook(exc_type, exc_value, exc_traceback):
    logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))


sys.excepthook = exception_hook


tab_mapping = {
    0: "saxs_2d",
    1: "saxs_1d",
    2: "stability",
    3: "intensity_t",
    4: "g2",
    5: "diffusion",
    6: "twotime",
    7: "qmap",
    8: "average",
    9: "metadata",
}


def create_param_tree(data_dict):
    """Convert a dictionary into PyQtGraph's ParameterTree format."""
    params = []
    for key, value in data_dict.items():
        if isinstance(value, dict):  # If value is a nested dictionary
            params.append(
                {"name": key, "type": "group", "children": create_param_tree(value)}
            )
        elif isinstance(value, (int, float, np.number)):  # Numeric types
            params.append({"name": key, "type": "float", "value": float(value)})
        elif isinstance(value, str):  # String types
            params.append({"name": key, "type": "str", "value": value})
        elif isinstance(value, np.ndarray):  # Numpy arrays
            params.append({"name": key, "type": "text", "value": str(value.tolist())})
        else:  # Default fallback
            params.append({"name": key, "type": "text", "value": str(value)})
    return params


class XpcsViewer(QtWidgets.QMainWindow, Ui):
    def __init__(self, path=None, label_style=None):
        super(XpcsViewer, self).__init__()
        self.setupUi(self)
        self.home_dir = home_dir
        self.label_style = label_style

        self.tabWidget.setCurrentIndex(0)  # show scattering 2d
        self.plot_kwargs_record = {}
        for _, v in tab_mapping.items():
            self.plot_kwargs_record[v] = {}

        self.thread_pool = QtCore.QThreadPool()
        logger.info("Maximal threads: %d", self.thread_pool.maxThreadCount())

        self.vk = None
        # list widget models
        self.source_model = None
        self.target_model = None
        self.timer = QtCore.QTimer()

        if path is not None:
            self.start_wd = path
            self.load_path(path)
        else:
            # use home directory
            self.start_wd = os.path.expanduser("~")

        self.start_wd = os.path.abspath(self.start_wd)
        logger.info("Start up directory is [{}]".format(self.start_wd))

        self.pushButton_plot_saxs2d.clicked.connect(self.plot_saxs_2d)
        self.pushButton_plot_saxs1d.clicked.connect(self.plot_saxs_1d)
        self.pushButton_plot_stability.clicked.connect(self.plot_stability)
        self.pushButton_plot_intt.clicked.connect(self.plot_intensity_t)
        # self.saxs1d_lb_type.currentIndexChanged.connect(self.switch_saxs1d_line)

        self.tabWidget.currentChanged.connect(self.update_plot)
        self.list_view_target.clicked.connect(self.update_plot)

        self.mp_2t_hdls = None
        self.init_twotime_plot_handler()

        self.avg_job_pop.clicked.connect(self.remove_avg_job)
        self.btn_submit_job.clicked.connect(self.submit_job)
        self.btn_start_avg_job.clicked.connect(self.start_avg_job)
        self.btn_set_average_save_path.clicked.connect(self.set_average_save_path)
        self.btn_set_average_save_name.clicked.connect(self.set_average_save_name)
        self.btn_avg_kill.clicked.connect(self.avg_kill_job)
        self.btn_avg_jobinfo.clicked.connect(self.show_avg_jobinfo)
        self.avg_job_table.clicked.connect(self.update_avg_info)
        self.show_g2_fit_summary.clicked.connect(self.show_g2_fit_summary_func)
        self.btn_g2_refit.clicked.connect(self.plot_g2)
        self.saxs2d_autolevel.stateChanged.connect(self.update_saxs2d_level)
        self.btn_deselect.clicked.connect(self.clear_target_selection)
        self.list_view_target.doubleClicked.connect(self.show_dataset)
        self.btn_select_bkgfile.clicked.connect(self.select_bkgfile)
        self.spinBox_saxs2d_selection.valueChanged.connect(self.plot_saxs_2d_selection)
        self.comboBox_twotime_selection.currentIndexChanged.connect(self.update_plot)

        self.g2_fitting_function.currentIndexChanged.connect(
            self.update_g2_fitting_function
        )
        self.btn_up.clicked.connect(lambda: self.reorder_target("up"))
        self.btn_down.clicked.connect(lambda: self.reorder_target("down"))

        self.btn_export_saxs1d.clicked.connect(self.saxs1d_export)

        self.comboBox_qmap_target.currentIndexChanged.connect(self.update_plot)
        self.update_g2_fitting_function()

        self.pg_saxs.getView().scene().sigMouseMoved.connect(self.saxs2d_mouseMoved)

        self.load_default_setting()
        self.show()

    def load_default_setting(self):
        if not os.path.isdir(self.home_dir):
            os.mkdir(self.home_dir)

        key_fname = os.path.join(self.home_dir, "default_setting.json")
        # copy the default values
        if not os.path.isfile(key_fname):
            from .default_setting import setting

            with open(key_fname, "w") as f:
                json.dump(setting, f, indent=4)

        # the display size might too big for some laptops
        with open(key_fname, "r") as f:
            config = json.load(f)
            if "window_size_h" in config:
                new_size = (config["window_size_w"], config["window_size_h"])
                logger.info("set mainwindow to size %s", new_size)
                self.resize(*new_size)

        cache_dir = os.path.join(
            os.path.expanduser("~"), ".pyxpcsviewer", "joblib/pyxpcsviewer"
        )
        if os.path.isdir(cache_dir):
            shutil.rmtree(cache_dir)

        return

    def get_selected_rows(self):
        selected_index = self.list_view_target.selectedIndexes()
        selected_row = [x.row() for x in selected_index]
        # the selected index is ordered;
        selected_row.sort()
        return selected_row

    def update_plot(self):
        idx = self.tabWidget.currentIndex()
        tab_name = tab_mapping[idx]
        if tab_name == "average":
            return
        func = getattr(self, "plot_" + tab_name)
        try:
            kwargs = func(dryrun=True)
            kwargs["target_timestamp"] = self.vk.timestamp
            if self.plot_kwargs_record[tab_name] != kwargs:
                self.plot_kwargs_record[tab_name] = kwargs
                func(dryrun=False)
        except Exception as e:
            logger.error(f"update selection in [{tab_name}] failed")
            logger.error(e)
            traceback.print_exc()

    def plot_metadata(self, dryrun=False):
        kwargs = {"rows": self.get_selected_rows()}
        if dryrun:
            return kwargs

        msg = self.vk.get_xf_list(**kwargs)[0].get_hdf_info()
        hdf_info_data = create_param_tree(msg)
        hdf_params = Parameter.create(
            name="Settings", type="group", children=hdf_info_data
        )
        self.hdf_info.setParameters(hdf_params, showTop=True)

    def saxs2d_mouseMoved(self, pos):
        if self.pg_saxs.view.sceneBoundingRect().contains(pos):
            mouse_point = self.pg_saxs.getView().mapSceneToView(pos)
            x, y = int(mouse_point.x()), int(mouse_point.y())
            rows = self.get_selected_rows()
            payload = self.vk.get_info_at_mouse(rows, x, y)
            if payload:
                self.saxs2d_display.setText(payload)

    def plot_saxs_2d_selection(self):
        selection = self.spinBox_saxs2d_selection.value()
        self.plot_saxs_2d(selection=selection)

    def plot_saxs_2d(self, selection=None, dryrun=False):
        kwargs = {
            "plot_type": self.cb_saxs2D_type.currentText(),
            "cmap": self.cb_saxs2D_cmap.currentText(),
            "rotate": self.saxs2d_rotate.isChecked(),
            "autolevel": self.saxs2d_autolevel.isChecked(),
            "vmin": self.saxs2d_min.value(),
            "vmax": self.saxs2d_max.value(),
        }
        if selection and selection >= 0:
            kwargs["rows"] = [selection]
        else:
            kwargs["rows"] = self.get_selected_rows()

        if dryrun:
            return kwargs
        else:
            self.vk.plot_saxs_2d(pg_hdl=self.pg_saxs, **kwargs)

    def saxs2d_roi_add(self):
        sl_type_idx = self.cb_saxs2D_roi_type.currentIndex()
        color = ("g", "y", "b", "r", "c", "m", "k", "w")[
            self.cb_saxs2D_roi_color.currentIndex()
        ]
        kwargs = {
            "sl_type": ("Pie", "Circle")[sl_type_idx],
            "width": self.sb_saxs2D_roi_width.value(),
            "color": color,
        }
        self.vk.add_roi(self.pg_saxs, **kwargs)

    def plot_saxs_1d(self, dryrun=False):
        kwargs = {
            "plot_type": self.cb_saxs_type.currentIndex(),
            "plot_offset": self.sb_saxs_offset.value(),
            "plot_norm": self.cb_saxs_norm.currentIndex(),
            "rows": self.get_selected_rows(),
            "qmin": self.saxs1d_qmin.value(),
            "qmax": self.saxs1d_qmax.value(),
            "loc": self.saxs1d_legend_loc.currentText(),
            "marker_size": self.sb_saxs_marker_size.value(),
            "sampling": self.saxs1d_sampling.value(),
            "all_phi": self.box_all_phi.isChecked(),
            "absolute_crosssection": self.cbox_use_abs.isChecked(),
            "subtract_background": self.cb_sub_bkg.isChecked(),
            "weight": self.bkg_weight.value(),
            "show_roi": self.box_show_roi.isChecked(),
            "show_phi_roi": self.box_show_phi_roi.isChecked(),
        }
        if kwargs["qmin"] >= kwargs["qmax"]:
            self.statusbar.showMessage("check qmin and qmax")
            return

        if dryrun:
            return kwargs
        else:
            self.vk.plot_saxs_1d(self.pg_saxs, self.mp_saxs, **kwargs)
            # adjust the line behavior
            self.switch_saxs1d_line()

    def switch_saxs1d_line(self):
        lb_type = self.saxs1d_lb_type.currentIndex()
        lb_type = [None, "slope", "hline"][lb_type]
        self.vk.switch_saxs1d_line(self.mp_saxs, lb_type)

    def saxs1d_export(self):
        folder = QtWidgets.QFileDialog.getExistingDirectory(
            self, caption="select a folder to export SAXS profiles"
        )

        if folder in [None, ""]:
            return

        self.vk.export_saxs_1d(self.pg_saxs, folder)

    def init_twotime_plot_handler(self):
        # self.mp_2t.setBackground('w')
        self.mp_2t_hdls = {}
        labels = ["saxs", "dqmap"]
        titles = ["scattering", "dynamic_qmap"]
        cmaps = ["viridis", "tab20"]
        self.mp_2t_map.setBackground("w")
        for n in range(2):
            plot_item = self.mp_2t_map.addPlot(row=0, col=n)
            # Remove axes
            plot_item.hideAxis("left")
            plot_item.hideAxis("bottom")
            plot_item.getViewBox().setDefaultPadding(0)

            plot_item.setMouseEnabled(x=False, y=False)
            image_item = pg.ImageItem(np.ones((128, 128)))
            image_item.setOpts(axisOrder="row-major")  # Set to row-major order

            plot_item.setTitle(titles[n])
            plot_item.addItem(image_item)
            plot_item.setAspectLocked(True)

            cmap = pg.colormap.getFromMatplotlib(cmaps[n])
            if n == 1:
                positions = cmap.pos
                colors = cmap.color
                new_color = [0, 0, 1, 1.0]
                colors[-1] = new_color
                # need to convert to 0-255 range for pyqtgraph ColorMap
                cmap = pg.ColorMap(positions, colors * 255)
            colorbar = plot_item.addColorBar(image_item, colorMap=cmap)
            self.mp_2t_hdls[labels[n]] = image_item
            self.mp_2t_hdls[labels[n] + "_colorbar"] = colorbar

        c2g2_plot = self.mp_2t_map.addPlot(row=0, col=2)
        self.mp_2t_hdls["c2g2"] = c2g2_plot

        self.mp_2t_hdls["dqmap"].mouseClickEvent = self.pick_twotime_index
        self.mp_2t_hdls["saxs"].mouseClickEvent = self.pick_twotime_index
        # self.mp_2t.getView().setBackgroundColor('w')
        self.mp_2t.ui.graphicsView.setBackground("w")
        self.mp_2t_hdls["tt"] = self.mp_2t
        self.mp_2t_hdls["tt"].view.invertY(False)
        self.mp_2t.view.setLabel("left", "t2", units="s")
        self.mp_2t.view.setLabel("bottom", "t1", units="s")

    def pick_twotime_index(self, event):
        if event.button() == QtCore.Qt.LeftButton:
            pos = event.pos()
            x, y = int(pos.x()), int(pos.y())
            self.plot_twotime(highlight_xy=(x, y))
        event.accept()  # Mark the event as handled

    def plot_qmap(self, dryrun=False):
        kwargs = {
            "rows": self.get_selected_rows(),
            "target": self.comboBox_qmap_target.currentText(),
        }
        if dryrun:
            return kwargs
        self.vk.plot_qmap(self.pg_qmap, **kwargs)

    def plot_twotime(self, dryrun=False, highlight_xy=None):
        kwargs = {
            "rows": self.get_selected_rows(),
            "auto_crop": self.twotime_autocrop.isChecked(),
            "highlight_xy": highlight_xy,
            "cmap": self.cb_twotime_cmap.currentText(),
            "vmin": self.c2_min.value(),
            "vmax": self.c2_max.value(),
            "correct_diag": self.twotime_correct_diag.isChecked(),
            "autolevel": self.checkBox_twotime_autolevel.isChecked(),
            "selection": max(0, self.comboBox_twotime_selection.currentIndex()),
        }
        if dryrun:
            return kwargs

        if self.mp_2t_hdls is None:
            self.init_twotime_plot_handler()
        new_labels = self.vk.plot_twotime(self.mp_2t_hdls, **kwargs)
        if new_labels is not None:
            self.comboBox_twotime_selection.clear()
            self.comboBox_twotime_selection.addItems(new_labels)
            self.horizontalSlider_twotime_selection.setMaximum(len(new_labels) - 1)

    def show_dataset(self):
        rows = self.get_selected_rows()
        self.tree = self.vk.get_pg_tree(rows)
        if self.tree:
            self.tree.show()

    def plot_stability(self, dryrun=False):
        kwargs = {
            "plot_type": self.cb_stab_type.currentIndex(),
            "plot_norm": self.cb_stab_norm.currentIndex(),
            "rows": self.get_selected_rows(),
            "loc": self.stab_legend_loc.currentText(),
        }
        if dryrun:
            return kwargs
        else:
            self.vk.plot_stability(self.mp_stab, **kwargs)

    def plot_intensity_t(self, dryrun=False):
        kwargs = {
            "sampling": max(1, self.sb_intt_sampling.value()),
            "window": self.sb_window.value(),
            "rows": self.get_selected_rows(),
            "xlabel": self.intt_xlabel.currentText(),
        }
        if dryrun:
            return kwargs
        else:
            self.vk.plot_intt(self.pg_intt, **kwargs)

    def init_diffusion(self):
        self.vk.plot_tauq_pre(hdl=self.mp_tauq_pre.hdl)

    def plot_diffusion(self, dryrun=False):
        keys = [self.tauq_amin, self.tauq_bmin, self.tauq_amax, self.tauq_bmax]
        bounds = np.array([float(x.text()) for x in keys]).reshape(2, 2)

        fit_flag = [self.tauq_afit.isChecked(), self.tauq_bfit.isChecked()]

        if sum(fit_flag) == 0:
            self.statusbar.showMessage("nothing to fit, really?", 1000)
            return

        tauq = [self.tauq_qmin, self.tauq_qmax]
        q_range = [float(x.text()) for x in tauq]

        kwargs = {
            "bounds": bounds.tolist(),
            "fit_flag": fit_flag,
            "offset": self.sb_tauq_offset.value(),
            "rows": self.get_selected_rows(),
            "q_range": q_range,
            "plot_type": self.cb_tauq_type.currentIndex(),
        }
        if dryrun:
            return kwargs
        else:
            msg = self.vk.plot_tauq(hdl=self.mp_tauq.hdl, **kwargs)
            self.mp_tauq.parent().repaint()
            self.tauq_msg.clear()
            self.tauq_msg.setData(msg)
            self.tauq_msg.parent().repaint()

    def select_bkgfile(self):
        path = self.work_dir.text()
        f = QtWidgets.QFileDialog.getOpenFileName(
            self, caption="select the file for background subtraction", dir=path
        )[0]
        if os.path.isfile(f):
            self.le_bkg_fname.setText(f)
            self.vk.select_bkgfile(f)
        else:
            return

    def remove_avg_job(self):
        index = self.avg_job_table.currentIndex().row()
        if index < 0:
            return
        self.vk.remove_job(index)

    def set_average_save_path(self):
        save_path = QtWidgets.QFileDialog.getExistingDirectory(self, "Open directory")
        self.avg_save_path.clear()
        self.avg_save_path.setText(save_path)
        return

    def set_average_save_name(self):
        save_name = QtWidgets.QFileDialog.getSaveFileName(self, "Save as")
        self.avg_save_name.clear()
        self.avg_save_name.setText(os.path.basename(save_name[0]))
        return

    def init_average(self):
        if len(self.vk.target) > 0:
            save_path = self.avg_save_path.text()
            if save_path == "":
                self.avg_save_path.setText(self.work_dir.text())
            else:
                logger.info("use the previous save path")

            save_name = self.avg_save_name.text()
            save_name = "Avg" + self.vk.target[0]
            self.avg_save_name.setText(save_name)

    def submit_job(self):
        if len(self.vk.target) < 2:
            self.statusbar.showMessage("select at least 2 files for averaging", 1000)
            return

        self.thread_pool.setMaxThreadCount(self.max_thread_count.value())

        save_path = self.avg_save_path.text()
        save_name = self.avg_save_name.text()

        if not os.path.isdir(save_path):
            logger.info("the average save_path doesn't exist; creating one")
            try:
                os.mkdir(save_path)
            except:
                logger.info("cannot create the folder: %s", save_path)
                return

        avg_fields = []
        if self.bx_avg_G2IPIF.isChecked():
            avg_fields.extend(["G2", "IP", "IF"])
        if self.bx_avg_g2g2err.isChecked():
            avg_fields.extend(["g2", "g2_err"])
        if self.bx_avg_saxs.isChecked():
            avg_fields.extend(["saxs_1d", "saxs_2d"])

        if len(avg_fields) == 0:
            self.statusbar.showMessage("No average field is selected. quit", 1000)
            return

        kwargs = {
            "save_path": os.path.join(save_path, save_name),
            "chunk_size": int(self.cb_avg_chunk_size.currentText()),
            "avg_blmin": self.avg_blmin.value(),
            "avg_blmax": self.avg_blmax.value(),
            "avg_qindex": self.avg_qindex.value(),
            "avg_window": self.avg_window.value(),
            "fields": avg_fields,
        }

        if kwargs["avg_blmax"] <= kwargs["avg_blmin"]:
            self.statusbar.showMessage("check avg min/max values.", 1000)
            return

        self.vk.submit_job(**kwargs)
        # the target_average has been reset
        self.update_box(self.vk.target, mode="target")

    def update_avg_info(self):
        index = self.avg_job_table.currentIndex().row()
        if index < 0 or index >= len(self.vk.avg_worker):
            self.statusbar.showMessage("select a job to start", 1000)
            return

        self.timer.stop()
        self.timer.setInterval(1000)

        try:
            self.timer.timeout.disconnect()
            logger.info("disconnect previous slot")
        except:
            pass

        worker = self.vk.avg_worker[index]
        worker.initialize_plot(self.mp_avg_g2)

        self.timer.timeout.connect(lambda x=index: self.vk.update_avg_info(x))
        self.timer.start()

    def start_avg_job(self):
        index = self.avg_job_table.currentIndex().row()
        if index < 0 or index >= len(self.vk.avg_worker):
            self.statusbar.showMessage("select a job to start", 1000)
            return
        worker = self.vk.avg_worker[index]
        if worker.status == "finished":
            self.statusbar.showMessage("this job has finished", 1000)
            return
        elif worker.status == "running":
            self.statusbar.showMessage("this job is running.", 1000)
            return

        # worker.signals.progress.connect(worker.update_plot)
        # worker.signals.progress.connect(self.vk.update_avg_worker)
        worker.signals.values.connect(self.vk.update_avg_values)
        self.thread_pool.start(worker)
        self.vk.avg_worker_active[worker.jid] = None

    def avg_kill_job(self):
        index = self.avg_job_table.currentIndex().row()
        if index < 0 or index >= len(self.vk.avg_worker):
            self.statusbar.showMessage("select a job to kill", 1000)
            return
        worker = self.vk.avg_worker[index]
        if worker.status != "running":
            self.statusbar.showMessage("the selected job isn's running", 1000)
            return
        worker.kill()

    def show_g2_fit_summary_func(self):
        rows = self.get_selected_rows()
        self.tree = self.vk.get_fitting_tree(rows)
        self.tree.show()

    def show_avg_jobinfo(self):
        index = self.avg_job_table.currentIndex().row()
        if index < 0 or index >= len(self.vk.avg_worker):
            logger.info("select a job to show it's settting")
            return
        worker = self.vk.avg_worker[index]
        self.tree = worker.get_pg_tree()
        self.tree.show()

    def init_g2(self, qd, tel):
        if qd is None or tel is None:
            return None

        q_auto = self.g2_qauto.isChecked()
        t_auto = self.g2_tauto.isChecked()

        # tel is a list of arrays, which may have diffent shape;
        t_min = np.min([t[0] for t in tel])
        t_max = np.max([t[-1] for t in tel])

        def to_e(x):
            return "%.2e" % x

        self.g2_bmin.setValue(t_min / 20)
        # self.g2_bmax.setText(to_e(t_max * 10))
        self.g2_bmax.setValue(t_max * 10)

        if t_auto:
            self.g2_tmin.setText(to_e(t_min / 1.1))
            self.g2_tmax.setText(to_e(t_max * 1.1))

        if q_auto:
            self.g2_qmin.setValue(np.min(qd) / 1.1)
            self.g2_qmax.setValue(np.max(qd) * 1.1)

    def plot_g2(self, dryrun=False):
        p = self.check_g2_number()
        bounds, fit_flag, fit_func = self.check_g2_fitting_number()

        kwargs = {
            "num_col": self.sb_g2_column.value(),
            "offset": self.sb_g2_offset.value(),
            "show_fit": self.g2_show_fit.isChecked(),
            "show_label": self.g2_show_label.isChecked(),
            "plot_type": self.g2_plot_type.currentText(),
            "q_range": (p[0], p[1]),
            "t_range": (p[2], p[3]),
            "y_range": (p[4], p[5]),
            "y_auto": self.g2_yauto.isChecked(),
            "q_auto": self.g2_qauto.isChecked(),
            "t_auto": self.g2_tauto.isChecked(),
            "rows": self.get_selected_rows(),
            "bounds": bounds,
            "fit_flag": fit_flag,
            "marker_size": self.g2_marker_size.value(),
            "subtract_baseline": self.g2_sub_baseline.isChecked(),
            "fit_func": fit_func,
            # 'label_size': self.sb_g2_label_size.value(),
        }
        if kwargs["show_fit"] and sum(kwargs["fit_flag"]) == 0:
            self.statusbar.showMessage("nothing to fit, really?", 1000)
            return

        if dryrun:
            return kwargs
        else:
            self.pushButton_4.setDisabled(True)
            self.pushButton_4.setText("plotting")
            try:
                qd, tel = self.vk.plot_g2(handler=self.mp_g2, **kwargs)
                self.init_g2(qd, tel)
                if kwargs["show_fit"]:
                    self.init_diffusion()
            except Exception as e:
                traceback.print_exc()
            finally:
                self.pushButton_4.setEnabled(True)
                self.pushButton_4.setText("plot")

    def export_g2(self):
        self.vk.export_g2()

    def reload_source(self):
        self.pushButton_11.setText("loading")
        self.pushButton_11.setDisabled(True)
        self.pushButton_11.parent().repaint()
        path = self.work_dir.text()
        self.vk.build(path=path, sort_method=self.sort_method.currentText())
        self.pushButton_11.setText("reload")
        self.pushButton_11.setEnabled(True)
        self.pushButton_11.parent().repaint()

        self.update_box(self.vk.source, mode="source")
        self.apply_filter_to_source()

    def load_path(self, path=None, debug=False):
        if path in [None, False]:
            # DontUseNativeDialog is used so files are shown along with dirs;
            folder = QtWidgets.QFileDialog.getExistingDirectory(
                self,
                "Open directory",
                self.start_wd,
                QtWidgets.QFileDialog.DontUseNativeDialog,
            )
        else:
            folder = path

        if not os.path.isdir(folder):
            self.statusbar.showMessage("{} is not a folder.".format(folder))
            folder = self.start_wd

        self.work_dir.setText(folder)

        if self.vk is None:
            self.vk = ViewerKernel(folder, self.statusbar)
        else:
            self.vk.set_path(folder)
            self.vk.clear()

        self.reload_source()
        self.avg_job_table.setModel(self.vk.avg_worker)
        self.source_model = self.vk.source
        self.update_box(self.vk.source, mode="source")

    def update_box(self, file_list, mode="source"):
        if file_list is None:
            return
        if mode == "source":
            self.list_view_source.setModel(file_list)
            self.box_source.setTitle("Source: %5d" % len(file_list))
            self.box_source.parent().repaint()
            self.list_view_source.parent().repaint()
        elif mode == "target":
            self.list_view_target.setModel(file_list)
            self.box_target.setTitle("Target: %5d" % (len(file_list)))
            # on macos, the target box doesn't seem to update; force it
            file_list.layoutChanged.emit()
            self.box_target.repaint()
            self.list_view_target.repaint()
            max_size = len(file_list) - 1
            self.horizontalSlider_saxs2d_selection.setMaximum(max_size)
            self.horizontalSlider_saxs2d_selection.setValue(0)
            self.spinBox_saxs2d_selection.setMaximum(max_size)
            self.spinBox_saxs2d_selection.setValue(0)
        self.statusbar.showMessage("Target file list updated.", 1000)
        return

    def add_target(self):
        target = []
        for x in self.list_view_source.selectedIndexes():
            # in some cases, it will return None
            val = x.data()
            if val is not None:
                target.append(val)
        if target == []:
            return

        tab_id = self.tabWidget.currentIndex()
        tab_name = tab_mapping[tab_id]
        preload = tab_name != "average"
        self.vk.add_target(target, preload=preload)
        self.list_view_source.clearSelection()
        self.update_box(self.vk.target, mode="target")

        if tab_name == "average":
            self.init_average()
        else:
            self.update_plot()

    def reorder_target(self, direction="up"):
        rows = self.get_selected_rows()
        if len(rows) != 1 or len(self.vk.target) <= 1:
            return
        idx = self.vk.reorder_target(rows[0], direction)
        self.list_view_target.setCurrentIndex(idx)
        self.list_view_target.repaint()
        self.update_plot()
        return

    def remove_target(self):
        rmv_list = []
        for x in self.list_view_target.selectedIndexes():
            rmv_list.append(x.data())

        self.vk.remove_target(rmv_list)
        # clear selection to avoid the bug: when the last one is selected, then
        # the list will out of bounds
        self.clear_target_selection()

        # if all files are removed; then go to state 1
        if self.vk.target in [[], None] or len(self.vk.target) == 0:
            self.reset_gui()
        self.update_box(self.vk.target, mode="target")

    def reset_gui(self):
        self.vk.reset_kernel()
        for x in [
            # self.pg_saxs,
            self.pg_intt,
            self.mp_tauq,
            self.mp_g2,
            self.mp_saxs,
            self.mp_stab,
        ]:
            x.clear()
        self.le_bkg_fname.clear()

    def apply_filter_to_source(self):
        min_length = 1
        val = self.filter_str.text()
        if len(val) == 0:
            self.source_model = self.vk.source
            self.update_box(self.vk.source, mode="source")
            return
        # avoid searching when the filter lister is too short
        if len(val) < min_length:
            self.statusbar.showMessage(
                "Please enter at least %d characters" % min_length, 1000
            )
            return

        filter_type = ["prefix", "substr"][self.filter_type.currentIndex()]
        self.vk.search(val, filter_type)
        self.source_model = self.vk.source_search
        self.update_box(self.source_model, mode="source")
        self.list_view_source.selectAll()

    def check_g2_number(self, default_val=(0, 0.0092, 1e-8, 1, 0.95, 1.35)):
        keys = (
            self.g2_qmin,
            self.g2_qmax,
            self.g2_tmin,
            self.g2_tmax,
            self.g2_ymin,
            self.g2_ymax,
        )
        vals = [None] * len(keys)
        for n, key in enumerate(keys):
            if isinstance(key, QtWidgets.QDoubleSpinBox):
                val = key.value()
            elif isinstance(key, QtWidgets.QLineEdit):
                try:
                    val = float(key.text())
                except Exception:
                    key.setText(str(default_val[n]))
                    self.statusbar.showMessage("g2 number is invalid", 1000)
            vals[n] = val

        def swap_min_max(id1, id2):
            if vals[id1] > vals[id2]:
                keys[id1].setValue(vals[id2])
                keys[id2].setValue(vals[id1])
                vals[id1], vals[id2] = vals[id2], vals[id1]

        swap_min_max(0, 1)
        # swap_min_max(2, 3, lambda x: '%.2e' % x)
        swap_min_max(4, 5)

        return vals

    def check_g2_fitting_number(self):
        fit_func = ["single", "double"][self.g2_fitting_function.currentIndex()]
        keys = (
            self.g2_amin,
            self.g2_amax,
            self.g2_bmin,
            self.g2_bmax,
            self.g2_cmin,
            self.g2_cmax,
            self.g2_dmin,
            self.g2_dmax,
            self.g2_b2min,
            self.g2_b2max,
            self.g2_c2min,
            self.g2_c2max,
            self.g2_fmin,
            self.g2_fmax,
        )

        vals = [None] * len(keys)
        for n, key in enumerate(keys):
            vals[n] = key.value()

        def swap_min_max(id1, id2):
            if vals[id1] > vals[id2]:
                keys[id1].setValue(vals[id2])
                keys[id2].setValue(vals[id1])
                vals[id1], vals[id2] = vals[id2], vals[id1]

        for n in range(0, 7):
            swap_min_max(2 * n, 2 * n + 1)

        vals = np.array(vals).reshape(len(keys) // 2, 2)
        bounds = vals.T

        fit_keys = (
            self.g2_afit,
            self.g2_bfit,
            self.g2_cfit,
            self.g2_dfit,
            self.g2_b2fit,
            self.g2_c2fit,
            self.g2_ffit,
        )
        fit_flag = [x.isChecked() for x in fit_keys]

        if fit_func == "single":
            fit_flag = fit_flag[0:4]
            bounds = bounds[:, 0:4]
        bounds = bounds.tolist()
        return bounds, fit_flag, fit_func

    def update_saxs2d_level(self, flag=True):
        if not flag:
            vmin = self.pg_saxs.levelMin
            vmax = self.pg_saxs.levelMax
            if vmin is not None:
                self.saxs2d_min.setValue(vmin)
            if vmax is not None:
                self.saxs2d_max.setValue(vmax)
            self.saxs2d_min.setEnabled(True)
            self.saxs2d_max.setEnabled(True)
        else:
            self.saxs2d_min.setDisabled(True)
            self.saxs2d_max.setDisabled(True)

        self.saxs2d_min.parent().repaint()

    def clear_target_selection(self):
        self.list_view_target.clearSelection()

    def update_g2_fitting_function(self):
        idx = self.g2_fitting_function.currentIndex()
        title = [
            "g2 fitting with Single Exp:  y = a·exp[-2(x/b)^c]+d",
            "g2 fitting with Double Exp:  y = a·[f·exp[-(x/b)^c +"
            + "(1-f)·exp[-(x/b2)^c2]^2+d",
        ]
        self.groupBox_2.setTitle(title[idx])

        pvs = [
            [self.g2_b2min, self.g2_b2max, self.g2_b2fit],
            [self.g2_c2min, self.g2_c2max, self.g2_c2fit],
            [self.g2_fmin, self.g2_fmax, self.g2_ffit],
        ]

        # change from double to single
        if idx == 0:
            for n in range(3):
                pvs[n][0].setDisabled(True)
                pvs[n][1].setDisabled(True)
                pvs[n][2].setDisabled(True)
        # change from single to double
        else:
            for n in range(3):
                pvs[n][2].setEnabled(True)
                pvs[n][1].setEnabled(True)
                if pvs[n][2].isChecked():
                    pvs[n][0].setEnabled(True)


def setup_windows_icon():
    # reference: https://stackoverflow.com/questions/1551605
    import ctypes
    from ctypes import wintypes

    lpBuffer = wintypes.LPWSTR()
    AppUserModelID = ctypes.windll.shell32.GetCurrentProcessExplicitAppUserModelID
    AppUserModelID(ctypes.cast(ctypes.byref(lpBuffer), wintypes.LPWSTR))
    appid = lpBuffer.value
    ctypes.windll.kernel32.LocalFree(lpBuffer)
    if appid is None:
        appid = "aps.xpcs_viewer.viewer.0.20"
    ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid)


def qt_message_handler(mode, context, message):
    """
    A custom message handler that intercepts Qt messages and prints a traceback
    for specific warnings.
    """
    # Check if the message contains the text of the warnings we're interested in
    if "QGraphicsItem::itemTransform: null pointer passed" in message:
        print("--- Caught 'null pointer' warning. Traceback: ---")
        traceback.print_stack()
        print("-------------------------------------------------")

    if "unique connections require a pointer" in message:
        print("--- Caught 'unique connections' warning. Traceback: ---")
        traceback.print_stack()
        print("-----------------------------------------------------")

    # Use the default handler to still print the original message
    # You might need to find the original handler if you want to be perfectly clean,
    # but for debugging, printing the message here is fine.
    print(
        f"Qt Message: {message} (type: {mode}, context: {context.file}:{context.line})"
    )


def main_gui(path=None, label_style=None):
    qInstallMessageHandler(qt_message_handler)

    if os.name == "nt":
        setup_windows_icon()
    QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)

    app = QtWidgets.QApplication([])
    window = XpcsViewer(path=path, label_style=label_style)
    window.show()
    app.exec_()


if __name__ == "__main__":
    main_gui()
