from collections import namedtuple

from qgis.core import (
    QgsFeature,
    QgsFeatureRequest,
    QgsFeatureSink,
    QgsGeometry,
    QgsLineString,
    QgsProcessing,
    QgsProcessingException,
    QgsProcessingParameterFeatureSource,
    QgsProcessingParameterField,
    QgsProcessingParameterFeatureSink,
    QgsWkbTypes,
)

from PreCourlis.core.precourlis_file import PreCourlisFileLine
from PreCourlis.processing.precourlis_algorithm import PreCourlisAlgorithm


class ImportPreparedPointsAlgorithm(PreCourlisAlgorithm):
    TRACKS = "TRACKS"
    TRACKS_FIELD_SEC_ID = "TRACKS_FIELD_SEC_ID"
    AXIS = "AXIS"
    SOURCE_POINTS = "SOURCE_POINTS"
    SOURCE_FIELD_SEC_ID = "SOURCE_FIELD_SEC_ID"
    SOURCE_FIELD_Z = "SOURCE_FIELD_Z"
    OUTPUT = "OUTPUT"

    def initAlgorithm(self, config=None):
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.TRACKS,
                self.tr("Tracks"),
                types=[QgsProcessing.TypeVectorLine],
                defaultValue=None,
            )
        )
        self.addParameter(
            QgsProcessingParameterField(
                self.TRACKS_FIELD_SEC_ID,
                self.tr("Tracks identifier field"),
                parentLayerParameterName=self.TRACKS,
                defaultValue="sec_id",
            )
        )
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.AXIS,
                self.tr("Axis"),
                types=[QgsProcessing.TypeVectorLine],
                defaultValue=None,
            )
        )
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.SOURCE_POINTS,
                self.tr("Source points"),
                types=[QgsProcessing.TypeVectorPoint],
                defaultValue=None,
            )
        )
        self.addParameter(
            QgsProcessingParameterField(
                self.SOURCE_FIELD_SEC_ID,
                self.tr("Source tracks identifier field"),
                parentLayerParameterName=self.SOURCE_POINTS,
                defaultValue="sec_id",
            )
        )
        self.addParameter(
            QgsProcessingParameterField(
                self.SOURCE_FIELD_Z,
                self.tr("Source Z field"),
                parentLayerParameterName=self.SOURCE_POINTS,
                defaultValue=None,
            )
        )
        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.OUTPUT,
                self.tr("Sections"),
                type=QgsProcessing.TypeVectorLine,
                createByDefault=True,
                defaultValue=None,
            )
        )

    def processAlgorithm(self, parameters, context, feedback):
        tracks_source = self.parameterAsSource(parameters, self.TRACKS, context)
        tracks_field_sec_id = self.parameterAsString(
            parameters, self.TRACKS_FIELD_SEC_ID, context
        )

        axis_source = self.parameterAsSource(parameters, self.AXIS, context)
        axis = next(axis_source.getFeatures())

        source_points_source = self.parameterAsSource(
            parameters, self.SOURCE_POINTS, context
        )
        source_field_sec_id = self.parameterAsString(
            parameters, self.SOURCE_FIELD_SEC_ID, context
        )
        source_field_z = self.parameterAsString(
            parameters, self.SOURCE_FIELD_Z, context
        )

        from qgis.core import QgsVectorLayer

        temp_layer = QgsVectorLayer("LineString", "temp", "memory")
        temp_layer.setCrs(tracks_source.sourceCrs())
        temp_provider = temp_layer.dataProvider()
        temp_provider.addAttributes(PreCourlisFileLine.base_fields())
        temp_layer.updateFields()  # tell the vector layer to fetch changes from the provider

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

            request = QgsFeatureRequest()
            request.setFilterExpression(
                f"{source_field_sec_id} = '{track.attribute(tracks_field_sec_id)}'"
            )
            source_points_features = list(source_points_source.getFeatures(request))

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

            Point = namedtuple("Point", ["distance", "projected_point", "elevation"])

            points = []
            for f in source_points_features:
                distance = support.lineLocatePoint(f.geometry())
                projected_point = support_line.interpolatePoint(distance)
                elevation = f.attribute(source_field_z)
                points.append(Point(distance, projected_point, elevation))
            points = sorted(points, key=lambda p: p.distance)

            # Geometry
            geometry = QgsGeometry(
                QgsLineString([p.projected_point.clone() for p in points])
            )

            # Section attributes
            sec_id = current + 1
            intersection = geometry.intersection(axis.geometry())
            if intersection.isEmpty():
                # raise QgsProcessingException(
                #     f"Section {sec_name} does not intersect axis"
                # )
                continue

            intersection_point = intersection.constGet().clone()
            abs_long = axis.geometry().lineLocatePoint(intersection)
            sec_name = "{}_{:04.3f}".format("P" if abs_long >= 0 else "N", abs_long)
            axis_x = intersection_point.x()
            axis_y = intersection_point.y()
            layers = []

            # Points attributes
            p_id = [i + 1 for i in range(len(points))]
            topo_bat = ["B" for i in range(len(points))]
            abs_lat = [
                geometry.lineLocatePoint(QgsGeometry(p.projected_point.clone()))
                for p in points
            ]
            zfond = [p.elevation for p in points]

            out = QgsFeature()
            out.setGeometry(geometry)
            out.setAttributes(
                [
                    sec_id,
                    sec_name,
                    abs_long,
                    axis_x,
                    axis_y,
                    ",".join(layers),
                    ",".join([str(v) for v in p_id]),
                    ",".join(topo_bat),
                    ",".join([str(v) for v in abs_lat]),
                    ",".join([str(v) for v in zfond]),
                ]
            )

            # Add a feature in the sink
            temp_provider.addFeature(out, QgsFeatureSink.FastInsert)

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

        temp_layer.reload()

        # Now sort by abs_long, fill sec_id and write output

        sink, dest_id = self.parameterAsSink(
            parameters,
            self.OUTPUT,
            context,
            PreCourlisFileLine.base_fields(),
            QgsWkbTypes.LineString,
            tracks_source.sourceCrs(),
        )
        if sink is None:
            raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))

        sec_id_index = temp_layer.fields().indexFromName("sec_id")

        request = QgsFeatureRequest()
        request.addOrderBy("abs_long", ascending=True)
        for current, feature in enumerate(temp_provider.getFeatures(request)):
            feature.setAttribute(sec_id_index, current + 1)
            sink.addFeature(feature, QgsFeatureSink.FastInsert)

        return {self.OUTPUT: dest_id}

    def name(self):
        return "import_prepared_points"

    def displayName(self):
        return self.tr("Import prepared points by projection on tracks")

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

    def groupId(self):
        return "Import"

    def createInstance(self):
        return ImportPreparedPointsAlgorithm()

    def shortHelpString(self):
        return self.tr(
            "This algorithm create a new profiles layer by projecting points on tracks using:"
            "\n- Tracks"
            "\n- Source points with elevation"
            " 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'."
        )
