import os.path

from qgis.PyQt.QtCore import QCoreApplication, QVariant
from qgis.core import (QgsProcessing, QgsProcessingAlgorithm, QgsProcessingMultiStepFeedback, QgsProcessingException,
                       QgsProcessingParameterRasterLayer, QgsProcessingParameterNumber, QgsProcessingParameterEnum,
                       QgsFields, QgsField, QgsProcessingParameterFeatureSource, QgsProcessingParameterFeatureSink,
                       QgsFeatureSink)
import processing

from ..compatibility_routines import Path, QT_DOUBLE, QT_STRING

class AssignElevations(QgsProcessingAlgorithm):

    def initAlgorithm(self, config=None):
        """
        Define input and output parameters.
        """
        # input polygon layer(s)
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                'merge_polygon',
                'Polygon Layer',
                types=[QgsProcessing.TypeVectorPolygon]
            )
        )
        # input raster layer
        self.addParameter(
            QgsProcessingParameterRasterLayer(
                'raster_layer',
                'Raster Layer',
                defaultValue=None
            )
        )
        # vertex distribution options
        self.addParameter(
            QgsProcessingParameterEnum(
                'vertex_distribution',
                'Vertex Distribution Option',
                options=['Densify', 'Use Existing'],
                defaultValue='Densify'
            )
        )
        # vertex interval
        self.addParameter(
            QgsProcessingParameterNumber(
                'vertex_interval',
                'Vertex Interval (only used for Densify)',
                type=QgsProcessingParameterNumber.Double,
                minValue=0,
                defaultValue=2.5
            )
        )
        # output points layer
        self.addParameter(
            QgsProcessingParameterFeatureSink(
                'OutputPoints',
                'Output Points Layer',
                type=QgsProcessing.TypeVectorPoint,
                createByDefault=True,
                defaultValue=None
            )
        )
        # output polygon layer
        self.addParameter(
            QgsProcessingParameterFeatureSink(
                'OutputPolygon',
                'Output Polygon Layer',
                type=QgsProcessing.TypeVectorPolygon,
                createByDefault=True,
                defaultValue=None
            )
        )


    def processAlgorithm(self, parameters, context, model_feedback):
        """
        Use QGIS algorithms 'Densify by Interval', 'Extract Vertices', 'Delete Duplicate Geometries',
        'Sample Raster Values', 'Field Calculator' and 'Retain Fields' to output a points and polygon layer.
        Points layer (Output Points Layer):
            - vertices of the input merge polygon (merge_polygon) at an interval defined by the user
            - contains elevation data extracted from the input raster layer (raster_layer)
            - the points are snapped to the output polygon
        Polygon layer (Output Polygon Layer):
            - contains all input polygons
            - Shape_Widt option is set to -1 so that the vertices generated by the tool are used
        """
        results = {}
        outputs = {}
        feedback = QgsProcessingMultiStepFeedback(6, model_feedback)
        densify = self.parameterAsInt(parameters, 'vertex_distribution', context) == 0
        layer = parameters['merge_polygon']

        # densify by interval - if needed
        if densify:
            feedback.pushInfo(f"Densifying vertices at an interval of {parameters['vertex_interval']}")
            alg_params = {
                'INPUT': layer,
                'INTERVAL': parameters['vertex_interval'],
                'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT
            }
            outputs['DensifyByInterval'] = processing.run(
                'native:densifygeometriesgivenaninterval',
                alg_params,
                context=context,
                feedback=feedback,
                is_child_algorithm=True
            )
        else:
            feedback.pushInfo(f"Using existing vertices")
            outputs['DensifyByInterval'] = {'OUTPUT': layer}

        feedback.setCurrentStep(1)
        if feedback.isCanceled():
            return {}

        # extract vertices
        alg_params = {
            'INPUT': outputs['DensifyByInterval']['OUTPUT'],
            'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT
        }
        outputs['ExtractVertices'] = processing.run(
            'native:extractvertices',
            alg_params,
            context=context,
            feedback=feedback,
            is_child_algorithm=True
        )

        feedback.setCurrentStep(2)
        if feedback.isCanceled():
            return {}

        # remove duplicate vertices
        alg_params = {
            'INPUT': outputs['ExtractVertices']['OUTPUT'],
            'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT
        }
        outputs['DeleteDuplicateGeometries'] = processing.run(
            'native:deleteduplicategeometries',
            alg_params,
            context=context,
            feedback=feedback,
            is_child_algorithm=True
        )

        feedback.setCurrentStep(3)
        if feedback.isCanceled():
            return {}

        # sample raster values
        feedback.pushInfo(f"\nExtracting elevation data from raster layer")
        alg_params = {
            'COLUMN_PREFIX': 'elevations',
            'INPUT': outputs['DeleteDuplicateGeometries']['OUTPUT'],
            'RASTERCOPY': parameters['raster_layer'],
            'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT
        }
        outputs['SampleRasterValues'] = processing.run(
            'native:rastersampling',
            alg_params,
            context=context,
            feedback=feedback,
            is_child_algorithm=True
        )

        feedback.setCurrentStep(4)
        if feedback.isCanceled():
            return {}

        # field calculator - copies elevation data into Z field
        feedback.pushInfo(f"\nConverting data into TUFLOW format")
        alg_params = {
            'FIELD_LENGTH': 15,
            'FIELD_NAME': 'Z',
            'FIELD_PRECISION': 5,
            'FIELD_TYPE': 0,  # Decimal (double)
            'FORMULA': '"elevations1"',
            'INPUT': outputs['SampleRasterValues']['OUTPUT'],
            'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT
        }
        outputs['FieldCalculator'] = processing.run(
            'native:fieldcalculator',
            alg_params,
            context=context,
            feedback=feedback,
            is_child_algorithm=True
        )

        # field calculator - polygons
        alg_params = {
            'FIELD_LENGTH': 15,
            'FIELD_NAME': 'Shape_Width',
            # 'FIELD_NAME': 'Shape_Widt',
            'FIELD_PRECISION': 5,
            'FIELD_TYPE': 0,  # Decimal (double)
            'FORMULA': '-1',
            'INPUT': outputs['DensifyByInterval']['OUTPUT'],
            'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT
        }
        outputs['PolyFieldCalculator'] = processing.run(
            'native:fieldcalculator',
            alg_params,
            context=context,
            feedback=feedback,
            is_child_algorithm=True
        )

        feedback.setCurrentStep(5)
        if feedback.isCanceled():
            return {}

        # retain fields - just keep Z field for points
        alg_params = {
            'FIELDS': ['Z'],
            'INPUT': outputs['FieldCalculator']['OUTPUT'],
            'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT
        }
        outputs['RetainFields'] = processing.run(
            'native:retainfields',
            alg_params,
            context=context,
            feedback=feedback,
            is_child_algorithm=True
        )

        model_feedback.pushInfo(f"\n Finished processing merge polygon(s). Generating outputs...\n")

        # setup output points and polygon layer with the correct fields
        fields = QgsFields()
        fields.append(QgsField('Z', type=QT_DOUBLE, len=15, prec=5))
        fields.append(QgsField('dZ', type=QT_DOUBLE, len=15, prec=5))
        fields.append(QgsField('Shape_Width', type=QT_DOUBLE, len=15, prec=5))
        fields.append(QgsField('Shape_Option', type=QT_STRING, len=20))

        point_out_id = outputs['RetainFields']['OUTPUT']
        point_out = context.takeResultLayer(point_out_id)
        poly_out_id = outputs['PolyFieldCalculator']['OUTPUT']
        poly_out = context.takeResultLayer(poly_out_id)
        if not point_out or not point_out.isValid():
            model_feedback.reportError(f"Failed to load the output points layer.")
        if not poly_out or not poly_out.isValid():
            model_feedback.reportError(f"Failed to load the output polygon layer.")

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

        poly_sink, poly_dest_id = self.parameterAsSink(
            parameters,
            'OutputPolygon',
            context,
            fields,
            poly_out.wkbType(),
            poly_out.sourceCrs()
        )
        if poly_sink is None:
            raise QgsProcessingException(self.invalidSinkError(parameters, 'OutputPolygon'))

        for feat in point_out.getFeatures():
            sink.addFeature(feat, QgsFeatureSink.FastInsert)

        for poly_feat in poly_out.getFeatures():
            poly_sink.addFeature(poly_feat, QgsFeatureSink.FastInsert)

        results['OutputPoints'] = dest_id
        results['OutputPolygon'] = poly_dest_id
        return results

    def name(self):
        return 'TuflowTINPolygonsAssignElevations'

    def displayName(self):
        return 'TIN Polygons - Assign Elevations'

    def group(self):
        return ''

    def groupId(self):
        return ''

    def createInstance(self):
        return AssignElevations()

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

    def shortDescription(self):
        folder = Path(os.path.realpath(__file__)).parent
        help_filename = folder / 'help' / 'html' / 'alg_merge_polygon_assign_elevations.html'
        with help_filename.open() as f:
            return self.tr(f.read().replace('\n', '<p>'))
