# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.  
# SPDX-License-Identifier: MPL-2.0

from __future__ import annotations
import numpy as np
from typing import Dict, List, Union, Any, Tuple, TYPE_CHECKING, Set, Sequence
from PySide6 import QtCore, QtWidgets, QtGui
from collections import defaultdict, OrderedDict

from VeraGridEngine.basic_structures import IntVec
from VeraGridEngine.data_logger import DataLogger
from VeraGridEngine.IO.cim.cgmes.cgmes_circuit import CgmesCircuit, CGMES_ASSETS
from VeraGridEngine.Devices.Branches.line_locations import LineLocations
from VeraGridEngine.Devices.types import ALL_DEV_TYPES
from VeraGrid.Gui.font_config import MENU_FONT_SIZE

if TYPE_CHECKING:
    from VeraGrid.Gui.object_model import ObjectsModel


class TreeDelegate(QtWidgets.QItemDelegate):
    """
    A delegate that places a fully functioning QComboBox in every
    cell of the column to which it's applied
    """
    commitData = QtCore.Signal(object)
    """
    
    """

    def __init__(self, parent, data=None):
        """
        Constructor
        :param parent: QTableView parent object
        :param data: dictionary of lists
        """
        QtWidgets.QItemDelegate.__init__(self, parent)

        # dictionary of lists
        self.data = data if data is not None else defaultdict()

    @QtCore.Slot()
    def double_click(self):
        """
        double click
        """
        self.commitData.emit(self.sender())

    def createEditor(self, parent, option, index):
        """

        :param parent:
        :param option:
        :param index:
        :return:
        """
        tree = QtWidgets.QTreeView(parent)

        model = QtGui.QStandardItemModel()
        model.setHorizontalHeaderLabels(['Template'])

        for key in self.data.keys():
            # add parent node
            parent1 = QtGui.QStandardItem(str(key))

            # add children to parent
            for elm in self.data[key]:
                child1 = QtGui.QStandardItem(str(elm))
                parent1.appendRow([child1])

            model.appendRow(parent1)

        tree.setModel(model)
        tree.doubleClicked.connect(self.double_click)
        return tree

    def setEditorData(self, editor, index):
        """

        :param editor:
        :param index:
        """
        print(editor)
        print(index)

    def setModelData(self, editor, model, index):
        """

        :param editor:
        :param model:
        :param index:
        """
        print(editor)
        print(model)
        print(index)

        # model.setData(index, self.object_names[editor.currentIndex()])


class ComboDelegate(QtWidgets.QItemDelegate):
    """
    A delegate that places a fully functioning QComboBox in every
    cell of the column to which it's applied
    """
    commitData = QtCore.Signal(object)

    def __init__(self, parent: QtWidgets.QTableView, objects: List[Any], object_names: List[str]) -> None:
        """
        Constructor
        :param parent: QTableView parent object
        :param objects: List of objects to set. i.e. [True, False] or [Line1, Line2, ...]
        :param object_names: List of Object names to display. i.e. ['True', 'False']
        """
        QtWidgets.QItemDelegate.__init__(self, parent)

        # objects to sent to the model associated to the combobox. i.e. [True, False]
        self.objects = objects

        # object description to display in the combobox. i.e. ['True', 'False']
        self.object_names = object_names

    @QtCore.Slot()
    def currentIndexChanged(self) -> None:
        """
        currentIndexChanged
        """
        self.commitData.emit(self.sender())

    def createEditor(self, parent, option, index: QtCore.QModelIndex):
        """

        :param parent:
        :param option:
        :param index:
        :return:
        """
        combo = QtWidgets.QComboBox(parent)
        combo.addItems(self.object_names)
        combo.currentIndexChanged.connect(self.currentIndexChanged)
        return combo

    def setEditorData(self, editor: QtWidgets.QComboBox, index: QtCore.QModelIndex):
        """

        :param editor:
        :param index:
        """
        editor.blockSignals(True)
        val = index.model().data(index, role=QtCore.Qt.ItemDataRole.DisplayRole)
        try:
            idx = self.object_names.index(val)
            editor.setCurrentIndex(idx)
            editor.blockSignals(False)
        except ValueError:
            pass

    def setModelData(self,
                     editor: QtWidgets.QComboBox,
                     model: QtCore.QAbstractItemModel,
                     index: QtCore.QModelIndex):
        """

        :param editor:
        :param model:
        :param index:
        """
        if len(self.objects) > 0:
            if editor.currentIndex() < len(self.objects):
                model.setData(index, self.objects[editor.currentIndex()])


