# -*- coding: utf-8 -*-
#!/usr/bin/env python3

import sys
import pkgutil
import importlib
import inspect
import os

from Qt import QtWidgets, QtCore, QtGui

# -------------------------------------------------------#
# MONKEY PATCHES - MODIFICATIONS TO OdenGraphQT BEHAVIOR #
# -------------------------------------------------------#

# PATCH: Override the color of interconnection pipes between nodes
try:
    from OdenGraphQt.qgraphics.pipe import PipeItem
    from OdenGraphQt.qgraphics.node_base import NodeItem
    from qtpy.QtGui import QPen, QColor
    from qtpy import QtCore

    # If you want the original paint logic, capture it first:
    _orig_paint_pipe = PipeItem.paint
    _orig_paint_node = NodeItem.paint

    # Custom pipe painting function
    def _custom_paint_pipe(self, painter, option, widget=None):
        painter.save()
        my_pen = QPen(QColor(0, 161, 115, 255))  # Match desired RGBA
        my_pen.setWidthF(2.0)
        painter.setPen(my_pen)
        _orig_paint_pipe(self, painter, option, widget)
        painter.restore()

    # Custom node painting function
    def _custom_paint_node(self, painter, option, widget=None):
        painter.save()
        _orig_paint_node(self, painter, option, widget)  # Call original method
        if self.isSelected():
            pen = QPen(QColor(0, 161, 115, 255))  # Set selected border color
            pen.setWidth(3)
            painter.setPen(pen)
            painter.drawRect(self.boundingRect())
        painter.restore()

    # Apply the patches
    PipeItem.paint = _custom_paint_pipe
    NodeItem.paint = _custom_paint_node

except ImportError as e:
    print(f"WARNING: Could not patch PipeItem or NodeItem: {e}")
except Exception as e:
    print(f"Patch for PipeItem or NodeItem override failed: {e}")

## PATCH: Fix "module 'qtpy.QtGui' has no attribute 'QUndoStack'" (KEEP AROUND FOR LEGACY DOCUMENTATION)
#try:
#    from qtpy.QtWidgets import QUndoStack
#    import qtpy
#    qtpy.QtGui.QUndoStack = QUndoStack
#except ImportError:
#    print("WARNING: Could not monkey-patch QUndoStack.")

# PATCH: Fix "'BackdropNodeItem' object has no attribute 'widgets'" by giving BackdropNodeItem a trivial widgets dictionary. 
try:
    from OdenGraphQt.nodes.backdrop_node import BackdropNodeItem
    if not hasattr(BackdropNodeItem, "widgets"):
        BackdropNodeItem.widgets = {}
except ImportError:
    print("WARNING: Could not monkey-patch BackdropNodeItem to add `widgets`.")

# PATCH: BEGIN ROBUST PATCH FOR QGraphicsScene.setSelectionArea
_original_setSelectionArea = QtWidgets.QGraphicsScene.setSelectionArea

def _patched_setSelectionArea(self, *args, **kwargs):
    """
    A robust patch that handles various call signatures for QGraphicsScene.setSelectionArea().
    """
    try:
        return _original_setSelectionArea(self, *args, **kwargs)
    except TypeError:
        if not args:
            raise
        painterPath = args[0]
        selection_op = QtCore.Qt.ReplaceSelection
        selection_mode = QtCore.Qt.IntersectsItemShape
        transform = QtGui.QTransform()
        return _original_setSelectionArea(self, painterPath, selection_op, selection_mode, transform)

QtWidgets.QGraphicsScene.setSelectionArea = _patched_setSelectionArea

# ----------------------------------------------------------------------------------------------------- #

# Import data_manager so we can start the Flask server
from Modules import data_manager

from OdenGraphQt import NodeGraph, BaseNode
from OdenGraphQt.widgets.dialogs import FileDialog

