#! /usr/bin/env python3

import sys
import os
import logging

from PyQt5.QtCore import QTimer, QSettings, QModelIndex, Qt, QCoreApplication, QObject, pyqtSignal
from PyQt5.QtGui import QIcon, QFont
from PyQt5.QtWidgets import QMainWindow, QApplication, QFileDialog, QMessageBox, QStyledItemDelegate, QMenu, QAction


from opcua import ua

from uawidgets import resources
from uawidgets.attrs_widget import AttrsWidget
from uawidgets.tree_widget import TreeWidget
from uawidgets.refs_widget import RefsWidget
from uawidgets.new_node_dialogs import NewNodeBaseDialog, NewUaObjectDialog, NewUaVariableDialog, NewUaMethodDialog
from uawidgets.utils import trycatchslot
from uawidgets.logger import QtHandler

from uamodeler.uamodeler_ui import Ui_UaModeler
from uamodeler.namespace_widget import NamespaceWidget
from uamodeler.refnodesets_widget import RefNodeSetsWidget
from uamodeler.model_manager import ModelManager


logger = logging.getLogger(__name__)


class BoldDelegate(QStyledItemDelegate):

    def __init__(self, parent, model, added_node_list):
        QStyledItemDelegate.__init__(self, parent)
        self.added_node_list = added_node_list
        self.model = model

    def paint(self, painter, option, idx):
        new_idx = idx.sibling(idx.row(), 0)
        item = self.model.itemFromIndex(new_idx)
        if item and item.data(Qt.UserRole) in self.added_node_list:
            option.font.setWeight(QFont.Bold)
        QStyledItemDelegate.paint(self, painter, option, idx)