class TextDelegate(QtWidgets.QItemDelegate):
    """
    A delegate that places a fully functioning QLineEdit in every
    cell of the column to which it's applied
    """

    commitData = QtCore.Signal(object)

    def __init__(self, parent: QtWidgets.QTableView) -> None:
        """
        Constructor
        :param parent: QTableView parent object
        """
        QtWidgets.QItemDelegate.__init__(self, parent)

    @QtCore.Slot()
    def returnPressed(self):
        """
        returnPressed
        """
        self.commitData.emit(self.sender())

    def createEditor(self, parent: QtWidgets.QWidget,
                     option: QtWidgets.QStyleOptionViewItem,
                     index: QtCore.QModelIndex) -> QtWidgets.QLineEdit:
        """

        :param parent:
        :param option:
        :param index:
        :return:
        """
        editor = QtWidgets.QLineEdit(parent)
        editor.returnPressed.connect(self.returnPressed)
        return editor

    def setEditorData(self, editor: QtWidgets.QLineEdit, index):
        """

        :param editor:
        :param index:
        """
        editor.blockSignals(True)
        val = index.model().data(index, role=QtCore.Qt.ItemDataRole.DisplayRole)
        editor.setText(val)
        editor.blockSignals(False)

    def setModelData(self, editor: QtWidgets.QLineEdit, model, index):
        """

        :param editor:
        :param model:
        :param index:
        """
        model.setData(index, editor.text())


class FloatDelegate(QtWidgets.QItemDelegate):
    """
    A delegate that places a fully functioning QDoubleSpinBox in every
    cell of the column to which it's applied
    """

    commitData = QtCore.Signal(object)

    def __init__(self,
                 parent: QtWidgets.QTableView,
                 min_: float = -1e200,
                 max_: float = 1e200,
                 decimals: int = 6) -> None:
        """
        Constructor
        :param parent: QTableView parent object
        """
        QtWidgets.QItemDelegate.__init__(self, parent)
        self.min = min_
        self.max = max_
        self.decimals = decimals

    @QtCore.Slot()
    def returnPressed(self) -> None:
        """
        returnPressed
        """
        self.commitData.emit(self.sender())

    def createEditor(self, parent: QtWidgets.QWidget,
                     option: QtWidgets.QStyleOptionViewItem,
                     index: QtCore.QModelIndex) -> QtWidgets.QDoubleSpinBox:
        """

        :param parent:
        :param option:
        :param index:
        :return:
        """
        editor = QtWidgets.QDoubleSpinBox(parent)
        editor.setMaximum(self.max)
        editor.setMinimum(self.min)
        editor.setDecimals(self.decimals)
        editor.editingFinished.connect(self.returnPressed)
        return editor

    def setEditorData(self, editor: QtWidgets.QDoubleSpinBox, index):
        """

        :param editor:
        :param index:
        """
        editor.blockSignals(True)
        try:
            val = float(index.model().data(index, role=QtCore.Qt.ItemDataRole.DisplayRole))
        except ValueError:
            val = 0.0
        editor.setValue(val)
        editor.blockSignals(False)

    def setModelData(self, editor: QtWidgets.QDoubleSpinBox, model, index):
        """

        :param editor:
        :param model:
        :param index:
        """
        model.setData(index, editor.value())


class IntDelegate(QtWidgets.QItemDelegate):
    """
    A delegate that places a fully functioning QDoubleSpinBox in every
    cell of the column to which it's applied
    """

    commitData = QtCore.Signal(object)

    def __init__(self, parent: QtWidgets.QTableView, min_: int = -99999, max_: int = 99999) -> None:
        """
        Constructor
        :param parent: QTableView parent object
        """
        QtWidgets.QItemDelegate.__init__(self, parent)
        self.min = min_
        self.max = max_

    @QtCore.Slot()
    def returnPressed(self):
        """
        returnPressed
        """
        self.commitData.emit(self.sender())

    def createEditor(self, parent, option, index):
        """

        :param parent:
        :param option:
        :param index:
        :return:
        """
        editor = QtWidgets.QSpinBox(parent)
        editor.setMaximum(self.max)
        editor.setMinimum(self.min)
        editor.editingFinished.connect(self.returnPressed)
        return editor

    def setEditorData(self, editor: QtWidgets.QDoubleSpinBox, index):
        """

        :param editor:
        :param index:
        """
        editor.blockSignals(True)
        val = int(index.model().data(index, role=QtCore.Qt.ItemDataRole.DisplayRole))
        editor.setValue(val)
        editor.blockSignals(False)

    def setModelData(self, editor: QtWidgets.QDoubleSpinBox, model, index):
        """

        :param editor:
        :param model:
        :param index:
        """
        model.setData(index, editor.value())


