from __future__ import annotations

import os
import re
from pathlib import Path

from numpy import geomspace, linspace
from pyqtgraph import ViewBox

from nmreval.configs import *
from nmreval.lib.logger import logger
from nmreval.io.sessionwriter import NMRWriter

from .management import UpperManagement
from ..lib.logger import QTextHandler
from ..Qt import QtGui, QtPrintSupport
from ..data.shift_graphs import QShift
from ..data.signaledit import QPreviewDialog, QBaselineDialog
from ..dsc.glass_dialog import TgCalculator
from ..fit.fit_toolbar import FitToolbar
from ..fit.result import FitExtension, QFitResult
from ..graphs.graphwindow import QGraphWindow
from ..graphs.movedialog import QMover
from ..io.fcbatchreader import QFCReader
from ..io.filedialog import *
from ..lib.iconloading import make_action_icons, get_icon
from ..lib.starter import make_starter
from ..math.binning import BinningWindow
from ..math.evaluation import QEvalDialog
from ..math.interpol import InterpolDialog
from ..math.mean_dialog import QMeanTimes
from ..math.smooth import QSmooth
from ..nmr.coupling_calc import QCoupCalcDialog
from ..nmr.t1_from_tau import QRelaxCalc
from .._py.basewindow import Ui_BaseWindow
from ..lib.update import UpdateDialog