class ActionsManager:
    """
    Manage actions of Modeler
    """

    def __init__(self, window, ui, model_mgr):
        self.window = window
        self.ui = ui
        self.model_mgr = model_mgr

        self._fix_icons()
        # actions
        self.ui.actionNew.triggered.connect(self.model_mgr.new)
        self.ui.actionOpen.triggered.connect(self.model_mgr.open)
        self.ui.actionCopy.triggered.connect(self.model_mgr.copy)
        self.ui.actionQuit.triggered.connect(self.window.close)
        self.ui.actionPaste.triggered.connect(self.model_mgr.paste)
        self.ui.actionDelete.triggered.connect(self.model_mgr.delete)
        self.ui.actionImport.triggered.connect(self.model_mgr.import_xml)
        self.ui.actionSave.triggered.connect(self.model_mgr.save)
        self.ui.actionSaveAs.triggered.connect(self.model_mgr.save_as)
        self.ui.actionCloseModel.triggered.connect(self.model_mgr.close_model)
        self.ui.actionAddObjectType.triggered.connect(self.model_mgr.add_object_type)
        self.ui.actionAddObject.triggered.connect(self.model_mgr.add_object)
        self.ui.actionAddFolder.triggered.connect(self.model_mgr.add_folder)
        self.ui.actionAddMethod.triggered.connect(self.model_mgr.add_method)
        self.ui.actionAddDataType.triggered.connect(self.model_mgr.add_data_type)
        self.ui.actionAddVariable.triggered.connect(self.model_mgr.add_variable)
        self.ui.actionAddVariableType.triggered.connect(self.model_mgr.add_variable_type)
        self.ui.actionAddProperty.triggered.connect(self.model_mgr.add_property)

        self.disable_all_actions()

    def _fix_icons(self):
        # fix icon stuff
        self.ui.actionNew.setIcon(QIcon(":/new.svg"))
        self.ui.actionOpen.setIcon(QIcon(":/open.svg"))
        self.ui.actionCopy.setIcon(QIcon(":/copy.svg"))
        self.ui.actionPaste.setIcon(QIcon(":/paste.svg"))
        self.ui.actionDelete.setIcon(QIcon(":/delete.svg"))
        self.ui.actionSave.setIcon(QIcon(":/save.svg"))
        self.ui.actionAddFolder.setIcon(QIcon(":/folder.svg"))
        self.ui.actionAddObject.setIcon(QIcon(":/object.svg"))
        self.ui.actionAddMethod.setIcon(QIcon(":/method.svg"))
        self.ui.actionAddObjectType.setIcon(QIcon(":/object_type.svg"))
        self.ui.actionAddProperty.setIcon(QIcon(":/property.svg"))
        self.ui.actionAddVariable.setIcon(QIcon(":/variable.svg"))
        self.ui.actionAddVariableType.setIcon(QIcon(":/variable_type.svg"))
        self.ui.actionAddDataType.setIcon(QIcon(":/data_type.svg"))
        self.ui.actionAddReferenceType.setIcon(QIcon(":/reference_type.svg"))

    def update_actions_states(self, node):
        self.disable_add_actions()
        if not node or node in (self.model_mgr.get_current_server().nodes.root,
                                self.model_mgr.get_current_server().nodes.types,
                                self.model_mgr.get_current_server().nodes.event_types,
                                self.model_mgr.get_current_server().nodes.object_types,
                                self.model_mgr.get_current_server().nodes.reference_types,
                                self.model_mgr.get_current_server().nodes.variable_types,
                                self.model_mgr.get_current_server().nodes.data_types):
            return
        path = node.get_path()
        nodeclass = node.get_node_class()
        typedefinition = node.get_type_definition()

        self.ui.actionCopy.setEnabled(True)
        self.ui.actionDelete.setEnabled(True)

        if typedefinition == ua.NodeId(ua.ObjectIds.PropertyType):
            return

        if nodeclass == ua.NodeClass.Variable:
            self.ui.actionAddVariable.setEnabled(True)
            self.ui.actionAddProperty.setEnabled(True)
            return

        self.ui.actionPaste.setEnabled(True)

        if self.model_mgr.get_current_server().nodes.base_object_type in path:
            self.ui.actionAddObjectType.setEnabled(True)

        if self.model_mgr.get_current_server().nodes.base_variable_type in path:
            self.ui.actionAddVariableType.setEnabled(True)

        if self.model_mgr.get_current_server().nodes.base_data_type in path:
            self.ui.actionAddDataType.setEnabled(True)
            if self.model_mgr.get_current_server().nodes.enum_data_type in path:
                self.ui.actionAddProperty.setEnabled(True)
            elif self.model_mgr.get_current_server().nodes.base_structure_type in path:
                self.ui.actionAddVariable.setEnabled(True)
            return  # not other nodes should be added here

        self.ui.actionAddFolder.setEnabled(True)
        self.ui.actionAddObject.setEnabled(True)
        self.ui.actionAddVariable.setEnabled(True)
        self.ui.actionAddProperty.setEnabled(True)
        self.ui.actionAddMethod.setEnabled(True)

    def disable_model_actions(self):
        self.ui.actionImport.setEnabled(False)
        self.ui.actionSave.setEnabled(False)
        self.ui.actionSaveAs.setEnabled(False)

    def disable_all_actions(self):
        self.disable_add_actions()
        self.disable_model_actions()

    def disable_add_actions(self):
        self.ui.actionPaste.setEnabled(False)
        self.ui.actionCopy.setEnabled(False)
        self.ui.actionDelete.setEnabled(False)
        self.ui.actionAddObject.setEnabled(False)
        self.ui.actionAddFolder.setEnabled(False)
        self.ui.actionAddVariable.setEnabled(False)
        self.ui.actionAddProperty.setEnabled(False)
        self.ui.actionAddDataType.setEnabled(False)
        self.ui.actionAddVariableType.setEnabled(False)
        self.ui.actionAddObjectType.setEnabled(False)
        self.ui.actionAddMethod.setEnabled(False)

    def enable_model_actions(self):
        self.ui.actionImport.setEnabled(True)
        self.ui.actionSave.setEnabled(True)
        self.ui.actionSaveAs.setEnabled(True)