class ComplexDelegate(QtWidgets.QItemDelegate):
    """
    A delegate that places a fully functioning Complex Editor in every
    cell of the column to which it's applied
    """

    commitData = QtCore.Signal(object)

    def __init__(self, parent):
        """
        Constructor
        :param parent: QTableView parent object
        """
        QtWidgets.QItemDelegate.__init__(self, parent)

    @QtCore.Slot()
    def returnPressed(self):
        """

        :return:
        """
        self.commitData.emit(self.sender())

    def createEditor(self, parent, option, index):
        """

        :param parent:
        :param option:
        :param index:
        :return:
        """
        editor = QtWidgets.QFrame(parent)
        main_layout = QtWidgets.QHBoxLayout(editor)
        main_layout.layout().setContentsMargins(0, 0, 0, 0)

        real = QtWidgets.QDoubleSpinBox()
        real.setMaximum(9999)
        real.setMinimum(-9999)
        real.setDecimals(8)

        imag = QtWidgets.QDoubleSpinBox()
        imag.setMaximum(9999)
        imag.setMinimum(-9999)
        imag.setDecimals(8)

        main_layout.addWidget(real)
        main_layout.addWidget(imag)
        # main_layout.addWidget(button)

        # button.clicked.connect(self.returnPressed)

        return editor

    def setEditorData(self, editor: QtWidgets.QFrame, index):
        """

        :param editor:
        :param index:
        :return:
        """
        editor.blockSignals(True)
        val = complex(index.model().data(index, role=QtCore.Qt.ItemDataRole.DisplayRole))
        editor.children()[1].setValue(val.real)
        editor.children()[2].setValue(val.imag)
        editor.blockSignals(False)

    def setModelData(self, editor: QtWidgets.QFrame, model, index):
        """

        :param editor:
        :param model:
        :param index:
        :return:
        """
        val = complex(editor.children()[1].value(), editor.children()[2].value())
        model.setData(index, val)


class LineLocationsDelegate(QtWidgets.QItemDelegate):
    """
    A delegate that places a fully functioning LineLocations Editor in every
    cell of the column to which it's applied
    """

    commitData = QtCore.Signal(object)

    def __init__(self, parent):
        """
        Constructor
        :param parent: QTableView parent object
        """
        QtWidgets.QItemDelegate.__init__(self, parent)

        self.line_locations: Union[None, LineLocations] = None

    @QtCore.Slot()
    def returnPressed(self):
        """

        :return:
        """
        self.commitData.emit(self.sender())

    def createEditor(self, parent: QtWidgets.QWidget,
                     option: QtWidgets.QStyleOptionViewItem,
                     index: QtCore.QModelIndex):
        """

        :param parent:
        :param option:
        :param index:
        :return:
        """
        editor = QtWidgets.QFrame(parent)
        main_layout = QtWidgets.QHBoxLayout(editor)
        main_layout.layout().setContentsMargins(0, 0, 0, 0)

        table = QtWidgets.QTableView()

        main_layout.addWidget(table)
        # main_layout.addWidget(button)

        # button.clicked.connect(self.returnPressed)
        editor.showNormal()

        return editor

    def setEditorData(self, editor: QtWidgets.QFrame, index):
        """

        :param editor:
        :param index:
        :return:
        """
        editor.blockSignals(True)
        self.line_locations: LineLocations = index.model().data(index, role=QtCore.Qt.ItemDataRole.DisplayRole)
        # editor.children()[1].setValue(val.real)
        editor.blockSignals(False)

    def setModelData(self, editor: QtWidgets.QFrame, model: ObjectsModel, index):
        """

        :param editor:
        :param model:
        :param index:
        :return:
        """
        table = editor.children()[1]
        # model.setData(index, val)
        print()


