"""
***************************************************************************
    AlgorithmExecutor.py
    ---------------------
    Date                 : August 2012
    Copyright            : (C) 2012 by Victor Olaya
    Email                : volayaf at gmail dot 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.                                   *
*                                                                         *
***************************************************************************
"""

__author__ = "Victor Olaya"
__date__ = "August 2012"
__copyright__ = "(C) 2012, Victor Olaya"

import sys
from qgis.PyQt.QtCore import QCoreApplication
from qgis.core import (
    Qgis,
    QgsApplication,
    QgsFeatureSink,
    QgsProcessingFeedback,
    QgsProcessingUtils,
    QgsMessageLog,
    QgsProcessingException,
    QgsProcessingFeatureSourceDefinition,
    QgsProcessingFeatureSource,
    QgsProcessingParameters,
    QgsProject,
    QgsFeatureRequest,
    QgsFeature,
    QgsExpression,
    QgsWkbTypes,
    QgsGeometry,
    QgsVectorLayerUtils,
    QgsVectorLayer,
)
from processing.gui.Postprocessing import handleAlgorithmResults
from processing.tools import dataobjects
from qgis.utils import iface


def execute(alg, parameters, context=None, feedback=None, catch_exceptions=True):
    """Executes a given algorithm, showing its progress in the
    progress object passed along.

    Return true if everything went OK, false if the algorithm
    could not be completed.
    """

    if feedback is None:
        feedback = QgsProcessingFeedback()
    if context is None:
        context = dataobjects.createContext(feedback)

    if catch_exceptions:
        try:
            results, ok = alg.run(parameters, context, feedback)
            return ok, results
        except QgsProcessingException as e:
            QgsMessageLog.logMessage(
                str(sys.exc_info()[0]), "Processing", Qgis.MessageLevel.Critical
            )
            if feedback is not None:
                feedback.reportError(e.msg)
            return False, {}
    else:
        results, ok = alg.run(parameters, context, feedback, {}, False)
        return ok, results


