"""
***************************************************************************
    EliminateSelection.py
    ---------------------
    Date                 : January 2017
    Copyright         : (C) 2017 by Bernhard Ströbl
    Email                : bernhard.stroebl@jena.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 2 of the License, or     *
*   (at your option) any later version.                                   *
*                                                                         *
***************************************************************************
"""

__author__ = "Bernhard Ströbl"
__date__ = "January 2017"
__copyright__ = "(C) 2017, Bernhard Ströbl"

import os

from qgis.core import (
    QgsApplication,
    QgsFeature,
    QgsFeatureRequest,
    QgsFeatureSink,
    QgsGeometry,
    QgsProcessing,
    QgsProcessingAlgorithm,
    QgsProcessingException,
    QgsProcessingParameterEnum,
    QgsProcessingParameterFeatureSink,
    QgsProcessingParameterVectorLayer,
    QgsProcessingUtils,
)
from qgis.PyQt.QtGui import QIcon

from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm

pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0]


class EliminateSelection(QgisAlgorithm):
    INPUT = "INPUT"
    OUTPUT = "OUTPUT"
    MODE = "MODE"

    MODE_LARGEST_AREA = 0
    MODE_SMALLEST_AREA = 1
    MODE_BOUNDARY = 2

    def icon(self):
        return QgsApplication.getThemeIcon("/algorithms/mAlgorithmDissolve.svg")

    def svgIconPath(self):
        return QgsApplication.iconPath("/algorithms/mAlgorithmDissolve.svg")

    def group(self):
        return self.tr("Vector geometry")

    def groupId(self):
        return "vectorgeometry"

    def __init__(self):
        super().__init__()

    def flags(self):
        return (
            super().flags()
            | QgsProcessingAlgorithm.Flag.FlagNoThreading
            | QgsProcessingAlgorithm.Flag.FlagNotAvailableInStandaloneTool
        )

    def initAlgorithm(self, config=None):
        self.modes = [
            self.tr("Largest Area"),
            self.tr("Smallest Area"),
            self.tr("Largest Common Boundary"),
        ]

        self.addParameter(
            QgsProcessingParameterVectorLayer(
                self.INPUT,
                self.tr("Input layer"),
                [QgsProcessing.SourceType.TypeVectorPolygon],
            )
        )
        self.addParameter(
            QgsProcessingParameterEnum(
                self.MODE,
                self.tr("Merge selection with the neighbouring polygon with the"),
                options=self.modes,
            )
        )

        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.OUTPUT,
                self.tr("Eliminated"),
                QgsProcessing.SourceType.TypeVectorPolygon,
            )
        )

    def name(self):
        return "eliminateselectedpolygons"

    def displayName(self):
        return self.tr("Eliminate selected polygons")

    def shortDescription(self):
        return self.tr(
            "Combines selected polygons from the input layer with certain adjacent polygons "
            "by erasing their common boundary."
        )

    def shortHelpString(self):
        return self.tr(
            "This algorithm combines selected polygons from the input layer with certain adjacent polygons "
            "by erasing their common boundary. The adjacent polygon can be either the one with the largest "
            "or smallest area or the one sharing the largest common boundary with the polygon to be eliminated.\n"
            "Eliminate is normally used to get rid of sliver polygons, i.e. tiny polygons that are a result "
            "of polygon intersection processes where boundaries of the inputs are similar but not identical."
        )

    def processAlgorithm(self, parameters, context, feedback):
        inLayer = self.parameterAsVectorLayer(parameters, self.INPUT, context)
        boundary = (
            self.parameterAsEnum(parameters, self.MODE, context) == self.MODE_BOUNDARY
        )
        smallestArea = (
            self.parameterAsEnum(parameters, self.MODE, context)
            == self.MODE_SMALLEST_AREA
        )

        if inLayer.selectedFeatureCount() == 0:
            feedback.reportError(
                self.tr('{0}: (No selection in input layer "{1}")').format(
                    self.displayName(), parameters[self.INPUT]
                )
            )

        featToEliminate = []
        selFeatIds = inLayer.selectedFeatureIds()

        (sink, dest_id) = self.parameterAsSink(
            parameters,
            self.OUTPUT,
            context,
            inLayer.fields(),
            inLayer.wkbType(),
            inLayer.sourceCrs(),
        )
        if sink is None:
            raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))

        for aFeat in inLayer.getFeatures():
            if feedback.isCanceled():
                break

            if aFeat.id() in selFeatIds:
                # Keep references to the features to eliminate
                featToEliminate.append(aFeat)
            else:
                # write the others to output
                sink.addFeature(aFeat, QgsFeatureSink.Flag.FastInsert)
                feedback.featureAddedToSink(self.OUTPUT)

        sink.finalize()
        feedback.featureSinkFinalized(self.OUTPUT)
        del sink

        # Delete all features to eliminate in processLayer
        processLayer = QgsProcessingUtils.mapLayerFromString(dest_id, context)
        processLayer.startEditing()

        # ANALYZE
        if len(featToEliminate) > 0:  # Prevent zero division
            start = 20.00
            add = 80.00 / len(featToEliminate)
        else:
            start = 100

        feedback.setProgress(start)
        madeProgress = True

        # We go through the list and see if we find any polygons we can
        # merge the selected with. If we have no success with some we
        # merge and then restart the whole story.
        while madeProgress:  # Check if we made any progress
            madeProgress = False
            featNotEliminated = []

            # Iterate over the polygons to eliminate
            for i in range(len(featToEliminate)):
                if feedback.isCanceled():
                    break

                feat = featToEliminate.pop()
                geom2Eliminate = feat.geometry()
                bbox = geom2Eliminate.boundingBox()
                fit = processLayer.getFeatures(
                    QgsFeatureRequest().setFilterRect(bbox).setSubsetOfAttributes([])
                )
                mergeWithFid = None
                mergeWithGeom = None
                max = 0
                min = -1
                selFeat = QgsFeature()

                # use prepared geometries for faster intersection tests
                engine = QgsGeometry.createGeometryEngine(geom2Eliminate.constGet())
                engine.prepareGeometry()

                while fit.nextFeature(selFeat):
                    if feedback.isCanceled():
                        break

                    selGeom = selFeat.geometry()

                    if engine.intersects(selGeom.constGet()):
                        # We have a candidate
                        iGeom = geom2Eliminate.intersection(selGeom)

                        if not iGeom:
                            continue

                        if boundary:
                            selValue = iGeom.length()
                        else:
                            # area. We need a common boundary in
                            # order to merge
                            if 0 < iGeom.length():
                                selValue = selGeom.area()
                            else:
                                selValue = -1

                        if -1 != selValue:
                            useThis = True

                            if smallestArea:
                                if -1 == min:
                                    min = selValue
                                else:
                                    if selValue < min:
                                        min = selValue
                                    else:
                                        useThis = False
                            else:
                                if selValue > max:
                                    max = selValue
                                else:
                                    useThis = False

                            if useThis:
                                mergeWithFid = selFeat.id()
                                mergeWithGeom = QgsGeometry(selGeom)
                # End while fit

                if mergeWithFid is not None:
                    # A successful candidate
                    newGeom = mergeWithGeom.combine(geom2Eliminate)

                    if processLayer.changeGeometry(mergeWithFid, newGeom):
                        madeProgress = True
                    else:
                        raise QgsProcessingException(
                            self.tr(
                                "Could not replace geometry of feature with id {0}"
                            ).format(mergeWithFid)
                        )

                    start = start + add
                    feedback.setProgress(start)
                else:
                    featNotEliminated.append(feat)

            # End for featToEliminate

            featToEliminate = featNotEliminated

        # End while
        if not processLayer.commitChanges():
            raise QgsProcessingException(self.tr("Could not commit changes"))

        for feature in featNotEliminated:
            if feedback.isCanceled():
                break

            processLayer.dataProvider().addFeature(
                feature, QgsFeatureSink.Flag.FastInsert
            )

        return {self.OUTPUT: dest_id}