class ColorPickerDelegate(QtWidgets.QItemDelegate):
    """
    Color picker delegate
    """
    commitData = QtCore.Signal(object)

    def __init__(self, parent):
        """

        :param parent:
        """
        super(ColorPickerDelegate, self).__init__(parent)

    @QtCore.Slot()
    def returnPressed(self):
        """

        :return:
        """
        self.commitData.emit(self.sender())

    def createEditor(self, parent, option, index):
        """

        :param parent:
        :param option:
        :param index:
        :return:
        """
        colorDialog = QtWidgets.QColorDialog(parent)
        return colorDialog

    def setEditorData(self, editor: QtWidgets.QColorDialog, index):
        """

        :param editor:
        :param index:
        """
        editor.blockSignals(True)
        val = index.model().data(index, role=QtCore.Qt.ItemDataRole.DisplayRole)
        color = QtGui.QColor.fromString(val)
        editor.setCurrentColor(color)
        editor.blockSignals(False)

    def setModelData(self, editor: QtWidgets.QColorDialog, model, index):
        """

        :param editor:
        :param model:
        :param index:
        :return:
        """
        model.setData(index, editor.currentColor().name())


class DateTimeDelegate(QtWidgets.QItemDelegate):
    """
    DateTime picker delegate that handles Unix epoch seconds
    and can be initialized with a default epoch date.
    """
    commitData = QtCore.Signal(object)

    def __init__(self, parent, default_epoch: int = 0):
        super().__init__(parent)
        # Store default epoch or None
        self.default_epoch: int = default_epoch

    @QtCore.Slot()
    def returnPressed(self):
        """

        """
        self.commitData.emit(self.sender())

    def createEditor(self, parent, option, index):
        """

        :param parent:
        :param option:
        :param index:
        :return:
        """
        editor = QtWidgets.QDateTimeEdit(parent)
        editor.setCalendarPopup(True)
        editor.setDisplayFormat("yyyy/MM/dd HH:mm:ss")

        # Set initial datetime from default_epoch if provided
        if self.default_epoch is not None:
            dt = QtCore.QDateTime.fromSecsSinceEpoch(int(self.default_epoch))
            editor.setDateTime(dt)

        editor.dateTimeChanged.connect(lambda: self.commitData.emit(editor))
        return editor

    def setEditorData(self, editor: QtWidgets.QDateTimeEdit, index):
        """

        :param editor:
        :param index:
        """
        editor.blockSignals(True)
        val = index.model().data(index, QtCore.Qt.ItemDataRole.DisplayRole)
        if isinstance(val, (int, float)):
            dt = QtCore.QDateTime.fromSecsSinceEpoch(int(val))
        elif isinstance(val, str):
            dt = QtCore.QDateTime.fromString(val, "yyyy/MM/dd")
        else:
            # fallback to current datetime or default_epoch if provided
            if self.default_epoch is not None:
                dt = QtCore.QDateTime.fromSecsSinceEpoch(int(self.default_epoch))
            else:
                dt = QtCore.QDateTime.currentDateTime()
        editor.setDateTime(dt)
        editor.blockSignals(False)

    def setModelData(self, editor: QtWidgets.QDateTimeEdit, model, index):
        """

        :param editor:
        :param model:
        :param index:
        """
        dt = editor.dateTime()
        epoch_seconds = dt.toSecsSinceEpoch()
        model.setData(index, epoch_seconds)


def get_list_model(lst: Sequence[Union[str, ALL_DEV_TYPES]],
                   checks=False,
                   check_value=False) -> QtGui.QStandardItemModel:
    """
    Pass a list to a list model
    """
    list_model = QtGui.QStandardItemModel()
    if lst is not None:
        if not checks:
            for val in lst:
                # for the list model
                item = QtGui.QStandardItem(str(val))
                item.setEditable(False)
                list_model.appendRow(item)
        else:
            for val in lst:
                # for the list model
                item = QtGui.QStandardItem(str(val))
                item.setEditable(False)
                item.setCheckable(True)
                if check_value:
                    item.setCheckState(QtCore.Qt.CheckState.Checked)
                list_model.appendRow(item)

    return list_model


def get_elm_chck_list_model(lst: List[ALL_DEV_TYPES], check_status: IntVec) -> QtGui.QStandardItemModel:
    """
    Pass a list to a list model
    """
    list_model = QtGui.QStandardItemModel()
    for elm, val in zip(lst, check_status):
        # for the list model
        item = QtGui.QStandardItem(str(elm.name))
        item.setEditable(False)
        item.setCheckable(True)
        if val:
            item.setCheckState(QtCore.Qt.CheckState.Checked)
        list_model.appendRow(item)

    return list_model