def execute_in_place_run(
    alg, parameters, context=None, feedback=None, raise_exceptions=False
):
    """Executes an algorithm modifying features in-place in the input layer.

    :param alg: algorithm to run
    :type alg: QgsProcessingAlgorithm
    :param parameters: parameters of the algorithm
    :type parameters: dict
    :param context: context, defaults to None
    :type context: QgsProcessingContext, optional
    :param feedback: feedback, defaults to None
    :type feedback: QgsProcessingFeedback, optional
    :param raise_exceptions: useful for testing, if True exceptions are raised, normally exceptions will be forwarded to the feedback
    :type raise_exceptions: boo, default to False
    :raises QgsProcessingException: raised when there is no active layer, or it cannot be made editable
    :return: a tuple with true if success and results
    :rtype: tuple
    """

    if feedback is None:
        feedback = QgsProcessingFeedback()
    if context is None:
        context = dataobjects.createContext(feedback)

    # Only feature based algs have sourceFlags
    try:
        if (
            alg.sourceFlags()
            & QgsProcessingFeatureSource.Flag.FlagSkipGeometryValidityChecks
        ):
            context.setInvalidGeometryCheck(
                QgsFeatureRequest.InvalidGeometryCheck.GeometryNoCheck
            )
    except AttributeError:
        pass

    in_place_input_parameter_name = "INPUT"
    if hasattr(alg, "inputParameterName"):
        in_place_input_parameter_name = alg.inputParameterName()

    active_layer = parameters[in_place_input_parameter_name]

    # prepare expression context for feature iteration
    alg_context = context.expressionContext()
    alg_context.appendScope(active_layer.createExpressionContextScope())
    context.setExpressionContext(alg_context)

    # Run some checks and prepare the layer for in-place execution by:
    # - getting the active layer and checking that it is a vector
    # - making the layer editable if it was not already
    # - selecting all features if none was selected
    # - checking in-place support for the active layer/alg/parameters
    # If one of the check fails and raise_exceptions is True an exception
    # is raised, else the execution is aborted and the error reported in
    # the feedback
    try:
        if active_layer is None:
            raise QgsProcessingException(tr("There is no active layer."))

        if not isinstance(active_layer, QgsVectorLayer):
            raise QgsProcessingException(tr("Active layer is not a vector layer."))

        if not active_layer.isEditable():
            if not active_layer.startEditing():
                raise QgsProcessingException(
                    tr(
                        "Active layer is not editable (and editing could not be turned on)."
                    )
                )

        if not alg.supportInPlaceEdit(active_layer):
            raise QgsProcessingException(
                tr(
                    "Selected algorithm and parameter configuration are not compatible with in-place modifications."
                )
            )
    except QgsProcessingException as e:
        if raise_exceptions:
            raise e
        QgsMessageLog.logMessage(
            str(sys.exc_info()[0]), "Processing", Qgis.MessageLevel.Critical
        )
        if feedback is not None:
            feedback.reportError(getattr(e, "msg", str(e)), fatalError=True)
        return False, {}

    if not active_layer.selectedFeatureIds():
        active_layer.selectAll()

    # Make sure we are working on selected features only
    parameters[in_place_input_parameter_name] = QgsProcessingFeatureSourceDefinition(
        active_layer.id(), True
    )
    parameters["OUTPUT"] = "memory:"

    req = QgsFeatureRequest(QgsExpression(r"$id < 0"))
    req.setFlags(QgsFeatureRequest.Flag.NoGeometry)
    req.setSubsetOfAttributes([])

    # Start the execution
    # If anything goes wrong and raise_exceptions is True an exception
    # is raised, else the execution is aborted and the error reported in
    # the feedback
    try:
        new_feature_ids = []

        active_layer.beginEditCommand(alg.displayName())

        # Checks whether the algorithm has a processFeature method
        if hasattr(alg, "processFeature"):  # in-place feature editing
            # Make a clone or it will crash the second time the dialog
            # is opened and run
            alg = alg.create({"IN_PLACE": True})
            if not alg.prepare(parameters, context, feedback):
                raise QgsProcessingException(
                    tr("Could not prepare selected algorithm.")
                )
            # Check again for compatibility after prepare
            if not alg.supportInPlaceEdit(active_layer):
                raise QgsProcessingException(
                    tr(
                        "Selected algorithm and parameter configuration are not compatible with in-place modifications."
                    )
                )

            # some algorithms have logic in outputFields/outputCrs/outputWkbType which they require to execute before
            # they can start processing features
            _ = alg.outputFields(active_layer.fields())
            _ = alg.outputWkbType(active_layer.wkbType())
            _ = alg.outputCrs(active_layer.crs())

            field_idxs = range(len(active_layer.fields()))
            iterator_req = QgsFeatureRequest(active_layer.selectedFeatureIds())
            iterator_req.setInvalidGeometryCheck(context.invalidGeometryCheck())
            feature_iterator = active_layer.getFeatures(iterator_req)
            step = (
                100 / len(active_layer.selectedFeatureIds())
                if active_layer.selectedFeatureIds()
                else 1
            )
            current = 0
            for current, f in enumerate(feature_iterator):
                if feedback.isCanceled():
                    break

                # need a deep copy, because python processFeature implementations may return
                # a shallow copy from processFeature
                input_feature = QgsFeature(f)

                context.expressionContext().setFeature(input_feature)

                new_features = alg.processFeature(input_feature, context, feedback)
                new_features = QgsVectorLayerUtils.makeFeaturesCompatible(
                    new_features, active_layer
                )

                if len(new_features) == 0:
                    active_layer.deleteFeature(f.id())
                elif len(new_features) == 1:
                    new_f = new_features[0]
                    if not f.geometry().equals(new_f.geometry()):
                        active_layer.changeGeometry(f.id(), new_f.geometry())
                    if f.attributes() != new_f.attributes():
                        active_layer.changeAttributeValues(
                            f.id(),
                            dict(zip(field_idxs, new_f.attributes())),
                            dict(zip(field_idxs, f.attributes())),
                        )
                    new_feature_ids.append(f.id())
                else:
                    active_layer.deleteFeature(f.id())
                    # Get the new ids
                    old_ids = {f.id() for f in active_layer.getFeatures(req)}
                    # If multiple new features were created, we need to pass
                    # them to createFeatures to manage constraints correctly
                    features_data = []
                    for f in new_features:
                        features_data.append(
                            QgsVectorLayerUtils.QgsFeatureData(
                                f.geometry(), dict(enumerate(f.attributes()))
                            )
                        )
                    new_features = QgsVectorLayerUtils.createFeatures(
                        active_layer, features_data, context.expressionContext()
                    )
                    if not active_layer.addFeatures(new_features):
                        raise QgsProcessingException(
                            tr("Error adding processed features back into the layer.")
                        )
                    new_ids = {f.id() for f in active_layer.getFeatures(req)}
                    new_feature_ids += list(new_ids - old_ids)

                feedback.setProgress(int((current + 1) * step))

            results, ok = {"__count": current + 1}, True

        else:  # Traditional 'run' with delete and add features cycle

            # There is no way to know if some features have been skipped
            # due to invalid geometries
            if (
                context.invalidGeometryCheck()
                == QgsFeatureRequest.InvalidGeometryCheck.GeometrySkipInvalid
            ):
                selected_ids = active_layer.selectedFeatureIds()
            else:
                selected_ids = []

            results, ok = alg.run(
                parameters, context, feedback, configuration={"IN_PLACE": True}
            )

            if ok:
                result_layer = QgsProcessingUtils.mapLayerFromString(
                    results["OUTPUT"], context
                )
                # TODO: check if features have changed before delete/add cycle

                new_features = []

                # Check if there are any skipped features
                if (
                    context.invalidGeometryCheck()
                    == QgsFeatureRequest.InvalidGeometryCheck.GeometrySkipInvalid
                ):
                    missing_ids = list(
                        set(selected_ids) - set(result_layer.allFeatureIds())
                    )
                    if missing_ids:
                        for f in active_layer.getFeatures(
                            QgsFeatureRequest(missing_ids)
                        ):
                            if not f.geometry().isGeosValid():
                                new_features.append(f)

                active_layer.deleteFeatures(active_layer.selectedFeatureIds())

                regenerate_primary_key = result_layer.customProperty(
                    "OnConvertFormatRegeneratePrimaryKey", False
                )
                sink_flags = (
                    QgsFeatureSink.SinkFlags(
                        QgsFeatureSink.SinkFlag.RegeneratePrimaryKey
                    )
                    if regenerate_primary_key
                    else QgsFeatureSink.SinkFlags()
                )

                for f in result_layer.getFeatures():
                    new_features.extend(
                        QgsVectorLayerUtils.makeFeaturesCompatible(
                            [f], active_layer, sink_flags
                        )
                    )

                # Get the new ids
                old_ids = {f.id() for f in active_layer.getFeatures(req)}
                if not active_layer.addFeatures(new_features):
                    raise QgsProcessingException(
                        tr("Error adding processed features back into the layer.")
                    )
                new_ids = {f.id() for f in active_layer.getFeatures(req)}
                new_feature_ids += list(new_ids - old_ids)
                results["__count"] = len(new_feature_ids)

        active_layer.endEditCommand()

        if ok and new_feature_ids:
            active_layer.selectByIds(new_feature_ids)
        elif not ok:
            active_layer.rollBack()

        return ok, results

    except QgsProcessingException as e:
        active_layer.endEditCommand()
        active_layer.rollBack()
        if raise_exceptions:
            raise e
        QgsMessageLog.logMessage(
            str(sys.exc_info()[0]), "Processing", Qgis.MessageLevel.Critical
        )
        if feedback is not None:
            feedback.reportError(getattr(e, "msg", str(e)), fatalError=True)

    return False, {}


