#    “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 hashlib import new
import json
# from hashlib import new
import pathlib

import orjson
from copy import deepcopy
from typing import List, Dict

# import numpy as np
from PySide6.QtCore import (
    QLineF,
    QPoint,
    QPointF,
    Qt,
    Signal,
)
from PySide6.QtGui import (
    QColor,
    QGuiApplication,
    QPen,
)
from PySide6.QtWidgets import (
    QDialog,
    QGraphicsLineItem,
    QGraphicsSceneMouseEvent,
)

import revedaEditor.backend.dataDefinitions as ddef
import revedaEditor.backend.undoStack as us
import revedaEditor.common.labels as lbl
import revedaEditor.common.shapes as shp  # import the shapes
import revedaEditor.fileio.loadJSON as lj
import revedaEditor.fileio.symbolEncoder as symenc
import revedaEditor.gui.alignItems as alg
import revedaEditor.gui.propertyDialogues as pdlg
from revedaEditor.backend.pdkLoader import importPDKModule
from revedaEditor.scenes.editorScene import editorScene

symlyr = importPDKModule('symLayers')


# noinspection PyUnresolvedReferences
class symbolScene(editorScene):
    """
    Scene for Symbol editor.
    """
    alignLineFinished = Signal(shp.alignLine)

    def __init__(self, parent):
        super().__init__(parent)
        # drawing modes
        self.editModes = ddef.symbolModes(
            selectItem=True,
            deleteItem=False,
            moveItem=False,
            copyItem=False,
            rotateItem=False,
            changeOrigin=False,
            panView=False,
            zoomView=False,
            drawPin=False,
            drawArc=False,
            drawRect=False,
            drawLine=False,
            addLabel=False,
            drawCircle=False,
            drawPolygon=False,
            stretchItem=False,
            alignItems=False,
        )

        self.messages = {
            "selectItem": "Select Item.",
            "deleteItem": "Click on item to delete.",
            "moveItem": "Click and drag to move item.",
            "copyItem": "Click on item to copy.",
            "rotateItem": "Click on item to rotate.",
            "changeOrigin": "Click to set new origin.",
            "panView": "Click and drag to pan view.",
            'zoomView': 'Draw rectangle to zoom in',
            "drawPin": "Add a pin.",
            "drawArc": "Click for the first point of Arc.",
            "drawRect": "Click for the first point of Rectangle.",
            "drawLine": "Click for the first point of Line.",
            "addLabel": "Adding a label.",
            "drawCircle": "Click for the centre of Circle.",
            "drawPolygon": "Click for the first point of Polygon.",
            "stretchItem": "Click and drag to stretch item.",
            "alignItems": "Select Align Option."
        }

        self.symbolShapes = ["line", "arc", "rect", "circle", "pin", "label", "polygon"]
        self.attributeList = []  # list of symbol attributes
        self.origin = QPoint(0, 0)
        # some default attributes

        self.pinName = ""
        self.pinType = shp.symbolPin.pinTypes[0]
        self.pinDir = shp.symbolPin.pinDirs[0]
        self.labelDefinition = ""
        self.labelType = lbl.symbolLabel.labelTypes[0]
        self.labelOrient = lbl.symbolLabel.labelOrients[0]
        self.labelAlignment = lbl.symbolLabel.labelAlignments[0]
        self.labelUse = lbl.symbolLabel.labelUses[0]
        self.labelVisible = False
        self.labelHeight = "12"
        self.labelOpaque = True
        self.newPin = None
        self.newLine = None
        self.newRect = None
        self.newCircle = None
        self.newArc = None
        self.newLabel = None
        self._newPolygon = None
        self.polygonGuideLine = None

        self.alignLineFinished.connect(alg.alignToLine)

    @property
    def drawMode(self):
        return any(
            (
                self.editModes.drawPin,
                self.editModes.drawArc,
                self.editModes.drawLine,
                self.editModes.drawRect,
                self.editModes.drawCircle,
                self.editModes.drawPolygon,
            )
        )

    def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        super().mouseMoveEvent(event)
        self.mouseMoveLoc = self.snapToGrid(event.scenePos().toPoint())

        # Update message only when needed
        message = None
        if self.editModes.drawLine and self.newLine:
            message = "Release mouse on the end point"
            self.newLine.end = self.mouseMoveLoc
        elif self.editModes.drawPin and self.newPin:
            message = "Place pin"
            self.newPin.setPos(self.mouseMoveLoc - self.mouseReleaseLoc)
        elif self.editModes.drawCircle and self.newCircle:
            message = "Extend Circle"
            # Optimize circle radius calculation
            dx = self.mouseMoveLoc.x() - self.newCircle.centre.x()
            dy = self.mouseMoveLoc.y() - self.newCircle.centre.y()
            self.newCircle.radius = (dx * dx + dy * dy) ** 0.5
        elif self.editModes.drawRect and self.newRect:
            message = "Click to finish the rectangle"
            self.newRect.end = self.mouseMoveLoc
        elif self.editModes.drawArc and self.newArc:
            message = "Extend Arc"
            self.newArc.end = self.mouseMoveLoc
        elif self.editModes.addLabel and self.newLabel:
            message = "Place Label"
            self.newLabel.setPos(self.mouseMoveLoc)
        elif (self.editModes.drawPolygon and
              self._newPolygon and
              self.polygonGuideLine):
            message = "Add another point to Polygon"
            self.polygonGuideLine.setLine(
                QLineF(self._newPolygon.points[-1], self.mouseMoveLoc)
            )
        elif self.newAlignLine and self.editModes.alignItems:
            self.newAlignLine.draftLine = QLineF(
                self.newAlignLine.draftLine.p1(), self.mouseMoveLoc)
        if message:
            self.editorWindow.messageLine.setText(message)

        self.statusLine.showMessage(
            f"Cursor Position: {(self.mouseMoveLoc - self.origin).toTuple()}")

    def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        super().mouseReleaseEvent(event)

    def _handleMouseRelease(self, mousePos: QPoint, button: Qt.MouseButton) -> None:
        modifiers = QGuiApplication.keyboardModifiers()
        try:
        #    if self.editModes.changeOrigin:
        #         self.origin = mousePos
            if self.editModes.drawLine:
                if self.newLine:
                    if self.newLine.length <= 1:
                        self.undoStack.removeLastCommand()
                    self.newLine = None
                self.newLine = self.lineDraw(mousePos, mousePos)
                self.newLine.setSelected(True)
            elif self.editModes.drawCircle:
                if self.newCircle:
                    if self.newCircle.radius <= 1:
                        self.undoStack.removeLastCommand()
                    self.newCircle = None

                self.newCircle = self.circleDraw(mousePos, mousePos)
                self.newCircle.setSelected(True)
            elif self.editModes.drawPin:
                if self.newPin:
                    self.newPin = None
                self.newPin = self.pinDraw(mousePos)
                self.newPin.setSelected(True)
            elif self.editModes.drawRect:
                if self.newRect:
                    if self.newRect.width <= 1 or self.newRect.height <= 1:
                        self.undoStack.removeLastCommand()
                    self.newRect = None
                self.newRect = self.rectDraw(mousePos, mousePos)
                self.newRect.setSelected(True)
            elif self.editModes.drawArc:
                if self.newArc:
                    if self.newArc.width <= 1 or self.newArc.height <= 1:
                        self.undoStack.removeLastCommand()
                    self.newArc = None
                self.newArc = self.arcDraw(mousePos, mousePos)
                self.newArc.setSelected(True)
            elif self.editModes.addLabel:
                if self.newLabel:
                    self.newLabel = None
                self.newLabel = self.labelDraw(
                    mousePos,
                    self.labelDefinition,
                    self.labelType,
                    self.labelHeight,
                    self.labelAlignment,
                    self.labelOrient,
                    self.labelUse,
                )
                self.newLabel.setSelected(True)
            elif self.editModes.drawPolygon:
                if self._newPolygon:
                    self._newPolygon.addPoint(mousePos)
                else:
                    self._newPolygon, self.polygonGuideLine = self.startPolygon(mousePos)
            elif self.editModes.rotateItem:
                self.rotateSelectedItems(mousePos)
            elif self.editModes.alignItems:
                self._handleAlignItemLine(mousePos)
            self.messageLine.setText(self.messages.get(self.editModes.mode(), ""))
        except Exception as e:
            self.logger.error(f"Error in Mouse Release Event: {e} ")

    def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        super().mouseDoubleClickEvent(event)
        try:
            self.finishPolygon(event)
        except Exception as e:
            self.logger.error(f"Error in mouse Double Click Event: {e}")

    def lineDraw(self, start: QPoint, current: QPoint):
        line = shp.symbolLine(start, current)
        self.addUndoStack(line)
        # self.addItem(line)
        # undoCommand = us.addShapeUndo(self, line)
        # self.undoStack.push(undoCommand)
        return line

    def rectDraw(self, start: QPoint, end: QPoint):
        """
        Draws a rectangle on the scene
        """
        rect = shp.symbolRectangle(start, end)
        self.addUndoStack(rect)
        # undoCommand = us.addShapeUndo(self, rect)
        # self.undoStack.push(undoCommand)
        return rect

    def circleDraw(self, start: QPoint, end: QPoint):
        """
        Draws a circle on the scene
        """
        circle = shp.symbolCircle(start, end)

        self.addUndoStack(circle)
        return circle

    def arcDraw(self, start: QPoint, end: QPoint):
        """
        Draws an arc inside the rectangle defined by start and end points.
        """
        arc = shp.symbolArc(start, end)

        self.addUndoStack(arc)
        return arc

    def pinDraw(self, current):
        pin = shp.symbolPin(current, self.pinName, self.pinDir, self.pinType)
        # self.addItem(pin)
        self.addUndoStack(pin)
        return pin

    def labelDraw(
            self,
            start,
            labelDefinition,
            labelType,
            labelHeight,
            labelAlignment,
            labelOrient,
            labelUse,
    ):
        label = lbl.symbolLabel(
            start,
            labelDefinition,
            labelType,
            labelHeight,
            labelAlignment,
            labelOrient,
            labelUse,
        )
        label.labelVisible = self.labelOpaque
        label.labelDefs()
        label.setOpacity(1)
        self.addUndoStack(label)
        return label

    def addProperty(self, labelDefinition: str):
        # Extract core expression between [@ and ]
        if labelDefinition.startswith("[@") and labelDefinition.endswith("]"):
            core = labelDefinition[2:-1]
            # Split into components
            parts = core.split(":")
            instProp = parts[0]

    def startPolygon(self, startLoc: QPoint):
        newPolygon = shp.symbolPolygon([startLoc])
        self.addUndoStack(newPolygon)
        # Create guide line
        guideLine = QLineF(startLoc, startLoc)
        polygonGuideLine = QGraphicsLineItem(guideLine)
        polygonGuideLine.setPen(QPen(QColor(255, 255, 0), 1, Qt.DashLine))
        self.addUndoStack(polygonGuideLine)
        return newPolygon, polygonGuideLine

    def finishPolygon(self, event):
        if (hasattr(event, 'button') and event.button() == Qt.MouseButton.LeftButton
                and self.editModes.drawPolygon and self._newPolygon):
            self.editModes.setMode("selectItem")
            self._newPolygon = None
            self.removeItem(self.polygonGuideLine)
            self.polygonGuideLine = None
            self.editorWindow.messageLine.setText('Select Item.')

    def _handleAlignItemLine(self, eventLoc: QPoint) -> None:

        if self.newAlignLine:
            if not self.newAlignLine.draftLine.isNull():
                self.alignLineFinished.emit(self.newAlignLine)
        else:
            from PySide6.QtWidgets import (QApplication, )
            alignDialogs = [w for w in QApplication.topLevelWidgets() if isinstance(w,
                                                                                    alg.alignItemsDialogue) and w.isVisible() and w.scene == self]
            if not alignDialogs:
                self.logger.warning("No align items dialogue found")
                return
            alignDlg = alignDialogs[0]
            if alignDlg.horizontalAlignButton.isChecked():
                self.newAlignLine = shp.alignLine(QLineF(eventLoc, eventLoc), 1,
                                                  0)  # horizontal
            else:
                self.newAlignLine = shp.alignLine(QLineF(eventLoc, eventLoc), 1,
                                                  1)
            self.addUndoStack(self.newAlignLine)

    def moveBySelectedItems(self):
        if self.selectedItems():
            dlg = pdlg.moveByDialogue(self.editorWindow)
            dlg.xEdit.setText("0")
            dlg.yEdit.setText("0")
            if dlg.exec() == QDialog.DialogCode.Accepted:
                for item in self.selectedItems():
                    item.moveBy(
                        self.snapToBase(float(dlg.xEdit.text()), self.snapTuple[0]),
                        self.snapToBase(float(dlg.yEdit.text()), self.snapTuple[1]),
                    )
            self.editorWindow.messageLine.setText(
                f"Moved items by {dlg.xEdit.text()} and {dlg.yEdit.text()}"
            )
            self.editModes.setMode("selectItem")

    def itemProperties(self):
        """
        When item properties is queried.
        """
        # Define the mapping of types to update methods
        UPDATE_METHODS = {
            shp.symbolRectangle: self.updateSymbolRectangle,
            shp.symbolCircle: self.updateSymbolCircle,
            shp.symbolArc: self.updateSymbolArc,
            shp.symbolLine: self.updateSymbolLine,
            shp.symbolPin: self.updateSymbolPin,
            lbl.symbolLabel: self.updateSymbolLabel,
            shp.symbolPolygon: self.updateSymbolPolygon
        }

        # Filter selected items and update them
        for item in (item for item in self.selectedItems() if item.parentItem() is None):
            updateMethod = UPDATE_METHODS.get(type(item))
            if updateMethod:
                updateMethod(item)

    def updateSymbolRectangle(self, item):
        queryDlg = pdlg.rectPropertyDialog(self.editorWindow)
        [left, top, width, height] = item.rect.getRect()
        sceneTopLeftPoint = item.mapToScene(QPoint(left, top))
        queryDlg.rectLeftLine.setText(str(sceneTopLeftPoint.x()))
        queryDlg.rectTopLine.setText(str(sceneTopLeftPoint.y()))
        queryDlg.rectWidthLine.setText(str(width))  # str(width))
        queryDlg.rectHeightLine.setText(str(height))  # str(height))
        if queryDlg.exec() == QDialog.DialogCode.Accepted:
            newRectItem = shp.symbolRectangle(QPoint(0, 0), QPoint(0, 0))
            newRectItem.left = self.snapToBase(
                float(queryDlg.rectLeftLine.text()), self.snapTuple[0]
            )
            newRectItem.top = self.snapToBase(
                float(queryDlg.rectTopLine.text()), self.snapTuple[1]
            )
            newRectItem.width = self.snapToBase(
                float(queryDlg.rectWidthLine.text()), self.snapTuple[0]
            )
            newRectItem.height = self.snapToBase(
                float(queryDlg.rectHeightLine.text()), self.snapTuple[1]
            )

            self.undoStack.push(us.addDeleteShapeUndo(self, newRectItem, item))

    def updateSymbolCircle(self, item):
        queryDlg = pdlg.circlePropertyDialog(self.editorWindow)
        centre = item.mapToScene(item.centre).toTuple()
        radius = item.radius
        queryDlg.centerXEdit.setText(str(centre[0]))
        queryDlg.centerYEdit.setText(str(centre[1]))
        queryDlg.radiusEdit.setText(str(radius))
        if queryDlg.exec() == QDialog.DialogCode.Accepted:
            newCircleItem = shp.symbolCircle(QPoint(0, 0), QPoint(0, 0))
            centerX = self.snapToBase(
                float(queryDlg.centerXEdit.text()), self.snapTuple[0]
            )
            centerY = self.snapToBase(
                float(queryDlg.centerYEdit.text()), self.snapTuple[1]
            )
            newCircleItem.centre = QPoint(centerX, centerY)
            radius = self.snapToBase(
                float(queryDlg.radiusEdit.text()), self.snapTuple[0]
            )
            newCircleItem.radius = radius
            self.undoStack.push(us.addDeleteShapeUndo(self, newCircleItem, item))

    def updateSymbolLine(self, item):
        queryDlg = pdlg.linePropertyDialog(self.editorWindow)
        sceneLineStartPoint = item.mapToScene(item.start).toPoint()
        sceneLineEndPoint = item.mapToScene(item.end).toPoint()
        queryDlg.startXLine.setText(str(sceneLineStartPoint.x()))
        queryDlg.startYLine.setText(str(sceneLineStartPoint.y()))
        queryDlg.endXLine.setText(str(sceneLineEndPoint.x()))
        queryDlg.endYLine.setText(str(sceneLineEndPoint.y()))
        if queryDlg.exec() == QDialog.DialogCode.Accepted:
            startX = self.snapToBase(
                float(queryDlg.startXLine.text()), self.snapTuple[0]
            )
            startY = self.snapToBase(
                float(queryDlg.startYLine.text()), self.snapTuple[1]
            )
            endX = self.snapToBase(
                float(queryDlg.endXLine.text()), self.snapTuple[0]
            )
            endY = self.snapToBase(
                float(queryDlg.endYLine.text()), self.snapTuple[1]
            )
            newLine = shp.symbolLine(QPoint(startX, startY), QPoint(endX, endY))
            self.undoStack.push(us.addDeleteShapeUndo(self, newLine, item))

    def updateSymbolArc(self, item):
        queryDlg = pdlg.arcPropertyDialog(self.editorWindow)
        sceneStartPoint = item.mapToScene(item.start).toPoint()
        queryDlg.startXEdit.setText(str(sceneStartPoint.x()))
        queryDlg.startYEdit.setText(str(sceneStartPoint.y()))
        queryDlg.widthEdit.setText(str(item.width))
        queryDlg.heightEdit.setText(str(item.height))
        arcType = item.arcType
        if queryDlg.exec() == QDialog.DialogCode.Accepted:
            startX = int(float(queryDlg.startXEdit.text()))
            startY = int(float(queryDlg.startYEdit.text()))
            start = self.snapToGrid(QPoint(startX, startY))
            width = int(float(queryDlg.widthEdit.text()))
            height = int(float(queryDlg.heightEdit.text()))
            end = start + QPoint(width, height)
            newArc = shp.symbolArc(start, end)
            newArc.arcType = arcType

            self.undoStack.push(us.addDeleteShapeUndo(self, newArc, item))
            newArc.height = height

    def updateSymbolLabel(self, item):
        queryDlg = pdlg.labelPropertyDialog(self.editorWindow)
        queryDlg.labelDefinition.setText(str(item.labelDefinition))
        queryDlg.labelHeightEdit.setText(str(item.labelHeight))
        queryDlg.labelAlignCombo.setCurrentText(item.labelAlign)
        queryDlg.labelOrientCombo.setCurrentText(item.labelOrient)
        queryDlg.labelUseCombo.setCurrentText(item.labelUse)
        if item.labelVisible:
            queryDlg.labelVisiCombo.setCurrentText("Yes")
        else:
            queryDlg.labelVisiCombo.setCurrentText("No")
        if item.labelType == "Normal":
            queryDlg.normalType.setChecked(True)
        elif item.labelType == "NLPLabel":
            queryDlg.NLPType.setChecked(True)
        elif item.labelType == "PyLabel":
            queryDlg.pyLType.setChecked(True)
        sceneStartPoint = item.pos()
        queryDlg.labelXLine.setText(str(sceneStartPoint.x()))
        queryDlg.labelYLine.setText(str(sceneStartPoint.y()))
        if queryDlg.exec() == QDialog.DialogCode.Accepted:
            startX = int(float(queryDlg.labelXLine.text()))
            startY = int(float(queryDlg.labelYLine.text()))
            start = self.snapToGrid(QPoint(startX, startY))
            labelDefinition = queryDlg.labelDefinition.text()
            labelHeight = int(float(queryDlg.labelHeightEdit.text()))
            labelAlign = queryDlg.labelAlignCombo.currentText()
            labelOrient = queryDlg.labelOrientCombo.currentText()
            labelUse = queryDlg.labelUseCombo.currentText()
            labelType = lbl.symbolLabel.labelTypes[0]
            if queryDlg.NLPType.isChecked():
                labelType = lbl.symbolLabel.labelTypes[1]
            elif queryDlg.pyLType.isChecked():
                labelType = lbl.symbolLabel.labelTypes[2]
            newLabel = lbl.symbolLabel(
                start,
                labelDefinition,
                labelType,
                labelHeight,
                labelAlign,
                labelOrient,
                labelUse,
            )
            newLabel.labelVisible = (
                    queryDlg.labelVisiCombo.currentText() == "Yes"
            )
            newLabel.labelDefs()
            newLabel.setOpacity(1)
            self.undoStack.push(us.addDeleteShapeUndo(self, newLabel, item))
        else:
            self.addItem(item)

    def updateSymbolPin(self, item):
        queryDlg = pdlg.pinPropertyDialog(self.editorWindow)
        queryDlg.pinName.setText(str(item.pinName))
        queryDlg.pinType.setCurrentText(item.pinType)
        queryDlg.pinDir.setCurrentText(item.pinDir)
        sceneStartPoint = item.mapToScene(item.start).toPoint()
        queryDlg.pinXLine.setText(str(sceneStartPoint.x()))
        queryDlg.pinYLine.setText(str(sceneStartPoint.y()))
        if queryDlg.exec() == QDialog.DialogCode.Accepted:
            sceneStartX = int(float(queryDlg.pinXLine.text()))
            sceneStartY = int(float(queryDlg.pinYLine.text()))
            start = self.snapToGrid(
                QPoint(sceneStartX, sceneStartY))
            pinName = queryDlg.pinName.text()
            pinType = queryDlg.pinType.currentText()
            pinDir = queryDlg.pinDir.currentText()
            newPin = shp.symbolPin(start, pinName, pinDir, pinType)
            self.undoStack.push(us.addDeleteShapeUndo(self, newPin, item))

    def updateSymbolPolygon(self, item):
        pointsTupleList = [(point.x(), point.y()) for point in item.points]
        queryDlg = pdlg.symbolPolygonProperties(
            self.editorWindow, pointsTupleList
        )
        if queryDlg.exec() == QDialog.DialogCode.Accepted:
            tempPoints = []
            for i in range(queryDlg.tableWidget.rowCount()):
                xcoor = queryDlg.tableWidget.item(i, 1).text()
                ycoor = queryDlg.tableWidget.item(i, 2).text()
                if xcoor != "" and ycoor != "":
                    tempPoints.append(QPointF(float(xcoor), float(ycoor)))
            newPolygon = shp.symbolPolygon(tempPoints)
            self.undoStack.push(us.addDeleteShapeUndo(self, newPolygon, item))

    def loadDesign(self, filePathObj: pathlib.Path) -> None:
        try:
            with filePathObj.open("rb") as file:
                decodedData = orjson.loads(file.read())
            self.blockSignals(True)
            with self.measureDuration():
                viewDict, gridSettings, *itemData = decodedData
                if viewDict.get("viewType") != "symbol":
                    raise Exception("Not a symbol file!")
                if gridSettings and gridSettings.get("snapGrid"):
                    self.editorWindow.configureGridSettings(decodedData[1].get(
                        "snapGrid", (self.majorGrid,
                                     self.snapGrid)))
                self.attributeList = []
                self.createSymbolItems(itemData)
            self.itemsRef = set(self.items())
        except (orjson.JSONDecodeError, FileNotFoundError) as e:
            self.logger.error(f"File error while loading symbol: {e}")
            self.attributeList = []
            self.clear()
            return
        except KeyError as e:
            self.logger.error(f"Invalid symbol format - missing key: {e}")
            self.attributeList = []
            self.clear()
            return
        except Exception as e:
            self.logger.error(f"Unexpected error loading symbol: {e}")
            self.attributeList = []
            self.clear()
            return
        finally:
            self.blockSignals(False)
            self.update()

    def createSymbolItems(self, itemsList: List[Dict]):
        factory = lj.symbolItems(self)
        for itemDict in itemsList:
            if itemDict:
                itemType = itemDict.get("type", None)
                if itemType in self.symbolShapes:
                    itemShape = factory.create(itemDict)
                    if itemType == "label":
                        itemShape.setOpacity(1)
                    self.addItem(itemShape)
                    continue
                if itemType == "attr":
                    self.attributeList.append(factory.createSymbolAttribute(itemDict))

    def saveSymbolCell(self, fileName: pathlib.Path) -> bool:
        """
        Save symbol cell to file.

        Args:
            fileName: Path to save the symbol cell

        Returns:
            bool: True if save successful, False otherwise
        """

        try:
            self.itemsRefSet = set(self.items())
            # Filter items and process labels in one pass

            symbolClasses = (shp.symbolPolygon, shp.symbolRectangle, shp.symbolArc,
                             shp.symbolLine, shp.symbolPin, lbl.symbolLabel,
                             shp.symbolCircle, shp.text)
            sceneItems = [item for item in self.items() if isinstance(item,
                                                                      symbolClasses)]
            # Build save data
            save_data = [
                {"viewType": "symbol"},
                {"snapGrid": (self.majorGrid, self.snapGrid)},
                *sceneItems,
                *getattr(self, "attributeList", [])
            ]

            # Write to file
            with fileName.open("w") as f:
                json.dump(save_data, f, cls=symenc.symbolEncoder, indent=4)

            self.undoStack.clear()
            return True

        except Exception as e:
            self.logger.error(f"Symbol save error: {e}")
            return False

    def viewSymbolProperties(self):
        """
        View symbol properties dialog.
        """
        # copy symbol attribute list to another list by deepcopy to be safe
        attributeListCopy = deepcopy(self.attributeList)
        symbolPropDialogue = pdlg.symbolLabelsDialogue(
            self.editorWindow, self.items(), attributeListCopy
        )
        if symbolPropDialogue.exec() == QDialog.DialogCode.Accepted:
            for i, item in enumerate(symbolPropDialogue.labelItemList):
                # label name is not changed.
                item.labelHeight = int(
                    float(symbolPropDialogue.labelHeightList[i].text())
                )
                item.labelAlign = symbolPropDialogue.labelAlignmentList[i].currentText()
                item.labelOrient = symbolPropDialogue.labelOrientationList[
                    i
                ].currentText()
                item.labelUse = symbolPropDialogue.labelUseList[i].currentText()
                item.labelType = symbolPropDialogue.labelTypeList[i].currentText()
                item.update(item.boundingRect())
            # create an empty attribute list. If the dialog is OK, the local attribute list
            # will be copied to the symbol attribute list.
            localAttributeList = []
            for i, item in enumerate(symbolPropDialogue.attributeNameList):
                if item.text().strip() != "":
                    localAttributeList.append(
                        symenc.symbolAttribute(
                            item.text(), symbolPropDialogue.attributeDefList[i].text()
                        )
                    )
                self.attributeList = deepcopy(localAttributeList)

    def copySelectedItems(self):
        # Only consider top-level items (same as schematic/layout editors)
        selectedItems = [item for item in self.selectedItems() if item.parentItem() is None]
        if not selectedItems:
            return
        copyShapesList = []
        factory = lj.symbolItems(self)
        for item in selectedItems:
            selectedItemJson = json.dumps(item, cls=symenc.symbolEncoder)
            itemCopyDict = json.loads(selectedItemJson)
            shape = factory.create(itemCopyDict)
            if shape is not None:
                copyShapesList.append(shape)
        if copyShapesList:
            # Add to undo stack before grouping (matches schematic/layout pattern)
            self.addListUndoStack(copyShapesList)
            # Use base-class attribute so mouseMoveEvent/mouseReleaseEvent can move
            # the group as the cursor moves and place it on mouse button release
            self.selectedItemGroup = self.createItemGroup(copyShapesList)
            self.selectedItemGroup.setSelected(True)