def get_chck_list_model(lst: List[str], check_status: List[bool]) -> QtGui.QStandardItemModel:
    """
    Pass a list to a list model
    """
    list_model = QtGui.QStandardItemModel()
    for elm, val in zip(lst, check_status):
        # for the list model
        item = QtGui.QStandardItem(elm)
        item.setEditable(False)
        item.setCheckable(True)
        if val:
            item.setCheckState(QtCore.Qt.CheckState.Checked)
        list_model.appendRow(item)

    return list_model


def enums_to_model(enums_lst: List[Any]) -> Tuple[Dict[str, Any], QtGui.QStandardItemModel]:
    """
    Get the model and dict from a list of Enum value
    :param enums_lst:
    :return: dictionary, model
    """
    d = OrderedDict()
    val_list = list()
    for e in enums_lst:
        d[e.value] = e
        val_list.append(e.value)

    mdl = get_list_model(val_list)

    return d, mdl


class CustomFileSystemModel(QtWidgets.QFileSystemModel):
    """
    CustomFileSystemModel
    """

    def __init__(self, root_path: str, ext_filter: Union[None, List[str]] = None):
        super(CustomFileSystemModel, self).__init__()

        self.ext_filter = ext_filter if ext_filter is not None else ['*.py']

        self.setNameFilters(ext_filter)
        self.setRootPath(root_path)


def get_logger_tree_model(logger: DataLogger) -> QtGui.QStandardItemModel:
    """
    Fill logger tree
    :param logger: Logger instance
    :return: QStandardItemModel instance
    """
    d = logger.to_dict()
    editable = False
    model = QtGui.QStandardItemModel()
    model.setHorizontalHeaderLabels(['Time', 'Element', 'Class', 'Property', 'Value', 'Expected value', 'comment'])
    parent = model.invisibleRootItem()

    for severity, messages_dict in d.items():
        severity_child = QtGui.QStandardItem(severity)

        # print(severity)

        for message, data_list in messages_dict.items():
            message_child = QtGui.QStandardItem(message)

            # print('\t', message)

            for time, elm, elm_class, elm_property, value, expected_value, comment in data_list:
                # print('\t', '\t', time, elm, value, expected_value)

                time_child = QtGui.QStandardItem(time)
                time_child.setEditable(editable)

                elm_child = QtGui.QStandardItem(str(elm))
                elm_child.setEditable(editable)

                elm_class_child = QtGui.QStandardItem(str(elm_class))
                elm_class_child.setEditable(editable)

                elm_property_child = QtGui.QStandardItem(str(elm_property))
                elm_property_child.setEditable(editable)

                value_child = QtGui.QStandardItem(str(value))
                value_child.setEditable(editable)

                expected_val_child = QtGui.QStandardItem(str(expected_value))
                expected_val_child.setEditable(editable)

                comment_val_child = QtGui.QStandardItem(str(comment))
                comment_val_child.setEditable(editable)

                message_child.appendRow([time_child, elm_child, elm_class_child,
                                         elm_property_child, value_child, expected_val_child, comment_val_child])

            message_child.setEditable(editable)

            severity_child.appendRow(message_child)

        severity_child.setEditable(editable)
        parent.appendRow(severity_child)

    return model


def get_icon_list_model(lst: List[Tuple[str, str]], checks=False,
                        check_value=False) -> QtGui.QStandardItemModel:
    """

    :param lst:
    :param checks:
    :param check_value:
    :return:
    """
    list_model = QtGui.QStandardItemModel()
    if lst is not None:
        if not checks:
            for val, icon_path in lst:
                # for the list model
                item = QtGui.QStandardItem(str(val))
                item.setEditable(False)
                icon = QtGui.QIcon()
                icon.addPixmap(QtGui.QPixmap(icon_path))
                item.setIcon(icon)
                list_model.appendRow(item)
        else:
            for val, icon_path in lst:
                # for the list model
                item = QtGui.QStandardItem(str(val))
                icon = QtGui.QIcon()
                icon.addPixmap(QtGui.QPixmap(icon_path))
                item.setIcon(icon)
                item.setEditable(False)
                item.setCheckable(True)
                if check_value:
                    item.setCheckState(QtCore.Qt.CheckState.Checked)
                list_model.appendRow(item)

    return list_model


def get_checked_indices(mdl: QtGui.QStandardItemModel) -> IntVec:
    """
    Get a list of the selected indices in a QStandardItemModel
    :param mdl:
    :return:
    """
    idx = list()
    for row in range(mdl.rowCount()):
        item = mdl.item(row)
        if item.checkState() == QtCore.Qt.CheckState.Checked:
            idx.append(row)

    return np.array(idx)


