# -*- coding: utf-8 -*-

"""
/***************************************************************************
 PipelineEngineer
                                 A QGIS plugin
 A collection of Pipeline Engineering Tools.
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2025-12-06
        copyright            : (C) 2025 by Pipeline Engineer
        email                : info@pipelineengineer.com.au
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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__ = 'Pipeline Engineer'
__date__ = '2025-12-06'
__copyright__ = '(C) 2025 by Pipeline Engineer'

# This will get replaced with a git SHA1 when you do a git archive

__revision__ = '$Format:%H$'

from qgis.PyQt.QtCore import QCoreApplication
from qgis.core import (
    QgsProcessing,
    QgsFeatureSink,
    QgsProcessingAlgorithm,
    QgsProcessingParameterFeatureSource,
    QgsProcessingParameterFeatureSink,
    QgsProcessingParameterField,
    QgsFeatureRequest,
    QgsProcessingParameterNumber,
    QgsProcessingParameterEnum,
    QgsProcessingParameterBoolean,
    QgsProcessingParameterExpression,
    QgsProcessingParameterRasterLayer

)

from .logic.tee_junctions import summarise_tees
from .logic.bends import summarise_bends
from .logic.four_way_unions import summarise_four_way_unions
from .logic.attach_layer_attributes import attach_layer_attributes

try:
    from scipy.signal import find_peaks as fp
    import scipy.signal as s
    import pandas as pd

    from .logic.vents_drains import *
    drains_avail = True
except:
    drains_avail = False

class TeeJunctionsAlgorithm(QgsProcessingAlgorithm):

    INPUT = 'INPUT'
    OUTPUT = 'OUTPUT'
    ID_FIELD = 'ID_FIELD'
    ID_EXPRESSION = 'ID_EXPRESSION'

    def initAlgorithm(self, config):

        # Input line layer
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.INPUT,
                self.tr('Input layer'),
                [QgsProcessing.TypeVectorLine]
            )
        )

        # Field to use as ID (e.g. name, corridor ID, pipe ID, etc)
        self.addParameter(
            QgsProcessingParameterField(
                self.ID_FIELD,
                self.tr('ID field'),
                parentLayerParameterName=self.INPUT,
                type=QgsProcessingParameterField.Any
            )
        )

        self.addParameter(
            QgsProcessingParameterExpression(
                self.ID_EXPRESSION,
                'ID Expression',
                defaultValue="CONCAT('JUNCTION_',LPAD($id,5,'00000'))",
                parentLayerParameterName=None  # or a layer parameter ID
            )
        )


        # Output
        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.OUTPUT,
                self.tr('Tees')
            )
        )

    def processAlgorithm(self, parameters, context, feedback):

        # Retrieve layer source
        source = self.parameterAsSource(parameters, self.INPUT, context)

        # Convert to a materialised QgsVectorLayer (so it can be processed normally)
        layer = source.materialize(QgsFeatureRequest())

        # Retrieve ID field selected by user
        id_field = self.parameterAsString(parameters, self.ID_FIELD, context)
        
        id_expression = self.parameterAsExpression(parameters, self.ID_EXPRESSION, context)

        feedback.pushInfo(f"Using ID field: {id_field}")

        # Run your tee junction pipeline
        result_layer = summarise_tees(layer, id_field=id_field,id_expression=id_expression)

        # Create sink with the fields, geometry type and CRS of the output layer
        sink, dest_id = self.parameterAsSink(
            parameters,
            self.OUTPUT,
            context,
            result_layer.fields(),
            result_layer.wkbType(),
            result_layer.sourceCrs()
        )

        # Add resulting features to sink
        for f in result_layer.getFeatures():
            if feedback.isCanceled():
                break
            sink.addFeature(f, QgsFeatureSink.FastInsert)

        return {self.OUTPUT: dest_id}

    def name(self):
        return 'tee_junctions'

    def displayName(self):
        return self.tr('Tees')

    def group(self):
        return self.tr(self.groupId())

    def tr(self, string):
        return QCoreApplication.translate('Processing', string)

    def shortHelpString(self):
        return self.tr("""
    Indentifies all tee branches within a line layer.
    
    Includes angle between branch and header.
    
    ID expression of tees can be modified to better suit user's needs.
    """)

    def createInstance(self):
        return TeeJunctionsAlgorithm()

class BendsAlgorithm(QgsProcessingAlgorithm):

    INPUT = 'INPUT'
    OUTPUT = 'OUTPUT'
    ID_FIELD = 'ID_FIELD'
    ID_EXPRESSION = 'ID_EXPRESSION'

    def initAlgorithm(self, config):

        # Input line layer
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.INPUT,
                self.tr('Input layer'),
                [QgsProcessing.TypeVectorLine]
            )
        )

        # Field to use as ID (e.g. name, corridor ID, pipe ID, etc)
        self.addParameter(
            QgsProcessingParameterField(
                self.ID_FIELD,
                self.tr('ID field'),
                parentLayerParameterName=self.INPUT,
                type=QgsProcessingParameterField.Any
            )
        )

        # Minimum Bend Angle
        self.addParameter(
            QgsProcessingParameterNumber(
                'MIN_ANGLE',
                self.tr('Minimum bend angle (degrees)'),
                type=QgsProcessingParameterNumber.Double,
                minValue=0.0,
                maxValue=180.0,
                defaultValue=30.0     # choose your own default
            )
        )

        self.addParameter(
            QgsProcessingParameterExpression(
                self.ID_EXPRESSION,
                'ID Expression',
                defaultValue="CONCAT('BEND_',LPAD($id,5,'00000'))",
                parentLayerParameterName=None  # or a layer parameter ID
            )
        )

        # Output
        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.OUTPUT,
                self.tr('Bends')
            )
        )

    def processAlgorithm(self, parameters, context, feedback):

        # Retrieve layer source
        source = self.parameterAsSource(parameters, self.INPUT, context)

        # Convert to a materialised QgsVectorLayer (so it can be processed normally)
        layer = source.materialize(QgsFeatureRequest())

        # Retrieve ID field selected by user
        id_field = self.parameterAsString(parameters, self.ID_FIELD, context)

        # Retrieve Minimum Bend Angle
        min_bend_angle = self.parameterAsDouble(parameters, 'MIN_ANGLE', context)
        
        id_expression = self.parameterAsExpression(parameters, self.ID_EXPRESSION, context)

        feedback.pushInfo(f"Using ID field: {id_field}")
        feedback.pushInfo(f"Minimum bend angle: {min_bend_angle}")

        # Run your tee junction pipeline
        result_layer = summarise_bends(layer, id_field=id_field,min_bend_angle=min_bend_angle,feature_id_exp=id_expression)

        # Create sink with the fields, geometry type and CRS of the output layer
        sink, dest_id = self.parameterAsSink(
            parameters,
            self.OUTPUT,
            context,
            result_layer.fields(),
            result_layer.wkbType(),
            result_layer.sourceCrs()
        )

        # Add resulting features to sink
        for f in result_layer.getFeatures():
            if feedback.isCanceled():
                break
            sink.addFeature(f, QgsFeatureSink.FastInsert)

        return {self.OUTPUT: dest_id}

    def name(self):
        return 'bends'

    def displayName(self):
        return self.tr('Bends')

    def group(self):
        return self.tr(self.groupId())

    def tr(self, string):
        return QCoreApplication.translate('Processing', string)

    def shortHelpString(self):
        return self.tr("""
    Indentifies all bends within a line layer.
    
    Bends with an angle smaller than the specified Minimum Bend Angle will be filtered out.
    
    ID expression of bends can be modified to better suit user's needs.
    """)

    def createInstance(self):
        return BendsAlgorithm()

class FourWayUnionsAlgorithm(QgsProcessingAlgorithm):

    INPUT = 'INPUT'
    OUTPUT = 'OUTPUT'
    ID_FIELD = 'ID_FIELD'
    ID_EXPRESSION = 'ID_EXPRESSION'

    def initAlgorithm(self, config):

        # Input line layer
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.INPUT,
                self.tr('Input layer'),
                [QgsProcessing.TypeVectorLine]
            )
        )

        # Field to use as ID (e.g. name, corridor ID, pipe ID, etc)
        self.addParameter(
            QgsProcessingParameterField(
                self.ID_FIELD,
                self.tr('ID field'),
                parentLayerParameterName=self.INPUT,
                type=QgsProcessingParameterField.Any
            )
        )

        self.addParameter(
            QgsProcessingParameterExpression(
                self.ID_EXPRESSION,
                'ID Expression',
                defaultValue="CONCAT('UNION_',LPAD($id,5,'00000'))",
                parentLayerParameterName=None  # or a layer parameter ID
            )
        )

        # Output
        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.OUTPUT,
                self.tr('Four-Way Unions')
            )
        )

    def processAlgorithm(self, parameters, context, feedback):

        # Retrieve layer source
        source = self.parameterAsSource(parameters, self.INPUT, context)

        # Convert to a materialised QgsVectorLayer (so it can be processed normally)
        layer = source.materialize(QgsFeatureRequest())

        # Retrieve ID field selected by user
        id_field = self.parameterAsString(parameters, self.ID_FIELD, context)

        id_expression = self.parameterAsExpression(parameters, self.ID_EXPRESSION, context)

        feedback.pushInfo(f"Using ID field: {id_field}")

        # Run your tee junction pipeline
        result_layer = summarise_four_way_unions(layer, id_field=id_field,id_expression=id_expression)

        # Create sink with the fields, geometry type and CRS of the output layer
        sink, dest_id = self.parameterAsSink(
            parameters,
            self.OUTPUT,
            context,
            result_layer.fields(),
            result_layer.wkbType(),
            result_layer.sourceCrs()
        )

        # Add resulting features to sink
        for f in result_layer.getFeatures():
            if feedback.isCanceled():
                break
            sink.addFeature(f, QgsFeatureSink.FastInsert)

        return {self.OUTPUT: dest_id}

    def name(self):
        return 'four_way_unions'

    def displayName(self):
        return self.tr('Four Way Unions')

    def group(self):
        return self.tr(self.groupId())

    def tr(self, string):
        return QCoreApplication.translate('Processing', string)

    def shortHelpString(self):
        return self.tr("""
    Indentifies all four-way unions within a line layer.
    
    ID expression of unions can be modified to better suit user's needs.
    """)

    def createInstance(self):
        return FourWayUnionsAlgorithm()
    
class JoinAttributesAlgorithm(QgsProcessingAlgorithm):

    ATTRIBUTES_LAYER = 'ATTRIBUTES_LAYER'
    ATTRIBUTE_ID_FIELD = 'ATTRIBUTE_ID_FIELD'
    ATTRIBUTED_LAYER = 'ATTRIBUTED_LAYER'
    ATTRIBUTE_FIELDS = 'ATTRIBUTE_FIELDS'
    FIELDS_TO_COPY = 'FIELDS_TO_COPY'
    OUTPUT = 'OUTPUT'

    def initAlgorithm(self, config):

        # Input line layer
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.ATTRIBUTES_LAYER,
                self.tr('Layer with Attributes'),
                [QgsProcessing.TypeVector]
            )
        )

        # Field to use as ID (e.g. name, corridor ID, pipe ID, etc)
        self.addParameter(
            QgsProcessingParameterField(
                self.ATTRIBUTE_ID_FIELD,
                self.tr('Attribute Layer ID Field'),
                parentLayerParameterName=self.ATTRIBUTES_LAYER,
                type=QgsProcessingParameterField.Any
            )
        )

        self.addParameter(
            QgsProcessingParameterField(
                self.FIELDS_TO_COPY,
                self.tr('Attribute Fields to Copy'),
                parentLayerParameterName=self.ATTRIBUTES_LAYER,
                type=QgsProcessingParameterField.Any,
                allowMultiple=True
            )
        )

        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.ATTRIBUTED_LAYER,
                self.tr('Layer to Add Attributes To'),
                [QgsProcessing.TypeVector]
            )
        )

        self.addParameter(
            QgsProcessingParameterField(
                self.ATTRIBUTE_FIELDS,
                self.tr('Fields Containing Attributes'),
                parentLayerParameterName=self.ATTRIBUTED_LAYER,
                type=QgsProcessingParameterField.Any,
                allowMultiple=True
            )
        )

        # Output
        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.OUTPUT,
                self.tr('Layer With Attributes Added')
            )
        )

    ATTRIBUTES_LAYER = 'ATTRIBUTES_LAYER'
    ATTRIBUTE_ID_FIELD = 'ATTRIBUTE_ID_FIELD'
    ATTRIBUTED_LAYER = 'ATTRIBUTED_LAYER'
    ATTRIBUTE_FIELDS = 'ATTRIBUTE_FIELDS'
    FIELDS_TO_COPY = 'FIELDS_TO_COPY'
    OUTPUT = 'OUTPUT'


    def processAlgorithm(self, parameters, context, feedback):

        # Retrieve layer source
        source_one = self.parameterAsSource(parameters, self.ATTRIBUTES_LAYER, context)
        source_two = self.parameterAsSource(parameters, self.ATTRIBUTED_LAYER, context)

        attribute_layer = source_one.materialize(QgsFeatureRequest())
        layer_to_be_attributed = source_two.materialize(QgsFeatureRequest())

        # Retrieve ID field selected by user
        attribute_layer_field = self.parameterAsString(parameters, self.ATTRIBUTE_ID_FIELD, context)
        fields_containing_attributes = self.parameterAsFields(parameters, self.ATTRIBUTE_FIELDS, context)
        attributes_to_copy = self.parameterAsFields(parameters, self.FIELDS_TO_COPY, context)


        #feedback.pushInfo(f"Using ID field: {id_field}")

        # Run your tee junction pipeline
        result_layer = attach_layer_attributes(attribute_layer,attribute_layer_field,layer_to_be_attributed,fields_containing_attributes,attributes_to_copy)

        # Create sink with the fields, geometry type and CRS of the output layer
        sink, dest_id = self.parameterAsSink(
            parameters,
            self.OUTPUT,
            context,
            result_layer.fields(),
            result_layer.wkbType(),
            result_layer.sourceCrs()
        )

        # Add resulting features to sink
        for f in result_layer.getFeatures():
            if feedback.isCanceled():
                break
            sink.addFeature(f, QgsFeatureSink.FastInsert)

        return {self.OUTPUT: dest_id}

    def name(self):
        return 'add_attributes_to_layer'

    def displayName(self):
        return self.tr('Add Attributes to Layer')

    def group(self):
        return self.tr(self.groupId())

    def tr(self, string):
        return QCoreApplication.translate('Processing', string)

    def shortHelpString(self):
        return self.tr("""
    Indentifies all four-way unions within a line layer.
    
    ID expression of unions can be modified to better suit user's needs.
    """)

    def createInstance(self):
        return JoinAttributesAlgorithm()

if drains_avail:
    class VentsDrainsAlgorithm(QgsProcessingAlgorithm):

        INPUT = 'INPUT'
        OUTPUT = 'OUTPUT'
        ID_FIELD = 'ID_FIELD'
        CHAINAGE = 'CHAINAGE'
        DEM = 'DEM'
        ELEV_OFFSET_LOWER = 'ELEV_OFFSET_LOWER'
        ELEV_OFFSET_UPPER = 'ELEV_OFFSET_UPPER'
        VENT_DELTA_TRIGGER = 'VENT_DELTA_TRIGGER'
        DRAIN_DELTA_TRIGGER = 'DRAIN_DELTA_TRIGGER'

        def initAlgorithm(self, config):

            # Input line layer
            self.addParameter(
                QgsProcessingParameterFeatureSource(
                    self.INPUT,
                    self.tr('Input layer'),
                    [QgsProcessing.TypeVectorLine]
                )
            )

            # Field to use as ID (e.g. name, corridor ID, pipe ID, etc)
            self.addParameter(
                QgsProcessingParameterField(
                    self.ID_FIELD,
                    self.tr('ID field'),
                    parentLayerParameterName=self.INPUT,
                    type=QgsProcessingParameterField.Any
                )
            )

            self.addParameter(
                QgsProcessingParameterRasterLayer(
                    self.DEM,
                    self.tr('Select DEM Layer')
                )
            )
            
            self.addParameter(QgsProcessingParameterNumber(
                self.CHAINAGE, 'XYZ Chainage',
                type=QgsProcessingParameterNumber.Double,
                minValue=0, maxValue=1e5, defaultValue=20
            ))
            
            
            self.addParameter(QgsProcessingParameterNumber(
                self.VENT_DELTA_TRIGGER, 'Change in Elevation that Triggers a Vent',
                type=QgsProcessingParameterNumber.Integer,
                minValue=0, maxValue=1e5, defaultValue=2
            ))
            
            self.addParameter(QgsProcessingParameterNumber(
                self.ELEV_OFFSET_LOWER, 'Vent Minimum Elevation Offset',
                type=QgsProcessingParameterNumber.Double,
                minValue=0, maxValue=1e5, defaultValue=0.3
            ))
            
            self.addParameter(QgsProcessingParameterNumber(
                self.ELEV_OFFSET_UPPER, 'Vent Maximum Elevation Offset',
                type=QgsProcessingParameterNumber.Double,
                minValue=0, maxValue=1e5, defaultValue=1
            ))
            
            self.addParameter(QgsProcessingParameterNumber(
                self.DRAIN_DELTA_TRIGGER, 'Change in Elevation that Triggers a Drain',
                type=QgsProcessingParameterNumber.Integer,
                minValue=0, maxValue=1e5, defaultValue=2
            ))

            # Output
            self.addParameter(
                QgsProcessingParameterFeatureSink(
                    self.OUTPUT,
                    self.tr('Vents and Drains')
                )
            )

        def processAlgorithm(self, parameters, context, feedback):

            # Retrieve layer source
            source = self.parameterAsSource(parameters, self.INPUT, context)

            # Convert to a materialised QgsVectorLayer (so it can be processed normally)
            layer = source.materialize(QgsFeatureRequest())

            # Retrieve ID field selected by user
            id_field = self.parameterAsString(parameters, self.ID_FIELD, context)
            chainage = self.parameterAsDouble(parameters, self.CHAINAGE, context)
            dem = self.parameterAsRasterLayer(parameters, self.DEM, context) 
            vent_delta_trigger = self.parameterAsInt(parameters, self.VENT_DELTA_TRIGGER, context)
            elev_offset_lower = self.parameterAsDouble(parameters, self.ELEV_OFFSET_LOWER, context)
            elev_offset_upper = self.parameterAsDouble(parameters, self.ELEV_OFFSET_UPPER, context)
            drain_delta_trigger = self.parameterAsInt(parameters, self.DRAIN_DELTA_TRIGGER, context)

            feedback.pushInfo(f"Using ID field: {id_field}")

            # Run your tee junction pipeline
            result_layer = vent_drain_placer(line_layer=layer,
                                             id_field=id_field,
                                             chainage=chainage,
                                             dem=dem,
                                             elev_offset_lower=elev_offset_lower,
                                             elev_offset_upper=elev_offset_upper,
                                             vent_delta_trigger=vent_delta_trigger,
                                             drain_delta_trigger=drain_delta_trigger)

            # Create sink with the fields, geometry type and CRS of the output layer
            sink, dest_id = self.parameterAsSink(
                parameters,
                self.OUTPUT,
                context,
                result_layer.fields(),
                result_layer.wkbType(),
                result_layer.sourceCrs()
            )

            # Add resulting features to sink
            for f in result_layer.getFeatures():
                if feedback.isCanceled():
                    break
                sink.addFeature(f, QgsFeatureSink.FastInsert)

            return {self.OUTPUT: dest_id}

        def name(self):
            return 'vents_and_drains'

        def displayName(self):
            return self.tr('Vents and Drains')

        def group(self):
            return self.tr(self.groupId())

        def tr(self, string):
            return QCoreApplication.translate('Processing', string)

        def shortHelpString(self):
            return self.tr("""
        Identifies potential vent and drain locations for a network based on topography.
        
        User can identify how far off the peak the vent is to be placed downstream (Vent Minimum and Maximum Elevation Offsets).
        
        Works best when trunks are dissolved into one continuous layer.
        
        """)

        def createInstance(self):
            return VentsDrainsAlgorithm()
    