#    “Commons Clause” License Condition v1.0
#   #
#    The Software is provided to you by the Licensor under the License, as defined
#    below, subject to the following condition.
#
#    Without limiting other conditions in the License, the grant of rights under the
#    License will not include, and the License does not grant to you, the right to
#    Sell the Software.
#
#    For purposes of the foregoing, “Sell” means practicing any or all of the rights
#    granted to you under the License to provide to third parties, for a fee or other
#    consideration (including without limitation fees for hosting) a product or service whose value
#    derives, entirely or substantially, from the functionality of the Software. Any
#    license notice or attribution required by the License must also include this
#    Commons Clause License Condition notice.
#
#   Add-ons and extensions developed for this software may be distributed
#   under their own separate licenses.
#
#    Software: Revolution EDA
#    License: Mozilla Public License 2.0
#    Licensor: Revolution Semiconductor (Registered in the Netherlands)
#

from __future__ import annotations

import datetime
import functools
import json
import pathlib
import re
from copy import deepcopy
from typing import List

from PySide6.QtCore import (QPoint, Qt, )
from PySide6.QtGui import (QAction, QIcon, )
from PySide6.QtWidgets import (QApplication, QDialog, QGridLayout, QMenu, QToolBar, )

import revedaEditor.backend.dataDefinitions as ddef
import revedaEditor.backend.libBackEnd as libb
import revedaEditor.backend.libraryMethods as libm
import revedaEditor.backend.libraryModelView as lmview
import revedaEditor.common.shapes as shp  # import the shapes
import revedaEditor.fileio.symbolEncoder as symenc
import revedaEditor.gui.editorViews as edv
import revedaEditor.gui.editorWindow as edw
import revedaEditor.gui.fileDialogues as fd
import revedaEditor.gui.propertyDialogues as pdlg
import revedaEditor.gui.toolsDialogues as tdlg
import revedaEditor.scenes.schematicScene as schscn
from revedaEditor.gui.editorFactory import EditorFactory