def get_checked_values(mdl: QtGui.QStandardItemModel) -> List[str]:
    """
    Get a list of the selected values in a QStandardItemModel
    :param mdl:
    :return:
    """
    idx = list()
    for row in range(mdl.rowCount()):
        item = mdl.item(row)
        if item.checkState() == QtCore.Qt.CheckState.Checked:
            idx.append(item.text())

    return idx


def fill_model_from_dict(parent: QtGui.QStandardItem,
                         d: Dict[str, Union[Dict[str, Any], List[str]]],
                         editable=False,
                         icons: Dict[str, str] = None,
                         font_size: int = MENU_FONT_SIZE):
    """
    Fill TreeViewModel from dictionary
    :param parent: Parent QStandardItem
    :param d: item
    :param editable
    :param icons
    :param font_size
    :return: Nothing
    """
    font = QtGui.QFont()
    font.setPointSize(font_size)

    if isinstance(d, dict):
        for k, v in d.items():
            name = str(k)
            child = QtGui.QStandardItem(name)
            child.setEditable(editable)
            child.setFont(font)

            if icons is not None:
                icon_path = icons.get(name, None)
                if icon_path is not None:
                    icon_path = icons[name]
                    _icon = QtGui.QIcon()
                    _icon.addPixmap(QtGui.QPixmap(icon_path))
                    child.setIcon(_icon)
                else:
                    pass
                    # print(f"path {name} has no icon: check 'setup_objects_tree()'")

            parent.appendRow(child)
            fill_model_from_dict(parent=child, d=v, icons=icons)
    elif isinstance(d, list):
        for v in d:
            fill_model_from_dict(parent=parent, d=v, icons=icons)
    else:
        name = str(d)
        item = QtGui.QStandardItem(name)
        item.setFont(font)

        if icons is not None:
            icon_path = icons.get(name, None)
            if icon_path is not None:
                icon_path = icons[name]
                _icon = QtGui.QIcon()
                _icon.addPixmap(QtGui.QPixmap(icon_path))
                item.setIcon(_icon)
            else:
                pass
                # print(f"path {name} has no icon: check 'setup_objects_tree()'")
        item.setEditable(editable)
        parent.appendRow(item)


def get_tree_model(d, top='', icons: Dict[str, str] = None) -> QtGui.QStandardItemModel:
    """
    Build a tree model from a dictionary
    :param d: Any dictionary
    :param top: Table header
    :param icons: Dictionary of icons (name of the node, icon to match)
    :return:
    """
    model = QtGui.QStandardItemModel()
    model.setHorizontalHeaderLabels([top])
    fill_model_from_dict(model.invisibleRootItem(), d=d, editable=False, icons=icons)

    return model


def get_tree_item_path(item: QtGui.QStandardItem) -> List[str]:
    """
    Get the path of an item in a tree
    :param item: QStandardItem
    :return: path in a list
    """
    item_parent = item.parent()
    path = [item.text()]
    while item_parent is not None:
        parent_text = item_parent.text()
        path.append(parent_text)
        item_parent = item_parent.parent()
    path.reverse()
    return path


CIM_OBJECT_ROLE = QtCore.Qt.UserRole + 120
CIM_LOADED_ROLE = QtCore.Qt.UserRole + 121


def _get_cim_object_id(obj: Any):
    if hasattr(obj, 'rdfid') and obj.rdfid is not None:
        return obj.rdfid
    return id(obj)


def _get_cim_object_label(class_tag, device):
    if class_tag is not None:
        return class_tag
    if hasattr(device, 'name'):
        if device.name is not None:
            if device.name != '':
                return device.name
    if hasattr(device, 'rdfid'):
        return device.rdfid
    return str(device)


def _collect_cim_ancestor_ids(item: QtGui.QStandardItem) -> Set[Any]:
    ids: Set[Any] = set()
    current = item
    while current is not None:
        obj = current.data(CIM_OBJECT_ROLE)
        if obj is not None:
            ids.add(_get_cim_object_id(obj))
        current = current.parent()
    return ids