class ModelManagerUI(QObject):
    """
    Interface to ModelMgr that displays dialogs to interact with users.
    Logic is inside ModelManager, this class only handle the UI and dialogs
    """

    error = pyqtSignal(Exception)
    titleChanged = pyqtSignal(str)

    def __init__(self, modeler):
        QObject.__init__(self)
        self.modeler = modeler
        self._model_mgr = ModelManager(modeler)
        self._model_mgr.error.connect(self.error)
        self._model_mgr.titleChanged.connect(self.titleChanged)
        self.settings = QSettings()
        self._last_model_dir = self.settings.value("last_model_dir", ".")
        self._copy_clipboard = None

    def get_current_server(self):
        return self._model_mgr.server_mgr

    def get_new_nodes(self):
        return self._model_mgr.new_nodes

    def setModified(self, val=True):
        self._model_mgr.modified = val

    @trycatchslot
    def new(self):
        if not self.try_close_model():
            return
        self._model_mgr.new_model()

    @trycatchslot
    def delete(self):
        node = self.modeler.get_current_node()
        self._model_mgr.delete_node(node)

    @trycatchslot
    def copy(self):
        node = self.modeler.get_current_node()
        if node:
            self._copy_clipboard = node

    @trycatchslot
    def paste(self):
        if self._copy_clipboard:
            self._model_mgr.paste_node(self._copy_clipboard)

    @trycatchslot
    def close_model(self):
        self.try_close_model()

    def try_close_model(self):
        if self._model_mgr.modified:
            reply = QMessageBox.question(
                self.modeler,
                "OPC UA Modeler",
                "Model is modified, do you really want to close model?",
                QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
            )
            if reply != QMessageBox.Yes:
                return False
        self._model_mgr.close_model(force=True)
        return True

    @trycatchslot
    def open(self):
        if not self.try_close_model():
            return
        path, ok = QFileDialog.getOpenFileName(self.modeler, caption="Open OPC UA XML", filter="XML Files (*.xml *.XML *.uamodel)", directory=self._last_model_dir)
        if not ok:
            return
        if self._last_model_dir != os.path.dirname(path):
            self._last_model_dir = os.path.dirname(path)
            self.settings.setValue("last_model_dir", self._last_model_dir)
        self._model_mgr.open(path)
        self.modeler.update_recent_files(path)

    def open_file(self, path):
        self._model_mgr.open(path)

    @trycatchslot
    def import_xml(self):
        last_import_dir = self.settings.value("last_import_dir", ".")
        path, ok = QFileDialog.getOpenFileName(self.modeler, caption="Import reference OPC UA XML", filter="XML Files (*.xml *.XML)", directory=last_import_dir)
        if not ok:
            return
        self.settings.setValue("last_import_dir", last_import_dir)
        self._model_mgr.import_xml(path)

    @trycatchslot
    def save_as(self):
        self._save_as()

    def _save_as(self):
        path, ok = QFileDialog.getSaveFileName(self.modeler, caption="Save OPC UA XML", filter="XML Files (*.xml *.XML)")
        if ok:
            print("PATH", path)
            if self._last_model_dir != os.path.dirname(path):
                self._last_model_dir = os.path.dirname(path)
                self.settings.setValue("last_model_dir", self._last_model_dir)
            self._model_mgr.save_xml(path)
            path = self._model_mgr.save_ua_model(path)
            self.modeler.update_recent_files(path)

    @trycatchslot
    def save(self):
        if not self._model_mgr.current_path:
            self.save_as()
        else:
            self._model_mgr.save_xml()
            self._model_mgr.save_ua_model()

    @trycatchslot
    def add_method(self):
        args, ok = NewUaMethodDialog.getArgs(self.modeler, "Add Method", self._model_mgr.server_mgr)
        if ok:
            nodes = self._model_mgr.add_method(*args)
            print("ADDED", [c.get_browse_name() for c in nodes])
            self._add_modelling_rule(nodes)

    @trycatchslot
    def add_object_type(self):
        args, ok = NewNodeBaseDialog.getArgs(self.modeler, "Add Object Type", self._model_mgr.server_mgr)
        if ok:
            self._model_mgr.add_object_type(*args)

    @trycatchslot
    def add_folder(self):
        args, ok = NewNodeBaseDialog.getArgs(self.modeler, "Add Folder", self._model_mgr.server_mgr)
        if ok:
            node = self._model_mgr.add_folder(*args)
            self._add_modelling_rule(node)

    @trycatchslot
    def add_object(self):
        args, ok = NewUaObjectDialog.getArgs(self.modeler, "Add Object", self._model_mgr.server_mgr, base_node_type=self._model_mgr.server_mgr.nodes.base_object_type)
        if ok:
            nodes = self._model_mgr.add_object(*args)
            # FIXME: in this particular case we may want to navigate recursively to add ref
            self._add_modelling_rule(nodes)

    def _add_modelling_rule(self, nodes):
        if not isinstance(nodes, (list, tuple)):
            nodes = [nodes]
        for node in nodes:
            path = node.get_path()
            if self._model_mgr.server_mgr.nodes.base_object_type in path:
                # we are creating a new type, add modeling rule
                node.set_modelling_rule(True)

    @trycatchslot
    def add_data_type(self):
        args, ok = NewNodeBaseDialog.getArgs(self.modeler, "Add Data Type", self._model_mgr.server_mgr)
        if ok:
            self._model_mgr.add_data_type(*args)

    @trycatchslot
    def add_variable(self):
        args, ok = NewUaVariableDialog.getArgs(self.modeler, "Add Variable", self._model_mgr.server_mgr)
        if ok:
            node = self._model_mgr.add_variable(*args)
            self._add_modelling_rule(node)

    @trycatchslot
    def add_property(self):
        args, ok = NewUaVariableDialog.getArgs(self.modeler, "Add Property", self._model_mgr.server_mgr)
        if ok:
            node = self._model_mgr.add_property(*args)
            self._add_modelling_rule(node)

    @trycatchslot
    def add_variable_type(self):
        args, ok = NewUaObjectDialog.getArgs(self.modeler, "Add Variable Type", self._model_mgr.server_mgr, base_node_type=self._model_mgr.server_mgr.get_node(ua.ObjectIds.BaseVariableType))
        if ok:
            self._model_mgr.add_variable_type(*args)