class schematicEditor(edw.editorWindow):
    MAJOR_GRID_DEFAULT = 20
    SNAP_GRID_DEFAULT = 10

    def __init__(self, viewItem: libb.viewItem, libraryDict: dict, libraryView) -> None:
        super().__init__(viewItem, libraryDict, libraryView)
        self.setWindowTitle(f"Schematic Editor - {self.cellName} - {self.viewName}")
        self.setWindowIcon(QIcon(":/icons/edLayer-shape.png"))
        self.configDict = dict()
        # self.processedCells = set()  # cells included in config view
        self.symbolChooser = None
        self.majorGrid = self.MAJOR_GRID_DEFAULT  # dot/line grid spacing
        self.snapGrid = self.SNAP_GRID_DEFAULT  # snapping grid size
        self.snapTuple = (self.snapGrid, self.snapGrid)
        self.symbolViews = [
            "symbol"]  # only symbol can be instantiated in the schematic window.
        self._schematicContextMenu()

    def init_UI(self):
        self.resize(1600, 800)
        self._createActions()
        self._createMenuBar()
        self._createToolBars()
        self._addActions()
        self._createTriggers()
        self._createShortcuts()
        self.centralW = schematicContainer(self)
        self.setCentralWidget(self.centralW)

    def __repr__(self):
        return f'schematicEditor({self.libName}-{self.cellName}-{self.viewName})'

    def _createActions(self):
        super()._createActions()
        self.netNameAction = QAction("Net Name", self)
        self.netNameAction.setToolTip("Set Net Name")
        self.netNameAction.setShortcut(Qt.Key.Key_L)
        self.hilightNetAction = QAction("Highlight Net", self)
        self.hilightNetAction.setToolTip("Highlight Selected Net Connections")
        self.hilightNetAction.setCheckable(True)
        self.renumberInstanceAction = QAction("Renumber Instances", self)
        self.renumberInstanceAction.setToolTip("Renumber Instances")
        simulationIcon = QIcon("icons/application-run.png")
        self.simulateAction = QAction(simulationIcon, "Revolution EDA SAE...", self)
        self.findRelatedEditors = QAction("Find Related Editors", self)

    def _addActions(self):
        super()._addActions()
        # edit menu
        self.menuEdit.addAction(self.netNameAction)
        self.menuEdit.removeAction(self.stretchAction)

        self.propertyMenu = self.menuEdit.addMenu("Properties")
        self.propertyMenu.addAction(self.objPropAction)

        # hierarchy submenu
        self.hierMenu = self.menuEdit.addMenu("Hierarchy")
        self.hierMenu.addAction(self.goUpAction)
        self.hierMenu.addAction(self.goDownAction)

        # create menu
        self.menuCreate.addAction(self.createInstAction)
        self.menuCreate.addAction(self.createNetAction)
        self.menuCreate.addAction(self.createBusAction)
        self.menuCreate.addAction(self.createPinAction)
        self.menuCreate.addAction(self.createTextAction)
        self.menuCreate.addAction(self.createSymbolAction)

        # check menu
        self.menuCheck.addAction(self.viewErrorsAction)
        self.menuCheck.addAction(self.deleteErrorsAction)

        # tools menu
        self.menuTools.addAction(self.hilightNetAction)
        self.menuTools.addAction(self.renumberInstanceAction)
        self.menuTools.addAction(self.findRelatedEditors)
        # utilities Menu
        self.selectMenu = self.menuUtilities.addMenu("Selection")
        self.selectMenu.addAction(self.selectDeviceAction)
        self.selectMenu.addAction(self.selectNetAction)
        self.selectMenu.addAction(self.selectPinAction)
        self.selectMenu.addSeparator()
        self.selectMenu.addAction(self.removeSelectFilterAction)
        self.simulationMenu = QMenu("&Simulation")
        # help menu
        self.simulationMenu.addAction(self.netlistAction)
        self.editorMenuBar.insertMenu(self.menuHelp.menuAction(), self.simulationMenu)
        # self.menuHelp = self.editorMenuBar.addMenu("&Help")
        if hasattr(self._app, 'pluginsObj') and hasattr(self._app.pluginsObj, 'applyPluginMenus'):
            self._app.pluginsObj.applyPluginMenus(self)

    def _createTriggers(self):
        super()._createTriggers()

        self.createNetAction.triggered.connect(self.createNetClick)
        self.createBusAction.triggered.connect(self.createBusClick)
        self.createInstAction.triggered.connect(self.createInstClick)
        self.createPinAction.triggered.connect(self.createPinClick)
        self.createTextAction.triggered.connect(self.createNoteClick)
        self.createSymbolAction.triggered.connect(self.createSymbolClick)

        self.objPropAction.triggered.connect(self.objPropClick)
        self.netlistAction.triggered.connect(self.createNetlistClick)
        self.ignoreAction.triggered.connect(self.ignoreClick)
        self.goDownAction.triggered.connect(self.goDownClick)
        self.viewErrorsAction.triggered.connect(self.checkErrorsClick)
        self.deleteErrorsAction.triggered.connect(self.deleteErrorsClick)

        self.hilightNetAction.triggered.connect(self.hilightNetClick)
        self.netNameAction.triggered.connect(self.createNetNameClick)
        self.selectDeviceAction.triggered.connect(self.selectDeviceClick)
        self.selectNetAction.triggered.connect(self.selectNetClick)
        self.selectPinAction.triggered.connect(self.selectPinClick)
        self.removeSelectFilterAction.triggered.connect(self.removeSelectFilterClick)
        self.renumberInstanceAction.triggered.connect(self.renumberInstanceClick)
        self.findRelatedEditors.triggered.connect(self.findRelatedEditorsClick)

    def _createToolBars(self):
        super()._createToolBars()
        self.toolbar.addAction(self.objPropAction)
        self.toolbar.addAction(self.viewPropAction)

        self.schematicToolbar = QToolBar("Schematic Toolbar", self)
        self.addToolBar(self.schematicToolbar)
        self.schematicToolbar.addAction(self.createInstAction)
        self.schematicToolbar.addAction(self.createNetAction)
        self.schematicToolbar.addAction(self.createBusAction)
        self.schematicToolbar.addAction(self.createPinAction)
        # self.schematicToolbar.addAction(self.createLabelAction)
        self.schematicToolbar.addAction(self.createSymbolAction)
        self.schematicToolbar.addSeparator()
        self.schematicToolbar.addAction(self.viewCheckAction)
        self.schematicToolbar.addSeparator()
        self.schematicToolbar.addAction(self.goDownAction)
        self.schematicToolbar.addSeparator()
        self.schematicToolbar.addAction(self.selectDeviceAction)
        self.schematicToolbar.addAction(self.selectNetAction)
        self.schematicToolbar.addAction(self.selectPinAction)
        self.schematicToolbar.addAction(self.removeSelectFilterAction)

    def _schematicContextMenu(self):
        super()._editorContextMenu()
        self.centralW.scene.itemContextMenu.addAction(self.ignoreAction)
        self.centralW.scene.itemContextMenu.addAction(self.goDownAction)

    def _createShortcuts(self):
        super()._createShortcuts()
        self.createInstAction.setShortcut(Qt.Key.Key_I)
        self.createNetAction.setShortcut(Qt.Key.Key_W)
        self.createPinAction.setShortcut(Qt.Key.Key_P)
        self.goDownAction.setShortcut("Shift+E")
        # unset stretch shortcut for schematic editor
        self.stretchAction.setShortcut("")

    def createNetClick(self, s):
        self.centralW.scene.editModes.setMode("drawWire")

    def createBusClick(self, s):
        self.centralW.scene.editModes.setMode("drawBus")

    def createInstClick(self, s):
        # create a designLibrariesView
        libraryModel = lmview.symbolViewsModel(self.libraryDict, self.symbolViews)
        if self.symbolChooser is None:
            self.symbolChooser = fd.selectCellViewDialog(self, libraryModel)
            self.symbolChooser.show()
        else:
            self.symbolChooser.raise_()
        if self.symbolChooser.exec() == QDialog.DialogCode.Accepted:
            instanceTuple = ddef.viewNameTuple(self.symbolChooser.libNamesCB.currentText(),
                                               self.symbolChooser.cellCB.currentText(),
                                               self.symbolChooser.viewCB.currentText(), )

            self.centralW.scene.newInstanceTuple = instanceTuple

            self.centralW.scene.editModes.setMode("addInstance")

    def createPinClick(self, s):
        createPinDlg = pdlg.createSchematicPinDialog(self)
        if createPinDlg.exec() == QDialog.DialogCode.Accepted:
            self.centralW.scene.pinName = createPinDlg.pinName.text()
            self.centralW.scene.pinType = createPinDlg.pinType.currentText()
            self.centralW.scene.pinDir = createPinDlg.pinDir.currentText()
            self.centralW.scene.editModes.setMode("drawPin")

    def createNoteClick(self, s):
        textDlg = pdlg.noteTextEdit(self)
        if textDlg.exec() == QDialog.DialogCode.Accepted:
            noteText = textDlg.plainTextEdit.toPlainText()
            noteFontFamily = textDlg.familyCB.currentText()
            noteFontSize = textDlg.fontsizeCB.currentText()
            noteFontStyle = textDlg.fontStyleCB.currentText()
            noteAlign = textDlg.textAlignmCB.currentText()
            noteOrient = textDlg.textOrientCB.currentText()
            self.centralW.scene.textTuple = (noteText, noteFontFamily, noteFontStyle,
                                             noteFontSize, noteAlign, noteOrient,)
            self.centralW.scene.editModes.setMode("drawText")

    def createSymbolClick(self, s):
        self.createSymbol()

    def objPropClick(self, s):
        self.centralW.scene.editModes.setMode("selectItem")
        self.centralW.scene.viewObjProperties()

    def renumberInstanceClick(self, s):
        self.centralW.scene.renumberInstances()

    def checkSaveCell(self):
        self.centralW.scene.nameSceneNets()
        self.centralW.scene.saveSchematic(self.file)
        if self.parentEditor:
            self.parentEditor.centralW.scene.reloadScene()

    def saveCell(self):
        self.centralW.scene.saveSchematic(self.file)

    def loadSchematic(self):
        try:
            self.logger.info(f'Loading schematic from {self.cellName} - {self.viewName}')
            self.centralW.scene.loadDesign(self.file)
            viewNameTuple = ddef.viewNameTuple(self.libItem.libraryName, self.cellItem.cellName,
                                               self.viewName)
            self.appMainW.openViews[viewNameTuple] = self
        except Exception as e:
            self.logger.error(f"Error during loading schematic for {self.cellName}: {e}")

    def createConfigView(self, configItem: libb.viewItem, newConfigDict: dict,
                         processedCells: set):
        sceneSymbolSet = self.centralW.scene.findSceneSymbolSet()
        for item in sceneSymbolSet:
            libItem = libm.getLibItem(self.libraryView.libraryModel, item.libraryName)
            cellItem = libm.getCellItem(libItem, item.cellName)
            viewItems = [cellItem.child(row) for row in range(cellItem.rowCount())]
            viewNames = [viewItem.viewName for viewItem in viewItems]
            netlistableViews = [viewItemName for viewItemName in self.switchViewList if
                                viewItemName in viewNames]
            itemSwitchViewList = deepcopy(netlistableViews)
            viewDict = dict(zip(viewNames, viewItems))
            itemCellTuple = ddef.cellTuple(libItem.libraryName, cellItem.cellName)
            if itemCellTuple not in processedCells:
                if cellLine := newConfigDict.get(cellItem.cellName):
                    netlistableViews = [cellLine[1]]
                for viewName in netlistableViews:
                    match viewDict[viewName].viewType:
                        case "schematic":
                            newConfigDict[cellItem.cellName] = [libItem.libraryName,
                                                                viewName,
                                                                itemSwitchViewList, ]
                            schematicObj = schematicEditor(viewDict[viewName],
                                                           self.libraryDict,
                                                           self.libraryView, )
                            schematicObj.loadSchematic()
                            schematicObj.createConfigView(configItem, newConfigDict,
                                                          processedCells)
                            break
                        case _:
                            newConfigDict[cellItem.cellName] = [libItem.libraryName,
                                                                viewName,
                                                                itemSwitchViewList, ]
                            break
                processedCells.add(itemCellTuple)

    def closeEvent(self, event):
        try:
            self.centralW.scene.saveSchematic(self.file)
            cellViewNameTuple = ddef.viewNameTuple(self.libName, self.cellName,
                                                   self.viewName)
            self.appMainW.openViews.pop(cellViewNameTuple, None)
        except Exception as e:
            self.appMainW.logger.error(f"Error in closing schematic editor window"
                                       f":{self.cellName}-{self.viewName}:{e}")
        finally:
            event.accept()
            super().closeEvent(event)

    def createNetlistClick(self, s):
        dlg = fd.netlistExportDialogue(self)
        dlg.libNameEdit.setText(self.libName)
        dlg.cellNameEdit.setText(self.cellName)
        netlistableViews = self._getNetlistableViews()
        dlg.viewNameCombo.addItems(netlistableViews)

        if hasattr(self.appMainW, "outputPrefixPath"):
            dlg.netlistDirEdit.setText(str(self.appMainW.outputPrefixPath))

        if dlg.exec() == QDialog.DialogCode.Accepted:
            self._startNetlisting(dlg)

    def _getNetlistableViews(self):
        views = [self.viewItem.viewName]
        config_items = [self.cellItem.child(row) for row in range(self.cellItem.rowCount())
                        if self.cellItem.child(row).viewType == "config"]

        for item in config_items:
            with item.data(Qt.ItemDataRole.UserRole + 2).open(mode="r") as f:
                config = json.load(f)
                if config[1]["reference"] == self.viewItem.viewName:
                    views.append(item.viewName)

        return views

    def _startNetlisting(self, dlg: fd.netlistExportDialogue):
        # try:
        self.appMainW.simulationOutputPath = pathlib.Path(dlg.netlistDirEdit.text())
        selectedViewName = dlg.viewNameCombo.currentText()

        self.switchViewList = [item.strip() for item in
                               dlg.switchViewEdit.text().split(",")]
        self.stopViewList = [dlg.stopViewEdit.text().strip()]

        subDirPath = self.appMainW.simulationOutputPath / self.libName / self.cellName / selectedViewName
        subDirPath.mkdir(parents=True, exist_ok=True)

        netlistFilePath = subDirPath / f"{self.cellName}_{selectedViewName}.cir"
        if dlg.topAsSubcktCheckBox.isChecked():
            topSubCkt = True
        else:
            topSubCkt = False

        netlistObj = self.createNetlistObject(self.viewItem, netlistFilePath, topSubCkt)

        if netlistObj:
            # self.runNetlisting(netlistObj, self.appMainW.threadPool)
            with self.measureDuration():
                netlistObj.writeNetlist()

    def createNetlistObject(self, viewItem: libb.viewItem, filePath: pathlib.Path,
                            topSubCkt: bool):
        if viewItem.viewType == "schematic":
            return xyceNetlist(self, filePath, False, topSubCkt)
        elif viewItem.viewType == "config":
            netlistObj = xyceNetlist(self, filePath, True, topSubCkt)
            configItem = libm.findViewItem(self.libraryView.libraryModel, self.libName,
                                           self.cellName, viewItem.viewName)
            if configItem:
                with configItem.data(Qt.ItemDataRole.UserRole + 2).open(mode="r") as f:
                    netlistObj.configDict = json.load(f)[2]
            return netlistObj
        return None

    def goDownClick(self, s):
        self.centralW.scene.goDownHier()

    def ignoreClick(self, s):
        self.centralW.scene.ignoreSymbol()

    def createNetNameClick(self, s):
        dlg = pdlg.netNameDialog(self)
        if dlg.exec() == QDialog.DialogCode.Accepted:
            self.centralW.scene.netNameString = dlg.netNameEdit.text().strip()
            self.centralW.scene.editModes.setMode('nameNet')
        self.messageLine.setText("Select a net to attach the name")

    def hilightNetClick(self, s):
        self.centralW.scene.hilightNets()

    def selectDeviceClick(self):
        self.centralW.scene.selectModes.setMode("selectDevice")
        self.messageLine.setText("Select Only Instances")

    def selectNetClick(self):
        self.centralW.scene.selectModes.setMode("selectNet")
        self.messageLine.setText("Select Only Nets")

    def selectPinClick(self):
        self.centralW.scene.selectModes.setMode("selectPin")
        self.messageLine.setText("Select Only Pins")

    def removeSelectFilterClick(self):
        self.centralW.scene.selectModes.setMode("selectAll")
        self.messageLine.setText("Select All Objects")

    def createSymbol(self) -> None:
        """
        Create a symbol view for a schematic.
        """
        oldSymbolItem = False

        askViewNameDlg = pdlg.symbolNameDialog(self.file.parent, self.cellName, self, )
        if askViewNameDlg.exec() == QDialog.DialogCode.Accepted:
            symbolViewName = askViewNameDlg.symbolViewsCB.currentText()
            if symbolViewName in askViewNameDlg.symbolViewNames:
                oldSymbolItem = True
            if oldSymbolItem:
                deleteSymViewDlg = fd.deleteSymbolDialog(self.cellName, symbolViewName,
                                                         self)
                if deleteSymViewDlg.exec() == QDialog.DialogCode.Accepted:
                    self.generateSymbol(symbolViewName)
            else:
                self.generateSymbol(symbolViewName)

    def _parse_pin_names(self, text: str) -> list[str]:
        """Parse comma-separated pin names, filtering empty strings."""
        return [name.strip() for name in text.split(",") if name.strip()]

    def _categorize_pins(self, schematic_pins: list[shp.schematicPin]) -> dict:
        """Categorize pins by direction."""
        pin_dirs = shp.schematicPin.pinDirs
        return {
            'input': [pin.pinName for pin in schematic_pins if pin.pinDir == pin_dirs[0]],
            'output': [pin.pinName for pin in schematic_pins if pin.pinDir == pin_dirs[1]],
            'inout': [pin.pinName for pin in schematic_pins if pin.pinDir == pin_dirs[2]]}

    def _create_pin_map(self, schematic_pins: list[shp.schematicPin]) -> dict:
        """Create mapping from pin name to pin object."""
        return {pin.pinName: pin for pin in schematic_pins}

    def _draw_pins(self, symbol_scene, pin_names: list[str], locations: list[QPoint],
                   offsets: QPoint, pin_map: dict) -> None:
        """Draw pins and their connecting lines."""
        for name, loc in zip(pin_names, locations):
            symbol_scene.lineDraw(loc, loc + offsets)
            symbol_scene.addItem(pin_map[name].toSymbolPin(loc))

    def generateSymbol(self, symbolViewName: str) -> None:
        # Get schematic pins and categorize them
        schematic_pins = list(self.centralW.scene.findSceneSchemPinsSet())
        pin_categories = self._categorize_pins(schematic_pins)
        pin_map = self._create_pin_map(schematic_pins)

        # Setup dialog with categorized pins
        dlg = pdlg.symbolCreateDialog(self)
        dlg.leftPinsEdit.setText(", ".join(pin_categories['input']))
        dlg.rightPinsEdit.setText(", ".join(pin_categories['output']))
        dlg.topPinsEdit.setText(", ".join(pin_categories['inout']))

        if dlg.exec() != QDialog.DialogCode.Accepted:
            return

        # Create symbol view and editor
        libItem = libm.getLibItem(self.libraryView.libraryModel, self.libName)
        cellItem = libm.getCellItem(libItem, self.cellName)
        symbolViewItem = libb.createCellView(self, symbolViewName, cellItem)

        symbolWindow = EditorFactory.createEditor("symbol", symbolViewItem,
                                                  self.libraryDict, self.libraryView)

        try:
            # Parse pin configurations
            pin_names = {'left': self._parse_pin_names(dlg.leftPinsEdit.text()),
                         'right': self._parse_pin_names(dlg.rightPinsEdit.text()),
                         'top': self._parse_pin_names(dlg.topPinsEdit.text()),
                         'bottom': self._parse_pin_names(dlg.bottomPinsEdit.text())}

            stub_length = int(float(dlg.stubLengthEdit.text().strip()))
            pin_distance = int(float(dlg.pinDistanceEdit.text().strip()))

            # Calculate rectangle dimensions
            rect_x_dim = (max(len(pin_names['top']),
                              len(pin_names['bottom'])) + 1) * pin_distance
            rect_y_dim = (max(len(pin_names['left']),
                              len(pin_names['right'])) + 1) * pin_distance

        except ValueError:
            self.logger.error("Enter valid value")
            return

        # Draw symbol components
        sceneSymbol = symbolWindow.centralW.scene
        sceneSymbol.rectDraw(QPoint(0, 0), QPoint(rect_x_dim, rect_y_dim))

        # Add labels
        sceneSymbol.labelDraw(QPoint(int(0.25 * rect_x_dim), int(0.4 * rect_y_dim)),
                              "[@cellName]", "NLPLabel", "12", "Center", "R0", "Instance")
        sceneSymbol.labelDraw(QPoint(int(rect_x_dim), int(-0.1 * rect_y_dim)),
                              "[@instName]", "NLPLabel", "12", "Center", "R0", "Instance")

        # Calculate pin locations and draw pins
        pin_configs = [(pin_names['left'],
                        [QPoint(-stub_length, (i + 1) * pin_distance) for i in
                         range(len(pin_names['left']))], QPoint(stub_length, 0)),
                       (pin_names['right'],
                        [QPoint(rect_x_dim + stub_length, (i + 1) * pin_distance) for i in
                         range(len(pin_names['right']))], QPoint(-stub_length, 0)),
                       (pin_names['top'],
                        [QPoint((i + 1) * pin_distance, -stub_length) for i in
                         range(len(pin_names['top']))], QPoint(0, stub_length)),
                       (pin_names['bottom'],
                        [QPoint((i + 1) * pin_distance, rect_y_dim + stub_length) for i in
                         range(len(pin_names['bottom']))], QPoint(0, -stub_length))]

        for names, locations, offset in pin_configs:
            self._draw_pins(sceneSymbol, names, locations, offset, pin_map)

        # Add symbol attributes
        all_pin_names = [pin.pinName for pin in schematic_pins]
        sceneSymbol.attributeList = [
            symenc.symbolAttribute("SpiceNetlistLine", "X@instName %pinOrder @cellName"),
            symenc.symbolAttribute("SpectreNetlistLine",
                                   "X@instName  ( %pinOrder ) @cellName"),
            symenc.symbolAttribute("pinOrder", ", ".join(all_pin_names))]

        # Finalize and show
        symbolWindow.checkSaveCell()
        self.libraryView.reworkDesignLibrariesView(self.appMainW.libraryDict)

        openCellViewTuple = ddef.viewNameTuple(self.libName, self.cellName, symbolViewName)
        self.appMainW.openViews[openCellViewTuple] = symbolWindow
        symbolWindow.show()

    def checkErrorsClick(self):
        self.centralW.scene.checkErrors()
        self.logger.info("Schematic Errors checked.")

    def deleteErrorsClick(self):
        self.centralW.scene.deleteErrors()
        self.logger.info("Schematic Errors checked.")

    def findRelated(self, editor, visited, openWindows):
        if editor in visited or editor not in openWindows:
            return
        visited.add(editor)
        if hasattr(editor, 'parentEditor') and editor.parentEditor:
            self.findRelated(editor.parentEditor, visited, openWindows)
        for w in openWindows:
            if hasattr(w, 'parentEditor') and w.parentEditor == editor:
                self.findRelated(w, visited, openWindows)

    def findRelatedEditorsClick(self):
        openWindows = {w for w in QApplication.instance().topLevelWidgets() if
                       isinstance(w, schematicEditor)}
        relatedEditors = set()
        self.findRelated(self, relatedEditors, openWindows)
        dlg = tdlg.findProjectEditors(self)
        relatedEditorNameList = [editor.cellName for editor in relatedEditors]
        dlg.relatedEditorsCB.addItems(relatedEditorNameList)
        dlg.relatedEditorsCB.setCurrentIndex(0)
        if dlg.exec() == QDialog.DialogCode.Accepted:
            relatedEditorName = dlg.relatedEditorsCB.currentText()
            relatedEditor = next((editor for editor in relatedEditors if
                                  editor.cellName == relatedEditorName), None)
            if relatedEditor:
                relatedEditor.show()
                relatedEditor.raise_()
                relatedEditor.activateWindow()


