# -*- coding: utf-8 -*-
# noinspection PyPep8Naming
"""
/***************************************************************************
                              qps/maptools.py
        Re-implementation of *.cpp QgsMapTools for python and some more
                              -------------------
        begin                : 2019-01-20
        git sha              : $Format:%H$
        copyright            : (C) 2019 by benjamin jakimow
        email                : benjamin.jakimow@geo.hu-berlin.de
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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 3 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""

import enum
import math
import sys
from typing import Dict, Any, List, Optional

from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtCore import pyqtSignal, QTimer, QObject, QPoint, QRect, QSize, QEvent
from qgis.PyQt.QtGui import QColor, QKeyEvent, QIcon, QCursor
from qgis.PyQt.QtWidgets import QApplication, QAction, QLabel, QHBoxLayout, QWidget, QSizePolicy, QAbstractButton
from qgis.core import QgsRasterDataProvider, QgsSettingsRegistryCore, QgsSettingsEntryBool, \
    QgsWkbTypes, QgsVectorLayerTools, QgsProject, QgsVectorLayer, QgsPoint, QgsGeometry, \
    QgsCoordinateReferenceSystem, QgsPointXY, QgsFeature, QgsEditFormConfig, QgsMultiPoint, \
    QgsFeatureRequest, QgsExpressionContextUtils, QgsRenderContext, QgsCsException, QgsDistanceArea, QgsLineString, \
    QgsRectangle, QgsVectorLayerUtils, QgsVectorDataProvider, Qgis, QgsRasterLayer, QgsAction, QgsMapLayer, \
    QgsApplication, QgsPointLocator, QgsCurvePolygon, QgsPolygon, QgsFeatureIterator, QgsCoordinateTransform, \
    QgsExpressionContextScope
from qgis.gui import QgsAttributeEditorContext, \
    QgsMapTool, QgsMapToolZoom, QgsMapToolPan, QgsMapToolCapture, QgsMapToolIdentify, QgsMapToolEmitPoint, \
    QgsAttributeDialog, QgsIdentifyMenu, QgsSnapIndicator, QgsMapCanvas, QgsRubberBand, QgsMapMouseEvent, \
    QgsAttributeForm, \
    QgsUserInputWidget, QgsFloatingWidget, QgsAdvancedDigitizingDockWidget, \
    QgsDoubleSpinBox, \
    QgsVertexMarker, QgsHighlight
from .utils import SpatialPoint, SpatialExtent
from .vectorlayertools import VectorLayerTools


def tr(t: str) -> str:
    return t


def createCursor(resourcePath: str):
    """
    Creates a QCursor from a icon path
    :param resourcePath: str
    :return: QCursor
    """
    icon = QIcon(resourcePath)
    app = QgsApplication.instance()
    activeX = activeY = 13
    if icon.isNull():
        print('Unable to load icon from {}. Maybe resources not initialized?'.format(resourcePath))
    scale = Qgis.UI_SCALE_FACTOR * app.fontMetrics().height() / 32.
    size = QSize(int(scale * 32), int(scale * 32))
    cursor = QCursor(icon.pixmap(size), int(scale * activeX), int(scale * activeY))
    return cursor


def createQgsMapCanvasUserInputWidget(canvas: QgsMapCanvas) -> QgsUserInputWidget:
    """
    Create a QgsUserInputWidget that is linked to the top-right
    QgsMapCanvas corner (as in the QGIS Desktop main canvas).
    :param canvas: QgsMapCanvas
    :return: QgsUserInputWidget
    """
    if not (isinstance(canvas, QgsMapCanvas)):
        raise AssertionError
    mUserInputWidget = canvas.findChild(QgsUserInputWidget)
    if not isinstance(mUserInputWidget, QgsUserInputWidget):
        mUserInputWidget = QgsUserInputWidget(canvas)
        mUserInputWidget.setObjectName('UserInputDockWidget')
        mUserInputWidget.setAnchorWidget(canvas)
        mUserInputWidget.setAnchorWidgetPoint(QgsFloatingWidget.AnchorPoint.TopRight)
        mUserInputWidget.setAnchorPoint(QgsFloatingWidget.AnchorPoint.TopRight)
    return mUserInputWidget


class MapTools(enum.Enum):
    """
    Static class to support the creation of QgsMapTools.
    """
    # def __init__(self):
    #    raise Exception('This class is not for any instantiation')
    ZoomIn = 'ZOOM_IN'
    ZoomOut = 'ZOOM_OUT'
    ZoomFull = 'ZOOM_FULL'
    Pan = 'PAN'
    ZoomPixelScale = 'ZOOM_PIXEL_SCALE'
    CursorLocation = 'CURSOR_LOCATION'
    SpectralProfile = 'SPECTRAL_PROFILE'
    TemporalProfile = 'TEMPORAL_PROFILE'
    MoveToCenter = 'MOVE_CENTER'
    AddFeature = 'ADD_FEATURE'
    SelectFeature = 'SELECT_FEATURE'
    SelectFeatureByPolygon = 'SELECT_FEATURE_POLYGON'
    SelectFeatureByFreehand = 'SELECT_FEATURE_FREEHAND'
    SelectFeatureByRadius = 'SELECT_FEATURE_RADIUS'

    @staticmethod
    def toMapToolEnum(arg):
        if isinstance(arg, str):
            names = MapTools.mapToolNames()
            values = MapTools.mapToolValues()
            if arg in names:
                arg = MapTools.__members__.get(arg)
            elif arg in values:
                arg = MapTools.__members__.get(names[values.index(arg)])
        if not (isinstance(arg, MapTools)):
            raise AssertionError
        return arg

    @staticmethod
    def create(mapToolEnum, canvas, *args, activate=True, **kwds) -> QgsMapTool:
        """
        Creates
        :param mapToolEnum: str, identifies the requested QgsMapTool, e.g. 'ZOOM_IN'
        :param canvas: QgsMapCanvas to set the QgsMapTool on
        :param activate: bool, set True (default) to set the QgsMapTool to the QgsMapCanvas `canvas`
        :param args: optional arguments
        :param kwds: optional keywords
        :return: QgsMapTool
        """

        mapToolEnum = MapTools.toMapToolEnum(mapToolEnum)

        if not (isinstance(mapToolEnum, MapTools)):
            raise AssertionError
        if not (isinstance(canvas, QgsMapCanvas)):
            raise AssertionError

        mapTool = None
        if mapToolEnum == MapTools.ZoomIn:
            mapTool = QgsMapToolZoom(canvas, False)
        elif mapToolEnum == MapTools.ZoomOut:
            mapTool = QgsMapToolZoom(canvas, True)
        elif mapToolEnum == MapTools.Pan:
            mapTool = QgsMapToolPan(canvas)
        elif mapToolEnum == MapTools.ZoomPixelScale:
            mapTool = PixelScaleExtentMapTool(canvas)
        elif mapToolEnum == MapTools.ZoomFull:
            mapTool = FullExtentMapTool(canvas)
        elif mapToolEnum == MapTools.CursorLocation:
            mapTool = CursorLocationMapTool(canvas, *args, **kwds)
        elif mapToolEnum == MapTools.MoveToCenter:
            mapTool = CursorLocationMapTool(canvas, *args, **kwds)
            mapTool.sigLocationRequest.connect(lambda crs, pt, c=canvas: c.setCenter(pt))
        elif mapToolEnum == MapTools.SpectralProfile:
            mapTool = SpectralProfileMapTool(canvas, *args, **kwds)
        elif mapToolEnum == MapTools.TemporalProfile:
            mapTool = TemporalProfileMapTool(canvas, *args, **kwds)
        elif mapToolEnum == MapTools.AddFeature:
            mapTool = QgsMapToolAddFeature(canvas, *args, **kwds)
        elif mapToolEnum == MapTools.SelectFeature:
            mapTool = QgsMapToolSelect(canvas)
            mapTool.setSelectionMode(QgsMapToolSelectionHandler.SelectionMode.SelectSimple)
        elif mapToolEnum == MapTools.SelectFeatureByFreehand:
            mapTool = QgsMapToolSelect(canvas)
            mapTool.setSelectionMode(QgsMapToolSelectionHandler.SelectionMode.SelectFreehand)
        elif mapToolEnum == MapTools.SelectFeatureByPolygon:
            mapTool = QgsMapToolSelect(canvas)
            mapTool.setSelectionMode(QgsMapToolSelectionHandler.SelectionMode.SelectPolygon)
        elif mapToolEnum == MapTools.SelectFeatureByRadius:
            mapTool = QgsMapToolSelect(canvas)
            mapTool.setSelectionMode(QgsMapToolSelectionHandler.SelectionMode.SelectRadius)
        else:
            raise NotImplementedError('Unknown MapTool "{}"'.format(mapToolEnum))

        if activate:
            canvas.setMapTool(mapTool)

        return mapTool

    @staticmethod
    def mapToolKeys() -> list:
        import warnings
        warnings.warn('Deprecated. use .mapToolValues() instead', DeprecationWarning, stacklevel=2)
        return MapTools.mapToolValues()

    @staticmethod
    def mapToolNames() -> list:
        return [k.name for k in list(MapTools)]

    @staticmethod
    def mapToolValues() -> list:
        return [k.value for k in list(MapTools)]

    @staticmethod
    def mapToolEnums() -> list:
        return list(MapTools.__members__.values())


class CursorLocationMapTool(QgsMapToolEmitPoint):
    """
    A QgsMapTool to collect SpatialPoints
    """
    sigLocationRequest = pyqtSignal(QgsCoordinateReferenceSystem, QgsPointXY)

    def __init__(self, canvas: QgsMapCanvas, showCrosshair: bool = True):
        """
        :param canvas: QgsMapCanvas
        :param showCrosshair: bool, if True (default), a crosshair appears for some milliseconds to highlight
            the selected location
        """
        self.mShowCrosshair = showCrosshair

        self.mCrosshairTime = 250

        QgsMapToolEmitPoint.__init__(self, canvas)
        self.marker = QgsVertexMarker(self.canvas())
        self.rubberband = QgsRubberBand(self.canvas(), QgsWkbTypes.GeometryType.PolygonGeometry)

        color = QColor('red')
        self.mButtons = [Qt.MouseButton.LeftButton]
        self.rubberband.setLineStyle(Qt.PenStyle.SolidLine)
        self.rubberband.setColor(color)
        self.rubberband.setWidth(2)

        self.marker.setColor(color)
        self.marker.setPenWidth(3)
        self.marker.setIconSize(5)
        self.marker.setIconType(QgsVertexMarker.IconType.ICON_CROSS)  # or ICON_CROSS, ICON_X
        self.hideRubberband()

    def flags(self) -> QgsMapTool.Flags:
        return QgsMapTool.Flag.ShowContextMenu

    def setMouseButtons(self, listOfButtons):
        if not (isinstance(listOfButtons)):
            raise AssertionError
        self.mButtons = listOfButtons

    def canvasPressEvent(self, e):
        if not (isinstance(e, QgsMapMouseEvent)):
            raise AssertionError
        if e.button() in self.mButtons:
            geoPoint = self.toMapCoordinates(e.pos())
            self.marker.setCenter(geoPoint)

    def setStyle(self, color=None, brushStyle=None, fillColor=None, lineStyle=None):
        """
        Sets the Croshsair style
        :param color:
        :param brushStyle:
        :param fillColor:
        :param lineStyle:
        :return:
        """
        if color:
            self.rubberband.setColor(color)
        if brushStyle:
            self.rubberband.setBrushStyle(brushStyle)
        if fillColor:
            self.rubberband.setFillColor(fillColor)
        if lineStyle:
            self.rubberband.setLineStyle(lineStyle)

    def canvasReleaseEvent(self, e):

        if e.button() in self.mButtons:

            pixelPoint = e.pixelPoint()
            crs = self.canvas().mapSettings().destinationCrs()
            self.marker.hide()
            geoPoint: QgsPointXY = self.toMapCoordinates(pixelPoint)
            if self.mShowCrosshair:
                # show a temporary crosshair
                ext = SpatialExtent.fromMapCanvas(self.canvas())
                cen = geoPoint
                geom = QgsGeometry()
                lineH = QgsLineString(
                    [QgsPoint(ext.upperLeftPt().x(), cen.y()), QgsPoint(ext.lowerRightPt().x(), cen.y())])
                lineV = QgsLineString(
                    [QgsPoint(cen.x(), ext.upperLeftPt().y()), QgsPoint(cen.x(), ext.lowerRightPt().y())])

                if Qgis.versionInt() >= 33800:
                    geom.addPartV2(lineH, Qgis.WkbType.LineString)
                    geom.addPartV2(lineV, Qgis.WkbType.LineString)
                else:
                    geom.addPart(lineH, QgsWkbTypes.GeometryType.LineGeometry)
                    geom.addPart(lineV, QgsWkbTypes.GeometryType.LineGeometry)
                self.rubberband.addGeometry(geom, None)
                self.rubberband.show()

                # remove crosshair after a short while
                QTimer.singleShot(self.mCrosshairTime, self.hideRubberband)

            self.sigLocationRequest[QgsCoordinateReferenceSystem, QgsPointXY].emit(crs, geoPoint)

    def hideRubberband(self):
        """
        Hides the rubberband
        """
        self.rubberband.reset()


class MapToolCenter(CursorLocationMapTool):
    """
    This map tool centers a QgsMapCanvas on the clicked position
    """

    def __init__(self, canvas: QgsMapCanvas):
        """
        Constructor.
        :param canvas: QgsMapCanvas
        :type canvas: QgsMapCanvas
        """
        super(MapToolCenter, self).__init__(canvas)
        self.sigLocationRequest[QgsCoordinateReferenceSystem, QgsPointXY]. \
            connect(lambda crs, pt: self.setMapCenter(SpatialPoint(crs, pt)))

    def flags(self) -> QgsMapTool.Flags:
        return QgsMapTool.Flag.ShowContextMenu

    def setMapCenter(self, point: SpatialPoint):
        """
        Call to center the linked QgsMapCanvas to a point
        :param point: SpatialPoint to center the map canvas to.
        """
        if not (isinstance(point, SpatialPoint)):
            raise AssertionError
        point = point.toCrs(self.canvas().mapSettings().destinationCrs())
        if isinstance(point, SpatialPoint):
            self.canvas().setCenter(point)
            self.canvas().refresh()


class PixelScaleExtentMapTool(QgsMapTool):
    """
    A QgsMapTool to scale the QgsMapCanvas to the pixel resolution of a selected QgsRasterLayer pixel.
    """

    def __init__(self, canvas):
        super(PixelScaleExtentMapTool, self).__init__(canvas)

        self.mCursor = createCursor(':/qps/ui/icons/cursor_zoom_pixelscale.svg')
        self.setCursor(self.mCursor)
        canvas.setCursor(self.mCursor)

    def flags(self) -> QgsMapTool.Flags:
        """
        :return:
        """
        return QgsMapTool.Flag.Transient | QgsMapTool.Flag.ShowContextMenu

    def canvasReleaseEvent(self, mouseEvent: QgsMapMouseEvent):
        """

        :param mouseEvent:
        :return:
        """
        crs = self.canvas().mapSettings().destinationCrs()
        pt = SpatialPoint(crs, mouseEvent.mapPoint())

        for lyr in self.canvas().layers():
            if isinstance(lyr, QgsRasterLayer) and lyr.extent().contains(pt.toCrs(lyr.crs())):
                self.setRasterLayer(lyr)
                break

    def setRasterLayer(self, layer: QgsRasterLayer):
        if not isinstance(layer, QgsRasterLayer):
            return
        # reimplementation of QGIS\src\app\qgisapp.cpp
        canvas = self.canvas()
        nativeResolutions = None
        if isinstance(layer.dataProvider(), QgsRasterDataProvider):
            nativeResolutions = layer.dataProvider().nativeResolutions()

        # get length of central canvas pixel width in source raster crs
        e = canvas.extent()
        s = canvas.mapSettings().outputSize()
        p1 = QgsPointXY(e.center().x(), e.center().y())
        p2 = QgsPointXY(e.center().x() + e.width() / s.width(), e.center().y() + e.height() / s.height())
        ct = QgsCoordinateTransform(canvas.mapSettings().destinationCrs(), layer.crs(), QgsProject.instance())
        p1 = ct.transform(p1)
        p2 = ct.transform(p2)
        # width (actually the diagonal) of reprojected pixel
        diagonalSize = math.sqrt(p1.sqrDist(p2))

        if len(nativeResolutions) > 0:
            # find the closest native resolution
            diagonalNativeResolutionsDiff = [abs(diagonalSize - math.sqrt(2 * d * d)) for d in nativeResolutions]
            i = diagonalNativeResolutionsDiff.index(min(diagonalNativeResolutionsDiff))
            res = nativeResolutions[i]
            canvas.zoomByFactor(res / canvas.mapUnitsPerPixel())
        else:
            canvas.zoomByFactor(
                math.sqrt(layer.rasterUnitsPerPixelX() ** 2 + layer.rasterUnitsPerPixelY() ** 2) / diagonalSize)


class FullExtentMapTool(QgsMapTool):
    """
    A QgsMapTool to scale a QgsMapCanvas to the full extent of all available QgsMapLayers.
    """

    def __init__(self, canvas):
        super(FullExtentMapTool, self).__init__(canvas)
        self.mCursor = createCursor(':/qps/ui/icons/cursor_zoom_fullextent.svg')
        self.setCursor(self.mCursor)
        canvas.setCursor(self.mCursor)

    def canvasReleaseEvent(self, mouseEvent):
        self.canvas().zoomToFullExtent()

    def flags(self):
        return QgsMapTool.Flag.Transient | QgsMapTool.Flag.ShowContextMenu


class PointLayersMapTool(CursorLocationMapTool):

    def __init__(self, canvas):
        super(PointLayersMapTool, self).__init__(self, canvas)
        self.layerType = QgsMapToolIdentify.Type.AllLayers
        self.identifyMode = QgsMapToolIdentify.IdentifyMode.LayerSelection
        QgsMapToolIdentify.__init__(self, canvas)

    def flags(self) -> QgsMapTool.Flags:
        return QgsMapTool.Flag.ShowContextMenu


class SpatialExtentMapTool(QgsMapToolEmitPoint):
    """
    A QgsMapTool to select a SpatialExtent
    """
    sigSpatialExtentSelected = pyqtSignal(QgsCoordinateReferenceSystem, QgsRectangle)

    def __init__(self, canvas: QgsMapCanvas):
        super(SpatialExtentMapTool, self).__init__(canvas)
        self.isEmittingPoint = False
        self.rubberBand = QgsRubberBand(self.canvas(), QgsWkbTypes.GeometryType.PolygonGeometry)
        self.setStyle(Qt.GlobalColor.red, 1)
        self.reset()

    def flags(self) -> QgsMapTool.Flags:
        return QgsMapTool.Flag.ShowContextMenu

    def setStyle(self, color: QColor, width: int):
        """
        Sets the style of the rectangle shows when selecting the SpatialExtent
        :param color: QColor
        :param width: int
        """
        self.rubberBand.setColor(color)
        self.rubberBand.setWidth(width)

    def reset(self):
        """
        Removes the drawn rectangle
        """
        self.startPoint = self.endPoint = None
        self.isEmittingPoint = False
        self.rubberBand.reset(QgsWkbTypes.GeometryType.PolygonGeometry)

    def canvasPressEvent(self, e):
        self.startPoint = self.toMapCoordinates(e.pos())
        self.endPoint = self.startPoint
        self.isEmittingPoint = True
        self.showRect(self.startPoint, self.endPoint)

    def canvasReleaseEvent(self, e):
        self.isEmittingPoint = False

        crs = self.canvas().mapSettings().destinationCrs()
        rect = self.rectangle()

        self.reset()

        if crs is not None and rect is not None:
            self.sigSpatialExtentSelected[QgsCoordinateReferenceSystem, QgsRectangle].emit(crs, rect)

    def canvasMoveEvent(self, e):
        if not self.isEmittingPoint:
            return

        self.endPoint = self.toMapCoordinates(e.pos())
        self.showRect(self.startPoint, self.endPoint)

    def showRect(self, startPoint, endPoint):
        self.rubberBand.reset(QgsWkbTypes.GeometryType.PolygonGeometry)
        if startPoint.x() == endPoint.x() or startPoint.y() == endPoint.y():
            return

        point1 = QgsPointXY(startPoint.x(), startPoint.y())
        point2 = QgsPointXY(startPoint.x(), endPoint.y())
        point3 = QgsPointXY(endPoint.x(), endPoint.y())
        point4 = QgsPointXY(endPoint.x(), startPoint.y())

        self.rubberBand.addPoint(point1, False)
        self.rubberBand.addPoint(point2, False)
        self.rubberBand.addPoint(point3, False)
        self.rubberBand.addPoint(point4, True)  # true to update canvas
        self.rubberBand.show()

    def rectangle(self):
        if self.startPoint is None or self.endPoint is None:
            return None
        elif self.startPoint.x() == self.endPoint.x() or self.startPoint.y() == self.endPoint.y():
            return None
        return QgsRectangle(self.startPoint, self.endPoint)


class RectangleMapTool(QgsMapToolEmitPoint):
    rectangleDrawed = pyqtSignal(QgsRectangle, str)

    def __init__(self, canvas):

        QgsMapToolEmitPoint.__init__(self, canvas)
        self.rubberBand = QgsRubberBand(self.canvas(), QgsWkbTypes.GeometryType.PolygonGeometry)
        self.rubberBand.setColor(Qt.GlobalColor.red)
        self.rubberBand.setWidth(1)
        self.reset()

    def flags(self) -> QgsMapTool:
        return QgsMapTool.Flag.ShowContextMenu

    def reset(self):
        self.startPoint = self.endPoint = None
        self.isEmittingPoint = False
        self.rubberBand.reset(QgsWkbTypes.GeometryType.PolygonGeometry)

    def canvasPressEvent(self, e):
        self.startPoint = self.toMapCoordinates(e.pos())
        self.endPoint = self.startPoint
        self.isEmittingPoint = True
        self.showRect(self.startPoint, self.endPoint)

    def canvasReleaseEvent(self, e):
        self.isEmittingPoint = False

        wkt = self.canvas().mapSettings().destinationCrs().toWkt()
        r = self.rectangle()
        self.reset()

        if wkt is not None and r is not None:
            self.rectangleDrawed.emit(r, wkt)

    def canvasMoveEvent(self, e):

        if not self.isEmittingPoint:
            return

        self.endPoint = self.toMapCoordinates(e.pos())
        self.showRect(self.startPoint, self.endPoint)

    def showRect(self, startPoint, endPoint):
        self.rubberBand.reset(QgsWkbTypes.GeometryType.PolygonGeometry)
        if startPoint.x() == endPoint.x() or startPoint.y() == endPoint.y():
            return

        point1 = QgsPointXY(startPoint.x(), startPoint.y())
        point2 = QgsPointXY(startPoint.x(), endPoint.y())
        point3 = QgsPointXY(endPoint.x(), endPoint.y())
        point4 = QgsPointXY(endPoint.x(), startPoint.y())

        self.rubberBand.addPoint(point1, False)
        self.rubberBand.addPoint(point2, False)
        self.rubberBand.addPoint(point3, False)
        self.rubberBand.addPoint(point4, True)  # true to update canvas
        self.rubberBand.show()

    def rectangle(self):
        if self.startPoint is None or self.endPoint is None:
            return None
        elif self.startPoint.x() == self.endPoint.x() or self.startPoint.y() == self.endPoint.y():

            return None

        return QgsRectangle(self.startPoint, self.endPoint)


class TemporalProfileMapTool(CursorLocationMapTool):
    def __init__(self, *args, **kwds):
        super(TemporalProfileMapTool, self).__init__(*args, **kwds)


class SpectralProfileMapTool(CursorLocationMapTool):
    def __init__(self, *args, **kwds):
        super(SpectralProfileMapTool, self).__init__(*args, **kwds)


class QgsFeatureAction(QAction):
    """
    This is a python copy of the qgis/app/QgsFeatureAction.cpp
    """
    sLastUsedValues: Dict[str, Dict[int, Any]] = dict()

    def __init__(self, name: str, f: QgsFeature, layer: QgsVectorLayer,
                 actionID: id = '',
                 defaultAttr: int = -1,
                 parent: QObject = None):

        super(QgsFeatureAction, self).__init__(name, parent)

        if not (isinstance(layer, QgsVectorLayer)):
            raise AssertionError
        self.mLayer = layer
        self.mFeature = f
        self.mActionId = actionID
        self.mIdx = defaultAttr
        self.mFeatureSaved = False
        self.mForceSuppressFormPopup = False
        self.mAttributeEditorContext = QgsAttributeEditorContext()
        self.mVectorLayerTools = VectorLayerTools()

    def setVectorLayerTools(self, tools: QgsVectorLayerTools):
        if not (isinstance(VectorLayerTools, QgsVectorLayerTools)):
            raise AssertionError
        self.mVectorLayerTools = tools

    def setAttributeEditorContext(self, context: QgsAttributeEditorContext):
        if not (isinstance(context, QgsAttributeEditorContext)):
            raise AssertionError
        self.mAttributeEditorContext = context

    def execute(self):
        self.mLayer.actions().doAction(self.mActionId, self.mFeature, self.mIdx)

    def newDialog(self, cloneFeatures: bool) -> QgsAttributeDialog:
        """
        Creates a new dialog
        :param cloneFeatures: bool
        :return: QgsAttributeDialog
        """
        f = QgsFeature(self.mFeature) if cloneFeatures else self.mFeature

        myDa = QgsDistanceArea()
        myDa.setSourceCrs(self.mLayer.crs(), QgsProject.instance().transformContext())
        myDa.setEllipsoid(QgsProject.instance().ellipsoid())
        context = QgsAttributeEditorContext(self.mAttributeEditorContext)
        context.setDistanceArea(myDa)
        context.setVectorLayerTools(self.mVectorLayerTools)
        # context.setMapCanvas()
        context.setFormMode(QgsAttributeEditorContext.FormMode.StandaloneDialog)

        dialog = QgsAttributeDialog(self.mLayer, f, cloneFeatures, self.parentWidget(), True, context)
        dialog.setWindowFlags(dialog.windowFlags() | Qt.WindowType.Tool)
        dialog.setObjectName('featureaction {} {}'.format(self.mLayer.id(), f.id()))

        actions = self.mLayer.actions().actions("Feature")
        if len(actions) == 0:
            dialog.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)

        a = QAction(tr("Run Actions"), dialog)
        a.setEnabled(False)
        dialog.addAction(a)

        for action in actions:
            if not (isinstance(action, QgsAction)):
                raise AssertionError
            if not action.runable():
                continue

            if not self.mLayer.isEditable() and action.isEnabledOnlyWhenEditable():
                continue

            feat = dialog.feature()
            a = QgsFeatureAction(action.name(), feat, self.mLayer, action.id(), -1, dialog)
            dialog.addAction(a)
            a.triggered.connect(a.execute)

            pb = dialog.findChild(QAbstractButton, action.name())
            if isinstance(pb, QAbstractButton):
                pb.clicked.connect(a.execute)

        return dialog

    def viewFeatureForm(self, h: QgsHighlight) -> bool:

        if not self.mLayer or not self.mFeature:
            return False

        _ = "featureactiondlg:{}:{}".format(self.mLayer.id(), self.mFeature.id())

        dialog = self.newDialog(True)
        dialog.setHighlight(h)
        # // delete the dialog when it is closed
        dialog.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
        dialog.show()

        return True

    def editFeature(self, showModal: bool) -> bool:

        if not self.mLayer:
            return False

        if showModal:

            dialog = self.newDialog(False)

            if not self.mFeature.isValid():
                dialog.setMode(QgsAttributeEditorContext.Mode.AddFeatureMode)

            rv = dialog.exec()
            self.mFeature.setAttributes(dialog.feature().attributes())
            return rv

        else:

            _ = "featureactiondlg:{}:{}".format(self.mLayer.id(), self.mFeatureid())

            # QgsAttributeDialog *dialog = QgisApp::instance() -> findChild<QgsAttributeDialog *>( name );
            # if ( dialog )
            # {
            #  dialog->raise();
            #  return true;
            # }

            dialog = self.newDialog(False)

            if not self.mFeature.isValid():
                dialog.setMode(QgsAttributeEditorContext.Mode.AddFeatureMode)

            # // delete the dialog when it is closed
            dialog.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
            dialog.show()

        return True

    def addFeature(self,
                   defaultAttributeValues: dict = dict(),
                   showModal: bool = True,
                   scope: QgsExpressionContextScope = None) -> bool:

        if not (isinstance(self.mLayer, QgsVectorLayer) and self.mLayer.isEditable()):
            return

        qgsSettingsRegistry = QgsSettingsRegistryCore()
        initialAttributeValues = dict()
        reuseLastValues = False
        entry = qgsSettingsRegistry.settingsEntry('qgis/digitizing/reuseLastValues')
        if isinstance(entry, QgsSettingsEntryBool):
            reuseLastValues = entry.value()

        lyr: QgsVectorLayer = self.mLayer
        for idx in range(lyr.fields().count()):

            if idx in defaultAttributeValues.keys():
                initialAttributeValues[idx] = defaultAttributeValues[idx]
            elif (
                (reuseLastValues or lyr.editFormConfig().reuseLastValue(idx))
                and self.mLayer.id() in self.sLastUsedValues.keys()  # noqa: W503
                and idx in self.sLastUsedValues[lyr.id()].keys()  # noqa: W503
            ):

                lastUsed = self.sLastUsedValues[lyr.id()][idx]
                """
                // Only set initial attribute value if it's different from the default clause or we may trigger
                // unique constraint checks for no reason, see https://github.com/qgis/QGIS/issues/42909
                """
                if lyr.dataProvider() and lyr.dataProvider().defaultValueClause(idx) != lastUsed:
                    initialAttributeValues[idx] = lastUsed

        fields = self.mLayer.fields()

        context = self.mLayer.createExpressionContext()
        if scope:
            context.appendScope(scope)

        newFeature = QgsVectorLayerUtils.createFeature(self.mLayer, self.mFeature.geometry(), initialAttributeValues,
                                                       context)

        self.mFeature = newFeature

        isDisabledAttributesValueDlg = False
        entry = qgsSettingsRegistry.settingsEntry('qgis/digitizing/disable_enter_attribute_values_dialog')
        if isinstance(entry, QgsSettingsEntryBool):
            isDisabledAttributesValueDlg = entry.value()

        if not self.mLayer.isSpatial():
            isDisabledAttributesValueDlg = False

        if fields.count() == 0:
            isDisabledAttributesValueDlg = True

        opt = self.mLayer.editFormConfig().suppress()
        if opt == QgsEditFormConfig.FeatureFormSuppress.SuppressOn:
            isDisabledAttributesValueDlg = True
        elif opt == QgsEditFormConfig.FeatureFormSuppress.SuppressOff:
            isDisabledAttributesValueDlg = False

        if self.mForceSuppressFormPopup:
            isDisabledAttributesValueDlg = True

        if isDisabledAttributesValueDlg:
            self.mLayer.beginEditCommand(self.text())
            self.mFeatureSaved = self.mLayer.addFeature(self.mFeature)
            if self.mFeatureSaved:
                self.mLayer.endEditCommand()
                self.mLayer.triggerRepaint()
            else:
                self.mLayer.destroyEditComand()
        else:

            dialog = self.newDialog(False)
            dialog.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
            dialog.setMode(QgsAttributeEditorContext.Mode.AddFeatureMode)
            dialog.setEditCommandMessage(self.text())

            dialog.attributeForm().featureSaved.connect(
                lambda f, form=dialog.attributeForm(): self.onFeatureSaved(f, form))
            self._d = dialog
            if not showModal:
                self.setParent(dialog)
                dialog.show()

                self.mFeature = None
                return True

            dialog.exec()

        return self.mFeatureSaved

    def setForceSuppressFormPopup(self, force: bool):
        self.mForceSuppressFormPopup = force

    def onFeatureSaved(self, feature: QgsFeature, form: QgsAttributeForm):
        form = self.sender()
        if not isinstance(form, QgsAttributeForm):
            return

        # // Assign provider generated values
        if self.mFeature:
            self.mFeature = feature

        self.mFeatureSaved = True

        qgsSettingsRegistry = QgsSettingsRegistryCore()

        reuseLastValues = False
        entry = qgsSettingsRegistry.settingsEntry('qgis/digitizing/reuseLastValues')
        if isinstance(entry, QgsSettingsEntryBool):
            reuseLastValues = entry.value()

        lyr = self.mLayer

        if reuseLastValues:
            fields = lyr.fields()
            origValues: Dict[int, Any] = self.sLastUsedValues.get(lyr.id(), dict())
            newValues: List = feature.attributes()
            for idx in range(fields.count()):
                origValues[idx] = newValues[idx]
            self.sLastUsedValues[lyr.id()] = origValues


class QgsMapToolDigitizeFeature(QgsMapToolCapture):
    digitizingCompleted = pyqtSignal(QgsFeature)
    digitizingFinished = pyqtSignal()

    def __init__(self,
                 canvas: QgsMapCanvas,
                 layer: QgsMapLayer,
                 cadDockWidget: QgsAdvancedDigitizingDockWidget,
                 mode: QgsMapToolCapture.CaptureMode = QgsMapToolCapture.CaptureMode.CaptureNone,
                 vectorLayerTools: QgsVectorLayerTools = None):

        super(QgsMapToolDigitizeFeature, self).__init__(canvas, cadDockWidget, mode)

        self.mCheckGeometryType = True
        self.mLayer = layer
        self.mCurrentLayer: QgsMapLayer = None
        self.mVectorLayerTools = vectorLayerTools

    def setVectorLayerTools(self, vectorLayerTools: QgsVectorLayerTools):
        self.mVectorLayerTools = vectorLayerTools

    def digitized(self, f: QgsFeature):
        self.digitizingCompleted.emit(f)

    def activate(self):
        vlayer = self.mLayer
        if not isinstance(self.mLayer, QgsVectorLayer):
            vlayer = self.currentVectorLayer()

        if isinstance(vlayer, QgsVectorLayer) and vlayer.geometryType() == QgsWkbTypes.GeometryType.NullGeometry:
            f = QgsFeature()
            self.digitized(f)
            return

        if self.mLayer:
            # //remember current layer
            self.mCurrentLayer = self.canvas().currentLayer()
            # //set the layer with the given
            self.canvas().setCurrentLayer(self.mLayer)

        super(QgsMapToolDigitizeFeature, self).activate()

    def deactivate(self):
        super(QgsMapToolDigitizeFeature, self).deactivate()

        if self.mCurrentLayer:
            # //set the layer back to the one remembered
            self.canvas().setCurrentLayer(self.mCurrentLayer)
            self.mCurrentLayer = None
            self.digitizingFinished.emit()

    def checkGeometryType(self) -> bool:
        return self.mCheckGeometryType

    def setCheckGeometryType(self, checkGeometryType: bool):

        self.mCheckGeometryType = checkGeometryType

    def cadCanvasReleaseEvent(self, e: QgsMapMouseEvent):

        vlayer = self.mLayer
        if not isinstance(vlayer, QgsVectorLayer):
            # //if no given layer take the current from canvas
            vlayer = self.currentVectorLayer()

        if not isinstance(vlayer, QgsVectorLayer):
            self.notifyNotVectorLayer()
            return

        layerWKBType = vlayer.wkbType()
        provider = vlayer.dataProvider()

        if not (provider.capabilities() & QgsVectorDataProvider.Capability.AddFeatures):
            self.messageEmitted.emit("The data provider for this layer does not support the addition of features.",
                                     Qgis.MessageLevel.Warning)

            return

        if not vlayer.isEditable():
            self.notifyNotEditableLayer()
            return

        # // POINT CAPTURING
        if self.mode() == self.CapturePoint:

            if e.button() != Qt.MouseButton.LeftButton:
                return

            # //check we only use this tool for point/multipoint layers
            if vlayer.geometryType() != QgsWkbTypes.GeometryType.PointGeometry and self.mCheckGeometryType:
                self.messageEmitted.emit(
                    "Wrong editing tool, cannot apply the 'capture point' tool on this vector layer",
                    Qgis.MessageLevel.Warning
                )
                return

            savePoint = None  # ; //point in layer coordinates
            isMatchPointZ = False
            try:

                fetchPoint = QgsPoint()
                res = self.fetchLayerPoint(e.mapPointMatch(), fetchPoint)
                if QgsWkbTypes.hasZ(fetchPoint.wkbType()):
                    isMatchPointZ = True

                if res == 0:

                    if isMatchPointZ:
                        savePoint = fetchPoint
                    else:
                        savePoint = QgsPoint(fetchPoint.x(), fetchPoint.y())

                else:

                    layerPoint = self.toLayerCoordinates(vlayer, e.mapPoint())
                    if isMatchPointZ:
                        savePoint = QgsPoint(QgsWkbTypes.Type.PointZ, layerPoint.x(), layerPoint.y(), fetchPoint.z())
                    else:
                        savePoint = QgsPoint(layerPoint.x(), layerPoint.y())
            except QgsCsException:
                self.messageEmitted.emit("Cannot transform the point to the layers coordinate system",
                                         Qgis.MessageLevel.Warning)
                return

            # //only do the rest for provider with feature addition support
            # //note that for the grass provider, this will return false since
            # //grass provider has its own mechanism of feature addition
            if provider.capabilities() & QgsVectorDataProvider.Capability.AddFeatures:

                f = QgsFeature(vlayer.fields(), 0)

                g = None
                if layerWKBType == QgsWkbTypes.Type.Point:
                    g = QgsGeometry(savePoint)
                elif QgsWkbTypes.isMultiType(layerWKBType) and not QgsWkbTypes.hasZ(layerWKBType):
                    # g = QgsGeometry::fromMultiPointXY( QgsMultiPointXY() << savePoint );
                    # g = QgsGeometry.fromMultiPointXY(savePoint)
                    g = QgsGeometry(savePoint)
                elif not QgsWkbTypes.isMultiType(layerWKBType) and QgsWkbTypes.hasZ(layerWKBType):
                    g = QgsGeometry(QgsPoint(savePoint.x(), savePoint.y(),
                                             savePoint.z() if isMatchPointZ else self.defaultZValue()))
                elif QgsWkbTypes.isMultiType(layerWKBType) and not QgsWkbTypes.hasZ(layerWKBType):
                    mp = QgsMultiPoint()
                    mp.addGeometry(savePoint)
                    g = QgsGeometry()
                    g.set(mp)

                elif QgsWkbTypes.isMultiType(layerWKBType) and QgsWkbTypes.hasZ(layerWKBType):

                    mp = QgsMultiPoint()
                    mp.addGeometry(QgsPoint(QgsWkbTypes.Type.PointZ, savePoint.x(), savePoint.y(),
                                            savePoint.z() if isMatchPointZ else self.defaultZValue()))
                    g = QgsGeometry()
                    g.set(mp)
                else:
                    # // if layer supports more types (mCheckGeometryType is false)
                    g = QgsGeometry(QgsPoint(savePoint))

                if QgsWkbTypes.hasM(layerWKBType):
                    g.get().addMValue()

                f.setGeometry(g)
                f.setValid(True)

                self.digitized(f)

                # // we are done with digitizing for now so instruct advanced digitizing dock to reset its CAD points
                self.cadDockWidget().clearPoints()

        # // LINE AND POLYGON CAPTURING
        elif self.mode() == self.CaptureLine or self.mode() == self.CapturePolygon:

            # //check we only use the line tool for line/multiline layers
            if (
                self.mode() == self.CaptureLine
                and vlayer.geometryType() != QgsWkbTypes.GeometryType.LineGeometry  # noqa: W503
                and self.mCheckGeometryType  # noqa: W503
            ):
                self.messageEmitted.emit(
                    tr("Wrong editing tool, cannot apply the 'capture line' tool on this vector layer"),
                    Qgis.MessageLevel.Warning)
                return

            # //check we only use the polygon tool for polygon/multipolygon layers
            if (
                self.mode() == self.CapturePolygon
                and vlayer.geometryType() != QgsWkbTypes.GeometryType.PolygonGeometry  # noqa: W503
                and self.mCheckGeometryType  # noqa: W503
            ):
                self.messageEmitted.emit(
                    tr("Wrong editing tool, cannot apply the 'capture polygon' tool on this vector layer"),
                    Qgis.MessageLevel.Warning)
                return

            # //add point to list and to rubber band
            if e.button() == Qt.MouseButton.LeftButton:

                error = self.addVertex(e.mapPoint(), e.mapPointMatch())
                if error == 1:
                    # //current layer is not a vector layer
                    return

                elif error == 2:

                    # //problem with coordinate transformation
                    self.messageEmitted.emit(tr("Cannot transform the point to the layers coordinate system"),
                                             Qgis.MessageLevel.Warning)
                    return

                self.startCapturing()

            elif e.button() == Qt.MouseButton.RightButton:

                # // End of string
                self.deleteTempRubberBand()

                # //lines: bail out if there are not at least two vertices
                if self.mode() == self.CaptureLine and self.size() < 2:
                    self.stopCapturing()
                    return

                # //polygons: bail out if there are not at least two vertices
                if self.mode() == self.CapturePolygon and self.size() < 3:
                    self.stopCapturing()
                    return

                if self.mode() == self.CapturePolygon:
                    self.closePolygon()

                # //create QgsFeature with wkb representation
                f = QgsFeature(vlayer.fields(), 0)

                # //does compoundcurve contain circular strings?
                # //does provider support circular strings?
                hasCurvedSegments = self.captureCurve().hasCurvedSegments()
                providerSupportsCurvedSegments = \
                    vlayer.dataProvider().capabilities() & QgsVectorDataProvider.Capability.CircularGeometries

                _ = []
                curveToAdd = None
                if hasCurvedSegments and providerSupportsCurvedSegments:

                    curveToAdd = self.captureCurve().clone()

                else:

                    curveToAdd = self.captureCurve().curveToLine()

                if self.mode() == self.CaptureLine:

                    g = QgsGeometry(curveToAdd)
                    f.setGeometry(g)

                else:
                    poly = None
                    if hasCurvedSegments and providerSupportsCurvedSegments:
                        poly = QgsCurvePolygon()
                    else:
                        poly = QgsPolygon()

                    poly.setExteriorRing(curveToAdd)
                    g = QgsGeometry(poly)
                    f.setGeometry(g)

                    featGeom = f.geometry()
                    avoidIntersectionsReturn = featGeom.avoidIntersections(
                        QgsProject.instance().avoidIntersectionsLayers())
                    f.setGeometry(featGeom)
                    if avoidIntersectionsReturn == 1:
                        # //not a polygon type. Impossible to get there
                        pass

                    if f.geometry().isEmpty():  # //avoid intersection might have removed the whole geometry

                        self.messageEmitted.emit(tr(
                            "The feature cannot be added because it's geometry "
                            "collapsed due to intersection avoidance"),
                            Qgis.MessageLevel.Critical)
                        self.stopCapturing()
                        return

                f.setValid(True)

                self.digitized(f)

                self.stopCapturing()


class QgsMapToolAddFeature(QgsMapToolDigitizeFeature):

    def __init__(self,
                 canvas: QgsMapCanvas,
                 cadDockWidget: QgsAdvancedDigitizingDockWidget,
                 mode=QgsMapToolCapture.CaptureMode.CaptureNone,
                 vectorLayerTools: QgsVectorLayerTools = None):
        super(QgsMapToolAddFeature, self).__init__(canvas, canvas.currentLayer(), cadDockWidget, mode=mode,
                                                   vectorLayerTools=vectorLayerTools)
        self._d: QWidget = None
        self.setCheckGeometryType(True)
        QgsProject.instance().readProject.connect(self.stopCapturing)
        QgsProject.instance().projectSaved.connect(self.stopCapturing)
        QgsProject.instance().cleared.connect(self.stopCapturing)

    def addFeature(self, vlayer: QgsVectorLayer, f: QgsFeature, showModal: bool) -> bool:

        context = QgsAttributeEditorContext()
        context.setVectorLayerTools(self.mVectorLayerTools)
        context.setMapCanvas(self.canvas())
        context.setCadDockWidget(self.cadDockWidget())
        context.setFormMode(QgsAttributeEditorContext.FormMode.StandaloneDialog)
        scope = QgsExpressionContextUtils.mapToolCaptureScope(self.snappingMatches())
        action = QgsFeatureAction("add feature", f, vlayer, '', -1, self)
        action.setAttributeEditorContext(context)
        res = action.addFeature({}, showModal, scope)
        if showModal:
            del action
        return res

    def digitized(self, f: QgsFeature):

        vlayer = self.currentVectorLayer()
        res = self.addFeature(vlayer, f, False)

        if res and (self.mode() == self.CaptureLine or self.mode() == self.CapturePolygon):

            # //add points to other features to keep topology up-to-date
            topologicalEditing = QgsProject.instance().topologicalEditing()

            # //use always topological editing for avoidIntersection.
            # //Otherwise, no way to guarantee the geometries don't have a small gap in between.
            intersectionLayers = QgsProject.instance().avoidIntersectionsLayers()
            avoidIntersection = len(intersectionLayers) > 0
            if avoidIntersection:  # //try to add topological points also to background layers

                for vl in intersectionLayers:

                    # //can only add topological points if background layer is editable...
                    if vl.geometryType() == QgsWkbTypes.GeometryType.PolygonGeometry and vl.isEditable():
                        vl.addTopologicalPoints(f.geometry())

            elif topologicalEditing:

                vlayer.addTopologicalPoints(f.geometry())


class QgsDistanceWidget(QWidget):
    distanceChanged = pyqtSignal(float)
    distanceEditingCanceled = pyqtSignal()
    distanceEditingFinished = pyqtSignal(float, Qt.KeyboardModifiers)
    distanceEditingCanceled = pyqtSignal()

    def __init__(self, label: str, parent: QWidget = None):

        super(QgsDistanceWidget, self).__init__(parent)

        self.mLayout = QHBoxLayout(self)
        self.mLayout.setContentsMargins(0, 0, 0, 0)
        self.mLayout.setAlignment(Qt.AlignmentFlag.AlignLeft)
        self.setLayout(self.mLayout)

        if label:
            lbl = QLabel(label, self)
            lbl.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignCenter)
            self.mLayout.addWidget(lbl)

        self.mDistanceSpinBox = QgsDoubleSpinBox(self)
        self.mDistanceSpinBox.setSingleStep(1)
        self.mDistanceSpinBox.setValue(0)
        self.mDistanceSpinBox.setMinimum(0)
        self.mDistanceSpinBox.setMaximum(1000000000)
        self.mDistanceSpinBox.setDecimals(6)
        self.mDistanceSpinBox.setShowClearButton(False)
        self.mDistanceSpinBox.setSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Preferred)
        self.mLayout.addWidget(self.mDistanceSpinBox)

        # connect signals
        self.mDistanceSpinBox.installEventFilter(self)
        self.mDistanceSpinBox.valueChanged.connect(self.distanceChanged)

        # config focus
        self.setFocusProxy(self.mDistanceSpinBox)

    def setDistance(self, distance: float):

        self.mDistanceSpinBox.setValue(distance)
        self.mDistanceSpinBox.selectAll()

    def distance(self) -> float:
        return self.mDistanceSpinBox.value()

    def eventFilter(self, obj: QObject, ev: QEvent) -> bool:

        if obj == self.mDistanceSpinBox and ev.type() == QEvent.Type.KeyPress:

            event = QKeyEvent(ev)
            if event.key() == Qt.Key.Key_Escape:
                self.distanceEditingCanceled.emit()
                return False

            if event.key() == Qt.Key.Key_Enter or event.key() == Qt.Key.Key_Return:
                self.distanceEditingFinished.emit(self.distance(), event.modifiers())
                return True

        return False


class QgsMapToolSelectUtils(object):
    """
    Mimics the QgsMapToolSelectUtils C++ implementation from the not-python-accessible QGIS app code
    """

    @staticmethod
    def getCurrentVectorLayer(canvas: QgsMapCanvas) -> Optional[QgsVectorLayer]:

        vlayer = canvas.currentLayer()
        if isinstance(vlayer, QgsVectorLayer) and vlayer.isValid():
            return vlayer
        else:
            return None

    @staticmethod
    def setRubberBand(canvas: QgsMapCanvas, selectRect: QRect, rubberBand: QgsRubberBand):

        transform = canvas.getCoordinateTransform()
        ll = transform.toMapCoordinates(selectRect.left(), selectRect.bottom())
        lr = transform.toMapCoordinates(selectRect.right(), selectRect.bottom())
        ul = transform.toMapCoordinates(selectRect.left(), selectRect.top())
        ur = transform.toMapCoordinates(selectRect.right(), selectRect.top())

        if isinstance(rubberBand, QgsRubberBand):
            rubberBand.reset(QgsWkbTypes.GeometryType.PolygonGeometry)
            rubberBand.addPoint(ll, False)
            rubberBand.addPoint(lr, False)
            rubberBand.addPoint(ur, False)
            rubberBand.addPoint(ul, True)

    @staticmethod
    def expandSelectRectangle(mapPoint: QgsPointXY, canvas: QgsMapCanvas, vlayer: QgsVectorLayer):

        boxSize = 0
        if (not vlayer or vlayer.geometryType() != QgsWkbTypes.GeometryType.PolygonGeometry):

            # if point or line use an artificial bounding box of 10x10 pixels
            # to aid the user to click on a feature accurately
            boxSize = 5

        else:
            # otherwise just use the click point for polys
            boxSize = 1

        transform = canvas.getCoordinateTransform()
        point = transform.transform(mapPoint)
        ll = transform.toMapCoordinates(int(point.x() - boxSize), int(point.y() + boxSize))
        ur = transform.toMapCoordinates(int(point.x() + boxSize), int(point.y() - boxSize))
        return QgsRectangle(ll, ur)

    @staticmethod
    def selectMultipleFeatures(canvas: QgsMapCanvas, selectGeometry: QgsGeometry, modifiers: Qt.KeyboardModifiers):

        behavior = QgsVectorLayer.SelectBehavior.SetSelection
        if modifiers & Qt.KeyboardModifier.ShiftModifier and modifiers & Qt.KeyboardModifier.ControlModifier:
            behavior = QgsVectorLayer.SelectBehavior.IntersectSelection
        elif modifiers & Qt.KeyboardModifier.ShiftModifier:
            behavior = QgsVectorLayer.SelectBehavior.AddToSelection
        elif modifiers & Qt.KeyboardModifier.ControlModifier:
            behavior = QgsVectorLayer.SelectBehavior.RemoveFromSelection

        doContains = modifiers & Qt.KeyboardModifier.AltModifier
        QgsMapToolSelectUtils.setSelectedFeatures(canvas, selectGeometry, behavior, doContains)

    @staticmethod
    def selectSingleFeature(canvas: QgsMapCanvas, selectGeometry: QgsGeometry, modifiers: Qt.KeyboardModifiers):

        vlayer = QgsMapToolSelectUtils.getCurrentVectorLayer(canvas)
        if not isinstance(vlayer, QgsVectorLayer):
            return

        QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)

        selectedFeatures = QgsMapToolSelectUtils.getMatchingFeatures(canvas, selectGeometry, False, True)
        if len(selectedFeatures) == 0:

            if not (modifiers & Qt.KeyboardModifier.ShiftModifier or modifiers & Qt.KeyboardModifier.ControlModifier):
                # if no modifiers then clicking outside features clears the selection
                # but if there's a shift or ctrl modifier, then it's likely the user was trying
                # to modify an existing selection by adding or subtracting features and just
                # missed the feature
                vlayer.removeSelection()

            QApplication.restoreOverrideCursor()
            return

        behavior = QgsVectorLayer.SelectBehavior.SetSelection

        # either shift or control modifier switches to "toggle" selection mode
        if (
            modifiers & Qt.KeyboardModifier.ShiftModifier or modifiers & Qt.KeyboardModifier.ControlModifier
        ) and not Qt.KeyboardModifier.AltModifier:

            selectId = selectedFeatures[0]
            layerSelectedFeatures = vlayer.selectedFeatureIds()
            if selectId in layerSelectedFeatures:
                behavior = QgsVectorLayer.SelectBehavior.RemoveFromSelection
            else:
                behavior = QgsVectorLayer.SelectBehavior.AddToSelection

        vlayer.selectByIds(selectedFeatures, behavior)

        QApplication.restoreOverrideCursor()

    @staticmethod
    def setSelectedFeatures(canvas: QgsMapCanvas, selectGeometry: QgsGeometry,
                            selectBehavior: QgsVectorLayer.SelectBehavior,
                            doContains: bool = True,
                            singleSelect: bool = False):

        vlayer = QgsMapToolSelectUtils.getCurrentVectorLayer(canvas)
        if not isinstance(vlayer, QgsVectorLayer):
            return

        QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)

        selectedFeatures = QgsMapToolSelectUtils.getMatchingFeatures(canvas, selectGeometry, doContains, singleSelect)
        vlayer.selectByIds(selectedFeatures, selectBehavior)

        QApplication.restoreOverrideCursor()

    @staticmethod
    def getMatchingFeatures(canvas: QgsMapCanvas, selectGeometry: QgsGeometry, doContains: bool,
                            singleSelect: bool) -> list:

        newSelectedFeatures = []

        if selectGeometry.type() != QgsWkbTypes.GeometryType.PolygonGeometry:
            return newSelectedFeatures

        vlayer = QgsMapToolSelectUtils.getCurrentVectorLayer(canvas)
        if not isinstance(vlayer, QgsVectorLayer):
            return newSelectedFeatures

        # toLayerCoordinates will throw an exception for any 'invalid' points in
        # the rubber band.
        # For example, if you project a world map onto a globe using EPSG 2163
        # and then click somewhere off the globe, an exception will be thrown.
        selectGeomTrans = selectGeometry

        try:
            ct = QgsCoordinateTransform()
            ct.setSourceCrs(canvas.mapSettings().destinationCrs())
            ct.setDestinationCrs(vlayer.crs())
            ct.setContext(QgsProject.instance().transformContext())

            # todo: ...

            selectGeomTrans.transform(ct)

        except QgsCsException:
            # catch exception for 'invalid' point and leave existing selection unchanged
            return newSelectedFeatures

        context = QgsRenderContext.fromMapSettings(canvas.mapSettings())
        context.expressionContext().appendScope(QgsExpressionContextUtils.layerScope(vlayer))
        r = None
        if vlayer.renderer():
            r = vlayer.renderer().clone()
            r.startRender(context, vlayer.fields())

        request = QgsFeatureRequest()
        request.setFilterRect(selectGeomTrans.boundingBox())
        request.setFlags(QgsFeatureRequest.Flag.ExactIntersect)
        if r:
            request.setSubsetOfAttributes(r.usedAttributes(context), vlayer.fields())
        else:
            request.setNoAttributes()

        fit = vlayer.getFeatures(request)
        if not (isinstance(fit, QgsFeatureIterator)):
            raise AssertionError

        f = QgsFeature()
        closestFeatureId = 0
        foundSingleFeature = False
        # double closestFeatureDist = std::numeric_limits<double>::max();
        closestFeatureDist = sys.float_info.max
        while fit.nextFeature(f):
            context.expressionContext().setFeature(f)
            # // make sure to only use features that are visible
            if r and not r.willRenderFeature(f, context):
                continue

            g = f.geometry()
            if doContains:
                if not selectGeomTrans.contains(g):
                    continue
            else:
                if not selectGeomTrans.intersects(g):
                    continue

            if singleSelect:

                foundSingleFeature = True
                distance = g.distance(selectGeomTrans)
                if distance <= closestFeatureDist:
                    closestFeatureDist = distance
                    closestFeatureId = f.id()

            else:

                newSelectedFeatures.append(f.id())

        if singleSelect and foundSingleFeature:
            newSelectedFeatures.append(closestFeatureId)

        if r:
            r.stopRender(context)

        return newSelectedFeatures


class QgsMapToolSelectionHandler(QObject):
    """
    Mimics the QgsMapToolSelectionHandler C++ implementation
    """

    class SelectionMode(enum.Enum):
        SelectSimple = 0
        SelectPolygon = 1
        SelectFreehand = 2
        SelectRadius = 3

    geometryChanged = pyqtSignal(Qt.KeyboardModifiers)

    def __init__(self, canvas: QgsMapCanvas, selectionMode, parent: QObject = None):
        super(QgsMapToolSelectionHandler, self).__init__(parent=parent)
        self.mSelectionGeometry = None
        if not (isinstance(selectionMode, QgsMapToolSelectionHandler.SelectionMode)):
            raise AssertionError
        self.mCanvas = canvas
        if not (isinstance(canvas, QgsMapCanvas)):
            raise AssertionError

        self.mSelectionMode = selectionMode
        self.mSnapIndicator = QgsSnapIndicator(canvas)
        self.mIdentifyMenu = QgsIdentifyMenu(canvas)
        self.mIdentifyMenu.setAllowMultipleReturn(False)
        self.mIdentifyMenu.setExecWithSingleResult(True)
        self.mSelectionActive = False

        self.mDistanceWidget = None

        # create own user-input widget or use the first that is child to the map canvas
        self.mUserInputWidget = createQgsMapCanvasUserInputWidget(self.mCanvas)
        self.mSelectionRubberBand = None
        self.mInitDragPos = None
        self.mRadiusCenter = None
        self.mFillColor = QColor(254, 178, 76, 63)
        self.mStrokeColor = QColor(254, 58, 29, 100)

    def canvasReleaseEvent(self, e: QgsMapMouseEvent):
        if self.mSelectionMode == QgsMapToolSelectionHandler.SelectionMode.SelectSimple:
            self.selectFeaturesReleaseEvent(e)
        elif self.mSelectionMode == QgsMapToolSelectionHandler.SelectionMode.SelectPolygon:
            pass
        elif self.mSelectionMode == QgsMapToolSelectionHandler.SelectionMode.SelectFreehand:
            self.selectFreehandReleaseEvent(e)
        elif self.mSelectionMode == QgsMapToolSelectionHandler.SelectionMode.SelectRadius:
            self.selectRadiusReleaseEvent(e)

    def canvasMoveEvent(self, e: QgsMapMouseEvent):
        if self.mSelectionMode == QgsMapToolSelectionHandler.SelectionMode.SelectSimple:
            self.selectFeaturesMoveEvent(e)
        elif self.mSelectionMode == QgsMapToolSelectionHandler.SelectionMode.SelectPolygon:
            self.selectPolygonMoveEvent(e)
        elif self.mSelectionMode == QgsMapToolSelectionHandler.SelectionMode.SelectFreehand:
            self.selectFreehandMoveEvent(e)
        elif self.mSelectionMode == QgsMapToolSelectionHandler.SelectionMode.SelectRadius:
            self.selectRadiusMoveEvent(e)

    def canvasPressEvent(self, e: QgsMapMouseEvent):
        if self.mSelectionMode == QgsMapToolSelectionHandler.SelectionMode.SelectSimple:
            self.selectFeaturesPressEvent(e)
        elif self.mSelectionMode == QgsMapToolSelectionHandler.SelectionMode.SelectPolygon:
            self.selectPolygonPressEvent(e)
        elif self.mSelectionMode == QgsMapToolSelectionHandler.SelectionMode.SelectFreehand:
            pass
        elif self.mSelectionMode == QgsMapToolSelectionHandler.SelectionMode.SelectRadius:
            pass

    def keyReleaseEvent(self, e: QKeyEvent) -> bool:
        if self.mSelectionActive and e.key() == Qt.Key.Key_Escape:
            self.cancel()
            return True
        else:
            return False

    def deactivate(self):
        self.cancel()

    def selectFeaturesPressEvent(self, e: QgsMapMouseEvent):
        if not self.mSelectionRubberBand:
            self.initRubberBand()
        self.mInitDragPos = e.pos()

    def selectFeaturesMoveEvent(self, e: QgsMapMouseEvent):

        if e.buttons() != Qt.MouseButton.LeftButton:
            return

        if not self.mSelectionActive:
            self.mSelectionActive = True
            rect = QRect(e.pos(), e.pos())
        else:
            rect = QRect(e.pos(), self.mInitDragPos)

        if isinstance(self.mSelectionRubberBand, QgsRubberBand):
            self.mSelectionRubberBand.setToCanvasRectangle(rect)

    def selectFeaturesReleaseEvent(self, e: QgsMapMouseEvent):

        point = e.pos() - self.mInitDragPos

        if not self.mSelectionActive or (point.manhattanLength() < QApplication.startDragDistance()):
            self.mSelectionActive = False
            self.setSelectedGeometry(QgsGeometry.fromPointXY(self.toMapCoordinates(e.pos())), e.modifiers())

        if self.mSelectionRubberBand and self.mSelectionActive:
            self.setSelectedGeometry(self.mSelectionRubberBand.asGeometry(), e.modifiers())
            self.mSelectionRubberBand.reset()

        self.mSelectionActive = False

    def toMapCoordinates(self, point: QPoint) -> QgsPointXY:
        return self.mCanvas.getCoordinateTransform().toMapCoordinates(point)

    def selectPolygonMoveEvent(self, e: QgsMapMouseEvent):
        if not isinstance(self.mSelectionRubberBand, QgsRubberBand):
            return

        if self.mSelectionRubberBand.numberOfVertices() > 0:
            self.mSelectionRubberBand.movePoint(self.toMapCoordinates(e.pos()))

    def selectPolygonPressEvent(self, e: QgsMapMouseEvent):

        # // Handle immediate right-click on feature to show context menu
        if not self.mSelectionRubberBand and e.button() == Qt.MouseButton.RightButton:

            # QList<QgsMapToolIdentify::IdentifyResult> results;
            # QMap< QString, QString > derivedAttributes;

            results = []
            derivedAttributes = dict()

            mapPoint = self.toMapCoordinates(e.pos())
            x = mapPoint.x()
            y = mapPoint.y()
            sr = QgsMapTool.searchRadiusMU(self.mCanvas)

            # const QList<QgsMapLayer *> layers = mCanvas->layers();
            layers = self.mCanvas.layers()

            for vectorLayer in layers:
                if isinstance(vectorLayer, QgsVectorLayer):
                    if vectorLayer.geometryType() == QgsWkbTypes.GeometryType.PolygonGeometry:
                        fit = vectorLayer.getFeatures(QgsFeatureRequest()
                                                      .setDestinationCrs(self.mCanvas.mapSettings().destinationCrs(),
                                                                         self.mCanvas.mapSettings().transformContext())
                                                      .setFilterRect(QgsRectangle(x - sr, y - sr, x + sr, y + sr))
                                                      .setFlags(QgsFeatureRequest.Flag.ExactIntersect))
                        f = None
                        while fit.nextFeature(f):
                            results.append(QgsMapToolIdentify.IdentifyResult(vectorLayer, f, derivedAttributes))

            globalPos = self.mCanvas.mapToGlobal(QPoint(e.pos().x() + 5, e.pos().y() + 5))
            selectedFeatures = self.mIdentifyMenu.exec(results, globalPos)
            if not selectedFeatures.empty() and selectedFeatures[0].mFeature.hasGeometry():
                self.setSelectedGeometry(selectedFeatures[0].mFeature.geometry(), e.modifiers())
            return

            # // Handle definition of polygon by clicking points on canvas
        if not self.mSelectionRubberBand:
            self.initRubberBand()

        if e.button() == Qt.MouseButton.LeftButton:
            self.mSelectionRubberBand.addPoint(self.toMapCoordinates(e.pos()))
            self.mSelectionActive = True
        else:
            if self.mSelectionRubberBand.numberOfVertices() > 2:
                self.setSelectedGeometry(self.mSelectionRubberBand.asGeometry(), e.modifiers())

            self.mSelectionRubberBand.reset()
            self.mSelectionActive = False

    def selectFreehandMoveEvent(self, e: QgsMapMouseEvent):

        if not (self.mSelectionActive or self.mSelectionRubberBand):
            return

        self.mSelectionRubberBand.addPoint(self.toMapCoordinates(e.pos()))

    def selectFreehandReleaseEvent(self, e: QgsMapMouseEvent):

        if self.mSelectionActive:
            if e.button() != Qt.MouseButton.LeftButton:
                return

            if not self.mSelectionRubberBand:
                self.initRubberBand()

            self.mSelectionRubberBand.addPoint(self.toMapCoordinates(e.pos()))
            self.mSelectionActive = True

        else:
            if e.button() == Qt.MouseButton.LeftButton:
                if self.mSelectionRubberBand and self.mSelectionRubberBand.numberOfVertices() > 2:
                    self.setSelectedGeometry(self.mSelectionRubberBand.asGeometry(), e.modifiers())

            if self.mSelectionRubberBand:
                self.mSelectionRubberBand.reset()
            self.mSelectionActive = False

    def selectRadiusMoveEvent(self, e: QgsMapMouseEvent):

        radiusEdge = e.snapPoint()

        self.mSnapIndicator.setMatch(e.mapPointMatch())

        if not self.mSelectionActive:
            return

        if not self.mSelectionRubberBand:
            self.initRubberBand()

        self.updateRadiusFromEdge(radiusEdge)

    def selectRadiusReleaseEvent(self, e: QgsMapMouseEvent):

        if e.button() == Qt.MouseButton.RightButton:
            self.cancel()
            return

        if e.button() != Qt.MouseButton.LeftButton:
            return

        if not self.mSelectionActive:

            self.mSelectionActive = True
            self.mRadiusCenter = e.snapPoint()
            self.createDistanceWidget()
        else:
            if isinstance(self.mSelectionRubberBand, QgsRubberBand):
                self.setSelectedGeometry(self.mSelectionRubberBand.asGeometry(), e.modifiers())

            self.cancel()

    def initRubberBand(self):

        self.mSelectionRubberBand = QgsRubberBand(self.mCanvas, QgsWkbTypes.GeometryType.PolygonGeometry)
        self.mSelectionRubberBand.setFillColor(self.mFillColor)
        self.mSelectionRubberBand.setStrokeColor(self.mStrokeColor)

    def createDistanceWidget(self):
        if not isinstance(self.mCanvas, QgsMapCanvas):
            return

        self.deleteDistanceWidget()

        self.mDistanceWidget = QgsDistanceWidget("Selection radius:", parent=self.mUserInputWidget)
        # emulate
        # QgisApp::instance() -> addUserInputWidget( mDistanceWidget );
        # by adding the distance widget to the MapTool's QgsMapCanvas directly
        self.mUserInputWidget.addUserInputWidget(self.mDistanceWidget)

        self.mDistanceWidget.setFocus(Qt.FocusReason.TabFocusReason)
        self.mDistanceWidget.distanceChanged.connect(self.updateRadiusRubberband)
        self.mDistanceWidget.distanceEditingFinished.connect(self.radiusValueEntered)
        self.mDistanceWidget.distanceEditingCanceled.connect(self.cancel)

    def deleteDistanceWidget(self):
        if isinstance(self.mDistanceWidget, QWidget):
            self.mDistanceWidget.releaseKeyboard()
            self.mDistanceWidget.setParent(None)
            self.mDistanceWidget.deleteLater()

        self.mDistanceWidget = None

    def radiusValueEntered(self, radius: float, modifiers: Qt.KeyboardModifiers):

        if not isinstance(self.mSelectionRubberBand, QgsRubberBand):
            return

        self.updateRadiusRubberband(radius)
        self.setSelectedGeometry(self.mSelectionRubberBand.asGeometry(), modifiers)
        self.cancel()

    def cancel(self):

        self.deleteDistanceWidget()
        self.mSnapIndicator.setMatch(QgsPointLocator.Match())
        if isinstance(self.mSelectionRubberBand, QgsRubberBand):
            self.mSelectionRubberBand.reset()
        self.mSelectionActive = False

    def updateRadiusRubberband(self, radius: float):

        if not isinstance(self.mSelectionRubberBand, QgsRubberBand):
            self.initRubberBand()

        RADIUS_SEGMENTS = 80
        self.mSelectionRubberBand.reset(QgsWkbTypes.GeometryType.PolygonGeometry)
        for i in range(RADIUS_SEGMENTS):
            theta = i * (2.0 * math.pi / RADIUS_SEGMENTS)
            radiusPoint = QgsPointXY(self.mRadiusCenter.x() + radius * math.cos(theta),
                                     self.mRadiusCenter.y() + radius * math.sin(theta))
            self.mSelectionRubberBand.addPoint(radiusPoint, False)

        self.mSelectionRubberBand.closePoints(True)

    def updateRadiusFromEdge(self, radiusEdge: QgsPointXY):
        radius = math.sqrt(self.mRadiusCenter.sqrDist(radiusEdge))
        if self.mDistanceWidget:
            self.mDistanceWidget.setDistance(radius)
            self.mDistanceWidget.setFocus(Qt.FocusReason.TabFocusReason)

        else:
            self.updateRadiusRubberband(radius)

    def selectedGeometry(self) -> QgsGeometry:
        return self.mSelectionGeometry

    def setSelectedGeometry(self, geometry: QgsGeometry, modifiers: Qt.KeyboardModifiers):

        self.mSelectionGeometry = geometry
        self.geometryChanged.emit(modifiers)

    def setSelectionMode(self, mode):
        if not (isinstance(mode, QgsMapToolSelectionHandler.SelectionMode)):
            raise AssertionError
        self.mSelectionMode = mode

    def selectionMode(self):
        return self.mSelectionMode


class QgsMapToolSelect(QgsMapTool):

    def __init__(self, canvas: QgsMapCanvas):
        super(QgsMapToolSelect, self).__init__(canvas)

        self.mSelectionHandler = QgsMapToolSelectionHandler(canvas,
                                                            QgsMapToolSelectionHandler.SelectionMode.SelectSimple,
                                                            parent=self)
        self.mSelectionHandler.setParent(self)
        self.mSelectionHandler.geometryChanged.connect(self.selectFeatures)
        self.setSelectionMode(QgsMapToolSelectionHandler.SelectionMode.SelectSimple)

    def flags(self) -> QgsMapTool.Flags:
        return QgsMapTool.Flag.ShowContextMenu

    def setSelectionMode(self, selectionMode: QgsMapToolSelectionHandler.SelectionMode):
        self.mSelectionHandler.setSelectionMode(selectionMode)
        if selectionMode == QgsMapToolSelectionHandler.SelectionMode.SelectSimple:
            self.setCursor(QgsApplication.getThemeCursor(QgsApplication.Cursor.Select))
        else:
            self.setCursor(Qt.CursorShape.ArrowCursor)

    def canvasPressEvent(self, e: QgsMapMouseEvent):
        self.mSelectionHandler.canvasPressEvent(e)

    def canvasMoveEvent(self, e: QgsMapMouseEvent):
        self.mSelectionHandler.canvasMoveEvent(e)

    def canvasReleaseEvent(self, e: QgsMapMouseEvent):
        self.mSelectionHandler.canvasReleaseEvent(e)

    def keyReleaseEvent(self, e: QKeyEvent):
        if self.mSelectionHandler.keyReleaseEvent(e):
            return

        super(QgsMapToolSelect, self).keyPressEvent(e)

    def deactivate(self):
        self.mSelectionHandler.deactivate()
        super(QgsMapToolSelect, self).deactivate()

    def selectFeatures(self, modifiers: Qt.KeyboardModifiers):

        if (
            self.mSelectionHandler.selectionMode() == QgsMapToolSelectionHandler.SelectionMode.SelectSimple
            and self.mSelectionHandler.selectedGeometry().type() == QgsWkbTypes.GeometryType.PointGeometry  # noqa: W503
        ):

            vlayer = QgsMapToolSelectUtils.getCurrentVectorLayer(self.canvas())
            if isinstance(vlayer, QgsVectorLayer):
                r = QgsMapToolSelectUtils.expandSelectRectangle(self.mSelectionHandler.selectedGeometry().asPoint(),
                                                                self.canvas(),
                                                                vlayer)
                QgsMapToolSelectUtils.selectSingleFeature(self.canvas(), QgsGeometry.fromRect(r), modifiers)

        else:
            QgsMapToolSelectUtils.selectMultipleFeatures(self.canvas(), self.mSelectionHandler.selectedGeometry(),
                                                         modifiers)