def import_nodes_from_folder(package_name):
    """
    Recursively import all modules from the given package.
    Returns a dictionary where keys are subfolder names, and values are lists of BaseNode subclasses.
    """
    nodes_by_category = {}
    package = importlib.import_module(package_name)
    package_path = package.__path__[0]

    for root, _, files in os.walk(package_path):
        rel_path = os.path.relpath(root, package_path).replace(os.sep, '.')
        module_prefix = f"{package_name}.{rel_path}" if rel_path != '.' else package_name
        category_name = os.path.basename(root)

        for file in files:
            if file.endswith(".py") and file != "__init__.py":
                module_name = f"{module_prefix}.{file[:-3]}"
                try:
                    module = importlib.import_module(module_name)
                    for name, obj in inspect.getmembers(module, inspect.isclass):
                        if issubclass(obj, BaseNode) and obj.__module__ == module.__name__:
                            if category_name not in nodes_by_category:
                                nodes_by_category[category_name] = []
                            nodes_by_category[category_name].append(obj)
                except Exception as e:
                    print(f"Failed to import {module_name}: {e}")
    
    return nodes_by_category


def make_node_command(graph, node_type_str):
    """
    Return a function that creates a node of the given type at the current cursor position.
    Ensures that only one FlyffCharacterStatusNode exists.
    """
    def real_create():
        if node_type_str.startswith("bunny-lab.io.flyff_character_status_node"):
            for node in graph.all_nodes():
                if node.__class__.__name__ == "FlyffCharacterStatusNode":
                    QtWidgets.QMessageBox.critical(
                        None,
                        "Error",
                        "Only one Flyff Character Status Collector node is allowed."
                    )
                    return
        try:
            pos = graph.cursor_pos()
            graph.create_node(node_type_str, pos=pos)
        except Exception as e:
            QtWidgets.QMessageBox.critical(None, "Error", str(e))

    def command():
        if QtWidgets.QApplication.instance():
            real_create()
        else:
            QtCore.QTimer.singleShot(0, real_create)

    return command

def ensure_workflows_folder():
    """
    Ensures a 'Workflows' subfolder exists.
    """
    if not os.path.exists("Workflows"):
        os.makedirs("Workflows")

def close_workflow(graph: NodeGraph):
    """
    Closes the current workflow (removes all nodes and connections).
    """
    graph.clear_session()

def save_workflow(graph: NodeGraph):
    """
    Saves the current workflow (including custom names, positions, wires, etc.) into a JSON file
    in the 'Workflows' subfolder.
    """
    ensure_workflows_folder()
    file_filter = "JSON Files (*.json);;All Files (*.*)"
    dlg = FileDialog.getSaveFileName(None, "Save Workflow", os.path.join("Workflows", ""), file_filter)
    file_path = dlg[0]
    if not file_path:
        return  # User canceled

    if not file_path.lower().endswith(".json"):
        file_path += ".json"

    try:
        graph.save_session(file_path)
        print(f"Workflow saved to {file_path}")
    except Exception as e:
        QtWidgets.QMessageBox.critical(None, "Error Saving Workflow", str(e))

def load_workflow(graph: NodeGraph):
    """
    Loads a workflow (including node values, connections, positions, etc.) from a specified JSON file
    and centers it within the graph.
    """
    ensure_workflows_folder()
    file_filter = "JSON Files (*.json);;All Files (*.*)"
    dlg = FileDialog.getOpenFileName(None, "Load Workflow", os.path.join("Workflows", ""), file_filter)
    file_path = dlg[0]
    if not file_path:
        return  # User canceled

    try:
        graph.load_session(file_path)
        print(f"Workflow loaded from {file_path}")

        # Center the workflow within the graph
        nodes = graph.all_nodes()
        if nodes:
            graph.center_on(nodes)
        else:
            print("No nodes found in the loaded workflow.")

    except Exception as e:
        QtWidgets.QMessageBox.critical(None, "Error Loading Workflow", str(e))