class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
    closeSignal = QtCore.pyqtSignal()
    openpoints = QtCore.pyqtSignal(dict)
    save_ses_sig = QtCore.pyqtSignal(str)
    rest_ses_sig = QtCore.pyqtSignal(str)

    def __init__(self, parents=None, path=None, bck_file=None):
        super().__init__(parent=parents)

        if path is None:
            self.path = Path.home()
        else:
            self.path = Path(path)

        self.read_state()

        self.management = UpperManagement(self)

        self.fitlimitvalues = [None, None]
        self.fitpreview = []
        self._fit_plot_id = None
        self.savefitdialog = None
        self._tg_dialog = None
        self.fitresult_dialog = None
        self.eval = None
        self.editor = None
        self._interpol_dialog = None
        self.fc_reader = None

        self.logtext = QTextHandler(self)
        logger.addHandler(self.logtext)
        self.addDockWidget(QtCore.Qt.DockWidgetArea.BottomDockWidgetArea, self.logtext.console)
        self.logtext.console.hide()

        self.movedialog = QMover(self)

        self.current_graph_widget = None
        self.current_plotitem = None
        self._block_window_change = False

        self.fname = None

        self.settings = QtCore.QSettings('NMREVal', 'settings')
        self._init_gui()
        self._init_signals()

        self.fit_timer = QtCore.QTimer()
        self.fit_timer.setInterval(500)
        self.fit_timer.timeout.connect(
            lambda: self.status.setText(f'Fit running... ({self.management.fitter.step} evaluations)')
        )

        self.__backup_path = pathlib.Path(bck_file)
        if os.getenv('APPIMAGE'):
            # ignore AppImages if not running from AppImage
            self.look_for_update()

    def _init_gui(self):
        self.setupUi(self)
        make_action_icons(self)
        self.setWindowIcon(get_icon('logo'))

        self.norm_toolbutton = QtWidgets.QToolButton(self)
        self.norm_toolbutton.setMenu(self.menuNormalize)
        self.norm_toolbutton.setPopupMode(self.norm_toolbutton.InstantPopup)
        self.norm_toolbutton.setIcon(get_icon('normal'))
        self.toolbar_edit.addWidget(self.norm_toolbutton)

        while self.tabWidget.count() > 2:
            self.tabWidget.removeTab(self.tabWidget.count()-1)

        # Prevent closing "data" and "values"
        for i in [0, 1]:
            self.tabWidget.tabBar().tabButton(i, QtWidgets.QTabBar.ButtonPosition.RightSide).resize(0, 0)

        self.setAcceptDrops(True)

        self.mousepos = QtWidgets.QLabel('')
        self.status = QtWidgets.QLabel('')

        # noinspection PyUnresolvedReferences
        self.statusBar.addWidget(self.status)
        # noinspection PyUnresolvedReferences
        self.statusBar.addWidget(self.mousepos)

        self._fit_plot_id = None

        self.fit_toolbar = FitToolbar(self.action_FitWidget, self.menuLimits, self)
        self.addToolBar(self.fit_toolbar)

        self.setGeometry(QtWidgets.QStyle.alignedRect(
            QtCore.Qt.LayoutDirection.LeftToRight,
            QtCore.Qt.AlignmentFlag.AlignCenter,
            self.size(),
            QtWidgets.qApp.desktop().availableGeometry()),
        )

        self.datawidget.management = self.management
        self.integralwidget.management = self.management

        self.ac_group = QtWidgets.QActionGroup(self)
        self.ac_group.addAction(self.action_lm_fit)
        self.ac_group.addAction(self.action_nm_fit)
        self.ac_group.addAction(self.action_odr_fit)

    def _init_signals(self):
        self.actionRedo = self.management.undostack.createRedoAction(self)
        icon = QtGui.QIcon.fromTheme("edit-redo")
        self.actionRedo.setIcon(icon)
        self.actionRedo.setShortcuts(QtGui.QKeySequence.Redo)
        self.menuData.insertAction(self.action_new_set, self.actionRedo)

        self.actionUndo = self.management.undostack.createUndoAction(self)
        self.actionUndo.setShortcuts(QtGui.QKeySequence.Undo)
        icon = QtGui.QIcon.fromTheme("edit-undo")
        self.actionUndo.setIcon(icon)
        self.menuData.insertAction(self.actionRedo, self.actionUndo)

        self.action_save_fit_parameter.triggered.connect(self.save_fit_parameter)
        # noinspection PyUnresolvedReferences
        self.fit_toolbar.limit_group.triggered.connect(self.change_fit_limits)

        self.t1action.triggered.connect(lambda: self._show_tab('t1_temp'))
        self.action_edit.triggered.connect(self.do_preview)
        self.actionPick_position.triggered.connect(lambda: self._show_tab('pick'))
        self.actionIntegration.triggered.connect(lambda: self._show_tab('integrate'))
        self.action_FitWidget.triggered.connect(lambda: self._show_tab('fit'))

        self.action_new_set.triggered.connect(self.management.create_empty)

        self.actionDelete_window.triggered.connect(self.management.delete_sets)
        self.actionCascade_windows.triggered.connect(self.area.cascadeSubWindows)
        self.actionTile.triggered.connect(self.area.tileSubWindows)
        self.actionTileHorizontal.triggered.connect(self.area.tileSubWindowsHorizontally)
        self.actionTileVertical.triggered.connect(self.area.tileSubWindowsVertically)

        self.datawidget.keyChanged.connect(self.management.change_keys)
        self.datawidget.tree.deleteItem.connect(self.management.delete_sets)
        self.datawidget.tree.moveItem.connect(self.management.move_sets)
        self.datawidget.tree.copyItem.connect(self.management.copy_sets)
        self.datawidget.graph_toolButton.clicked.connect(self.new_graph)
        self.datawidget.empty_toolButton.clicked.connect(self.management.create_empty)
        self.datawidget.func_toolButton.clicked.connect(self.make_data_from_function)
        self.datawidget.tree.stateChanged.connect(self.management.change_visibility)
        self.datawidget.startShowProperty.connect(self.management.get_properties)
        self.datawidget.propertyChanged.connect(self.management.update_property)
        self.datawidget.tree.saveFits.connect(self.save_fit_parameter)
        self.datawidget.tree.extendFits.connect(self.extend_fit)

        self.management.newData[list, str].connect(self.show_new_data)
        self.management.newData[list, str, bool].connect(self.show_new_data)
        self.management.newGraph.connect(self.new_graph)
        self.management.dataChanged.connect(self.update_data)
        self.management.deleteData.connect(self.delete_data)
        self.management.deleteGraph.connect(self.remove_graph)
        self.management.restoreGraph.connect(self.set_graph)
        self.management.properties_collected.connect(self.datawidget.set_properties)
        self.management.unset_state.connect(lambda x: self.datawidget.uncheck_sets(x))
        self.management.fitFinished.connect(self.show_fit_results)

        self.area.newData.connect(lambda x: self.management.load_files(x))

        self.fit_dialog._management = self.management
        self.fit_dialog.preview_emit.connect(self.show_fit_preview)
        self.fit_dialog.fitStartSig.connect(self.start_fit)
        self.fit_dialog.abortFit.connect(lambda: self.management.stopFit.emit())

        self.movedialog.moveData.connect(self.move_sets)
        self.movedialog.copyData.connect(self.management.copy_sets)

        self.ptsselectwidget.points_selected.connect(self.management.extract_points)

        self.t1tauwidget.newData.connect(self.management.add_new_data)

        self.editsignalwidget.do_something.connect(self.management.apply)
        # self.editsignalwidget.preview_triggered.connect(self.do_preview)

        self.action_sort_pts.triggered.connect(lambda: self.management.apply('sort', ()))
        self.action_calc_eps_derivative.triggered.connect(self.management.bds_deriv)
        self.action_magnitude.triggered.connect(self.management.calc_magn)
        self.actionCenterMax.triggered.connect(lambda: self.management.apply('center', ()))

        self.valuewidget.requestData.connect(self.show_data_values)
        self.valuewidget.itemChanged.connect(self.management.set_values)
        self.valuewidget.itemDeleted.connect(self.management.remove_values)
        self.valuewidget.itemAdded.connect(self.management.append)
        self.valuewidget.maskSignal.connect(self.management.mask_value)
        self.valuewidget.values_selected.connect(self.plot_selected_values)
        self.valuewidget.split_signal.connect(self.management.split_set)

        self.actionMaximize.triggered.connect(lambda: self.current_graph_widget.showMaximized())
        self.actionNext_window.triggered.connect(lambda: self.area.activateNextSubWindow())
        self.actionPrevious.triggered.connect(lambda: self.area.activatePreviousSubWindow())

        self.action_norm_max.triggered.connect(lambda: self.management.apply('norm', ('max',)))
        self.action_norm_max_abs.triggered.connect(lambda: self.management.apply('norm', ('maxabs',)))
        self.action_norm_first.triggered.connect(lambda: self.management.apply('norm', ('first',)))
        self.action_norm_last.triggered.connect(lambda: self.management.apply('norm', ('last',)))
        self.action_norm_area.triggered.connect(lambda: self.management.apply('norm', ('area',)))
        self.action_cut_xaxis.triggered.connect(lambda: self.management.cut(True, False))
        self.action_cut_yaxis.triggered.connect(lambda: self.management.cut(False, True))

        self.actionConcatenate_sets.triggered.connect(lambda: self.management.cat())

        self.management.graphs.valueChanged.connect(self.update_graph_list)

    @QtCore.pyqtSlot(name='on_action_open_triggered')
    def open(self):
        filedialog = OpenFileDialog(directory=self.path, caption='Open files',
                                    filter='All files (*.*);;'
                                           'Program session (*.nmr);;'
                                           'HDF files (*.h5);;'
                                           'Text files (*.txt *.dat);;'
                                           'Novocontrol Alpha (*.EPS);;'
                                           'TecMag files (*.tnt);;'
                                           'Grace files (*.agr)')

        filedialog.set_graphs(self.management.graphs.list())

        accepted = filedialog.exec()
        if accepted:
            fname = filedialog.selectedFiles()
        else:
            fname = []

        if fname:
            self.path = Path(fname[0]).parent
            self.management.load_files(fname, new_plot=filedialog.add_to_graph)

    @QtCore.pyqtSlot(name='on_actionOpen_FC_triggered')
    def read_fc(self):
        if self.fc_reader is None:
            self.fc_reader = QFCReader(path=self.path, parent=self)
            self.fc_reader.data_read.connect(self.management.add_new_data)
        else:
            self.fc_reader(path=self.path)
        self.fc_reader.add_graphs(self.management.graphs.list())
        self.fc_reader.exec()

        self.path = self.fc_reader.path

    @QtCore.pyqtSlot(name='on_actionPrint_triggered')
    def print(self):
        QtPrintSupport.QPrintDialog().exec()

    @QtCore.pyqtSlot(name='on_actionExportData_triggered')
    @QtCore.pyqtSlot(name='on_actionSave_triggered')
    def save(self):
        save_dialog = SaveDirectoryDialog(directory=str(self.path), parent=self)

        mode = save_dialog.exec()
        if mode == QtWidgets.QDialog.Accepted:
            savefile = save_dialog.save_file()
            selected_filter = save_dialog.selectedNameFilter()

            if savefile is not None:
                self.path = savefile.parent
                use_underscore = save_dialog.checkBox.isChecked()
                self.management.save(savefile, selected_filter, strip_spaces=use_underscore)

                param_outfile = re.sub(r'[_\s-]?<label>[_\s-]?', '', savefile.stem)

                bad_character = r'/*<>\|:"'
                for c in bad_character:
                    param_outfile = param_outfile.replace(c, '')

                if save_dialog.agr_cb.isChecked():
                    self.current_graph_widget.export(savefile.with_name(param_outfile + '.agr'))

                if save_dialog.fit_cb.isChecked():
                    self.management.save_fit_parameter(savefile.with_name(param_outfile + '.dat'))

    @QtCore.pyqtSlot()
    @QtCore.pyqtSlot(list)
    def save_fit_parameter(self, fit_sets: list[str] = None):
        save_dialog = FileDialog(parent=self, caption='Save fit parameter', directory=str(self.path),
                                 filter='All files(*, *);;Text files(*.dat *.txt)')
        save_dialog.setAcceptMode(FileDialog.AcceptSave)
        save_dialog.setOption(QtWidgets.QFileDialog.DontConfirmOverwrite, True)

        mode = save_dialog.exec()
        if mode == QtWidgets.QDialog.Accepted:
            savefile = save_dialog.save_file()
            if savefile:
                self.management.save_fit_parameter(savefile, fit_sets=fit_sets)

    @QtCore.pyqtSlot(name='on_actionExportGraphic_triggered')
    def export_graphic(self):
        self.current_graph_widget.export_dialog()

    @QtCore.pyqtSlot(name='on_actionNew_window_triggered')
    def new_graph(self):
        w = QGraphWindow()
        self.management.graphs[w.id] = w
        self.set_graph(w.id)

        if self.eval is not None:
            self.eval.set_graphs(self.management.graphs.list())

        return w.id

    @QtCore.pyqtSlot(list, str)
    @QtCore.pyqtSlot(list, str, bool)
    def show_new_data(self, sets: list, graph: str, skip_change: bool = False):
        if len(sets) == 0:
            return

        prev_graph = ''
        if skip_change:
            prev_graph = self.management.current_graph

        if graph == '':
            graph = self.new_graph()

        self.management.plots_to_graph(sets, graph)

        for idd in sets:
            new_item = self.management[idd]
            self.datawidget.blockSignals(True)
            self.datawidget.add_item(new_item.id, new_item.name, new_item.value, graph, update=False)
            self.datawidget.blockSignals(False)
        self.datawidget.tree.update_indexes()

        # if graph == self.fit_dialog.connected_figure:
        #     self.fit_dialog.load(self.management.graphs.active(graph))
        if skip_change:
            self.area.setActiveSubWidget(prev_graph)

        if self.valuewidget.isVisible():
            self.valuewidget(self.management.graphs.tree())

    @QtCore.pyqtSlot(str)
    def remove_graph(self, gid: str):
        self.datawidget.remove_item([gid])
        val_figure = self.valuewidget.connected_figure
        self.valuewidget.remove_graph()

        _remove_pts = False
        _remove_t1 = False
        _move_to_data_tab = False

        w = None
        for w in self.area.subWindowList():
            wdgt = w.widget()
            if wdgt.id == gid:
                wdgt.disconnect()
                wdgt.scene.disconnect()

                if wdgt == self.current_graph_widget:
                    if self.ptsselectwidget.connected_figure == gid:
                        self.ptsselectwidget.connected_figure = None
                        for line in self.ptsselectwidget.pts_lines:
                            self.current_graph_widget.remove_external(line)
                        _remove_pts = True

                    if self.t1tauwidget.connected_figure == gid:
                        self.t1tauwidget.connected_figure = None
                        self.current_graph_widget.remove_external(self.t1tauwidget.min_pos)
                        self.current_graph_widget.remove_external(self.t1tauwidget.parabola)
                        _remove_t1 = True

                    if self.fit_dialog.connected_figure == gid:
                        self.fit_dialog.connected_figure = None
                        for item in self.fit_dialog.preview_lines:
                            self.current_graph_widget.remove_external(item)

                    if val_figure == gid:
                        self.valuewidget.connected_figure = None
                        self.current_graph_widget.remove_external(self.valuewidget.selection_real)
                        self.current_graph_widget.remove_external(self.valuewidget.selection_imag)
                        _move_to_data_tab = True

                    self.current_graph_widget.enable_picking(False)

                    self.current_graph_widget = None
                    self.management.current_graph = None
                    self.current_plotitem = None

                wdgt.setParent(None)
                try:
                    wdgt.deleteLater()
                except AttributeError:
                    pass

                break

        if _remove_t1:
            self.tabWidget.removeTab(self.tabWidget.indexOf(self.t1tauwidget))
        if _remove_pts:
            self.tabWidget.removeTab(self.tabWidget.indexOf(self.ptsselectwidget))
        if _move_to_data_tab:
            self.tabWidget.setCurrentIndex(0)

        if w is not None:
            self.area.removeSubWindow(w)
            w.close()
            try:
                w.deleteLater()
            except AttributeError:
                pass

        if self.current_graph_widget is None:
            if self.area.subWindowList():
                self.area.activateNextSubWindow()

    @QtCore.pyqtSlot()
    def update_graph_list(self):
        graph_list = self.management.graphs.list()
        self.t1tauwidget.set_graphs(graph_list)
        self.ptsselectwidget.set_graphs(graph_list)

    @QtCore.pyqtSlot(str)
    def set_graph(self, key: str):
        w = self.management.graphs[key]

        subwindow = self.area.addSubWindow(w)
        subwindow.setOption(QtWidgets.QMdiSubWindow.RubberBandMove, True)
        subwindow.setOption(QtWidgets.QMdiSubWindow.RubberBandResize, True)
        subwindow.setMinimumHeight(400)
        subwindow.setMinimumWidth(600)

        self.datawidget.blockSignals(True)
        self.datawidget.tree.blockSignals(True)
        self.datawidget.add_graph(w.id, w.title)
        self.datawidget.tree.blockSignals(False)
        self.datawidget.blockSignals(False)

        w.mousePositionChanged.connect(self.mousemoved)
        w.aboutToClose.connect(self.management.delete_sets)
        w.positionClicked.connect(self.point_selected)
        w.newData.connect(lambda x, y: self.management.load_files(x, new_plot=y))
        w.show()

        graph_list = self.management.graphs.list()
        self.t1tauwidget.set_graphs(graph_list)
        self.ptsselectwidget.set_graphs(graph_list)

    @QtCore.pyqtSlot(QtWidgets.QMdiSubWindow, name='on_area_subWindowActivated')
    def change_window(self, wd):
        """ Called every time focus moves from or to a subwindow. Returns None if current focus is not on a subwindow"""
        if wd is None:
            return

        if self.current_graph_widget is not None:
            self.current_graph_widget.closable = True

            if self.ptsselectwidget.isVisible():
                self._select_ptswidget(False, False, False)

            if self.fit_dialog.isVisible():
                self._select_fitwidget(False, False)

        self.current_graph_widget = wd.widget()
        self.management.current_graph = wd.widget().id
        self.current_plotitem = self.current_graph_widget.graphic

        self.change_mouse_mode(self.actionMouse_behaviour.isChecked())

        pick = False
        block = False
        if self.ptsselectwidget.isVisible():
            pick, block = self._select_ptswidget(True, pick, block)
        if self.fit_dialog.isVisible():
            block = self._select_fitwidget(True, block)

        self._set_pick_block(pick, block)

        self.datawidget.tree.blockSignals(True)
        self.datawidget.tree.highlight(self.management.current_graph)
        self.datawidget.tree.blockSignals(False)

    @QtCore.pyqtSlot(name='on_actionChange_datatypes_triggered')
    def type_change_dialog(self):
        from ..data.conversion import ConversionDialog

        dialog = ConversionDialog(self)
        dialog.set_graphs(self.management.graphs.tree())
        dialog.convertSets.connect(self.management.convert_sets)

        _ = dialog.exec()

        dialog.disconnect()

    def _set_pick_block(self, pick: bool, block: bool):
        self.current_graph_widget.enable_picking(pick)
        self.current_graph_widget.closable = not block

    def _show_tab(self, mode: str):
        widget, name = {
            't1_temp': (self.t1tauwidget, 'T1 mininmon'),
            'signal': (self.editsignalwidget, 'Signals'),
            'pick': (self.ptsselectwidget, 'Pick points'),
            'fit': (self.fit_dialog, 'Fit'),
            'integrate': (self.integralwidget, 'Integrate'),
        }[mode]

        for idx in range(self.tabWidget.count()):
            if self.tabWidget.widget(idx) == widget:
                self.tabWidget.setCurrentIndex(idx)
                return

        self.tabWidget.addTab(widget, name)
        self.tabWidget.setCurrentIndex(self.tabWidget.count()-1)

    @QtCore.pyqtSlot(int, name='on_tabWidget_tabCloseRequested')
    def close_tab(self, idx: int):
        if idx == 0:
            pass
        else:
            self.tabWidget.setCurrentIndex(0)
            self.tabWidget.removeTab(idx)

    @QtCore.pyqtSlot(int, name='on_tabWidget_currentChanged')
    def toggle_tabs(self, idx: int):
        widget = self.tabWidget.widget(idx)
        if self.current_graph_widget is None:
            if self.tabWidget.currentIndex() > 1:
                self.tabWidget.removeTab(self.tabWidget.indexOf(widget))
                self.tabWidget.setCurrentIndex(0)
            return

        if self.current_graph_widget is not None:
            self.current_graph_widget.enable_picking(False)

        pick_required, block_window = self._select_ptswidget(widget == self.ptsselectwidget, False, False)
        self._select_valuewidget(widget == self.valuewidget)
        pick_required, block_window = self._select_t1tauwidget(widget == self.t1tauwidget, pick_required, block_window)
        block_window = self._select_fitwidget(widget == self.fit_dialog, block_window)
        pick_required = self._select_integralwidget(widget == self.integralwidget, pick_required, block_window)

        self._set_pick_block(pick_required, block_window)

    def _select_ptswidget(self, onoff: bool, pick_required: bool, block_window: bool) -> tuple[bool, bool]:
        if self.current_graph_widget is None:
            return pick_required, block_window

        if onoff:  # point selection
            for line in self.ptsselectwidget.pts_lines:
                self.current_graph_widget.add_external(line)
            self.ptsselectwidget.point_removed.connect(self.current_graph_widget.remove_external)
            self.ptsselectwidget.connected_figure = self.management.current_graph
            pick_required = True
        else:
            if self.ptsselectwidget.connected_figure in self.management.graphs:
                g = self.management.graphs[self.ptsselectwidget.connected_figure]
                for line in self.ptsselectwidget.pts_lines:
                    g.remove_external(line)
            else:
                self.ptsselectwidget.connected_figure = None
            # self.ptsselectwidget.clear()

        return pick_required, block_window

    def _select_valuewidget(self, onoff: bool):
        if onoff:  # Values
            self.valuewidget(self.management.graphs.tree())
            current_graph = self.valuewidget.connected_figure
            if current_graph is not None:
                self.management.graphs[current_graph].add_external(self.valuewidget.selection_real)
                self.management.graphs[current_graph].add_external(self.valuewidget.selection_imag)
        else:
            if self.valuewidget.connected_figure in self.management.graphs:
                conn_fig = self.valuewidget.connected_figure
                self.management.graphs[conn_fig].remove_external(self.valuewidget.selection_real)
                self.management.graphs[conn_fig].remove_external(self.valuewidget.selection_imag)
            else:
                self.valuewidget.connected_figure = None

    def _select_integralwidget(self, onoff: bool, pick_required: bool, block_window: bool) -> tuple[bool, bool]:
        if self.current_graph_widget is None:
            return pick_required, block_window

        if onoff:
            self.integralwidget(self.current_graph_widget.title,
                                self.management.graphs.current_sets(self.management.current_graph))
            self.integralwidget.item_deleted.connect(self.current_graph_widget.remove_external)
            self.integralwidget.connected_figure = self.management.current_graph
            pick_required = True
            block_window = True
        else:
            if self.integralwidget.connected_figure in self.management.graphs:
                g = self.management.graphs[self.integralwidget.connected_figure]
                for line in self.integralwidget.lines:
                    g.remove_external(line[0])
                    g.remove_external(line[1])
            else:
                self.integralwidget.connected_figure = None
            self.integralwidget.clear()

        return pick_required, block_window

    def _select_t1tauwidget(self, onoff: bool, pick_required: bool, block_window: bool):
        if onoff:  # tau from t1
            if self.current_graph_widget is None:
                return pick_required, block_window

            idx = self.tabWidget.indexOf(self.t1tauwidget)
            if len(self.current_graph_widget) != 1:
                QtWidgets.QMessageBox.information(self, 'Too many datasets',
                                                  'Only one T1 curve can be evaluated at once')
                self.tabWidget.removeTab(idx)
                self.tabWidget.setCurrentIndex(0)
                return pick_required, block_window

            self.tabWidget.setTabText(idx, 'T1 min (%s)' % {self.current_graph_widget.title})

            self.t1tauwidget.set_graphs(self.management.graphs.list())
            self.t1tauwidget.set_data(*self.management.get_data(self.current_graph_widget.active[0], xy_only=True),
                                      name=self.management[self.current_graph_widget.active[0]].name)
            self.t1tauwidget.connected_figure = self.management.current_graph
            self.current_graph_widget.add_external(self.t1tauwidget.min_pos)
            self.current_graph_widget.add_external(self.t1tauwidget.parabola)
            pick_required = True
            block_window = True
        else:
            if self.t1tauwidget.connected_figure in self.management.graphs:
                g = self.management.graphs[self.t1tauwidget.connected_figure]
                g.remove_external(self.t1tauwidget.min_pos)
                g.remove_external(self.t1tauwidget.parabola)
            else:
                self.t1tauwidget.connected_figure = None

        return pick_required, block_window

    @QtCore.pyqtSlot(str)
    def get_data(self, key: str):
        if hasattr(self.sender(), 'set_data'):
            self.sender().set_data(self.management[key])

    @QtCore.pyqtSlot(name='on_actionCalculateT1_triggered')
    def show_t1calc_dialog(self):
        dialog = QRelaxCalc(self)
        dialog.set_graphs(self.management.graphs.tree(key_only=True))
        dialog.newData.connect(self.management.calc_relaxation)
        dialog.show()

    @QtCore.pyqtSlot(name='on_actionSkip_points_triggered')
    def skip_pts_dialog(self):
        from ..math.skipping import QSkipDialog

        dial = QSkipDialog(self)
        res = dial.exec()

        if res:
            self.management.skip_points(**dial.get_arguments())

    @QtCore.pyqtSlot(name='on_action_coup_calc_triggered')
    def coupling_dialog(self):
        dialog = QCoupCalcDialog(self)
        dialog.show()

    @QtCore.pyqtSlot(name='on_action_mean_t1_triggered')
    def mean_dialog(self):
        gnames = self.management.graphs.tree(key_only=True)
        dialog = QMeanTimes(gnames, parent=self)
        dialog.newValues.connect(self.management.calc_mean)
        dialog.show()

    @QtCore.pyqtSlot(name='on_actionRunning_values_triggered')
    def smooth_dialog(self):
        dialog = QSmooth(parent=self)
        dialog.newValues.connect(self.management.smooth_data)
        dialog.show()

    @QtCore.pyqtSlot(name='on_actionInterpolation_triggered')
    def interpol_dialog(self):
        if self.current_graph_widget is None:
            return

        gnames = self.management.graphs.tree()
        if self._interpol_dialog is None:
            self._interpol_dialog = InterpolDialog(parent=self)
            self._interpol_dialog.new_data.connect(self.management.interpolate_data)
        else:
            self._interpol_dialog()
        self._interpol_dialog.set_data(gnames, self.current_graph_widget.id)
        self._interpol_dialog.show()

    @QtCore.pyqtSlot(name='on_action_calc_triggered')
    def open_eval_dialog(self):
        if self.eval is None:
            self.eval = QEvalDialog(parent=self)
            self.eval.do_eval.connect(self.management.eval_expression)
            self.eval.do_calc.connect(self.management.create_from_function)

        self.eval.set_mode('e')
        self.eval.set_namespace(self.management.get_namespace())
        self.eval.add_data(self.management.active_sets)
        self.eval.set_graphs(self.management.graphs.list())

        self.eval.exec()

    @QtCore.pyqtSlot(name='on_actionAddlines_triggered')
    def make_data_from_function(self):
        if self.eval is None:
            self.eval = QEvalDialog(parent=self)
            self.eval.do_eval.connect(self.management.eval_expression)
            self.eval.do_calc.connect(self.management.create_from_function)

        self.eval.set_mode('c')
        self.eval.set_namespace(self.management.get_namespace())
        self.eval.set_graphs(self.management.graphs.list())

        self.eval.exec()

    @QtCore.pyqtSlot(name='on_actionBinning_triggered')
    def open_binning(self):
        dialog = BinningWindow(self)
        res = dialog.exec()
        if res:
            digits = float(dialog.spinbox.text())
            self.management.binning(digits)

    @QtCore.pyqtSlot(name='on_actionDerivation_triggered')
    # @QtCore.pyqtSlot(name='on_actionIntegration_triggered')
    @QtCore.pyqtSlot(name='on_actionFilon_triggered')
    def int_diff_ft(self):
        sets = self.management.active_sets
        mode = {
            # self.actionIntegration: 'int',
            self.actionDerivation: 'diff',
            self.actionFilon: 'logft'}[self.sender()]

        if sets:
            from ..math.integrate_derive import QDeriveIntegrate

            dialog = QDeriveIntegrate(mode)
            dialog.add_graphs(self.management.graphs.list())
            dialog.set_sets(sets)

            res = dialog.exec()

            if res:
                options = dialog.get_options()
                mode = options['mode']
                if mode in ['i', 'd']:
                    self.management.integrate(**options)
                elif mode == 'l':
                    self.management.logft(**options)
                else:
                    raise ValueError('Unknown mode %s, not `i`, `d`, `l`.' % str(mode))

    @QtCore.pyqtSlot(str)
    def update_data(self, sid: str):
        if self.valuewidget.shown_set == sid:
            self.show_data_values(sid)

        self.datawidget.set_name(sid, self.management[sid].name)

    @QtCore.pyqtSlot(list)
    def delete_data(self, sid: list[str]):
        for key in sid:
            if self.valuewidget.shown_set == key:
                self.tabWidget.setCurrentIndex(0)

        self.datawidget.remove_item(sid)

    @QtCore.pyqtSlot(name='on_actionBaseline_triggered')
    def baseline_dialog(self):
        if not self.current_graph_widget:
            return

        if len(self.current_graph_widget) != 1:
            QtWidgets.QMessageBox.information(self, 'Invalid number of sets',
                                              'Baseline correction can only applied to one set at a time.')
            return

        for sid in self.current_graph_widget.active:
            data_mode = self.management[sid].mode
            if data_mode in ['spectrum']:
                editor = QBaselineDialog(self)
                editor.add_data(*self.management.get_data(sid, xy_only=True))
                editor.finished.connect(self.management.apply)
                editor.exec()

    @QtCore.pyqtSlot()
    def do_preview(self):
        dialog = QPreviewDialog(self)

        success = True

        for sid in self.current_graph_widget.active:
            data_mode = self.management[sid].mode
            if data_mode in ('fid', 'spectrum'):
                success = dialog.add_data(self.management[sid].data)

                if not success:
                    break

        if success and dialog.exec() == QtWidgets.QDialog.Accepted:
            self.management.edit_signals(dialog.get_value())

    @QtCore.pyqtSlot(name='on_actionMove_between_plots_triggered')
    def move_sets_dialog(self):
        gnames = self.management.graphs.tree()
        self.movedialog.setup(gnames)
        self.movedialog.exec()

    @QtCore.pyqtSlot(list, str, str)
    def move_sets(self, sets, dest, src):
        self.management.move_sets(sets, dest, src)
        for s in sets:
            self.datawidget.tree.move_sets(s, dest, src)

    @QtCore.pyqtSlot(str)
    def show_data_values(self, sid: str):
        if sid == '':
            return

        data, mask = self.management.get_data(sid)
        self.valuewidget.set_data(data, mask)

    def plot_selected_values(self, old_gid: str, new_gid: str):
        for pts in (self.valuewidget.selection_real, self.valuewidget.selection_imag):
            if old_gid:
                self.management.graphs[old_gid].remove_external(pts)
            if new_gid:
                self.management.graphs[new_gid].add_external(pts)

    @QtCore.pyqtSlot(object, str)
    def item_to_graph(self, item, graph_id):
        self.management.graphs[graph_id].add_external(item)

    @QtCore.pyqtSlot(object, str)
    def item_from_graph(self, item, graph_id):
        self.management.graphs[graph_id].remove_external(item)

    @QtCore.pyqtSlot(int)
    def request_data(self, idx):
        idd = self.datawidget.get_indexes(idx=idx-1)
        try:
            x = self.management[idd].x
            y = self.management[idd].y
            ret_val = (x, y)
        except KeyError:
            ret_val = None

        # noinspection PyUnresolvedReferences
        self.sender().receive_data(ret_val)

        return ret_val

    @QtCore.pyqtSlot(tuple, bool)
    def point_selected(self, pos, double):
        w = self.tabWidget.currentWidget()
        if w == self.ptsselectwidget:
            line = self.ptsselectwidget.add(pos, double)
            self.current_graph_widget.add_external(line)

        elif w == self.t1tauwidget:
            self.t1tauwidget.t1min_picked(pos)

        elif w == self.integralwidget:
            region, integral_plot = self.integralwidget.add(pos)
            self.current_graph_widget.add_external(region)
            self.current_graph_widget.add_external(integral_plot)

    def _select_fitwidget(self, onoff: bool, block_window: bool):
        if self.current_graph_widget is not None:
            pass

        if onoff:
            self.fit_dialog.connected_figure = self.management.current_graph
            self.fit_dialog.load(self.management.active_sets)
            for item in self.fit_dialog.preview_lines:
                self.current_graph_widget.add_external(item)
            if self.action_custom_range.isChecked() or self.actionExclude_region.isChecked():
                self.current_graph_widget.add_external(self.fit_toolbar.region)

            block_window = True
        else:
            for item in self.fit_dialog.preview_lines:
                self.current_graph_widget.remove_external(item)
            self.current_graph_widget.remove_external(self.fit_toolbar.region)

        return block_window

    @QtCore.pyqtSlot(QtWidgets.QAction)
    def change_fit_limits(self, action: QtWidgets.QAction):
        if self.current_graph_widget is None:
            return

        if action in [self.action_custom_range, self.actionExclude_region] and self.fit_dialog.isVisible():
            self.current_graph_widget.add_external(self.fit_toolbar.region)
        else:
            self.current_graph_widget.remove_external(self.fit_toolbar.region)

    def start_fit(self, parameter, links, fit_options):
        fit_options['limits'] = self.fit_toolbar.get_limit()

        fit_options['fit_mode'] = {
            self.action_lm_fit: 'lsq',
            self.action_nm_fit: 'nm',
            self.action_odr_fit: 'odr'
        }[self.ac_group.checkedAction()]

        fit_is_ready = self.management.prepare_fit(parameter, links, fit_options)
        if fit_is_ready:
            self.management.start_fit()
            self.fit_dialog.fit_button.setEnabled(False)
            self.status.setText('Fit running...'.format(self.management.fitter.step))
            self.fit_timer.start(500)

    @QtCore.pyqtSlot(dict, int, bool)
    def show_fit_preview(self, funcs: dict, num: int, show: bool):
        if not self.fit_dialog.connected_figure:
            logger.warning(f'Fit dialog is not connected graph: Fit {self.fit_dialog.connected_figure}, '
                           f'current graph: {self.management.current_graph} ({self.management.current_graph in self.management.graphs})')
            return

        g = self.management.graphs[self.fit_dialog.connected_figure]
        for item in self.fit_dialog.preview_lines:
            g.remove_external(item)

        if show:
            x_lim, _ = g.ranges
            space = geomspace if g.log[0] else linspace
            x = space(1.01*x_lim[0], 0.99*x_lim[1], num=num)

            self.fit_dialog.make_previews(x, funcs)

            for item in self.fit_dialog.preview_lines:
                g.add_external(item)

    @QtCore.pyqtSlot(list, dict)
    def show_fit_results(self, results: list, sub_colors: dict[str, tuple[float, float, float]]):
        self.fit_dialog.fit_button.setEnabled(True)
        self.fit_timer.stop()
        self.status.setText('')
        if results:
            if self.fitresult_dialog is None:
                self.fitresult_dialog = QFitResult(results, sub_colors, self.management, parent=self)
                self.fitresult_dialog.add_graphs(self.management.graphs.list())
                self.fitresult_dialog.closed.connect(self.accepts_fit)
                self.fitresult_dialog.redoFit.connect(self.management.redo_fits)
            else:
                self.fitresult_dialog(results, sub_colors)
                self.fitresult_dialog.add_graphs(self.management.graphs.list())
            self.fitresult_dialog.show()

    @QtCore.pyqtSlot(dict, list, str, bool, bool, list)
    def accepts_fit(self, res: dict, opts: list, param_graph: str,
                    show_fit: bool, parts: bool, extrapolate: list) -> None:
        self.fit_dialog.set_parameter(res)
        self.management.make_fits(res, opts, param_graph, show_fit, parts, extrapolate)

    @QtCore.pyqtSlot(name='on_actionFunction_editor_triggered')
    def edit_models(self):
        if self.editor is None:
            from ..editors.usermodeleditor import QUsermodelEditor

            self.editor = QUsermodelEditor(config_paths() / 'usermodels.py', parent=self)
            self.editor.modelsChanged.connect(lambda: self.fit_dialog.read_and_load_functions())
            self.editor.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
        self.editor.show()

    @QtCore.pyqtSlot(name='on_actionUse_script_triggered')
    def open_editor(self):
        from ..editors.script_editor import QEditor

        editor = QEditor(self.path, parent=self)
        editor.runSignal.connect(self.management.run_script)
        editor.show()

    @QtCore.pyqtSlot(list, bool)
    def extend_fit(self, sets: list, only_subplots: bool):
        if only_subplots:
            self.management.extend_fits(sets, None, True)
            return

        w = FitExtension(self)
        res = w.exec()
        if res:
            p = w.values
            spacefunc = geomspace if p[3] else linspace
            x = spacefunc(p[0], p[1], num=p[2])
            self.management.extend_fits(sets, x, False)

    @QtCore.pyqtSlot(name='on_action_create_fit_function_triggered')
    def open_fitmodel_wizard(self):
        from ..fit.function_creation_dialog import QUserFitCreator

        helper = QUserFitCreator(config_paths() / 'usermodels.py', parent=self)
        helper.classCreated.connect(lambda: self.fit_dialog.read_and_load_functions())

        helper.show()

    @QtCore.pyqtSlot(name='on_actionShift_triggered')
    def shift_dialog(self):
        s = QShift(self)
        s.set_graphs(self.management.graphs.list())
        for key, name in self.management.active_sets:
            data = self.management.data[key]
            s.add_item(key, name, data.x, data.y)
        s.valuesChanged.connect(self.management.shift_scale)
        s.show()

    @staticmethod
    @QtCore.pyqtSlot(name='on_actionDocumentation_triggered')
    def open_doc():
        pass
        # docpath = '/autohome/dominik/auswerteprogramm3/doc/_build/html/index.html'
        # import webbrowser
        # webbrowser.open(docpath)

    def dropEvent(self, evt):
        if evt.mimeData().hasUrls():
            files = [str(url.toLocalFile()) for url in evt.mimeData().urls()]
            self.management.load_files(files)

    def dragEnterEvent(self, evt):
        evt.accept()

    @QtCore.pyqtSlot(bool, name='on_actionMouse_behaviour_toggled')
    def change_mouse_mode(self, is_checked: bool):
        if is_checked:
            self.current_plotitem.plotItem.vb.setMouseMode(ViewBox.RectMode)
        else:
            self.current_plotitem.plotItem.vb.setMouseMode(ViewBox.PanMode)

    def mousemoved(self, xpos, ypos):
        self.mousepos.setText(f'x={xpos:.3g}; y={ypos:.3g}')

    @QtCore.pyqtSlot(name='on_actionSnake_triggered')
    @QtCore.pyqtSlot(name='on_actionTetris_triggered')
    @QtCore.pyqtSlot(name='on_actionLife_triggered')
    @QtCore.pyqtSlot(name='on_actionMine_triggered')
    def spannung_spiel_und_spass(self):

        if self.sender() == self.actionLife:
            from ..lib.gol import QGameOfLife
            game = QGameOfLife(parent=self)
            game.setWindowModality(QtCore.Qt.WindowModality.NonModal)
            game.show()

        elif self.sender() == self.actionMine:
            from ..lib.stuff import QMines
            game = QMines(parent=self)
            game.setWindowModality(QtCore.Qt.WindowModality.NonModal)
            game.show()

        else:
            from ..lib.stuff import Game
            if self.sender() == self.actionSnake:
                gtype = 'snake'
            else:
                gtype = 'tetris'

            game = Game(gtype, parent=self)
            game.show()

    @QtCore.pyqtSlot(name='on_actionConfiguration_triggered')
    def open_configuration(self):
        from ..lib.configurations import GeneralConfiguration
        dialog = GeneralConfiguration(self)
        dialog.show()

    @QtCore.pyqtSlot(name='on_action_colorcycle_triggered')
    def open_color_dialog(self):
        from ..lib.color_dialog import ColorDialog
        dialog = ColorDialog(self)
        dialog.show()

    def close(self):
        write_state({'History': {'recent path': str(self.path)}})

        super().close()

    def read_state(self):
        opts = read_state()
        self.path = pathlib.Path(opts['History'].get('recent path', Path.home()))

    @QtCore.pyqtSlot(name='on_actionBugs_triggered')
    def report_bug(self):
        import webbrowser

        webbrowser.open('https://gitea.pkm.physik.tu-darmstadt.de/IPKM-Public/nmreval/issues/new')

    @QtCore.pyqtSlot(name='on_actionUpdate_triggered')
    def look_for_update(self):
        w = UpdateDialog(parent=self)
        w.show()

    @QtCore.pyqtSlot(name='on_actionShow_error_log_triggered')
    def open_log(self):
        from ..lib.logger import QLog

        QLog(parent=self).show()

    def autosave(self) -> bool:
        self.status.setText('Autosave...')
        success = NMRWriter(self.management.graphs, self.management.data).export(self.__backup_path.with_suffix('.nmr.0'))

        if success:
            self.__backup_path.with_suffix('.nmr.0').rename(self.__backup_path)

        self.status.setText('')

        return success

    @QtCore.pyqtSlot(name='on_actionCreate_starter_triggered')
    def create_starter(self):
        make_starter(os.getenv('APPIMAGE'))

    @QtCore.pyqtSlot(name='on_actionAbout_triggered')
    def show_version(self):
        from nmreval.version import __version__
        QtWidgets.QMessageBox.about(self, 'Version', f'Build date of AppImage: {__version__}')

    @QtCore.pyqtSlot(name='on_actionTNMH_model_triggered')
    @QtCore.pyqtSlot(name='on_actionTNMH_triggered')
    def show_tg_dialog(self):
        if self._tg_dialog is None:
            self._tg_dialog = TgCalculator(self.management, parent=self)
            self._tg_dialog.newData.connect(self.management.addTg)
        else:
            self._tg_dialog()
        self._tg_dialog.show()
