"""
/***************************************************************************
 SeilaplanPlugin
                                 A QGIS plugin
 Seillinien-Layoutplaner
                              -------------------
        begin                : 2013
        copyright            : (C) 2015 by ETH Zürich
        email                : seilaplanplugin@gmail.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""

from typing import List

from numpy import nan
from qgis.PyQt.QtCore import pyqtSignal, QCoreApplication, QObject, QSize, Qt
from qgis.PyQt.QtGui import QIcon, QPixmap
from qgis.PyQt.QtWidgets import (
    QCheckBox,
    QDoubleSpinBox,
    QGridLayout,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QPushButton,
    QSpinBox,
    QWidget,
)

from SEILAPLAN.tools.poles import Poles
from SEILAPLAN.utils.plugin_utils import getAbsoluteIconPath


class CustomPoleWidget(QObject):
    """
    Display pole properties in a gui object, one row per pole.
    """

    # Signals
    sig_zoomOut = pyqtSignal()
    sig_zoomIn = pyqtSignal(int)
    sig_createPole = pyqtSignal(int)
    sig_updatePole = pyqtSignal(int, str, object)
    sig_deletePole = pyqtSignal(int)

    def __init__(self, widget, layout, poles, mode="standard"):
        """
        :type widget: qgis.PyQt.QtWidgets.QWidget
        :type layout: qgis.PyQt.QtWidgets.QGridLayout
        :type poles: Poles|Array
        """
        super().__init__()
        self.widget: QWidget = widget
        self.layout: QGridLayout = layout
        self.mode = mode
        if self.mode == "standard":
            self.poles: Poles = poles
            self.poleArr = poles.poles
        else:
            self.poles = None
            self.poleArr = poles
        self.poleRows: List[PoleRow] = []
        self.editActive = False
        self.pole_dist_step = Poles.POLE_DIST_STEP
        self.pole_height_step = Poles.POLE_HEIGHT_STEP
        self.distRange = []
        self.poleCount = 0

    def setInitialGui(self, distRange):
        """
        :type distRange: list
        """
        self.distRange = distRange
        self.layout.setAlignment(Qt.AlignmentFlag.AlignTop)
        self.addHeader()
        self.createPoleRows()

    def createPoleRows(self):
        self.poleRows.clear()
        for idx, pole in enumerate(self.poleArr):
            delBtn = False
            addBtn = False

            # Distance input field: ranges are defined by neighbouring poles
            [lowerRange, upperRange] = self.getDistanceRange(idx)
            # Delete button: anchor and first and last pole cannot be deleted
            if self.mode != "standard" or self.poles.idxA < idx < self.poles.idxE:
                delBtn = True
            # Add button: Pole can only be added between first and last pole
            if self.mode == "standard" and self.poles.idxA <= idx < self.poles.idxE:
                addBtn = True

            # Create layout
            self.poleRows.append(
                PoleRow(
                    self,
                    self.widget,
                    self.layout,
                    idx,
                    pole["nr"],
                    pole["name"],
                    pole["poleType"],
                    pole["d"],
                    [lowerRange, upperRange],
                    pole["h"],
                    pole["angle"],
                    pole["bundstelle"],
                    delBtn,
                    addBtn,
                )
            )
            if not pole["active"]:
                self.poleRows[-1].deactivate()

        self.widget.setLayout(self.layout)

    def addHeader(self):
        self.layout.addWidget(QLabel(""), 0, 0, 1, 1, Qt.AlignmentFlag.AlignLeft)
        self.layout.addWidget(
            QLabel(self.tr("Nr.")), 0, 1, 1, 1, Qt.AlignmentFlag.AlignLeft
        )
        self.layout.addWidget(
            QLabel(self.tr("Stuetzenbezeichnung")),
            0,
            2,
            1,
            1,
            Qt.AlignmentFlag.AlignLeft,
        )
        self.layout.addWidget(
            QLabel(self.tr("Horiz.distanz")),
            0,
            3,
            1,
            1,
            Qt.AlignmentFlag.AlignLeft,
        )
        self.layout.addWidget(
            QLabel(self.tr("Stuetzenhoehe")),
            0,
            4,
            1,
            1,
            Qt.AlignmentFlag.AlignLeft,
        )
        if self.mode == "standard":
            self.layout.addWidget(
                QLabel(self.tr("Neigung")),
                0,
                5,
                1,
                1,
                Qt.AlignmentFlag.AlignLeft,
            )
            self.layout.addWidget(
                QLabel(self.tr("O Bundst.")),
                0,
                6,
                1,
                2,
                Qt.AlignmentFlag.AlignLeft,
            )
            # When window grows, the second column (Stuetzenbezeichnung) should grow as well
            self.layout.setColumnStretch(2, 1)
        else:
            # When in the profile dialog, the last column (Stuetzenhoehe) should grow
            self.layout.setColumnStretch(4, 1)

    def onRowChange(self, newVal=None, idx=None, property_name=None):
        if self.editActive:
            return
        if property_name == "name" and len(newVal) > Poles.POLE_NAME_MAX_LENGTH:
            text = newVal[: Poles.POLE_NAME_MAX_LENGTH]
            self.poleRows[idx].fieldName.setText(text)
            return

        self.editActive = True
        # Emit signal
        self.sig_updatePole.emit(idx, property_name, newVal)

    def onRowAdd(self, idx=False):
        if self.editActive:
            return
        self.editActive = True
        # Emit signal
        self.sig_createPole.emit(idx)

    def onRowDel(self, idx=False):
        if self.editActive:
            return
        self.editActive = True
        if idx:
            # This will remove the selected row, so user sees a UI feedback
            # immediately.However, deleting a pole will trigger a recalculation,
            # which will rebuild the PoleWidget completely.
            self.poleRows[idx].remove()
            del self.poleRows[idx]
        # Emit signal
        self.sig_deletePole.emit(idx)

    def changeRow(self, idx, property_name, newVal, prevAnchorA=None, prevAnchorE=None):
        self.updateAnchorState(prevAnchorA, prevAnchorE)

        self.updateBundstelle()
        if property_name == "d":
            self.updateNeighbourDistRange(idx, newVal, newVal)
        elif property_name == "h":
            # When changing first or last pole to/from 0m height, the anchor gets
            #  deactivated/activated and the row numbers need to reflect this
            self.updatePoleRowNumbers()
        self.editActive = False

    def refresh(self):
        self.removeAll()
        self.createPoleRows()
        self.editActive = False

    def updatePoleRowNumbers(self):
        labels = None
        if self.poles:
            [_, _, _, _, _, labels, _, _, _, _] = self.poles.getAsArray(True, True)

        pole: PoleRow
        for i, pole in enumerate(self.poleRows):
            nr = i + 1
            if labels:
                nr = labels[i]
            pole.updateLabelNr(nr)

    def updateBundstelle(self):
        pole: PoleRow
        for i, pole in enumerate(self.poleRows):
            pole.updateBundstelle(self.poleArr[i]["bundstelle"])

    def updateNeighbourDistRange(self, idx, rightLimit, leftLimit):
        if idx > 0:
            # Left neighbour: update its upper range
            self.poleRows[idx - 1].updateUpperDistRange(leftLimit - self.pole_dist_step)
        if idx < self.poleCount - 1:
            # Right neighbour: update its lower range
            self.poleRows[idx + 1].updateLowerDistRange(
                rightLimit + self.pole_dist_step
            )

    def updateAnchorState(self, prevAnchorA, prevAnchorE):
        """Check new anchor state and activate / deactivate anchor row
        accordingly."""
        if not self.poles or prevAnchorA is None or prevAnchorE is None:
            return
        if prevAnchorA is not self.poles.hasAnchorA:
            idxA = 0
            if self.poles.hasAnchorA:
                # Anchor A was activated
                self.activateRow(idxA, float(self.poleArr[idxA]["d"]))
            else:
                # Anchor A was deactivated
                self.deactivateRow(idxA, self.distRange[0], self.poleArr[idxA + 1]["d"])

        if prevAnchorE is not self.poles.hasAnchorE:
            idxE = len(self.poles.poles) - 1
            if self.poles.hasAnchorE:
                # Anchor E was activated
                self.activateRow(idxE, float(self.poleArr[idxE]["d"]))
            else:
                # Anchor E was deactivated
                self.deactivateRow(
                    idxE, float(self.poleArr[idxE - 1]["d"]), self.distRange[1]
                )

    def deactivateRow(self, idx, leftLimit, rightLimit):
        # Update distance range of neighbours
        self.updateNeighbourDistRange(idx, leftLimit, rightLimit)
        self.poleRows[idx].deactivate()
        self.updatePoleRowNumbers()

    def activateRow(self, idx, dist):
        self.poleRows[idx].activate()
        self.poleRows[idx].fieldDist.setValue(dist)
        self.updateNeighbourDistRange(idx, dist, dist)
        self.updatePoleRowNumbers()

    def getDistanceRange(self, idx):
        lowerRange = self.distRange[0]
        upperRange = self.distRange[1]
        if idx > 0:
            lowerRange = self.poleArr[idx - 1]["d"]
        if idx < len(self.poleArr) - 1:
            upperRange = self.poleArr[idx + 1]["d"]
        return [lowerRange, upperRange]

    def zoomIn(self, idx):
        self.sig_zoomIn.emit(idx)

    def zoomOut(self):
        self.sig_zoomOut.emit()

    def removeAll(self):
        for pole in self.poleRows:
            pole.remove()
        self.poleRows.clear()

    def tr(self, message, **kwargs):
        return QCoreApplication.translate(type(self).__name__, message)


class PoleRow(object):
    """
    Creates all input fields necessary to change the properties of a pole in
    the cable layout. The layout is identified by the position (index) it has
    in the vertical layout.
    """

    POLE_LABEL_WIDTH = 180
    SPINNER_WIDTH = 85

    def __init__(
        self,
        parent,
        widget,
        layout,
        idx,
        nr,
        name,
        rowType,
        dist,
        distRange,
        height=False,
        angle=False,
        bundstelle=False,
        delBtn=False,
        addBtn=False,
    ):
        self.parent = parent
        self.widget = widget
        self.layout: QGridLayout = layout
        self.index = idx
        self.rowType = rowType
        self.parent.poleCount += 1

        self.labelNr = None
        self.statusSwitcher = None
        self.fieldName = None
        self.fieldDist = None
        self.fieldHeight = None
        self.fieldAngle = None
        self.fieldBundstelle = None
        self.addBtn = None
        self.delBtn = None

        self.addBtnPlus(addBtn)
        if self.rowType == "anchor":
            self.addSwitcher()
        else:
            self.addLabelNr(nr)
        self.addFieldName(name)
        self.addFieldDist(dist, distRange)
        if self.rowType not in ["anchor"]:
            self.addFieldHeight(height)
            self.addFieldAngle(angle)
        self.addFieldBundstelle(bundstelle)
        self.addBtnDel(delBtn)

    def addSwitcher(self):
        self.statusSwitcher = QCheckBox(self.widget)
        self.statusSwitcher.setText("")
        self.statusSwitcher.setChecked(True)
        self.layout.addWidget(
            self.statusSwitcher,
            self.index + 1,
            1,
            1,
            1,
            Qt.AlignmentFlag.AlignLeft,
        )

        # Connect events
        self.statusSwitcher.stateChanged.connect(
            lambda newVal: self.parent.onRowChange(newVal == 2, self.index, "active")
        )

    def addLabelNr(self, nr):
        self.labelNr = QLabel(self.widget)
        self.updateLabelNr(nr)
        self.layout.addWidget(
            self.labelNr, self.index + 1, 1, 1, 1, Qt.AlignmentFlag.AlignLeft
        )

    def updateLabelNr(self, label):
        if self.labelNr:
            if label:
                self.labelNr.setText(f"{label}:")
            else:
                self.labelNr.setText("")

    def addFieldName(self, value):
        self.fieldName = QLineEditWithFocus(self.widget)
        self.fieldName.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
        self.fieldName.setMinimumWidth(self.POLE_LABEL_WIDTH)
        self.fieldName.setText(value)
        # Add layout so the widget grows when the grid column grows
        hLayout = QHBoxLayout()
        hLayout.addWidget(self.fieldName)
        self.layout.addLayout(
            hLayout, self.index + 1, 2, 1, 1, Qt.AlignmentFlag.AlignLeft
        )
        # Connect events
        self.fieldName.inFocus.connect(lambda x: self.parent.zoomIn(self.index))
        self.fieldName.outFocus.connect(self.parent.zoomOut)
        self.fieldName.textChanged.connect(
            lambda newVal: self.parent.onRowChange(newVal, self.index, "name")
        )

    def addFieldDist(self, value, distRange):
        self.fieldDist = QDoubleSpinBoxWithFocus(self.widget)
        self.fieldDist.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
        self.fieldDist.setDecimals(0)
        self.fieldDist.setSingleStep(self.parent.pole_dist_step)
        self.fieldDist.setSuffix(" m")
        self.fieldDist.setMinimumWidth(self.SPINNER_WIDTH)
        self.fieldDist.setRange(float(distRange[0]), float(distRange[1]))
        self.fieldDist.setValue(float(value))
        # Add layout so the widget grows when the grid column grows
        hLayout = QHBoxLayout()
        hLayout.addWidget(self.fieldDist)
        self.layout.addLayout(
            hLayout, self.index + 1, 3, 1, 1, Qt.AlignmentFlag.AlignLeft
        )

        # Connect events
        self.fieldDist.inFocus.connect(lambda x: self.parent.zoomIn(self.index))
        self.fieldDist.outFocus.connect(self.parent.zoomOut)
        self.fieldDist.valueChanged.connect(
            lambda newVal: self.parent.onRowChange(newVal, self.index, "d")
        )

    def addFieldHeight(self, value):
        if value is False:
            return
        self.fieldHeight = QDoubleSpinBoxWithFocus(self.widget)
        self.fieldHeight.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
        self.fieldHeight.setDecimals(1)
        self.fieldHeight.setSingleStep(self.parent.pole_height_step)
        # Pole rows with type fixed are only used in profile window, so before
        #  optimization. That's why they only have 1 meter resolution.
        if self.rowType == "fixed":
            self.fieldHeight.setDecimals(0)
            self.fieldHeight.setSingleStep(1)
        self.fieldHeight.setSuffix(" m")
        self.fieldHeight.setMinimumWidth(self.SPINNER_WIDTH)
        self.fieldHeight.setRange(0.0, 50.0)
        if value is not None:
            self.fieldHeight.setValue(float(value))
        # Add layout so the widget grows when the grid column grows
        layout = QHBoxLayout()
        layout.addWidget(self.fieldHeight)
        self.layout.addLayout(
            layout, self.index + 1, 4, 1, 1, Qt.AlignmentFlag.AlignLeft
        )

        # Connect events
        self.fieldHeight.inFocus.connect(lambda x: self.parent.zoomIn(self.index))
        self.fieldHeight.outFocus.connect(self.parent.zoomOut)
        self.fieldHeight.valueChanged.connect(
            lambda newVal: self.parent.onRowChange(newVal, self.index, "h")
        )

    def addFieldAngle(self, value):
        if value is False:
            return
        self.fieldAngle = QSpinBoxWithFocus(self.widget)
        self.fieldAngle.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
        self.fieldAngle.setSuffix(" °")
        self.fieldAngle.setMinimumWidth(self.SPINNER_WIDTH - 20)
        self.fieldAngle.setRange(-180, 180)
        if value is not None:
            self.fieldAngle.setValue(int(value))
        # Add layout so the widget grows when the grid column grows
        layout = QHBoxLayout()
        layout.addWidget(self.fieldAngle)
        self.layout.addLayout(
            layout, self.index + 1, 5, 1, 1, Qt.AlignmentFlag.AlignLeft
        )

        # Connect events
        self.fieldAngle.inFocus.connect(lambda x: self.parent.zoomIn(self.index))
        self.fieldAngle.outFocus.connect(self.parent.zoomOut)
        self.fieldAngle.valueChanged.connect(
            lambda newVal: self.parent.onRowChange(newVal, self.index, "angle")
        )

    def addFieldBundstelle(self, value):
        if value is False:
            return
        self.fieldBundstelle = QLabel(self.widget)
        self.updateBundstelle(value)
        self.layout.addWidget(
            self.fieldBundstelle,
            self.index + 1,
            6,
            1,
            1,
            Qt.AlignmentFlag.AlignLeft,
        )

    def updateBundstelle(self, value):
        if self.fieldBundstelle:
            if value is False or value is nan:
                value = "-"
            else:
                value = f"{value} cm"
            self.fieldBundstelle.setText(value)

    def addBtnPlus(self, createButton):
        if createButton is False:
            return
        self.addBtn = QPushButton(self.widget)
        self.addBtn.setMaximumSize(QSize(27, 27))
        icon = QIcon()
        icon.addPixmap(
            QPixmap(getAbsoluteIconPath("icon_addrow.png")),
            QIcon.Mode.Normal,
            QIcon.State.Off,
        )
        self.addBtn.setIcon(icon)
        self.addBtn.setIconSize(QSize(16, 16))
        self.addBtn.setToolTip(self.tr("Fuegt eine neue Stuetze nach dieser hinzu"))
        self.addBtn.setAutoDefault(False)
        self.layout.addWidget(
            self.addBtn, self.index + 1, 0, 1, 1, Qt.AlignmentFlag.AlignLeft
        )

        self.addBtn.clicked.connect(lambda x: self.parent.onRowAdd(self.index))

    def addBtnDel(self, createButton):
        if createButton is False:
            return
        self.delBtn = QPushButton(self.widget)
        self.delBtn.setMaximumSize(QSize(27, 27))
        icon = QIcon()
        icon.addPixmap(
            QPixmap(getAbsoluteIconPath("icon_bin.png")),
            QIcon.Mode.Normal,
            QIcon.State.Off,
        )
        self.delBtn.setIcon(icon)
        self.delBtn.setIconSize(QSize(16, 16))
        self.delBtn.setToolTip(self.tr("Loescht die Stuetze"))
        self.delBtn.setAutoDefault(False)
        self.layout.addWidget(
            self.delBtn, self.index + 1, 7, 1, 1, Qt.AlignmentFlag.AlignLeft
        )

        self.delBtn.clicked.connect(lambda x: self.parent.onRowDel(self.index))

    def updateLowerDistRange(self, minimum):
        self.fieldDist.setMinimum(minimum)

    def updateUpperDistRange(self, maximum):
        self.fieldDist.setMaximum(maximum)

    def activate(self):
        self.statusSwitcher.blockSignals(True)
        self.statusSwitcher.setChecked(True)
        self.statusSwitcher.blockSignals(False)
        self.fieldName.setEnabled(True)
        self.fieldDist.setEnabled(True)

    def deactivate(self):
        self.statusSwitcher.blockSignals(True)
        self.statusSwitcher.setChecked(False)
        self.statusSwitcher.blockSignals(False)
        self.fieldName.setEnabled(False)
        self.fieldDist.setEnabled(False)

    def remove(self):
        # Disconnect all widgets
        for widget in [
            self.statusSwitcher,
            self.addBtn,
            self.labelNr,
            self.fieldName,
            self.fieldDist,
            self.fieldHeight,
            self.fieldAngle,
            self.fieldBundstelle,
            self.delBtn,
        ]:
            if widget:
                if not isinstance(widget, QLabel):
                    widget.disconnect()

                self.layout.removeWidget(widget)
                widget.deleteLater()

        # Remove layouts
        for column in reversed(range(self.layout.columnCount())):
            boxLayout = self.layout.itemAtPosition(self.index + 1, column)
            if boxLayout and isinstance(boxLayout, QHBoxLayout):
                self.layout.removeItem(boxLayout)

        self.parent.poleCount -= 1

    def tr(self, message, **kwargs):
        return QCoreApplication.translate("CustomPoleWidget", message)


class QLineEditWithFocus(QLineEdit):
    inFocus = pyqtSignal(bool)
    outFocus = pyqtSignal(bool)

    def focusInEvent(self, event):
        super(QLineEditWithFocus, self).focusInEvent(event)
        self.inFocus.emit(True)

    def focusOutEvent(self, event):
        super(QLineEditWithFocus, self).focusOutEvent(event)
        self.outFocus.emit(True)


class QDoubleSpinBoxWithFocus(QDoubleSpinBox):
    inFocus = pyqtSignal(bool)
    outFocus = pyqtSignal(bool)

    def focusInEvent(self, event):
        super(QDoubleSpinBoxWithFocus, self).focusInEvent(event)
        self.inFocus.emit(True)

    def focusOutEvent(self, event):
        super(QDoubleSpinBoxWithFocus, self).focusOutEvent(event)
        self.outFocus.emit(True)


class QSpinBoxWithFocus(QSpinBox):
    inFocus = pyqtSignal(bool)
    outFocus = pyqtSignal(bool)

    def focusInEvent(self, event):
        super(QSpinBoxWithFocus, self).focusInEvent(event)
        self.inFocus.emit(True)

    def focusOutEvent(self, event):
        super(QSpinBoxWithFocus, self).focusOutEvent(event)
        self.outFocus.emit(True)