def create_cim_object_item(class_tag,
                           device: CGMES_ASSETS,
                           editable=False,
                           lazy=True) -> QtGui.QStandardItem:
    item = QtGui.QStandardItem(_get_cim_object_label(class_tag, device))
    item.setEditable(editable)
    item.setData(device, CIM_OBJECT_ROLE)
    item.setData(False, CIM_LOADED_ROLE)

    if lazy and getattr(device, 'declared_properties', None):
        # Placeholder child to show expand arrow; removed on load.
        item.appendRow(QtGui.QStandardItem(""))

    if not lazy:
        populate_cim_item_children(item, editable=editable)

    return item


def populate_cim_item_children(item: QtGui.QStandardItem, editable=False):
    """
    Populate immediate children for a CIM object item. Designed for lazy loading.
    """
    device = item.data(CIM_OBJECT_ROLE)
    if device is None:
        return

    if item.data(CIM_LOADED_ROLE):
        return

    ancestor_ids = _collect_cim_ancestor_ids(item)
    item.removeRows(0, item.rowCount())

    for property_name, cim_prop in device.declared_properties.items():

        property_value = getattr(device, property_name)

        if hasattr(property_value, 'rdfid'):

            prop_id = _get_cim_object_id(property_value)
            we_are_in_a_recursive_loop = prop_id in ancestor_ids

            if not we_are_in_a_recursive_loop:
                tpe = str(property_value.tpe)
                class_name_child = create_cim_object_item(class_tag=tpe,
                                                          device=property_value,
                                                          editable=editable,
                                                          lazy=True)
                class_name_child.setEditable(editable)

                property_name_child = QtGui.QStandardItem(tpe)
                property_name_child.setEditable(editable)

                value_child = QtGui.QStandardItem(property_value.rdfid)
                value_child.setEditable(editable)
            else:
                class_name_child = QtGui.QStandardItem("Recursive object (" + str(len(ancestor_ids)) + ")")
                class_name_child.setEditable(editable)

                property_name_child = QtGui.QStandardItem(property_name)
                property_name_child.setEditable(editable)

                value_child = QtGui.QStandardItem(str(property_value))
                value_child.setEditable(editable)
        else:
            tpe = (str(type(property_value)).replace('class', '')
                   .replace("'", "")
                   .replace("<", "")
                   .replace(">", "").strip())

            class_name_child = QtGui.QStandardItem(tpe)
            class_name_child.setEditable(editable)

            property_name_child = QtGui.QStandardItem(property_name)
            property_name_child.setEditable(editable)

            value_child = QtGui.QStandardItem(str(property_value))
            value_child.setEditable(editable)

        item.appendRow([class_name_child, property_name_child, value_child])

    item.setData(True, CIM_LOADED_ROLE)


def add_cim_object_node(class_tag,
                        device: CGMES_ASSETS,
                        editable=False,
                        already_visited: Union[Set, None] = None):
    """

    :param class_tag:
    :param device:
    :param editable:
    :param already_visited:
    :return:
    """
    if already_visited is None:
        already_visited = set()

    if class_tag is None:
        class_tag = _get_cim_object_label(class_tag=None, device=device)

    # create root node
    device_child = QtGui.QStandardItem(class_tag)

    # register visit to avoid cyclic recursion
    device_id = _get_cim_object_id(device)
    already_visited.add(device_id)

    try:
        for property_name, cim_prop in device.declared_properties.items():

            property_value = getattr(device, property_name)

            if hasattr(property_value, 'rdfid'):

                prop_id = _get_cim_object_id(property_value)
                we_are_in_a_recursive_loop = prop_id in already_visited

                if not we_are_in_a_recursive_loop:

                    # if the property is an object, recursively add it
                    tpe = str(property_value.tpe)
                    class_name_child = add_cim_object_node(class_tag=tpe,
                                                           device=property_value,
                                                           editable=editable,
                                                           already_visited=already_visited)
                    class_name_child.setEditable(editable)

                    property_name_child = QtGui.QStandardItem(tpe)
                    property_name_child.setEditable(editable)

                    value_child = QtGui.QStandardItem(property_value.rdfid)
                    value_child.setEditable(editable)
                else:
                    # print('Recursive loop...')
                    # return device_child
                    class_name_child = QtGui.QStandardItem("Recursive object (" + str(len(already_visited)) + ")")
                    class_name_child.setEditable(editable)

                    property_name_child = QtGui.QStandardItem(property_name)
                    property_name_child.setEditable(editable)

                    value_child = QtGui.QStandardItem(str(property_value))
                    value_child.setEditable(editable)
            else:
                # if the property is a value (float, str, bool, etc.) just add it

                tpe = (str(type(property_value)).replace('class', '')
                       .replace("'", "")
                       .replace("<", "")
                       .replace(">", "").strip())

                class_name_child = QtGui.QStandardItem(tpe)
                class_name_child.setEditable(editable)

                property_name_child = QtGui.QStandardItem(property_name)
                property_name_child.setEditable(editable)

                value_child = QtGui.QStandardItem(str(property_value))
                value_child.setEditable(editable)

            device_child.appendRow([class_name_child, property_name_child, value_child])
    finally:
        already_visited.discard(device_id)

    return device_child


