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

"""
/***************************************************************************
 QProto
                                 A QGIS plugin
 QProto algorithm
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2018-04-19
        copyright            : (C) 2018 by Matteo Ghetta (Faunalia)
        email                : matteo.ghetta@faunalia.eu
        updated              : 2026 by Stefano Campus & Codex
        updated email        : skampus@gmail.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__ = 'Matteo Ghetta (Faunalia); 2026 update by Stefano Campus & Codex'
__date__ = '2018-04-19'
__copyright__ = '(C) 2018 by Matteo Ghetta (Faunalia); 2026 update by Stefano Campus'

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

__revision__ = '$Format:%H$'

from qgis.PyQt.QtCore import QCoreApplication, QVariant, QMetaType, QUrl
from qgis.PyQt.QtGui import QIcon
from qgis.core import (QgsProcessing,
                       QgsProcessingAlgorithm,
                       QgsProcessingException,
                       QgsRasterLayer,
                       QgsPointXY,
                       QgsGeometry,
                       QgsProcessingParameterFeatureSource,
                       QgsProcessingParameterRasterLayer,
                       QgsProcessingParameterField,
                       QgsProcessingParameterNumber,
                       QgsProcessingParameterFolderDestination,
                       QgsVectorLayer,
                       QgsVectorFileWriter,
                       QgsField,
                       QgsFields,
                       QgsFeature,
                       QgsWkbTypes,
                       QgsDistanceArea,
                       QgsCoordinateTransformContext,
                       QgsProcessingUtils,
                       QgsProcessingParameterString,
                       QgsProject,
                       QgsLayerTreeGroup,
                       QgsPointClusterRenderer,
                       QgsSingleSymbolRenderer,
                       QgsMarkerSymbol
                       )

import processing
import os
import math
import time
from osgeo import gdal
import numpy as np
from datetime import datetime

from .utils import (map2pixel,
                    triangolo,
                    pixel2map)

pluginPath = os.path.dirname(__file__)

class QProtoAlgorithm(QgsProcessingAlgorithm):

    INPUT = 'INPUT'
    DTM = 'DTM'
    ID_FIELD = 'ID_FIELD'
    ELEV_FIELD = 'ELEV_FIELD'
    ASPECT_FIELD = 'ASPECT_FIELD'
    WEIGHT_FIELD = 'WEIGHT_FIELD'
    MASS_FIELD = 'MASS_FIELD'
    SHADOW_FIELD = 'SHADOW_FIELD'
    FIX_SHADOW = 'FIX_SHADOW'
    LATERAL_FIELD = 'LATERAL_FIELD'
    FIX_LATERAL = 'FIX_LATERAL'
    VISIBILITY = 'VISIBILITY'
    OUTPUT = 'OUTPUT'
    ANALYSIS_NAME = 'ANALYSIS_NAME'

    def __init__(self):
        super().__init__()
        self._finalpoints_path = None
        self._results_summary_uri = None
        self._analysis_name = None
        self._results_folder = None
        self._coni_creati = 0
        self._analysis_log_path = None
        self._results_summary_feature_count = 0
        self._finalpoints_feature_count = 0
        self._source_points_total = 0
        self._skipped_sources = {}
        self._input_layer_name = ''
        self._input_layer_path = ''
        self._dtm_layer_name = ''
        self._dtm_layer_path = ''

    def _layer_tree_child_index(self, parent_node, child_node):
        for index, node in enumerate(parent_node.children()):
            if node is child_node:
                return index
        return -1

    def _get_or_create_analysis_group(self, root):
        if not self._analysis_name:
            return None

        target_group = root.findGroup(self._analysis_name)
        if not target_group:
            target_group = QgsLayerTreeGroup(self._analysis_name)
            root.insertChildNode(0, target_group)
        elif self._layer_tree_child_index(root, target_group) != 0:
            root.removeChild(target_group)
            root.insertChildNode(0, target_group)

        return target_group

    def _load_analysis_log_layer(self):
        if not self._analysis_log_path or not os.path.exists(self._analysis_log_path):
            return None

        log_uri = (
            QUrl.fromLocalFile(self._analysis_log_path).toString()
            + '?type=csv&delimiter=%5Ct&maxFields=4&detectTypes=yes&geomType=none&subsetIndex=no&watchFile=no&useHeader=yes'
        )
        log_layer_name = os.path.basename(self._analysis_log_path)
        log_layer = QgsVectorLayer(log_uri, log_layer_name, 'delimitedtext')
        if log_layer.isValid():
            return log_layer
        return None

    def _add_layer_to_group_last(self, group, layer):
        QgsProject.instance().addMapLayer(layer, False)
        group.addLayer(layer)

    def _unwrap_source_reference(self, source_reference):
        if source_reference is None:
            return None
        if isinstance(source_reference, str):
            return source_reference
        if hasattr(source_reference, 'staticValue'):
            try:
                static_value = source_reference.staticValue()
                if isinstance(static_value, str):
                    return static_value
            except Exception:
                pass
        return None

    def _resolve_input_layer_details(self, parameters, context, fallback_source):
        input_vector_layer = self.parameterAsVectorLayer(parameters, self.INPUT, context)
        input_parameter = parameters.get(self.INPUT)
        resolved_input_layer = None

        if input_vector_layer and input_vector_layer.isValid():
            resolved_input_layer = input_vector_layer
        else:
            source_reference = self._unwrap_source_reference(getattr(input_parameter, 'source', None))
            if source_reference:
                resolved_input_layer = QgsProject.instance().mapLayer(source_reference)
                if resolved_input_layer is None:
                    resolved_input_layer = QgsProcessingUtils.mapLayerFromString(source_reference, context)

        if resolved_input_layer and resolved_input_layer.isValid():
            return resolved_input_layer.name(), resolved_input_layer.source()

        source_reference = self._unwrap_source_reference(getattr(input_parameter, 'source', None))
        if source_reference:
            project_layer = QgsProject.instance().mapLayer(source_reference)
            if project_layer and project_layer.isValid():
                return project_layer.name(), project_layer.source()
            return '', source_reference

        if hasattr(fallback_source, 'source') and isinstance(fallback_source.source(), str):
            return fallback_source.sourceName() if hasattr(fallback_source, 'sourceName') else '', fallback_source.source()

        return str(input_parameter), str(input_parameter)

    def name(self):
        return 'QPROTO'

    def displayName(self):
        return self.tr('QPROTO main analysis')

    def group(self):
        return ''

    def groupId(self):
        return ''

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

    def createInstance(self):
        return QProtoAlgorithm()

    def _int_field_type(self):
        if hasattr(QMetaType, 'Type'):
            return QMetaType.Type.Int
        if hasattr(QMetaType, 'Int'):
            return QMetaType.Int
        return QVariant.Int

    def _double_field_type(self):
        if hasattr(QMetaType, 'Type'):
            return QMetaType.Type.Double
        if hasattr(QMetaType, 'Double'):
            return QMetaType.Double
        return QVariant.Double

    def _create_field(self, name, modern_type, fallback_type):
        try:
            return QgsField(name, modern_type)
        except TypeError:
            return QgsField(name, fallback_type)

    def _build_analysis_output_folder(self, base_output_folder, analysis_name):
        if not analysis_name:
            return base_output_folder

        # Basic sanitization for filesystem-invalid chars (Windows-safe).
        invalid_chars = '<>:"/\\|?*'
        safe_name = ''.join('_' if c in invalid_chars else c for c in analysis_name).strip().strip('.')
        if not safe_name:
            return base_output_folder

        return os.path.join(base_output_folder, safe_name)

    def _sanitize_analysis_name(self, analysis_name):
        if not analysis_name:
            return ''
        invalid_chars = '<>:"/\\|?*'
        return ''.join('_' if c in invalid_chars else c for c in analysis_name).strip().strip('.')

    def _safe_remove_file(self, path, feedback, max_attempts=8, wait_seconds=0.2):
        if not path or not os.path.exists(path):
            return
        for _ in range(max_attempts):
            try:
                os.remove(path)
                return
            except PermissionError:
                time.sleep(wait_seconds)
            except OSError as exc:
                feedback.pushWarning('Temporary file cleanup skipped for {}: {}'.format(path, str(exc)))
                return
        feedback.pushWarning('Temporary file still locked, could not remove: {}'.format(path))

    def _cleanup_viewshed_temp(self, output_path, feedback):
        if not output_path:
            return
        # Remove primary raster and common GDAL sidecar files, if present.
        self._safe_remove_file(output_path, feedback)
        self._safe_remove_file(output_path + '.aux.xml', feedback)
        self._safe_remove_file(output_path + '.ovr', feedback)

    def _geometry_key(self, geometry):
        return bytes(geometry.asWkb())

    def _coerce_numeric_value(self, value):
        if value is None:
            return None
        if isinstance(value, QVariant):
            if value.isNull():
                return None
            value = value.value()
        try:
            return float(value)
        except (TypeError, ValueError):
            return None

    def _record_skipped_source(self, source_id, reason):
        reason_key = str(reason)
        if reason_key not in self._skipped_sources:
            self._skipped_sources[reason_key] = []
        self._skipped_sources[reason_key].append(str(source_id))

    def _append_finalpoint_summary_attributes(self, features):
        grouped_stats = {}
        for feature in features:
            geometry = feature.geometry()
            if not geometry or geometry.isEmpty():
                continue
            key = self._geometry_key(geometry)
            group = grouped_stats.get(key)
            if group is None:
                group = {
                    'count': 0,
                    'sum_weight': 0.0,
                    'w_tot_en': 0.0,
                }
                grouped_stats[key] = group
            attributes = feature.attributes()
            group['count'] += 1
            group['sum_weight'] += float(attributes[6])
            group['w_tot_en'] += float(attributes[9])

        output_features = []
        for feature in features:
            geometry = feature.geometry()
            if not geometry or geometry.isEmpty():
                continue
            key = self._geometry_key(geometry)
            group = grouped_stats[key]
            output_feature = QgsFeature()
            output_feature.setGeometry(geometry)
            output_feature.setAttributes(
                list(feature.attributes()) + [
                    group['count'],
                    group['sum_weight'],
                    group['w_tot_en'],
                ]
            )
            output_features.append(output_feature)
        return output_features

    def _write_analysis_log(self, analysis_output_folder, safe_analysis_name, gpkg_path, total_time):
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        log_basename = '{}_analysis_log_{}.txt'.format(safe_analysis_name, timestamp) if safe_analysis_name else 'qproto_analysis_log_{}.txt'.format(timestamp)
        log_path = os.path.join(analysis_output_folder, log_basename)
        skipped_total = sum(len(source_ids) for source_ids in self._skipped_sources.values())
        analysis_label = self._analysis_name if self._analysis_name else '(not specified)'

        log_rows = [
            ('section', 'item', 'value', 'details'),
            ('inputs', 'source_points_name', self._input_layer_name, ''),
            ('inputs', 'source_points_path', self._input_layer_path, ''),
            ('inputs', 'dtm_name', self._dtm_layer_name, ''),
            ('inputs', 'dtm_path', self._dtm_layer_path, ''),
            ('analysis', 'analysis_name', analysis_label, ''),
            ('analysis', 'output_folder', analysis_output_folder, ''),
            ('analysis', 'geopackage_path', gpkg_path, ''),
            ('analysis', 'duration_hh_mm_ss', str(total_time), ''),
            ('analysis', 'source_points_original', str(self._source_points_total), ''),
            ('analysis', 'source_points_with_cones', str(self._coni_creati), ''),
            ('analysis', 'propagation_cones_created', str(self._coni_creati), ''),
            ('analysis', 'generated_finalpoints', str(self._finalpoints_feature_count), ''),
            ('analysis', 'generated_results_summary_points', str(self._results_summary_feature_count), ''),
            ('analysis', 'skipped_source_points_total', str(skipped_total), ''),
        ]

        if self._skipped_sources:
            for reason, source_ids in sorted(self._skipped_sources.items()):
                sorted_ids = sorted(
                    source_ids,
                    key=lambda value: (0, int(str(value))) if str(value).isdigit() else (1, str(value))
                )
                log_rows.append(
                    ('skipped_sources', reason, str(len(sorted_ids)), ' | '.join(sorted_ids))
                )
        else:
            log_rows.append(('skipped_sources', 'none', '0', ''))

        with open(log_path, 'w', encoding='utf-8', newline='') as log_file:
            for row in log_rows:
                escaped_row = [str(value).replace('\t', ' ').replace('\n', ' ') for value in row]
                log_file.write('\t'.join(escaped_row) + '\n')

        self._analysis_log_path = log_path

    def _build_results_summary_fast(self, finalpoints_uri, gpkg_path, context, feedback):
        finalpoints_layer = QgsVectorLayer(finalpoints_uri, 'finalpoints_summary_source', 'ogr')
        if not finalpoints_layer.isValid():
            feedback.reportError(self.tr("Failed to load finalpoints layer for results_summary generation."))
            return None

        required_fields = ['COUNT', 'SUM_WEIGHT', 'W_TOT_EN', 'V', 'E', 'W_EN']
        field_names = {field.name() for field in finalpoints_layer.fields()}
        missing_fields = [field for field in required_fields if field not in field_names]
        if missing_fields:
            feedback.reportError(
                self.tr("Results_summary generation aborted. Missing fields in finalpoints: {}").format(', '.join(missing_fields))
            )
            return None

        groups = {}
        finalpoints_count = max(finalpoints_layer.featureCount(), 1)
        for current, feature in enumerate(finalpoints_layer.getFeatures(), start=1):
            if feedback.isCanceled():
                return None

            geometry = feature.geometry()
            if not geometry or geometry.isEmpty():
                continue

            key = self._geometry_key(geometry)
            group = groups.get(key)
            if group is None:
                count_value = feature['COUNT']
                sum_weight_value = feature['SUM_WEIGHT']
                w_tot_en_value = feature['W_TOT_EN']
                group = {
                    'geometry': QgsGeometry(geometry),
                    'COUNT': 0 if count_value is None else int(count_value),
                    'SUM_WEIGHT': 0.0 if sum_weight_value is None else float(sum_weight_value),
                    'W_TOT_EN': 0.0 if w_tot_en_value is None else float(w_tot_en_value),
                    'V_values': [],
                    'E_values': [],
                    'W_EN_values': [],
                }
                groups[key] = group

            v_value = feature['V']
            e_value = feature['E']
            w_en_value = feature['W_EN']
            if v_value is not None:
                group['V_values'].append(float(v_value))
            if e_value is not None:
                group['E_values'].append(float(e_value))
            if w_en_value is not None:
                group['W_EN_values'].append(float(w_en_value))

            feedback.setProgress(90 + (current / float(finalpoints_count)) * 8)

        int_type = self._int_field_type()
        double_type = self._double_field_type()
        summary_fields = QgsFields()
        summary_fields.append(self._create_field('COUNT', int_type, QVariant.Int))
        summary_fields.append(self._create_field('SUM_WEIGHT', double_type, QVariant.Double))
        summary_fields.append(self._create_field('V_min', double_type, QVariant.Double))
        summary_fields.append(self._create_field('V_max', double_type, QVariant.Double))
        summary_fields.append(self._create_field('V_mean', double_type, QVariant.Double))
        summary_fields.append(self._create_field('E_min', double_type, QVariant.Double))
        summary_fields.append(self._create_field('E_max', double_type, QVariant.Double))
        summary_fields.append(self._create_field('E_mean', double_type, QVariant.Double))
        summary_fields.append(self._create_field('W_EN_min', double_type, QVariant.Double))
        summary_fields.append(self._create_field('W_EN_max', double_type, QVariant.Double))
        summary_fields.append(self._create_field('W_EN_mean', double_type, QVariant.Double))
        summary_fields.append(self._create_field('W_TOT_EN', double_type, QVariant.Double))

        summary_options = QgsVectorFileWriter.SaveVectorOptions()
        summary_options.driverName = 'GPKG'
        summary_options.layerName = 'results_summary'
        summary_options.fileEncoding = 'UTF-8'
        summary_options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer

        summary_writer = QgsVectorFileWriter.create(
            gpkg_path,
            summary_fields,
            QgsWkbTypes.Point,
            finalpoints_layer.sourceCrs(),
            QgsCoordinateTransformContext(),
            summary_options
        )

        if summary_writer.hasError() != QgsVectorFileWriter.NoError:
            feedback.reportError(self.tr("Failed to create results_summary in GeoPackage: {}").format(summary_writer.errorMessage()))
            del summary_writer
            return None

        def _stats(values):
            if not values:
                return 0.0, 0.0, 0.0
            return min(values), max(values), sum(values) / float(len(values))

        group_count = max(len(groups), 1)
        for index_group, group in enumerate(groups.values(), start=1):
            v_min, v_max, v_mean = _stats(group['V_values'])
            e_min, e_max, e_mean = _stats(group['E_values'])
            w_en_min, w_en_max, w_en_mean = _stats(group['W_EN_values'])

            out_feat = QgsFeature(summary_fields)
            out_feat.setGeometry(group['geometry'])
            out_feat.setAttributes([
                group['COUNT'],
                group['SUM_WEIGHT'],
                v_min, v_max, v_mean,
                e_min, e_max, e_mean,
                w_en_min, w_en_max, w_en_mean,
                group['W_TOT_EN'],
            ])
            summary_writer.addFeature(out_feat)
            feedback.setProgress(98 + (index_group / float(group_count)) * 2)

        del summary_writer
        self._results_summary_feature_count = len(groups)
        return '{}|layername=results_summary'.format(gpkg_path)

    def tags(self):
        return self.tr("qproto, rock, slope").split(",")

    def icon(self):
        return QIcon(os.path.join(pluginPath, "icons", "QPROTO_main_icon.svg"))

    def shortHelpString(self):
        return self.tr(
        '''

<p>QPROTO, based on the cone method developed by Jaboyedoff and Labiouse (2011), identifies the areas that are most exposed to a rockfall phenomenon according to the topography of the slope and the calibration of some empirical parameters. These parameters are used to characterize the potential rockfall runout cones generated from a a set of user-defined source points. Furthermore, the plugin allows the quantitative estimation of block velocity and kinetic energy and the evaluation of a time-independent rockfall relative hazard.</p>

<p><strong>Input</strong>:</p>

<p><u>Digital Elevation Model:</u> raster layer of the investigated area [m].</p>

<p><u>Source point layer:</u> vector layer of the source points, containing the following attributes:</p>

<p style="margin-left: 40px;"><u>ID:</u> identifying number of the source point;</p>

<p style="margin-left: 40px;"><u>Elevation:</u> elevation of the source point [m];</p>

<p style="margin-left: 40px;"><u>Aspect:</u> dip direction of the slope in the source point, ranging between 0-360 [&deg;]. It defines the orientation of the cone;</p>

<p style="margin-left: 40px;"><u>Detachment Propensity:</u>&nbsp;index defining the propensity to detachment associated to each source point, ranging between 0-1 [-];</p>

<p style="margin-left: 40px;"><u>Boulder mass:</u> boulder mass [kg];</p>

<p style="margin-left: 40px;"><u>Energy Line Angle:</u> energy line angle of the cone having apex in the source point, ranging between 0-90 [&deg;]. Alternatively, a constatnt value can be used for all source points, to be specified in the field <em>Fixed value of the Energy Line Angle</em>.</p>

<p style="margin-left: 40px;"><u>Lateral spreading angle:</u> lateral dispersion angle of the cone having apex in the source point, ranging between 0-90 [&deg;]. This angle is added to and subtracted from the dip direction of the slope (aspect) to define the limits of the cone in the horizontal plane. Alternatively, a constant value can be used for all source points, to be specified in the field <em>Fixed value of the Lateral Spreading Angle</em>.</p>

<p><u>Fixed value of the Energy Line Angle:</u> empty if <em>Energy Line Angle</em> is considered as an attribute of each source point [&deg;].</p>

<p><u>Fixed value of the Lateral Spreading Angle:</u> empty if <em>Lateral Dispersion Angle</em> is considered as an attribute of the point [&deg;].</p>

<p><u>Limit distance of analysis:</u> distance to which the analysis can be extended. [m]</p>

<p><u>Analysis name:</u> if specified, the results will be grouped under the defined name. The output GeoPackage and all generated layers are stored under this analysis name.</p>

<p><strong>Output</strong>:</p>
<p><span style="text-decoration: underline;">Finalpoints:</span> vector point layer containing the results in each cell of the DEM reached by at least one cone. The centre of the DEM cell is represented by a point, with attributes describing (see also the QPROTO post-processing module):</p>
<p style="margin-left: 40px;"><span style="text-decoration: underline;">ID</span> and <span style="text-decoration: underline;">Elevation</span> of the corresponding source point;</p>
<p style="margin-left: 40px;"><span style="text-decoration: underline;">ID </span>and <span style="text-decoration: underline;">Elevation </span>of the final point;</p>
<p style="margin-left: 40px;">Computed <span style="text-decoration: underline;">Velocity </span>and <span style="text-decoration: underline;">Energy</span>&nbsp; at the final point;</p>
<p style="margin-left: 40px;"><span style="text-decoration: underline;">W_en</span> (Maximum weighted kinetic energy), <span style="text-decoration: underline;">W_tot_en</span> (Total weighted kinetic energy), and <span style="text-decoration: underline;">Sum_Weight</span> (Weighted Count/Susceptibility i.e. Weighted view frequency of the final point).</p>

<p><span style="text-decoration: underline;">Results summary:</span> vector point layer containing a summary of the results derived from <em>finalpoints</em> layer. For each location where points are superimposed due to overlapped cones, aggregate counts and susceptibility indicators are provided, together with the minimum, maximum, and mean values of velocity, energy, and weighted energy.</p>
<p><span style="text-decoration: underline;">Analysis log:</span> tabular text report saved in the analysis output folder and loaded in QGIS. It summarizes analysis name, output paths, source-point counts, generated cones, skipped source points grouped by reason, and produced outputs.</p>

<p><strong>Notes</strong>:</p>
<p>If no propagation cones are generated, the analysis still terminates correctly. In this case, neither the <em>finalpoints</em> layer nor the <em>results summary</em> layer is generated, while the analysis log is generated and added to the analysis group.</p>

<p><strong>Help</strong>:</p>

<p><u>Documentation:</u> 
<a href="https://gitlab.com/faunalia/QPROTO/blob/master/help/QPROTO_User_Manual_1.4.pdf">QPROTO main analysis manual</a></p>
<p><a href="https://doi.org/10.3390/geosciences11020088"><em>Preliminary Modeling of Rockfall Runout: Definition of the Input Parameters for the QGIS Plugin QPROTO</em> (2021)</a></p>
<p><a href="https://doi.org/10.3390/geosciences13120386"><em>A Novel Approach to Assess the Influence of Rockfall Source Areas: The Case Study of Bardonecchia, Italy</em> (2023)</a></p>
<p><a href="https://gitlab.com/faunalia/QPROTO/raw/master/help/training_dataset.zip">Training dataset</a></p>

<p><u>Contact:</u> <a href="mailto:marta.castelli@polito.it">marta.castelli@polito.it</a></p>



        '''
        )

    def helpUrl(self):
        return "https://gitlab.com/faunalia/QPROTO/blob/master/help/QPROTO_User_Manual_1.4.pdf"

    def initAlgorithm(self, config):
        '''
        define all the input parameters
        '''

        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.INPUT,
                self.tr('Source point layer'),
                [QgsProcessing.TypeVectorPoint]
            )
        )

        self.addParameter(
            QgsProcessingParameterRasterLayer(
                self.DTM,
                self.tr('Digital Elevation Model')
            )
        )

        self.addParameter(
            QgsProcessingParameterField(
                self.ID_FIELD,
                self.tr('ID'),
                'ID_IN',
                self.INPUT
            )
        )

        self.addParameter(
            QgsProcessingParameterField(
                self.ELEV_FIELD,
                self.tr('Elevation [m]'),
                'QUOTA_IN',
                self.INPUT
            )
        )

        self.addParameter(
            QgsProcessingParameterField(
                self.ASPECT_FIELD,
                self.tr('Aspect [°]'),
                'ASPECT',
                self.INPUT
            )
        )

        self.addParameter(
            QgsProcessingParameterField(
                self.WEIGHT_FIELD,
                self.tr('Detachment propensity [-]'),
                'PESO',
                self.INPUT
            )
        )

        self.addParameter(
            QgsProcessingParameterField(
                self.MASS_FIELD,
                self.tr('Boulder mass [kg]'),
                'MASSA',
                self.INPUT
            )
        )

        self.addParameter(
            QgsProcessingParameterField(
                self.SHADOW_FIELD,
                self.tr('Energy line angle [°]'),
                30,
                self.INPUT,
                optional=True
            )
        )

        self.addParameter(
            QgsProcessingParameterNumber(
                self.FIX_SHADOW,
                self.tr('Fixed value for energy line angle [°]'),
                QgsProcessingParameterNumber.Double,
                30,
                optional=True
            )
        )

        self.addParameter(
            QgsProcessingParameterField(
                self.LATERAL_FIELD,
                self.tr('Lateral spreading angle [°]'),
                None,
                self.INPUT,
                optional=True
            )
        )

        self.addParameter(
            QgsProcessingParameterNumber(
                self.FIX_LATERAL,
                self.tr('Fixed value for lateral spreading angle [°]'),
                QgsProcessingParameterNumber.Double,
                10,
                optional=True
            )
        )

        self.addParameter(
            QgsProcessingParameterNumber(
                self.VISIBILITY,
                self.tr('Visibility distance [m]'),
                QgsProcessingParameterNumber.Double,
                800
            )
        )

        # Modifica la stringa di testo del parametro
        output_folder_param = QgsProcessingParameterFolderDestination(
            self.OUTPUT,
                self.tr('Output folder (physical path required)')
        )
        # Aggiunge la stringa di aiuto assegnando direttamente l'attributo
        output_folder_param.helpString = self.tr('Specify a physical folder on disk. Temporary output is not supported.')
        self.addParameter(output_folder_param)


        self.addParameter(
            QgsProcessingParameterString(
                self.ANALYSIS_NAME,
                self.tr('Analysis name'),
                optional=True
            )
        )

    def processAlgorithm(self, parameters, context, feedback):
        """
        Here is where the processing itself takes place.
        """

        # start time of the algorithm
        start_time = datetime.now()

        # get all the inputs
        layer = self.parameterAsSource(parameters, self.INPUT, context)
        if not layer:
            raise QgsProcessingException(self.tr('Invalid input point layer'))
        raster = self.parameterAsRasterLayer(parameters, self.DTM, context)
        if not raster or not raster.isValid():
            raise QgsProcessingException(self.tr('Invalid DTM raster layer'))
        raster_source = raster.source()
        raster_local = QgsRasterLayer(raster_source, 'dtm_local', 'gdal')
        if not raster_local.isValid():
            raise QgsProcessingException(self.tr('Failed to open DTM source as local layer'))
        ID_Field = self.parameterAsString(parameters, self.ID_FIELD, context).strip()
        Elevation_Field = self.parameterAsString(parameters, self.ELEV_FIELD, context).strip()
        Aspect_Field = self.parameterAsString(parameters, self.ASPECT_FIELD, context).strip()
        Weight_Field = self.parameterAsString(parameters, self.WEIGHT_FIELD, context).strip()
        Mass_Field = self.parameterAsString(parameters, self.MASS_FIELD, context).strip()
        Shadow_Angle_Field = self.parameterAsString(parameters, self.SHADOW_FIELD, context)
        Fixed_value_for_the_Shadow_Angle = self.parameterAsDouble(parameters, self.FIX_SHADOW, context)
        Lateral_Dispersion_Field = self.parameterAsString(parameters, self.LATERAL_FIELD, context)
        Fixed_value_for_the_Lateral_Dispersion = self.parameterAsDouble(parameters, self.FIX_LATERAL, context)
        Visibility_Distance = self.parameterAsDouble(parameters, self.VISIBILITY, context)

        # set the output folder
        Results = self.parameterAsString(parameters, self.OUTPUT, context)

        # Get the analysis name and derive effective output folder
        analysis_name = self.parameterAsString(parameters, self.ANALYSIS_NAME, context)
        safe_analysis_name = self._sanitize_analysis_name(analysis_name)
        analysis_output_folder = self._build_analysis_output_folder(Results, analysis_name)
        os.makedirs(analysis_output_folder, exist_ok=True)
        self._analysis_log_path = None
        self._results_summary_feature_count = 0
        self._finalpoints_feature_count = 0
        self._source_points_total = 0
        self._skipped_sources = {}
        self._input_layer_name, self._input_layer_path = self._resolve_input_layer_details(parameters, context, layer)
        self._dtm_layer_name = raster.name()
        self._dtm_layer_path = raster.source()

        feedback.pushConsoleInfo(str(parameters['OUTPUT']))

        # get the CRS from the source layer
        crs = layer.sourceCrs()
        # get the extent from the raster
        ex = raster_local.extent()
        extent = '{}, {}, {},{}'.format(ex.xMinimum(),
                                        ex.xMaximum(),
                                        ex.yMinimum(),
                                        ex.yMaximum())

        # read the dtm as a numpy array
        rast_ds = gdal.Open(raster_source)
        geoTransform = rast_ds.GetGeoTransform()
        Output_Resolution = geoTransform[1]
        rast_array = np.array(rast_ds.GetRasterBand(1).ReadAsArray())

        int_type = self._int_field_type()
        double_type = self._double_field_type()

        # Creation of the final vector containing the points whitin the rockfall propagation area (this vector is saved in the output directory).
        fields = QgsFields()
        fields.append(self._create_field('ID_FIN', int_type, QVariant.Int)) # adding of the 'ID_FIN' field
        fields.append(self._create_field('ELEV_FIN', int_type, QVariant.Int)) # adding of the 'ELEV_FIN' field
        fields.append(self._create_field('ID_IN', int_type, QVariant.Int)) # adding of the 'ID_IN' field
        fields.append(self._create_field('ELEV_IN', int_type, QVariant.Int)) # adding of the 'ELEV_IN' field
        fields.append(self._create_field('SHAD_ANGLE', int_type, QVariant.Int)) # adding of the 'SHAD_ANGLE' field
        fields.append(self._create_field('LAT_DISP', int_type, QVariant.Int)) # adding of the 'LAT_DISP' field
        fields.append(self._create_field('WEIGHT', double_type, QVariant.Double)) # adding of the 'WEIGHT' field
        fields.append(self._create_field('V', double_type, QVariant.Double)) # adding of the 'V' field
        fields.append(self._create_field('E', double_type, QVariant.Double)) # adding of the 'E' field
        fields.append(self._create_field('W_EN', double_type, QVariant.Double)) # adding of the 'W_EN' field
        fields.append(self._create_field('COUNT', int_type, QVariant.Int)) # adding of the 'COUNT' field
        fields.append(self._create_field('SUM_WEIGHT', double_type, QVariant.Double)) # adding of the 'SUM_WEIGHT' field
        fields.append(self._create_field('W_TOT_EN', double_type, QVariant.Double)) # adding of the 'W_TOT_EN' field
        gpkg_filename = '{}.gpkg'.format(safe_analysis_name) if safe_analysis_name else 'finalpoints.gpkg'
        pathh = os.path.join(analysis_output_folder, gpkg_filename)
        save_options = QgsVectorFileWriter.SaveVectorOptions()
        save_options.driverName = 'GPKG'
        save_options.layerName = 'finalpoints'
        save_options.fileEncoding = 'UTF-8'
        save_options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteFile
        finalpoints_writer = QgsVectorFileWriter.create(
            pathh,
            fields,
            QgsWkbTypes.Point,
            crs,
            QgsCoordinateTransformContext(),
            save_options
        )
        if finalpoints_writer.hasError() != QgsVectorFileWriter.NoError:
            error_message = finalpoints_writer.errorMessage()
            del finalpoints_writer
            raise QgsProcessingException(
                self.tr("Failed to create finalpoints in GeoPackage: {}").format(error_message)
            )
        totalfeatures = [] # this list contains all the features within the rockfall propagation area


        # Counting of the source points (it is necessary to evaluate the progress of the analysis)
        feat_count = layer.featureCount()
        self._source_points_total = feat_count
        feedback.setProgressText('Total number of source points to process: {}'.format(str(feat_count)))

        i = 1
        coni_creati = 0
        id = 0

        dist_calc = QgsDistanceArea()
        dist_calc.setSourceCrs(crs, QgsCoordinateTransformContext())
        dist_calc.setEllipsoid(crs.ellipsoidAcronym())

        layer_field_names = set(layer.fields().names())

        required_fields = [ID_Field, Elevation_Field, Aspect_Field, Weight_Field, Mass_Field]
        missing_required_fields = [field_name for field_name in required_fields if field_name not in layer_field_names]
        if missing_required_fields:
            feedback.reportError(
                self.tr(
                    'The following required fields were not found in the source layer: {}'
                ).format(', '.join(missing_required_fields)),
                fatalError=True
            )
            return {}

        if Shadow_Angle_Field:
            Shadow_Angle_Field = Shadow_Angle_Field.strip()
            if Shadow_Angle_Field not in layer_field_names:
                coerced_shadow = self._coerce_numeric_value(Shadow_Angle_Field)
                if coerced_shadow is not None:
                    Fixed_value_for_the_Shadow_Angle = coerced_shadow
                    Shadow_Angle_Field = ''
                else:
                    feedback.reportError(
                        self.tr(
                            'Energy line angle field "{}" was not found in the source layer '
                            'and could not be interpreted as a fixed numeric value.'
                        ).format(Shadow_Angle_Field),
                        fatalError=True
                    )
                    return {}

        if Lateral_Dispersion_Field:
            Lateral_Dispersion_Field = Lateral_Dispersion_Field.strip()
            if Lateral_Dispersion_Field not in layer_field_names:
                coerced_lateral = self._coerce_numeric_value(Lateral_Dispersion_Field)
                if coerced_lateral is not None:
                    Fixed_value_for_the_Lateral_Dispersion = coerced_lateral
                    Lateral_Dispersion_Field = ''
                else:
                    feedback.reportError(
                        self.tr(
                            'Lateral spreading angle field "{}" was not found in the source layer '
                            'and could not be interpreted as a fixed numeric value.'
                        ).format(Lateral_Dispersion_Field),
                        fatalError=True
                    )
                    return {}

        # Check for proper field types
        input_fields = required_fields.copy()
        if Shadow_Angle_Field:
            input_fields.append(Shadow_Angle_Field)
        if Lateral_Dispersion_Field:
            input_fields.append(Lateral_Dispersion_Field)


        for f in layer.getFeatures():

            if not Shadow_Angle_Field:
                shad_ang = self._coerce_numeric_value(Fixed_value_for_the_Shadow_Angle)
            else:
                shad_ang = self._coerce_numeric_value(f[Shadow_Angle_Field])

            if not Lateral_Dispersion_Field:
                lat_disp = self._coerce_numeric_value(Fixed_value_for_the_Lateral_Dispersion)
            else:
                lat_disp = self._coerce_numeric_value(f[Lateral_Dispersion_Field])

            source_id = f[ID_Field]
            if shad_ang is None:
                self._record_skipped_source(source_id, 'shadow angle is null or not numeric')
                feedback.pushWarning(
                    'Skipping source point ID={}: shadow angle is null or not numeric.'.format(source_id)
                )
                continue
            if lat_disp is None:
                self._record_skipped_source(source_id, 'lateral dispersion is null or not numeric')
                feedback.pushWarning(
                    'Skipping source point ID={}: lateral dispersion is null or not numeric.'.format(source_id)
                )
                continue

            message = 'Processing source point {} of {}...'.format(str(i), str(feat_count))
            feedback.setProgressText(message)
            feedback.setProgress((i/float(feat_count)) * 89)
            i = i + 1
            if feedback.isCanceled():
                break



            # Creation of a triangle that limits the lateral dispersion of the phenomenon (the triangle is created as geometry)
            point1 = f.geometry().asPoint() # Point 1 (it coincides with the i-th source point)
            tr = triangolo(point1, f, Aspect_Field, Visibility_Distance, lat_disp)
            tr_geom = tr.geometry()
            tr_ex = tr_geom.boundingBox()
            tr_extent = '{},{},{},{}'.format(
                tr_ex.xMinimum() - Output_Resolution,
                tr_ex.xMaximum() + Output_Resolution,
                tr_ex.yMinimum() - Output_Resolution,
                tr_ex.yMaximum() + Output_Resolution
            )

            # Execution of "r.viewshed" for the i-th source point
            coordStr = '{},{}'.format(point1.x(), point1.y())
            view_path = os.path.join(QgsProcessingUtils.tempFolder(), 'viewshed.tif')
            view_output = None
            view = None
            view_ds = None
            try:
                viewshed = processing.run('grass7:r.viewshed', {
                    'input': raster_source,
                    'coordinates': coordStr,
                    'observer_elevation': 0,
                    'target_elevation': 0,
                    'max_distance': Visibility_Distance,
                    'refraction_coeff': 0.14286,
                    'memory': 500,
                    '-c':False,
                    '-r':False,
                    '-b':False,
                    '-e':False,
                    'GRASS_REGION_PARAMETER': tr_extent,
                    'GRASS_REGION_CELLSIZE_PARAMETER':0,
                    'GRASS_RASTER_FORMAT_OPT':'',
                    'GRASS_RASTER_FORMAT_META':'',
                    'output': view_path
                }, context=context, feedback=feedback)
                view_output = viewshed.get('output', view_path)

                # Reading of the "r.viewshed" output as array
                view = QgsRasterLayer(view_output, 'viewshed', 'gdal')
                view_ds = gdal.Open(view_output)
                if view_ds is None:
                    self._record_skipped_source(source_id, 'viewshed output could not be opened')
                    feedback.pushWarning('Viewshed output could not be opened, skipping source point {}'.format(f[ID_Field]))
                    continue
                viewGeoTransform = view_ds.GetGeoTransform()
                view_rast_array = np.array(view_ds.GetRasterBand(1).ReadAsArray())
                # The "r.viewshed" output is filtered with the aim of saving all the points within the energy line
                v_ex = view.extent()

                (firstRow, firstColumn, lastRow, lastColumn) = map2pixel(viewGeoTransform, v_ex)
                below_enLine = []
                for row in range(firstRow, lastRow):
                    for col in range(firstColumn, lastColumn):
                        pt_in = (view_rast_array[col, row])
                        if pt_in <= (90 - shad_ang): # if this condition is true, the point is within the energy line
                            (x, y) = pixel2map(viewGeoTransform, row, col)
                            vpoint = QgsPointXY(x, y)
                            below_enLine.append(vpoint)
            finally:
                view_ds = None
                view = None
                self._cleanup_viewshed_temp(view_output if view_output else view_path, feedback)

            source_debug = 'ID={} elev={} shad_ang={} lat_disp={}'.format(
                f[ID_Field],
                f[Elevation_Field],
                shad_ang,
                lat_disp
            )

            if len(below_enLine) < 3:
                self._record_skipped_source(source_id, 'insufficient visible points to build a valid cone')
                feedback.pushWarning(
                    'Skipping source point ({}): insufficient visible points to build a valid cone.'.format(source_debug)
                )
                continue

            # Creation of the minimum convex hull which contains all the 'below_enLine' items
            outGeom = QgsGeometry()
            tmpGeom = QgsGeometry(outGeom.fromMultiPointXY(below_enLine))
            convex = tmpGeom.convexHull()
            if not convex or convex.isEmpty():
                self._record_skipped_source(source_id, 'convex hull is empty')
                feedback.pushWarning(
                    'Skipping source point ({}): convex hull is empty.'.format(source_debug)
                )
                continue

            # Clipping of 'convex' with the triangle
            intPol = convex.intersection(tr_geom)
            if not intPol or intPol.isEmpty() or not intPol.isGeosValid():
                self._record_skipped_source(source_id, 'cone intersection is empty or invalid')
                feedback.pushWarning(
                    'Skipping source point ({}): cone intersection is empty or invalid.'.format(source_debug)
                )
                continue

            # Generation of the points within the previous result ('intPol')
            intPol_ex = intPol.boundingBox()
            extent_values = (
                intPol_ex.xMinimum(),
                intPol_ex.xMaximum(),
                intPol_ex.yMinimum(),
                intPol_ex.yMaximum(),
            )
            if any(math.isnan(v) or math.isinf(v) for v in extent_values):
                self._record_skipped_source(source_id, 'cone extent is invalid (NaN/Inf)')
                feedback.pushWarning(
                    'Skipping source point ({}): cone extent is invalid (NaN/Inf).'.format(source_debug)
                )
                continue
            (firstRow, firstColumn, lastRow, lastColumn) = map2pixel(geoTransform, intPol_ex)
            cone_creation = False
            for row in range(firstRow, lastRow + 1):
                for col in range(firstColumn, lastColumn + 1):
                    (x, y) = pixel2map(geoTransform, row, col)
                    jPoint = QgsPointXY()
                    jPoint.setX(x)
                    jPoint.setY(y)
                    if intPol.contains(jPoint):
                        quota=int(rast_array[col, row]) # elevation value of the j-th point of 'intPol'
                        distance = dist_calc.measureLine([jPoint,point1])
                        det = ((f[Elevation_Field]) - quota - distance * math.tan(math.radians(shad_ang))) # from Jaboyedoff e Labiouse (2011)
                        if det >= 0: # if 'det' is positive, the current point is within the energy line
                            inAttr = [id] # adding of 'ID_FIN'
                            inAttr.append(quota) # adding of 'ELEV_FIN'
                            inAttr.append(f[ID_Field]) # adding of 'ID_IN' from the i-th source point
                            inAttr.append(f[Elevation_Field]) # adding of 'ELEV_IN' from the i-th source point
                            inAttr.append(shad_ang) # adding of 'SHAD_ANGLE' from the i-th source point
                            inAttr.append(lat_disp) # adding of 'LAT_DISP' from the i-th source point
                            inAttr.append(f[Weight_Field]) # adding of 'WEIGHT' from the i-th source point
                            v = math.sqrt(2 * 9.81 * det) # calculation of the velocity
                            e = 0.5 * (f[Mass_Field]) * (v ** 2) # calculation of the energy
                            w_en = f[Weight_Field] * e
                            inAttr.append(v) # adding of 'V'
                            inAttr.append(e) # adding of 'E'
                            inAttr.append(w_en) # adding of 'W_EN'
                            outFeature = QgsFeature()
                            outFeature.setGeometry(QgsGeometry.fromPointXY(jPoint)) # output geometry
                            outFeature.setAttributes(inAttr) # output attributes
                            totalfeatures.append(outFeature)
                            id += 1
                            cone_creation = True

            if cone_creation:
                message = 'Some points added to "finalpoints".'
                coni_creati += 1
                feedback.setProgressText(message)
            else:
                self._record_skipped_source(source_id, 'no final points satisfied the energy line condition')
                message = 'No points added to "finalpoints".'
                feedback.setProgressText(message)

        output_finalpoints = self._append_finalpoint_summary_attributes(totalfeatures)
        self._finalpoints_feature_count = len(output_finalpoints)

        end_time_pr = datetime.now()
        self._coni_creati = coni_creati
        message = '{} propagation cones generated from {} source points'.format(coni_creati, feat_count)
        feedback.setProgressText(message)
        message = '{} points processed in (hh:mm:ss): {}'.format(feat_count, end_time_pr - start_time)
        feedback.setProgressText(message)
        feedback.setProgress(90)

        for feature in output_finalpoints:
            finalpoints_writer.addFeature(feature)

        del finalpoints_writer # Close the writer to ensure the file is complete
        finalpoints_uri = '{}|layername=finalpoints'.format(pathh)
        try:
            processing.run('native:createspatialindex', {
                'INPUT': finalpoints_uri
            }, context=context, feedback=feedback)
        except Exception as exc:
            feedback.reportError(self.tr("Failed to create spatial index for finalpoints: {}").format(str(exc)))

        feedback.pushConsoleInfo('Creating results_summary from finalpoints using direct grouped aggregation.')
        results_summary_uri = self._build_results_summary_fast(finalpoints_uri, pathh, context, feedback)

        if results_summary_uri:
            try:
                processing.run('native:createspatialindex', {
                    'INPUT': results_summary_uri
                }, context=context, feedback=feedback)
            except Exception as exc:
                feedback.reportError(self.tr("Failed to create spatial index for results_summary: {}").format(str(exc)))

        # Save outputs for postProcessAlgorithm, which runs in the main thread.
        self._finalpoints_path = pathh
        self._results_summary_uri = results_summary_uri
        self._analysis_name = analysis_name
        self._results_folder = analysis_output_folder
       
        end_time = datetime.now()
        total_time = end_time - start_time
        self._write_analysis_log(analysis_output_folder, safe_analysis_name, pathh, total_time)
        feedback.pushConsoleInfo('Coni di propagazione creati: {}'.format(self._coni_creati))
        if self._analysis_log_path:
            feedback.pushConsoleInfo('Analysis log written to: {}'.format(self._analysis_log_path))
        feedback.setProgressText('Total analysis duration (hh:mm:ss): {}'.format(total_time))

        feedback.pushConsoleInfo('Total analysis duration (hh:mm:ss): {}'.format(total_time))

        return {self.OUTPUT: analysis_output_folder}

    def postProcessAlgorithm(self, context, feedback):
        root = QgsProject.instance().layerTreeRoot()
        target_group = self._get_or_create_analysis_group(root) if self._analysis_name else None
        analysis_log_layer = self._load_analysis_log_layer()

        if self._coni_creati == 0:
            no_cones_message = self.tr(
                "No propagation cones were generated. This is not a processing error: the analysis completed, but no valid finalpoints were produced for the current inputs."
            )
            feedback.setProgressText(no_cones_message)
            feedback.pushConsoleInfo(no_cones_message)
            if analysis_log_layer:
                if target_group:
                    self._add_layer_to_group_last(target_group, analysis_log_layer)
                else:
                    QgsProject.instance().addMapLayer(analysis_log_layer)
            return {self.OUTPUT: self._results_folder}

        if not self._finalpoints_path or not os.path.exists(self._finalpoints_path):
            feedback.reportError(self.tr("Output file not found after processing."))
            return {}

        layer_name_suffix = ''
        if self._analysis_name:
            layer_name_suffix = ' - {}'.format(self._analysis_name)

        finalpoints_uri = '{}|layername=finalpoints'.format(self._finalpoints_path)
        finalpoints_layer = QgsVectorLayer(finalpoints_uri, 'finalpoints{}'.format(layer_name_suffix), 'ogr')
        if not finalpoints_layer.isValid():
            feedback.reportError(self.tr("Failed to load output layer: finalpoints in GeoPackage is not valid."))
            return {}

        results_summary_layer = None
        if self._results_summary_uri:
            candidate_layer = QgsVectorLayer(self._results_summary_uri, 'results summary{}'.format(layer_name_suffix), 'ogr')
            if candidate_layer.isValid():
                results_summary_layer = candidate_layer
            else:
                feedback.reportError(self.tr("Failed to load output layer: results_summary in GeoPackage is not valid."))

        # Apply "Gruppo di punti" (point cluster) renderer.
        cluster_renderer = QgsPointClusterRenderer()
        marker = QgsMarkerSymbol.createSimple({'name': 'circle', 'size': '1.8', 'color': '31,120,180,220'})
        cluster_renderer.setEmbeddedRenderer(QgsSingleSymbolRenderer(marker))
        finalpoints_layer.setRenderer(cluster_renderer)
        finalpoints_layer.triggerRepaint()

        # Save style inside GeoPackage (layer_styles table), using the requested name.
        style_error = ''
        if hasattr(finalpoints_layer, 'saveStyleToDatabaseV2'):
            save_result = finalpoints_layer.saveStyleToDatabaseV2(
                'Gruppo di punti',
                'Stile cluster punti generato automaticamente da QPROTO',
                True,
                ''
            )
            if isinstance(save_result, tuple):
                if len(save_result) >= 2 and isinstance(save_result[0], bool):
                    if not save_result[0]:
                        style_error = str(save_result[1])
            elif hasattr(save_result, 'errorString'):
                maybe_error = save_result.errorString()
                if maybe_error:
                    style_error = str(maybe_error)
            elif isinstance(save_result, bool):
                if not save_result:
                    style_error = self.tr('Unknown error while saving style')
            elif isinstance(save_result, str):
                style_error = save_result
        else:
            style_error = finalpoints_layer.saveStyleToDatabase(
                'Gruppo di punti',
                'Stile cluster punti generato automaticamente da QPROTO',
                True,
                ''
            )

        if style_error:
            feedback.reportError(self.tr('Unable to save style in GeoPackage: {}').format(style_error))

        if self._analysis_name:
            self._add_layer_to_group_last(target_group, finalpoints_layer)
            if results_summary_layer:
                self._add_layer_to_group_last(target_group, results_summary_layer)
            if analysis_log_layer:
                self._add_layer_to_group_last(target_group, analysis_log_layer)
        else:
            QgsProject.instance().addMapLayer(finalpoints_layer)
            if results_summary_layer:
                QgsProject.instance().addMapLayer(results_summary_layer)
            if analysis_log_layer:
                QgsProject.instance().addMapLayer(analysis_log_layer)
            layer_node_to_move = root.findLayer(finalpoints_layer.id())
            if layer_node_to_move:
                parent_of_layer = layer_node_to_move.parent()
                if parent_of_layer:
                    parent_of_layer.removeChild(layer_node_to_move)
                root.insertChildNode(0, layer_node_to_move)

        return {self.OUTPUT: self._results_folder}


    def checkParameterValues(self, parameters, context):
        '''
        overwrite the checking of default parameters of Processing and throw
        an exception if the Result folder is the temporary folder
        '''
        def _has_value(value):
            if value is None:
                return False
            text = str(value).strip()
            return text != '' and text.lower() != 'none'

        output_path = parameters[self.OUTPUT]
        analysis_name = parameters.get(self.ANALYSIS_NAME, '')
        
        # 1. Controlla se il valore del parametro è la costante interna per l'output temporaneo
        if output_path == QgsProcessing.TEMPORARY_OUTPUT:
            return False, self.tr("Temporary output folders are not supported.\nPlease choose a different destination folder.")
        
        # 2. Controlla se il percorso inizia con la cartella temporanea di QGIS (come fallback)
        temp_qgis_folder = QgsProcessingUtils.tempFolder()
        # Normalizza i percorsi per gestire differenze tra slash (/) e backslash (\)
        normalized_output_path = os.path.normpath(output_path)
        normalized_temp_qgis_folder = os.path.normpath(temp_qgis_folder)

        if normalized_output_path.startswith(normalized_temp_qgis_folder):
             return False, self.tr("Temporary output folders are not supported.\nPlease choose a different destination folder.")

        shadow_field_value = parameters.get(self.SHADOW_FIELD)
        fixed_shadow_value = parameters.get(self.FIX_SHADOW)
        if not _has_value(shadow_field_value) and not _has_value(fixed_shadow_value):
            return False, self.tr("Specify either an Energy line angle field or a fixed value for the Energy line angle.")

        lateral_field_value = parameters.get(self.LATERAL_FIELD)
        fixed_lateral_value = parameters.get(self.FIX_LATERAL)
        if not _has_value(lateral_field_value) and not _has_value(fixed_lateral_value):
            return False, self.tr("Specify either a Lateral spreading angle field or a fixed value for the Lateral spreading angle.")

        return super(QProtoAlgorithm, self).checkParameterValues(parameters, context)
