import inspect
import re
from collections import namedtuple

import numpy as np

from nmreval import models
from nmreval.configs import config_paths
from nmreval.lib.importer import find_models, import_
from nmreval.utils import constants as constants
from nmreval.utils.text import convert

from ..Qt import QtWidgets, QtCore
from .._py.namespace_widget import Ui_Form


class Namespace:
    def __init__(self, fitfuncs=False, const=False, basic=False):
        self.namespace = {}
        self.groupings = {}
        self.top_levels = {}

        if basic:
            self.add_namespace({'x': (None, 'x values'), 'y': (None, 'x values'), 'y_err': (None, 'y error values'),
                                'fit': (None, 'dictionary of fit parameter', 'fit["PIKA"]'), 'np': (np, 'numpy module')},
                               parents=('Basic', 'General'))

            self.add_namespace({'sin': (np.sin, 'Sine', 'sin(PIKA)'), 'cos': (np.cos, 'Cosine', 'cos(PIKA)'),
                                'tan': (np.tan, 'Tangens', 'tan(PIKA)'), 'ln': (np.log, 'Natural Logarithm', 'ln(PIKA)'),
                                'log': (np.log10, 'Logarithm (base 10)', 'log(PIKA)'),
                                'exp': (np.exp, 'Exponential', 'exp(PIKA)'), 'sqrt': (np.sqrt, 'Root', 'sqrt(PIKA)'),
                                'lin_range': (np.linspace, 'N evenly spaced over interval [start, stop]',
                                              'lin_range(start, stop, N)'),
                                'log_range': (np.geomspace, 'N evenly spaced (log-scale) over interval [start, stop]',
                                              'lin_range(start, stop, N)')},
                               parents=('Basic', 'Functions'))
            
            self.add_namespace({'max': (np.max, 'Maximum value', 'max(PIKA)'),
                                'min': (np.min, 'Minimum value', 'min(PIKA)'),
                                'argmax': (np.argmax, 'Index of maximum value', 'argmax(PIKA)'),
                                'argmin': (np.argmax, 'Index of minimum value', 'argmin(PIKA)')},
                               parents=('Basic', 'Values'))

        if const:
            self.add_namespace({'e': (constants.e, 'e / As'), 'eps0': (constants.epsilon0, 'epsilon0 / As/Vm'),
                                'Eu': (constants.Eu,), 'h': (constants.h, 'h / eVs'),
                                'hbar': (constants.hbar, 'hbar / eVs'), 'kB': (constants.kB, 'kB / eV/K'),
                                'mu0': (constants.mu0, 'mu0 / Vs/Am'), 'NA': (constants.NA, 'NA / 1/mol'),
                                'pi': (constants.pi,), 'R': (constants.R, 'R / eV')},
                               parents=('Constants', 'Maybe useful'))

            self.add_namespace({f'gamma["{k}"]': (v, k, f'gamma["{k}"]') for k, v in constants.gamma.items()},
                               parents=('Constants', 'Magnetogyric ratios (in 1/(sT))'))

        if fitfuncs:
            self.make_dict_from_fitmodule(models)
        try:
            usermodels = import_(config_paths() / 'usermodels.py')
            self.make_dict_from_fitmodule(usermodels, parent='User-defined')
        except FileNotFoundError:
            pass

    def __str__(self):
        ret = '\n'.join([f'{k}: {v}' for k, v in self.groupings.items()])
        return ret

    def make_dict_from_fitmodule(self, module, parent='Fit function'):
        fitfunc_dict = {}
        for funcs in find_models(module):
            name = funcs.name
            if funcs.type not in fitfunc_dict:
                fitfunc_dict[funcs.type] = {}

            func_list = fitfunc_dict[funcs.type]

            func_args = 'x, ' + ', '.join(convert(p, old='latex', new='str', brackets=False) for p in funcs.params)

            # keywords arguments need names in function
            sig = inspect.signature(funcs.func)
            for p in sig.parameters.values():
                if p.default != p.empty:
                    func_args += ', ' + str(p)

            func_list[f"{funcs.__name__}"] = (funcs.func, name, f"{funcs.__name__}({func_args})")

        for k, v in fitfunc_dict.items():
            self.add_namespace(v, parents=(parent, k))

    def add_namespace(self, namespace, parents):
        if parents[0] in self.top_levels:
            if parents[1] not in self.top_levels[parents[0]]:
                self.top_levels[parents[0]].append(parents[1])
        else:
            self.top_levels[parents[0]] = [parents[1]]
        if (parents[0], parents[1]) not in self.groupings:
            self.groupings[(parents[0], parents[1])] = list(namespace.keys())
        else:
            self.groupings[(parents[0], parents[1])].extend(list(namespace.keys()))

        self.namespace.update(namespace)

    def flatten(self):
        ret_dic = {}
        graph = namedtuple('graphs', ['s'])
        sets = namedtuple('sets', ['x', 'y', 'y_err', 'value', 'fit'], defaults=(None,))

        gs = re.compile(r'g\[(\d+)].s\[(\d+)].(x|y(?:_err)*|value|fit)')

        for k, v in self.namespace.items():
            m = gs.match(k)
            if m:
                if 'g' not in ret_dic:
                    ret_dic['g'] = {}

                g_num, s_num, pos = m.groups()
                if int(g_num) not in ret_dic['g']:
                    ret_dic['g'][int(g_num)] = graph({})

                gg = ret_dic['g'][int(g_num)]
                if int(s_num) not in gg.s:
                    gg.s[int(s_num)] = sets('', '', '', '')

                ss = gg.s[int(s_num)]
                if pos == 'fit':
                    if ss.fit is None:
                        gg.s[int(s_num)] = ss._replace(**{pos: {}})
                        ss = gg.s[int(s_num)]
                    ss.fit[k[m.end()+2:-2]] = v[0]

                else:
                    ret_dic['g'][int(g_num)].s[int(s_num)] = ss._replace(**{pos: v[0]})

            else:
                ret_dic[k] = v[0]

        return ret_dic