def execute_in_place(alg, parameters, context=None, feedback=None):
    """Executes an algorithm modifying features in-place, if the INPUT
    parameter is not defined, the current active layer will be used as
    INPUT.

    :param alg: algorithm to run
    :type alg: QgsProcessingAlgorithm
    :param parameters: parameters of the algorithm
    :type parameters: dict
    :param context: context, defaults to None
    :param context: QgsProcessingContext, optional
    :param feedback: feedback, defaults to None
    :param feedback: QgsProcessingFeedback, optional
    :raises QgsProcessingException: raised when the layer is not editable or the layer cannot be found in the current project
    :return: a tuple with true if success and results
    :rtype: tuple
    """

    if feedback is None:
        feedback = QgsProcessingFeedback()
    if context is None:
        context = dataobjects.createContext(feedback)

    in_place_input_parameter_name = "INPUT"
    if hasattr(alg, "inputParameterName"):
        in_place_input_parameter_name = alg.inputParameterName()
    in_place_input_layer_name = "INPUT"
    if hasattr(alg, "inputParameterDescription"):
        in_place_input_layer_name = alg.inputParameterDescription()

    if (
        in_place_input_parameter_name not in parameters
        or not parameters[in_place_input_parameter_name]
    ):
        parameters[in_place_input_parameter_name] = iface.activeLayer()
    ok, results = execute_in_place_run(
        alg, parameters, context=context, feedback=feedback
    )
    if ok:
        if isinstance(
            parameters[in_place_input_parameter_name],
            QgsProcessingFeatureSourceDefinition,
        ):
            layer = alg.parameterAsVectorLayer(
                {
                    in_place_input_parameter_name: parameters[
                        in_place_input_parameter_name
                    ].source
                },
                in_place_input_layer_name,
                context,
            )
        elif isinstance(parameters[in_place_input_parameter_name], QgsVectorLayer):
            layer = parameters[in_place_input_parameter_name]
        if layer:
            layer.triggerRepaint()
    return ok, results


