# -*- coding: utf-8 -*-
"""
/***************************************************************************
 Detecting Spatial Clusters and Outliers
                                 A QGIS plugin
 Generated by Plugin Builder
 ***************************************************************************/
"""

__author__ = 'test'
__date__ = '2024-10-24'
__copyright__ = '(C) 2024 by test'

from qgis.PyQt.QtCore import QCoreApplication
from qgis.core import (
    QgsProcessing,
    QgsProcessingAlgorithm,
    QgsProcessingParameterVectorLayer,
    QgsProcessingParameterField,
    QgsProcessingParameterEnum,
    QgsProcessingParameterNumber,
    QgsProcessingParameterFeatureSink,
    QgsProcessingParameterFileDestination,
    QgsVectorLayer,
    QgsVectorFileWriter,
    QgsSymbol,
    QgsRendererCategory,
    QgsCategorizedSymbolRenderer,
    QgsProject
)
from PyQt5.QtGui import QColor
from qgis.utils import iface
from PyQt5.QtCore import QVariant, QCoreApplication
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import QUrl

import os
import tempfile
import geopandas as gpd
import pandas as pd
import libpysal
from esda.moran import Moran_Local

class LocalMoransI(QgsProcessingAlgorithm):
    INPUT = 'INPUT'
    VARIABLE = 'VARIABLE'
    ID_FIELD = 'ID_FIELD'
    METHOD = 'METHOD'
    KNN_DIST = 'KNN_DIST'
    OUTPUT = 'OUTPUT'
    CSV_OUTPUT = 'CSV_OUTPUT'

    def name(self):
        return 'Detecting Spatial Clusters and Outliers'

    def displayName(self):
        return self.tr('Detecting Spatial Clusters and Outliers')

    def group(self):
        return "Spatial Analysis"

    def groupId(self):
        return "Spatial Analysis"
    

    def icon(self):
        icon_path = os.path.join(os.path.dirname(__file__), '..', 'Icons', 'logo3.png')
        if not os.path.exists(icon_path):
            raise FileNotFoundError(f"Icon file not found at {icon_path}")
        return QIcon(icon_path)

    def helpUrl(self):
        file = os.path.dirname(__file__) + '/selcahelpEN.html'
        if not os.path.exists(file):
            return ''
        return QUrl.fromLocalFile(file).toString(QUrl.FullyEncoded)

    def initAlgorithm(self, config=None):
        self.addParameter(QgsProcessingParameterVectorLayer(
            self.INPUT, 'Input layer', types=[QgsProcessing.TypeVectorPolygon, QgsProcessing.TypeVectorPoint]))
        self.addParameter(QgsProcessingParameterField(
            self.VARIABLE, 'Variable X', type=QgsProcessingParameterField.Numeric, parentLayerParameterName=self.INPUT))
        self.addParameter(QgsProcessingParameterField(
            self.ID_FIELD, 'Identifier Field (e.g. Name, ID)', parentLayerParameterName=self.INPUT))
        self.addParameter(QgsProcessingParameterEnum(
            self.METHOD, 'Method', options=['Queen contiguity', 'Rook contiguity', 'K Nearest Neighbors', 'Distance Band'], defaultValue=0))
        self.addParameter(QgsProcessingParameterNumber(
            self.KNN_DIST, type=QgsProcessingParameterNumber.Integer, description='K Neighbors / Distance threshold (only for KNN / Distance Band methods)', defaultValue=1, minValue=1))
        self.addParameter(QgsProcessingParameterFileDestination(
            self.CSV_OUTPUT, 'CSV Output', fileFilter='CSV files (*.csv)'))
        self.addParameter(QgsProcessingParameterFeatureSink(
            self.OUTPUT, 'Local Moran\'s I result', createByDefault=True))

    def processAlgorithm(self, parameters, context, feedback):
        layerSource = self.parameterAsVectorLayer(parameters, self.INPUT, context)
        field = self.parameterAsString(parameters, self.VARIABLE, context)
        id_field = self.parameterAsString(parameters, self.ID_FIELD, context)
        method = self.parameterAsInt(parameters, self.METHOD, context)
        knn_dist = self.parameterAsDouble(parameters, self.KNN_DIST, context)
        csv_path = self.parameterAsFileOutput(parameters, self.CSV_OUTPUT, context)

        if layerSource.geometryType() == 0 and method in [0, 1]:
            raise Exception('Queen/Rook method only works for polygon layers.')

        temp = os.path.join(tempfile.gettempdir(), 'temp_layer.shp')
        QgsVectorFileWriter.writeAsVectorFormat(layerSource, temp, "utf-8", layerSource.crs(), "ESRI Shapefile")
        data = gpd.read_file(temp)

        if layerSource.geometryType() == 2 and method in [2, 3]:
            polygonGeom = data['geometry']
            data['geometry'] = data.centroid

        if method == 0:
            w = libpysal.weights.contiguity.Queen.from_dataframe(data)
        elif method == 1:
            w = libpysal.weights.contiguity.Rook.from_dataframe(data)
        elif method == 2:
            w = libpysal.weights.distance.KNN.from_dataframe(data, k=knn_dist)
        elif method == 3:
            w = libpysal.weights.distance.DistanceBand.from_dataframe(data, threshold=knn_dist)

        y = data[field]
        localMoran = Moran_Local(y, w)

        data['LMI'] = localMoran.Is
        data['LMQ'] = localMoran.q
        data['LMP'] = localMoran.p_z_sim

        cluster_labels = {
            1: 'HH',
            2: 'LH',
            3: 'LL',
            4: 'HL'
        }
        data['Cluster'] = [cluster_labels.get(q, 'Unknown') if p <= 0.05 else 'Insignificant'
                           for q, p in zip(data['LMQ'], data['LMP'])]

        if layerSource.geometryType() == 2 and method in [2, 3]:
            data['geometry'] = polygonGeom

        data.to_csv(csv_path, index=False, encoding='utf-8')

        outputPath = os.path.join(tempfile.gettempdir(), 'detecting_spatial_clusters_results.shp')
        data.to_file(outputPath, encoding='utf-8')

        vectorLayer = QgsVectorLayer(outputPath, "Detecting Spatial Clusters and Outliers", "ogr")
        vectorLayer.setCrs(layerSource.crs())

        # --- Apply symbology ---
        cluster_colors = {
            'HH': '#d7191c',          # red
            'LL': '#2c7bb6',          # blue
            'HL': '#fdae61',          # orange
            'LH': '#1a9641',          # green
            'Insignificant': '#bdbdbd'  # grey
        }

        categories = []
        for cluster_value, color in cluster_colors.items():
            symbol = QgsSymbol.defaultSymbol(vectorLayer.geometryType())
            symbol.setColor(QColor(color))
            category = QgsRendererCategory(cluster_value, symbol, cluster_value)
            categories.append(category)

        renderer = QgsCategorizedSymbolRenderer('Cluster', categories)
        vectorLayer.setRenderer(renderer)
        vectorLayer.triggerRepaint()

        # --- Add to canvas ---
        QgsProject.instance().addMapLayer(vectorLayer)
        dest_id = vectorLayer.id()

        # Descriptive statistics
        layer_name = layerSource.name()
        feature_count = layerSource.featureCount()
        variable_stats = y.describe()
        descriptive_text = (
            f"Descriptive Summary for Layer: {layer_name}\n"
            f"- Total Features: {feature_count}\n"
            f"- Variable: {field}\n"
            f"- Min: {variable_stats['min']:.2f}\n"
            f"- Max: {variable_stats['max']:.2f}\n"
            f"- Mean: {variable_stats['mean']:.2f}\n"
            f"- Std Dev: {variable_stats['std']:.2f}\n"
        )

        cluster_examples = {}
        for cluster in ['HH', 'LL', 'HL', 'LH']:
            sample_ids = data[data['Cluster'] == cluster][id_field].head(3).astype(str).tolist()
            if sample_ids:
                cluster_examples[cluster] = ', '.join(sample_ids)

        examples_text = "\nExample features for each cluster:\n"
        for label, examples in cluster_examples.items():
            examples_text += f"- {label}: {examples}\n"

        summary = data['Cluster'].value_counts().to_dict()
        interpretation = (
            "Interpretation of cluster types:\n"
            "- HH (High-High): High value surrounded by high values (hotspot).\n"
            "- LL (Low-Low): Low value surrounded by low values (coldspot).\n"
            "- HL (High-Low): High value surrounded by low values (outlier).\n"
            "- LH (Low-High): Low value surrounded by high values (outlier).\n"
            "- Insignificant: No significant spatial pattern found at 95% confidence level.\n"
        )
        counts = "\n".join([f"- {key}: {val} features" for key, val in summary.items()])

        log_message = (
            f"{descriptive_text}\n"
            f"Local Moran's I classification result:\n\n"
            f"{interpretation}\n"
            f"Cluster count summary:\n{counts}\n"
            f"{examples_text}\n"
            f"CSV file exported to: {csv_path}"
        )

        feedback.pushInfo(log_message)

        return {self.OUTPUT: dest_id, self.CSV_OUTPUT: csv_path}


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

    def shortHelpString(self):
        return (
            "Detects spatial clusters and outliers using Local Moran's I statistic.\n\n"
            "This plugin, Detecting Spatial Clusters and Outliers, identifies local patterns "
            "of spatial association in numeric data.\n\n"
            "Outputs:\n"
            "- Cluster classification: High-High (HH), Low-Low (LL), High-Low (HL), Low-High (LH), and Insignificant\n"
            "- Output vector layer with automatically applied symbology\n"
            "- CSV file with Local Moran's I, Z-scores, p-values, and cluster labels\n\n"
            "Useful for identifying local hotspots, cold spots, and spatial outliers in your dataset."
        )


    def createInstance(self):
        return LocalMoransI()