def get_cim_tree_model(cim_model: CgmesCircuit):
    """
    Fill logger tree
    :param cim_model: Logger instance
    :return: QStandardItemModel instance
    """

    editable = False
    model = QtGui.QStandardItemModel()
    model.setHorizontalHeaderLabels(['Object class', 'Property', 'Value'])
    root_node = model.invisibleRootItem()

    for class_name, device_list in cim_model.elements_by_type.items():

        class_child = QtGui.QStandardItem(class_name + " (" + str(len(device_list)) + ")")

        for device in device_list:
            # add device with all it's properties
            device_child = create_cim_object_item(class_tag=None,
                                                  device=device,
                                                  editable=editable,
                                                  lazy=True)

            device_child.setEditable(editable)

            class_child.appendRow(device_child)

        class_child.setEditable(editable)
        root_node.appendRow(class_child)

    return model


def add_sub_menu(menu: QtWidgets.QMenu,
                 text: str,
                 icon_path: str = "",
                 icon_pixmap: QtGui.QPixmap = None, ):
    entry = menu.addMenu(text)

    if icon_pixmap is None:
        if len(icon_path) > 0:
            edit_icon = QtGui.QIcon()
            edit_icon.addPixmap(QtGui.QPixmap(icon_path))
            entry.setIcon(edit_icon)
    else:
        # prefer the icon pixmap if provided
        edit_icon = QtGui.QIcon()
        edit_icon.addPixmap(icon_pixmap)
        entry.setIcon(edit_icon)

    return entry


def add_menu_entry(menu: QtWidgets.QMenu,
                   text: str,
                   icon_path: str = "",
                   icon_pixmap: QtGui.QPixmap = None,
                   function_ptr=None,
                   checkeable=False,
                   checked_value=False,
                   font_size: int = MENU_FONT_SIZE) -> QtGui.QAction:
    """
    Add a context menu entry
    :param menu:
    :param text:
    :param icon_path:
    :param icon_pixmap:
    :param function_ptr:
    :param checkeable:
    :param checked_value:
    :param font_size:
    :return:
    """

    entry: QtGui.QAction = menu.addAction(text)

    font = QtGui.QFont()
    font.setPointSize(font_size)
    entry.setFont(font)

    if checkeable:
        entry.setCheckable(checkeable)
        entry.setChecked(bool(checked_value))

    if icon_pixmap is None:
        if len(icon_path) > 0:
            edit_icon = QtGui.QIcon()
            edit_icon.addPixmap(QtGui.QPixmap(icon_path))
            entry.setIcon(edit_icon)
    else:
        # prefer the icon pixmap if provided
        edit_icon = QtGui.QIcon()
        edit_icon.addPixmap(icon_pixmap)
        entry.setIcon(edit_icon)

    if function_ptr is not None:
        entry.triggered.connect(function_ptr)

    return entry


def create_spinbox(value: float, minimum: float, maximum: float, decimals: int = 4) -> QtWidgets.QDoubleSpinBox:
    """

    :param value:
    :param minimum:
    :param maximum:
    :param decimals:
    :return:
    """
    sn_spinner = QtWidgets.QDoubleSpinBox()
    sn_spinner.setMinimum(minimum)
    sn_spinner.setMaximum(maximum)
    sn_spinner.setDecimals(decimals)
    sn_spinner.setValue(value)
    return sn_spinner


def create_int_spinbox(value: int, minimum: int, maximum: int) -> QtWidgets.QSpinBox:
    """

    :param value:
    :param minimum:
    :param maximum:
    :return:
    """
    sn_spinner = QtWidgets.QSpinBox()
    sn_spinner.setMinimum(minimum)
    sn_spinner.setMaximum(maximum)
    sn_spinner.setValue(value)
    return sn_spinner


class AsyncTask(QtCore.QRunnable):
    def __init__(self, task):
        super().__init__()
        self.task = task

    @QtCore.Slot()
    def run(self):
        self.task()