def executeIterating(alg, parameters, paramToIter, context, feedback):
    # Generate all single-feature layers
    parameter_definition = alg.parameterDefinition(paramToIter)
    if not parameter_definition:
        return False

    iter_source = QgsProcessingParameters.parameterAsSource(
        parameter_definition, parameters, context
    )
    sink_list = []
    if iter_source.featureCount() == 0:
        return False

    step = 100.0 / iter_source.featureCount()
    for current, feat in enumerate(iter_source.getFeatures()):
        if feedback.isCanceled():
            return False

        sink, sink_id = QgsProcessingUtils.createFeatureSink(
            "memory:",
            context,
            iter_source.fields(),
            iter_source.wkbType(),
            iter_source.sourceCrs(),
        )
        sink_list.append(sink_id)
        sink.addFeature(feat, QgsFeatureSink.Flag.FastInsert)
        del sink

        feedback.setProgress(int((current + 1) * step))

    # store output values to use them later as basenames for all outputs
    outputs = {}
    for out in alg.destinationParameterDefinitions():
        if out.name() in parameters:
            outputs[out.name()] = parameters[out.name()]

    # now run all the algorithms
    for i, f in enumerate(sink_list):
        if feedback.isCanceled():
            return False

        # clear any model result stored in the last iteration
        context.clearModelResult()

        parameters[paramToIter] = f
        for out in alg.destinationParameterDefinitions():
            if out.name() not in outputs:
                continue

            o = outputs[out.name()]
            parameters[out.name()] = QgsProcessingUtils.generateIteratingDestination(
                o, i, context
            )
        feedback.setProgressText(
            QCoreApplication.translate(
                "AlgorithmExecutor", "Executing iteration {0}/{1}…"
            ).format(i + 1, len(sink_list))
        )
        feedback.setProgress(int((i + 1) * 100 / len(sink_list)))
        ret, results = execute(alg, parameters, context, feedback)
        if not ret:
            return False

    handleAlgorithmResults(alg, context, feedback)
    return True


def tr(string, context=""):
    if context == "":
        context = "AlgorithmExecutor"
    return QCoreApplication.translate(context, string)