class QNamespaceWidget(QtWidgets.QWidget, Ui_Form):
    selected = QtCore.pyqtSignal(str)
    sendKey = QtCore.pyqtSignal(str)

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

        self.setupUi(self)
        self.namespace = None

    def make_namespace(self):
        self.set_namespace(Namespace(fitfuncs=True, const=True, basic=True))

    @QtCore.pyqtSlot(int, name='on_groups_comboBox_currentIndexChanged')
    def show_subgroup(self, idx: int):
        name = self.groups_comboBox.itemText(idx)

        self.subgroups_comboBox.blockSignals(True)
        self.subgroups_comboBox.clear()
        for item in self.namespace.top_levels[name]:
            self.subgroups_comboBox.addItem(item)
        self.subgroups_comboBox.blockSignals(False)

        self.show_namespace(0)

    @QtCore.pyqtSlot(int, name='on_subgroups_comboBox_currentIndexChanged')
    def show_namespace(self, idx: int):
        subspace = self.namespace.groupings[(self.groups_comboBox.currentText(), self.subgroups_comboBox.itemText(idx))]

        self.namespace_table.clear()
        self.namespace_table.setRowCount(0)

        for entry in subspace:
            key_item = QtWidgets.QTableWidgetItem(entry)
            key_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)

            vals = self.namespace.namespace[entry]

            if len(vals) < 3:
                alias = entry
            else:
                alias = vals[2]

            if len(vals) < 2:
                display = entry
            else:
                display = vals[1]

            value_item = QtWidgets.QTableWidgetItem(display)
            value_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)

            key_item.setData(QtCore.Qt.UserRole, alias)
            key_item.setData(QtCore.Qt.UserRole+1, entry)
            value_item.setData(QtCore.Qt.UserRole, alias)
            value_item.setData(QtCore.Qt.UserRole+1, entry)

            row = self.namespace_table.rowCount()
            self.namespace_table.setRowCount(row+1)
            self.namespace_table.setItem(row, 0, key_item)
            self.namespace_table.setItem(row, 1, value_item)

        self.namespace_table.resizeColumnsToContents()

    def set_namespace(self, namespace):
        self.namespace = namespace

        self.groups_comboBox.blockSignals(True)
        self.groups_comboBox.clear()
        for k in self.namespace.top_levels.keys():
            self.groups_comboBox.addItem(k)
        self.groups_comboBox.blockSignals(False)

        self.show_subgroup(0)

    @QtCore.pyqtSlot(QtWidgets.QTableWidgetItem, name='on_namespace_table_itemDoubleClicked')
    def item_selected(self, item: QtWidgets.QTableWidgetItem):
        self.selected.emit(item.data(QtCore.Qt.UserRole))
        self.sendKey.emit(item.data(QtCore.Qt.UserRole+1))
