import json
import numpy as np
from qgis.core import (
    QgsFeatureRequest,
    QgsGeometry,
    QgsLineString,
    QgsProcessing,
    QgsProcessingParameterFeatureSource,
    QgsProcessingParameterField,
    QgsProcessingParameterNumber,
    QgsProcessingParameterString,
    QgsProcessingParameterVectorLayer,
)

from PreCourlis.core import to_float
from PreCourlis.processing.precourlis_algorithm import PreCourlisAlgorithm


class ImportLayerFromPointsAlgorithm(PreCourlisAlgorithm):
    TARGET_PROFILES = "TARGET_PROFILES"
    TARGET_LAYER_NAME = "TARGET_LAYER_NAME"
    SOURCE_POINTS = "SOURCE_POINTS"
    SOURCE_FIELD_SEC_NAME = "SOURCE_FIELD_SEC_NAME"
    SOURCE_FIELD_Z = "SOURCE_FIELD_Z"
    MASK = "MASK"
    POINTS_FILTER = "POINTS_FILTER"
    DEFAULT_ELEVATION = "DEFAULT_ELEVATION"

    def initAlgorithm(self, config=None):
        self.addParameter(
            QgsProcessingParameterVectorLayer(
                self.TARGET_PROFILES,
                self.tr("Target profiles"),
                types=[QgsProcessing.TypeVectorLine],
                defaultValue=None,
            )
        )
        self.addParameter(
            QgsProcessingParameterField(
                self.TARGET_LAYER_NAME,
                self.tr("Target layer to fill"),
                parentLayerParameterName=self.TARGET_PROFILES,
                defaultValue=None,
            )
        )
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.SOURCE_POINTS,
                self.tr("Source points"),
                types=[QgsProcessing.TypeVectorPoint],
                defaultValue=None,
            )
        )
        self.addParameter(
            QgsProcessingParameterField(
                self.SOURCE_FIELD_SEC_NAME,
                self.tr("Source section name field"),
                parentLayerParameterName=self.SOURCE_POINTS,
                defaultValue=None,
            )
        )
        self.addParameter(
            QgsProcessingParameterField(
                self.SOURCE_FIELD_Z,
                self.tr("Source Z field"),
                parentLayerParameterName=self.SOURCE_POINTS,
                defaultValue=None,
            )
        )
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.MASK,
                self.tr("Mask"),
                types=[QgsProcessing.TypeVectorPolygon],
                optional=True,
            )
        )
        self.addParameter(
            QgsProcessingParameterString(
                self.POINTS_FILTER,
                self.tr(
                    "Points filter (formatted as a json array or arrays : [ [sec_id, p_id], ... ])"
                ),
                optional=True,
            )
        )
        self.addParameter(
            QgsProcessingParameterNumber(
                self.DEFAULT_ELEVATION,
                self.tr(
                    "Default elevation (the default value when there is no Z value to extract)"
                ),
                QgsProcessingParameterNumber.Double,
                optional=True,
            )
        )

    def processAlgorithm(self, parameters, context, feedback):
        target_profiles_layer = self.parameterAsVectorLayer(
            parameters, self.TARGET_PROFILES, context
        )
        target_layer_name = self.parameterAsString(
            parameters, self.TARGET_LAYER_NAME, context
        )
        source_points_source = self.parameterAsSource(
            parameters, self.SOURCE_POINTS, context
        )
        source_field_sec_name = self.parameterAsString(
            parameters, self.SOURCE_FIELD_SEC_NAME, context
        )
        source_field_z = self.parameterAsString(
            parameters, self.SOURCE_FIELD_Z, context
        )
        mask = self.parameterAsSource(parameters, self.MASK, context)

        points_filter = self.parameterAsString(parameters, self.POINTS_FILTER, context)
        has_filtering_points = len(points_filter) > 0
        if has_filtering_points:
            filtering_points = [
                [sec_id, p_id] for sec_id, p_id in json.loads(points_filter)
            ]
        else:
            filtering_points = []

        if parameters.get(self.DEFAULT_ELEVATION, None) is not None:
            default_elevation = self.parameterAsDouble(
                parameters, self.DEFAULT_ELEVATION, context
            )
        else:
            default_elevation = None

        target_field_index = target_profiles_layer.fields().indexFromName(
            target_layer_name
        )

        # This algorithm directly edit input layer in place using edit buffer
        target_profiles_layer.startEditing()
        target_profiles_layer.beginEditCommand("Set values from points")

        total = (
            100.0 / target_profiles_layer.featureCount()
            if target_profiles_layer.featureCount()
            else 0
        )
        for current, target_profile in enumerate(target_profiles_layer.getFeatures()):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break

            request = QgsFeatureRequest()
            request.setFilterExpression(
                f"{source_field_sec_name} = '{target_profile.attribute('sec_name')}'"
            )
            source_points_features = list(source_points_source.getFeatures(request))

            target_line: QgsLineString = next(
                target_profile.geometry().constParts()
            ).clone()
            support_line: QgsLineString = target_line.clone()
            support_line.extend(10, 10)
            support = QgsGeometry(support_line)

            target_distances = np.array(
                [
                    float(support.lineLocatePoint(QgsGeometry(target_point)))
                    for target_point in target_line.points()
                ]
            )
            projected_distances = np.array(
                [
                    float(support.lineLocatePoint(source_point.geometry()))
                    for source_point in source_points_features
                ]
            )
            source_values = np.array(
                [
                    to_float(source_point.attribute(source_field_z)) or np.nan
                    for source_point in source_points_features
                ]
            )

            # Sort source values
            permutation = projected_distances.argsort()
            projected_distances = projected_distances[permutation]
            source_values = source_values[permutation]

            # Remove empty source points
            nans = np.isnan(source_values)
            projected_distances = projected_distances[~nans]
            source_values = source_values[~nans]

            if len(source_values) == 0:
                values = np.array([np.nan for p in target_line.points()])
            else:
                values = np.interp(
                    target_distances,
                    projected_distances,
                    source_values,
                    left=np.nan,
                    right=np.nan,
                )

            # Replace nan by initial values
            initial_values = np.array(
                [
                    to_float(v) or np.nan
                    for v in target_profile.attribute(target_field_index).split(",")
                ]
            )
            values[np.isnan(values)] = initial_values[np.isnan(values)]

            # Apply default elevation
            if default_elevation:
                values = np.nan_to_num(values, nan=default_elevation)

            # Filter import with filtering_points and mask
            import_mask = np.array([True for p in target_line.points()])

            # with filtering_points
            if filtering_points:
                sec_id = int(target_profile.attribute("sec_id"))
                target_p_ids = [
                    int(s) for s in target_profile.attribute("p_id").split(",")
                ]
                import_mask = import_mask & np.array(
                    [[sec_id, p_id] in filtering_points for p_id in target_p_ids]
                )

            # with mask
            if mask:
                import_mask = import_mask & np.array(
                    [
                        any(
                            [
                                feature.geometry().contains(QgsGeometry(point.clone()))
                                for feature in mask.getFeatures()
                            ]
                        )
                        for point in target_line.points()
                    ]
                )
            values = np.where(
                import_mask,
                values,
                initial_values,
            )

            value = ",".join([str(v) if not np.isnan(v) else "NULL" for v in values])
            target_profiles_layer.changeAttributeValue(
                target_profile.id(), target_field_index, value
            )

            # Update the progress bar
            feedback.setProgress(int(current * total))

        target_profiles_layer.endEditCommand()

        return {}

    def name(self):
        return "import_layer_from_points"

    def displayName(self):
        return self.tr("Import layer from points")

    def group(self):
        return self.tr("Import")

    def groupId(self):
        return "Import"

    def createInstance(self):
        return ImportLayerFromPointsAlgorithm()

    def shortHelpString(self):
        return self.tr(
            "This algorithm fill or create a new sedimental layer interface"
            " in target profiles from source points with elevation"
            " and already having some field making the relation with tracks."
            "\n\nNote that it is possible to create the relation between points and tracks"
            " using function 'Join points to profiles'."
        )