class UaModeler(QMainWindow):
    """
    Main class of modeler. Should be as simple as possible, try to push things to other classes
    or even better python-opcua
    """

    def __init__(self):
        QMainWindow.__init__(self)
        self.ui = Ui_UaModeler()
        self.ui.setupUi(self)
        self.setWindowIcon(QIcon(":/network.svg"))

        # we only show statusbar in case of errors
        self.ui.statusBar.hide()

        # setup QSettings for application and get a settings object
        QCoreApplication.setOrganizationName("FreeOpcUa")
        QCoreApplication.setApplicationName("OpcUaModeler")
        self.settings = QSettings()

        self._restore_ui_geometri()

        self.tree_ui = TreeWidget(self.ui.treeView)
        self.tree_ui.error.connect(self.show_error)

        self.refs_ui = RefsWidget(self.ui.refView)
        self.refs_ui.error.connect(self.show_error)
        self.refs_ui.reference_changed.connect(self.tree_ui.reload_current)  # FIXME: shoudl reload a specific node
        self.attrs_ui = AttrsWidget(self.ui.attrView, show_timestamps=False)
        self.attrs_ui.error.connect(self.show_error)
        self.idx_ui = NamespaceWidget(self.ui.namespaceView)
        self.nodesets_ui = RefNodeSetsWidget(self.ui.refNodeSetsView)
        self.nodesets_ui.error.connect(self.show_error)
        self.nodesets_ui.nodeset_added.connect(self.nodesets_change)
        self.nodesets_ui.nodeset_removed.connect(self.nodesets_change)

        self.ui.treeView.activated.connect(self.show_refs)
        self.ui.treeView.clicked.connect(self.show_refs)
        self.ui.treeView.activated.connect(self.show_attrs)
        self.ui.treeView.clicked.connect(self.show_attrs)

        self.model_mgr = ModelManagerUI(self)
        self.model_mgr.error.connect(self.show_error)
        self.model_mgr.titleChanged.connect(self.update_title)
        self.actions = ActionsManager(self, self.ui, self.model_mgr)

        self.setup_context_menu_tree()

        delegate = BoldDelegate(self, self.tree_ui.model, self.model_mgr.get_new_nodes())
        self.ui.treeView.setItemDelegate(delegate)
        self.ui.treeView.selectionModel().currentChanged.connect(self._update_actions_state)

        self._recent_files = self.settings.value("recent_files", [])
        self._recent_files_max_count = int(self.settings.value("recent_files_max_count", 10))
        self._recent_files_acts = [QAction(self, visible=False, triggered=self.open_recent_files) for _ in range(self._recent_files_max_count)]
        for act in self._recent_files_acts:
            self.ui.menuRecentFiles.addAction(act)
        self._update_recent_files_ui()

    def open_recent_files(self):
        if not self.model_mgr.try_close_model():
            return
        action = self.sender()
        if action:
            path = action.data()
            self.model_mgr.open_file(path)
            self.update_recent_files(path)

    def update_recent_files(self, path):
        if self._recent_files and path == self._recent_files[0]:
            return
        if self._recent_files is not None:
            if path in self._recent_files:
                self._recent_files.remove(path)
            self._recent_files.insert(0, path)
            self._recent_files = self._recent_files[:self._recent_files_max_count]
            self._update_recent_files_ui()

    def _update_recent_files_ui(self):
        if self._recent_files is not None:
            for idx, path in enumerate(self._recent_files):
                self._recent_files_acts[idx].setText(path)
                self._recent_files_acts[idx].setData(path)
                self._recent_files_acts[idx].setVisible(True)

    def get_current_node(self, idx=None):
        return self.tree_ui.get_current_node(idx)

    def get_current_server(self):
        """
        Used by tests
        """
        return self.model_mgr.get_current_server()

    def clear_all_widgets(self):
        self.tree_ui.clear()
        self.refs_ui.clear()
        self.attrs_ui.clear()
        self.idx_ui.clear()
        self.nodesets_ui.clear()

    @trycatchslot
    def _update_actions_state(self, current, previous):
        node = self.get_current_node(current)
        self.actions.update_actions_states(node)

    def setup_context_menu_tree(self):
        self.ui.treeView.setContextMenuPolicy(Qt.CustomContextMenu)
        self.ui.treeView.customContextMenuRequested.connect(self._show_context_menu_tree)
        self._contextMenu = QMenu()

        # tree view menu
        self._contextMenu.addAction(self.ui.actionCopy)
        self._contextMenu.addAction(self.ui.actionPaste)
        self._contextMenu.addAction(self.ui.actionDelete)
        self._contextMenu.addSeparator()
        self._contextMenu.addAction(self.tree_ui.actionReload)
        self._contextMenu.addSeparator()
        self._contextMenu.addAction(self.ui.actionAddFolder)
        self._contextMenu.addAction(self.ui.actionAddObject)
        self._contextMenu.addAction(self.ui.actionAddVariable)
        self._contextMenu.addAction(self.ui.actionAddProperty)
        self._contextMenu.addAction(self.ui.actionAddMethod)
        self._contextMenu.addAction(self.ui.actionAddObjectType)
        self._contextMenu.addAction(self.ui.actionAddVariableType)
        self._contextMenu.addAction(self.ui.actionAddDataType)

    def _show_context_menu_tree(self, position):
        node = self.tree_ui.get_current_node()
        if node:
            self._contextMenu.exec_(self.ui.treeView.viewport().mapToGlobal(position))

    def _restore_ui_geometri(self):
        self.resize(int(self.settings.value("main_window_width", 800)),
                    int(self.settings.value("main_window_height", 600)))
        #self.restoreState(self.settings.value("main_window_state", b"", type="QByteArray"))
        self.restoreState(self.settings.value("main_window_state", bytearray()))
        self.ui.splitterLeft.restoreState(self.settings.value("splitter_left", bytearray()))
        self.ui.splitterRight.restoreState(self.settings.value("splitter_right", bytearray()))
        self.ui.splitterCenter.restoreState(self.settings.value("splitter_center", bytearray()))

    def update_title(self, path):
        self.setWindowTitle("FreeOpcUa Modeler " + str(path))

    def show_error(self, msg):
        self.ui.statusBar.show()
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : red; color : black; }")
        self.ui.statusBar.showMessage(str(msg))
        QTimer.singleShot(2500, self.ui.statusBar.hide)

    def show_msg(self, msg):
        self.ui.statusBar.show()
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : green; color : black; }")
        self.ui.statusBar.showMessage(str(msg))
        QTimer.singleShot(1500, self.ui.statusBar.hide)

    @trycatchslot
    def show_refs(self, idx=None):
        node = self.get_current_node(idx)
        if node:
            self.refs_ui.show_refs(node)

    @trycatchslot
    def show_attrs(self, idx=None):
        if not isinstance(idx, QModelIndex):
            idx = None
        node = self.get_current_node(idx)
        if node:
            self.attrs_ui.show_attrs(node)

    def nodesets_change(self, data):
        self.idx_ui.reload()
        self.tree_ui.reload()
        self.refs_ui.clear()
        self.attrs_ui.clear()
        self.model_mgr.setModified(True)

    def closeEvent(self, event):
        if not self.model_mgr.try_close_model():
            event.ignore()
            return
        self.attrs_ui.save_state()
        self.refs_ui.save_state()
        self.tree_ui.save_state()
        self.settings.setValue("main_window_width", self.size().width())
        self.settings.setValue("main_window_height", self.size().height())
        self.settings.setValue("main_window_state", self.saveState())
        self.settings.setValue("splitter_left", self.ui.splitterLeft.saveState())
        self.settings.setValue("splitter_right", self.ui.splitterRight.saveState())
        self.settings.setValue("splitter_center", self.ui.splitterCenter.saveState())
        self.settings.setValue("recent_files", self._recent_files)
        event.accept()


def main():
    app = QApplication(sys.argv)
    modeler = UaModeler()
    handler = QtHandler(modeler.ui.logTextEdit)
    logging.getLogger().addHandler(handler)
    logging.getLogger("uamodeler").setLevel(logging.INFO)
    logging.getLogger("uawidgets").setLevel(logging.INFO)
    #logging.getLogger("opcua").setLevel(logging.INFO)  # to enable logging of ua server
    modeler.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()