class schematicContainer(edw.editorContainer):
    def __init__(self, parent: schematicEditor):
        super().__init__(parent=parent)
        self.editorWindow = parent
        self.scene = schscn.schematicScene(self)
        self.view = edv.schematicView(self.scene, self)
        self.init_UI()

    def init_UI(self):
        # layout statements, using a grid layout
        gLayout = QGridLayout()
        gLayout.setSpacing(5)
        gLayout.setContentsMargins(0, 0, 0, 0)  # remove outer gaps
        gLayout.addWidget(self.view, 0, 0)
        gLayout.setColumnStretch(0, 5)
        gLayout.setRowStretch(0, 6)
        self.setLayout(gLayout)


class xyceNetlist:
    # Pre-compiled regex to strip dangling parameter assignments (e.g. ` width =`) left
    # after token substitution.  Compiled once at class level avoids re.compile() on every
    # netlist line inside the hot loop.
    _PARAM_RE = re.compile(r'\s+\w+\s*=(?=\s|$)')

    def __init__(self, schematic, filePathObj: pathlib.Path, useConfig: bool = False,
                 topSubckt: bool = False):
        self.filePathObj = filePathObj
        self.schematic = schematic
        self._useConfig = useConfig
        self._scene = self.schematic.centralW.scene
        self.libraryDict = self.schematic.libraryDict
        self.libraryView = self.schematic.libraryView
        self._configDict = {}
        self.topSubckt = topSubckt
        self.libItem = libm.getLibItem(self.schematic.libraryView.libraryModel,
                                       self.schematic.libName, )
        self.cellItem = libm.getCellItem(self.libItem, self.schematic.cellName)
        self.subcircuitDefs = []
        self._switchViewList = schematic.switchViewList
        self._stopViewList = schematic.stopViewList
        self.netlistedViewsSet = set()  # keeps track of netlisted views.
        self.includeLines = set()  # keeps track of include lines.
        self.vamodelLines = set()  # keeps track of vamodel lines.
        self.vahdlLines = set()  # keeps track of *.HDL lines.
        # Caches to avoid repeated Qt model traversals for the same cells.
        self._viewNameCache: dict[tuple, str] = {}  # (libName, cellName) -> netlist view name
        self._cellItemCache: dict[tuple, object] = {}  # (libName, cellName) -> cellItem

    def __repr__(self):
        return f"xyceNetlist(filePathObj={self.filePathObj}, schematic={self.schematic}, useConfig={self._useConfig})"

    @property
    def switchViewList(self) -> List[str]:
        return self._switchViewList

    @switchViewList.setter
    def switchViewList(self, value: List[str]):
        self._switchViewList = value

    @property
    def stopViewList(self) -> List[str]:
        return self._stopViewList

    @stopViewList.setter
    def stopViewList(self, value: List[str]):
        self._stopViewList = value

    @property
    def configDict(self):
        return self._configDict

    @configDict.setter
    def configDict(self, value: dict):
        self._configDict = value

    def writeNetlist(self):
        with self.filePathObj.open(mode="w") as cirFile:
            cirFile.write("*".join(["\n", 80 * "*", "\n", "* Revolution EDA CDL Netlist\n",
                                    f"* Library: {self.schematic.libName}\n",
                                    f"* Top Cell Name: {self.schematic.cellName}\n",
                                    f"* View Name: {self.schematic.viewName}\n",
                                    f"* Date: {datetime.datetime.now()}\n", 80 * "*", "\n",
                                    ".GLOBAL gnd!\n\n", ]))

            # Initialize subcircuit definitions list
            self.subcircuitDefs = []

            # now go down the rabbit hole to track all circuit elements.
            self.recursiveNetlisting(self.schematic, cirFile)

            # Write all subcircuit definitions at the end
            if self.subcircuitDefs:
                cirFile.write("\n* Subcircuit Definitions\n")
                for subcktDef in self.subcircuitDefs:
                    cirFile.write(subcktDef)

            # cirFile.write(".END\n")
            for line in self.includeLines:
                cirFile.write(f"{line}\n")
            for line in self.vamodelLines:
                cirFile.write(f"{line}\n")
            for line in self.vahdlLines:
                cirFile.write(f"{line}\n")

    def collectSubcircuitContent(self, schematic: schematicEditor, content):
        """Collect subcircuit content without writing to file."""
        schematicScene = schematic.centralW.scene
        schematicScene.nameSceneNets()
        sceneSymbolSet = schematicScene.findSceneSymbolSet()
        schematicScene.generatePinNetMap(sceneSymbolSet)
        for elementSymbol in sceneSymbolSet:
            if elementSymbol.symattrs.get("NetlistIgnore") != "1" and (
                    not elementSymbol.netlistIgnore):
                cellItem = self._getCellItem(elementSymbol.libraryName,
                                             elementSymbol.cellName)
                netlistView = self.determineNetlistView(elementSymbol, cellItem)

                if "schematic" in netlistView:
                    lines = self.createXyceSymbolLine(elementSymbol)
                    content.extend(lines if isinstance(lines, list) else [lines])
                    if netlistView not in self._stopViewList:
                        # Check deduplication before loading the schematic to avoid
                        # creating a schematicEditor object for every repeated instance.
                        viewTuple = ddef.viewNameTuple(elementSymbol.libraryName,
                                                       elementSymbol.cellName, netlistView)
                        if viewTuple not in self.netlistedViewsSet:
                            self.netlistedViewsSet.add(viewTuple)
                            schematicItem = libm.getViewItem(cellItem, netlistView)
                            schematicObj = schematicEditor(schematicItem, self.libraryDict,
                                                           self.libraryView)
                            schematicObj.loadSchematic()
                            expandedPinsString = self.expandPinNames(
                                list(elementSymbol.pinNetMap.keys()))
                            subcktContent = []
                            self.collectSubcircuitContent(schematicObj, subcktContent)
                            subcktDef = f".SUBCKT {schematicObj.cellName} {expandedPinsString}\n" + '\n'.join(
                                subcktContent) + "\n.ENDS\n"
                            self.subcircuitDefs.append(subcktDef)
                elif "symbol" in netlistView:
                    lines = self.createXyceSymbolLine(elementSymbol)
                    content.extend(lines if isinstance(lines, list) else [lines])
                elif "spice" in netlistView:
                    lines = self.createSpiceLine(elementSymbol)
                    content.extend(lines if isinstance(lines, list) else [lines])
                elif "veriloga" in netlistView:
                    lines = self.createVerilogaLine(elementSymbol)
                    content.extend(lines if isinstance(lines, list) else [lines])
            elif elementSymbol.netlistIgnore:
                content.append(f"*{elementSymbol.instanceName} is marked to be ignored\n")
            else:
                # NetlistIgnore attribute == "1" but netlistIgnore flag is False
                content.append(
                    f"*{elementSymbol.instanceName} is excluded via NetlistIgnore attribute\n")

    def recursiveNetlisting(self, schematicEdObj: schematicEditor, cirFile):
        """
        Recursively traverse all sub-circuits and netlist them.
        """
        if self.topSubckt:
            viewTuple = ddef.viewNameTuple(schematicEdObj.libName, schematicEdObj.cellName,
                                           schematicEdObj.viewName)
            self.netlistedViewsSet.add(viewTuple)
            schematicPinsSet = schematicEdObj.centralW.scene.findSceneSchemPinsSet()
            pinNames = [pin.pinName for pin in schematicPinsSet]
            expandedPinsString = self.expandPinNames(pinNames)

            subcktContent = []
            self.collectSubcircuitContent(schematicEdObj, subcktContent)
            subcktDef = f"\n.SUBCKT {schematicEdObj.cellName} {expandedPinsString}\n" + '\n'.join(
                subcktContent) + "\n.ENDS\n"
            self.subcircuitDefs.append(subcktDef)
        else:
            schematicScene = schematicEdObj.centralW.scene
            schematicScene.nameSceneNets()  # name all nets in the schematic
            sceneSymbolSet = schematicScene.findSceneSymbolSet()
            schematicScene.generatePinNetMap(sceneSymbolSet)
            for elementSymbol in sceneSymbolSet:
                self.processElementSymbol(elementSymbol, schematicEdObj, cirFile)

    def processElementSymbol(self, elementSymbol, schematic, cirFile):
        if elementSymbol.symattrs.get("NetlistIgnore") != "1" and (
                not elementSymbol.netlistIgnore):
            cellItem = self._getCellItem(elementSymbol.libraryName,
                                         elementSymbol.cellName)
            netlistView = self.determineNetlistView(elementSymbol, cellItem)

            # Create the netlist line for the item.
            self.createItemLine(cirFile, elementSymbol, cellItem, netlistView)
        elif elementSymbol.netlistIgnore:
            cirFile.write(f"*{elementSymbol.instanceName} is marked to be ignored\n")
        else:
            # NetlistIgnore attribute == "1" but netlistIgnore flag is False
            cirFile.write(
                f"*{elementSymbol.instanceName} is excluded via NetlistIgnore attribute\n")

    def _getCellItem(self, libraryName: str, cellName: str) -> libb.cellItem | None:
        """Return the cellItem for (libraryName, cellName), cached to avoid repeated model traversals."""
        key = (libraryName, cellName)
        if key not in self._cellItemCache:
            libItem = libm.getLibItem(self.libraryView.libraryModel, libraryName)
            self._cellItemCache[key] = libm.getCellItem(libItem, cellName)
        return self._cellItemCache[key]

    def determineNetlistView(self, elementSymbol, cellItem) -> str:
        cacheKey = (elementSymbol.libraryName, elementSymbol.cellName)
        if cacheKey in self._viewNameCache:
            return self._viewNameCache[cacheKey]

        viewItems = [cellItem.child(row) for row in range(cellItem.rowCount())]
        viewNames = [view.viewName for view in viewItems]

        if self._useConfig:
            config_entry = self.configDict.get(elementSymbol.cellName)
            result = config_entry[1] if config_entry else "symbol"
        else:
            # Iterate over the switch view list to determine the appropriate netlist view.
            for viewName in self._switchViewList:
                if viewName in viewNames:
                    result = viewName
                    break
            else:
                result = "symbol"

        self._viewNameCache[cacheKey] = result
        return result

    def createItemLine(self, cirFile, elementSymbol: shp.schematicSymbol,
                       cellItem: libb.cellItem, netlistView: str, ):
        if "schematic" in netlistView:
            elementLines = self.createXyceSymbolLine(elementSymbol)
            for line in elementLines:
                cirFile.write(f"{line}\n")

            if netlistView not in self._stopViewList:
                # Build viewTuple from symbol attributes before touching the filesystem;
                # this avoids creating a heavy schematicEditor object for every instance
                # of an already-netlisted cell (the common case in large designs).
                viewTuple = ddef.viewNameTuple(elementSymbol.libraryName,
                                               elementSymbol.cellName, netlistView)
                if viewTuple not in self.netlistedViewsSet:
                    self.netlistedViewsSet.add(viewTuple)
                    schematicItem = libm.getViewItem(cellItem, netlistView)
                    schematicObj = schematicEditor(schematicItem, self.libraryDict,
                                                   self.libraryView)
                    schematicObj.loadSchematic()
                    expandedPinsString = self.expandPinNames(
                        list(elementSymbol.pinNetMap.keys()))
                    subcktContent = []
                    self.collectSubcircuitContent(schematicObj, subcktContent)
                    subcktDef = f"\n.SUBCKT {schematicObj.cellName} {expandedPinsString}\n" + '\n'.join(
                        subcktContent) + "\n.ENDS\n"
                    self.subcircuitDefs.append(subcktDef)
        elif "symbol" in netlistView:
            symbolLines = self.createXyceSymbolLine(elementSymbol)
            for line in symbolLines:
                cirFile.write(f"{line}\n")
        elif "spice" in netlistView:
            spiceLines = self.createSpiceLine(elementSymbol)
            for line in spiceLines:
                cirFile.write(f"{line}\n")
        elif "veriloga" in netlistView:
            verilogaLines = self.createVerilogaLine(elementSymbol)
            for line in verilogaLines:
                cirFile.write(f"{line}\n")

    def _createNetlistLine(self, elementSymbol: shp.schematicSymbol, netlistLineKey: str) -> \
            list[str]:
        """Shared netlist line creation logic."""
        try:
            instNameLabel = elementSymbol.labels.get('@instName')
            # print(f"Creating netlist line for instance: {instNameLabel.labelValue if instNameLabel else 'N/A'}")
            if not instNameLabel:
                return []

            baseInstName, arrayTuple = self.parseArrayNotation(
                instNameLabel.labelValue.strip())
            arrayStep = -1 if arrayTuple[0] > arrayTuple[1] else 1
            arraySize = abs(arrayTuple[0] - arrayTuple[1]) + 1

            baseNetlistLine = elementSymbol.symattrs[netlistLineKey].strip()
            instNameToken = instNameLabel.labelName
            symbolLines = []

            # Pre-compute substitution lists once; avoids repeated dict iteration
            # for every element in an arrayed instance (e.g. inst<0:99>).
            attr_replacements = [(f"%{a}", v) for a, v in elementSymbol.symattrs.items()]
            label_replacements = [(lbl.labelName, lbl.labelValue)
                                  for lbl in elementSymbol.labels.values()]

            def processLine(line, netsList):
                line = line.replace("%pinOrder", netsList)
                for token, value in attr_replacements:
                    line = line.replace(token, value)
                return xyceNetlist._PARAM_RE.sub('', line)

            def createInstanceLine(instanceName):
                line = baseNetlistLine.replace(instNameToken, instanceName)
                for labelName, labelValue in label_replacements:
                    line = line.replace(labelName, labelValue)
                return line

            def expandNet(netName):
                baseName, netTuple = self.parseArrayNotation(netName)
                if netTuple[0] == netTuple[1] == -1:
                    return [baseName]
                netStep = 1 if netTuple[1] >= netTuple[0] else -1
                return [f"{baseName}<{i}>" for i in
                        range(netTuple[0], netTuple[1] + netStep, netStep)]

            # Expand nets per pin
            expandedPinNets = [expandNet(netName) for netName in elementSymbol.pinNetMap.values()]

            # Generate instance lines
            if arraySize == 1:
                # Scalar instance logic
                flatNetsList = " ".join([nets[0] for nets in expandedPinNets])
                symbolLines.append(processLine(createInstanceLine(baseInstName), flatNetsList))
            else:
                # Array instance logic
                arrayIndices = list(range(arrayTuple[0], arrayTuple[1] + arrayStep, arrayStep))
                
                for j, i in enumerate(arrayIndices):
                    instanceNets = []
                    for nets in expandedPinNets:
                        if len(nets) == arraySize:
                            instanceNets.append(nets[j])  # 1-to-1 matching across array width
                        elif len(nets) == 1:
                            instanceNets.append(nets[0])  # Scalar broadcasted to all array nodes
                        else:
                            # Log a connection width mismatch warning if len is neither 1 nor arraySize
                            self._scene.logger.warning(
                                f"Net connection width mismatch for {elementSymbol.instanceName}: "
                                f"expected 1 or {arraySize}, got {len(nets)}. Falling back to element 0."
                            )
                            instanceNets.append(nets[0])
                            
                    specificNetsList = " ".join(instanceNets)
                    symbolLines.append(
                        processLine(createInstanceLine(f"{baseInstName}<{i}>"), specificNetsList))

            return symbolLines


        except Exception as e:
            self._scene.logger.error(
                f"Error creating netlist line for {elementSymbol.instanceName}: {e}")
            return [
                f"*Netlist line is not defined for symbol of {elementSymbol.instanceName}\n"]

    def createXyceSymbolLine(self, elementSymbol: shp.schematicSymbol) -> list[str]:
        return self._createNetlistLine(elementSymbol, "SpiceNetlistLine")

    def createSpiceLine(self, elementSymbol: shp.schematicSymbol) -> list[str]:
        try:
            spiceLines = self.createXyceSymbolLine(elementSymbol)
            incFileName = elementSymbol.symattrs.get("incLine", "").strip()
            if incFileName:
                cellItem = self._getCellItem(elementSymbol.libraryName, elementSymbol.cellName)
                cellPath = cellItem.data(Qt.ItemDataRole.UserRole + 2)
                incFilePath = pathlib.Path(cellPath) / incFileName
                self.includeLines.add(f'.INC "{incFilePath}"')
            else:
                self.includeLines.add(f"* no include line found for {elementSymbol.cellName}")
            return spiceLines
        except Exception as e:
            self._scene.logger.error(f"Spice subckt netlist error: {e}")
            return [f"*Netlist line is not defined for symbol of {elementSymbol.instanceName}\n"]

    def createVerilogaLine(self, elementSymbol) -> list[str]:
        try:
            symbolLines = self._createNetlistLine(elementSymbol, "SpiceNetlistLine")
            self.vamodelLines.add(elementSymbol.symattrs.get("vaModelLine",
                                                             "* no model line is found for {item.cellName}").strip())
            self.vahdlLines.add(elementSymbol.symattrs.get("vaHDLLine",
                                                           "* no hdl line is found for {item.cellName}").strip())
            return symbolLines
        except Exception as e:
            self._scene.logger.error(e)
            return [f"*Netlist line is not defined for symbol of {elementSymbol.instanceName}\n"]

    @staticmethod
    @functools.lru_cache(maxsize=None)
    def parseArrayNotation(name: str) -> tuple[str, tuple[int, int]]:
        """
        Parse net/instance array notation like 'name<0:5>' into base name and index range.
        Also handles single instance/net notation like 'name<0>' or 'name<1>'.

        Args:
        name (str): The net name with optional bus notation.

        Returns:
        tuple[str, tuple[int, int]]: A tuple containing the base name and a tuple of start and end indices.
        """
        # Check if the name does not contain bus notation
        if '<' not in name or '>' not in name:
            return name, (-1, -1)

        baseName = name.split('<')[0]  # Extract the base name before '<'
        indexRange = name.split('<')[1].split('>')[0]  # Extract the content inside '<>'

        # Check if it's a single index (e.g., 'name<0>')
        if ':' not in indexRange:
            singleIndex = int(indexRange)
            return baseName, (singleIndex, singleIndex)

        # Handle range notation (e.g., 'name<0:5>')
        start, end = map(int, indexRange.split(':'))
        return baseName, (start, end)

    @staticmethod
    def expandPinNames(pinNamesList: List[str]) -> str:
        expandedPinNameList = []
        for pinName in pinNamesList:
            pinBaseName, pinTuple = xyceNetlist.parseArrayNotation(pinName)
            if pinTuple[0] == pinTuple[1] == -1:
                expandedPinNameList.append(pinBaseName)
            else:
                pinStep = 1 if pinTuple[1] >= pinTuple[0] else -1
                for i in range(pinTuple[0], pinTuple[1] + pinStep):
                    expandedPinNameList.append(f'{pinBaseName}<{i}>')
        return ' '.join(expandedPinNameList)