if __name__ == "__main__":
    app = QtWidgets.QApplication([])

    # Start Flask API Server
    data_manager.start_api_server()

    # Create the NodeGraph
    graph = NodeGraph()
    graph.widget.setWindowTitle("Borealis - Workflow Automation Tool")

    # Dynamically import custom node classes from the 'Nodes' package.
    custom_nodes_by_category = import_nodes_from_folder("Nodes")

    # Register each node in its category
    for category, node_classes in custom_nodes_by_category.items():
        for node_class in node_classes:
            graph.register_node(node_class)

    # Recursively apply the stylesheet to all submenus
    def apply_styles_to_submenus(menu):
        """ Recursively applies the stylesheet to all submenus in the menu. """
        menu.setStyleSheet(menu_stylesheet)
        for action in menu.actions():
            if action.menu():  # Check if action has a submenu
                apply_styles_to_submenus(action.menu())

    # Override the Color of the Context Menu to Blue
    menu_stylesheet = """
    QMenu {
        background-color: rgb(30, 30, 30);
        border: 1px solid rgba(200, 200, 200, 60);
    }
    QMenu::item {
        padding: 5px 18px 2px;
        background-color: transparent;
    }
    QMenu::item:selected {
        color: rgb(255, 255, 255);
        background-color: rgba(60, 120, 180, 150);
    }
    QMenu::separator {
        height: 1px;
        background: rgba(255, 255, 255, 50);
        margin: 4px 8px;
    }
    """

    # Create categorized context menu
    graph_context_menu = graph.get_context_menu("graph")
    add_node_menu = graph_context_menu.add_menu("Add Node")

    for category, node_classes in custom_nodes_by_category.items():
        category_menu = add_node_menu.add_menu(category)  # Create submenu
        category_menu.qmenu.setStyleSheet(menu_stylesheet)  # Apply to submenu
    
        for node_class in node_classes:
            node_type = f"{node_class.__identifier__}.{node_class.__name__}"
            node_name = node_class.NODE_NAME
            category_menu.add_command(f"{node_name}", make_node_command(graph, node_type))

    # Ensure styles are propagated across all dynamically created submenus
    apply_styles_to_submenus(graph_context_menu.qmenu)

    # Add a "Remove Selected Node" command
    graph_context_menu.add_command(
        "Remove Selected Node",
        lambda: [graph.remove_node(node) for node in graph.selected_nodes()] if graph.selected_nodes() else None
    )

    # ------------------------------#
    # WRAPPER: QMainWindow Integration with Additional UI Elements
    # ------------------------------#
    # SECTION: Enhanced Graph Wrapper for QMainWindow
    # This section wraps the NodeGraph widget in a QMainWindow with:
    # - A menu bar at the top (named "Workflows" menu)
    # - A status bar at the bottom
    # - A central QSplitter dividing the window horizontally:
    #   * Left side (2/3): the NodeGraph widget
    #   * Right side (1/3): an empty text box for future use
    _original_show = graph.widget.show  # Save original method

    def _wrapped_show():
        """
        Wrap the NodeGraph widget inside a QMainWindow with a "Workflows" menu,
        a status bar, and a central splitter for layout.
        """
        # Create a new QMainWindow instance
        main_window = QtWidgets.QMainWindow()

        # Create a menu bar and add a "Workflows" menu
        menu_bar = main_window.menuBar()
        workflows_menu = menu_bar.addMenu("Workflows")

        # Add "Open" action
        open_action = QtWidgets.QAction("Open", main_window)
        open_action.triggered.connect(lambda: load_workflow(graph))
        workflows_menu.addAction(open_action)

        # Add "Save" action
        save_action = QtWidgets.QAction("Save", main_window)
        save_action.triggered.connect(lambda: save_workflow(graph))
        workflows_menu.addAction(save_action)

        # Add "Close" action
        close_action = QtWidgets.QAction("Close", main_window)
        close_action.triggered.connect(lambda: close_workflow(graph))
        workflows_menu.addAction(close_action)

        # Create and set a blank status bar at the bottom.
        main_window.setStatusBar(QtWidgets.QStatusBar())

        # ---------------------------------------------------------------------
        # SECTION: Status Bar Enhancement - Dynamic Status Display
        # Add a QLabel to the status bar that shows:
        # - The number of nodes in the graph.
        # - A fixed update rate (500ms).
        # - A clickable hyperlink to the Flask API server.
        status_bar = main_window.statusBar()
        
        status_label = QtWidgets.QLabel()
        status_label.setTextFormat(QtCore.Qt.RichText)  # Enable rich text for clickable links.
        status_label.setStyleSheet("color: white;")      # Set default text color to white.
        status_label.setOpenExternalLinks(True)           # Allow hyperlinks to be clickable.
        status_bar.setSizeGripEnabled(False)               # Disable resizing via the size grip.
        status_bar.addWidget(status_label)
        status_bar.setStyleSheet("""
            QStatusBar::item {
                border: none;    /* remove the line around items */
            }
        """)

        def update_status():
            node_count = len(graph.all_nodes())
            api_link = (
                '<a href="http://127.0.0.1:5000/data" '
                'style="color: rgb(60, 120, 180); text-decoration: none;">'
                'http://127.0.0.1:5000/data</a>'
            )
            status_label.setText(
                f'Nodes: {node_count} | Update Rate: 500ms | Flask API Server: {api_link}'
            )

        # Create the timer, pass the main_window as parent, and store the reference.
        status_timer = QtCore.QTimer(main_window)
        status_timer.timeout.connect(update_status)
        status_timer.start(500)

        main_window._status_timer = status_timer  # Keep a reference so it's not GCed
        # ---------------------------------------------------------------------

        # Create a QSplitter for horizontal division.
        splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal)

        # SECTION: Left Pane - Graph Widget
        splitter.addWidget(graph.widget)

        # SECTION: Right Pane - Empty Text Box
        text_edit = QtWidgets.QTextEdit()
        splitter.addWidget(text_edit)

        # Set stretch factors
        splitter.setStretchFactor(0, 2) # Split of Left Side
        splitter.setStretchFactor(1, 3) # Split of Right Side

        # Reduce the Size of the Splitter Handle
        splitter.setHandleWidth(1)
        splitter.setStyleSheet("""
            QSplitter::handle {
                background: none;
            }
        """)

        # Set the splitter as the central widget of the main window.
        main_window.setCentralWidget(splitter)

        # Transfer the window title from the graph widget to the main window.
        main_window.setWindowTitle(graph.widget.windowTitle())
        # Resize the main window using the size set for the graph widget.
        main_window.resize(graph.widget.size())

        # Store a reference to the main window to prevent it from being garbage collected.
        graph.widget._main_window = main_window
        # Show the main window instead of the standalone graph widget.
        main_window.show()

    # Monkey-patch the show method of the graph widget.
    graph.widget.show = _wrapped_show

    # Grid styling changes
    graph.set_background_color(20, 20, 20)  # Dark gray
    graph.set_grid_color(60, 60, 60)        # Gray grid lines

    # Add gradient background
    scene = graph.scene()
    gradient = QtGui.QLinearGradient(0, 0, 0, 1)
    gradient.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode)
    gradient.setColorAt(0.0, QtGui.QColor(9, 44, 68))  
    gradient.setColorAt(0.3, QtGui.QColor(30, 30, 30))
    gradient.setColorAt(0.7, QtGui.QColor(30, 30, 30))
    gradient.setColorAt(1.0, QtGui.QColor(9, 44, 68))
    scene.setBackgroundBrush(QtGui.QBrush(gradient))

    # Resize and show the graph widget (which now triggers the QMainWindow wrapper)
    graph.widget.resize(1600, 900)
    graph.widget.show()

    graph_context_menu.qmenu.setStyleSheet(menu_stylesheet)

    # Global update function
    def global_update():
        for node in graph.all_nodes():
            if hasattr(node, "process_input"):
                try:
                    node.process_input()
                except Exception as e:
                    print("Error updating node", node, e)

    timer = QtCore.QTimer()
    timer.timeout.connect(global_update)
    timer.start(500)

    sys.exit(app.exec_())