# -*- coding: utf-8 -*-
"""
a5gen.py
***************************************************************************
*                                                                         *
*   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.                                   *
*                                                                         *
***************************************************************************
"""
#  Need to be checked and tested

__author__ = "Thang Quach"
__date__ = "2024-11-20"
__copyright__ = "(L) 2024, Thang Quach"

from qgis.core import (
    QgsApplication,
    QgsProject,
    QgsFeatureSink,
    QgsProcessingLayerPostProcessorInterface,
    QgsProcessingParameterExtent,
    QgsProcessingParameterNumber,
    QgsProcessingException,
    QgsProcessingParameterFeatureSink,
    QgsProcessingParameterBoolean,
    QgsProcessingAlgorithm,
    QgsFields,
    QgsField,
    QgsFeature,
    QgsGeometry,
    QgsWkbTypes,
    QgsCoordinateReferenceSystem,
    QgsVectorLayer,
    QgsPalLayerSettings,
    QgsVectorLayerSimpleLabeling,
)
from qgis.PyQt.QtGui import QIcon, QColor
from qgis.PyQt.QtCore import QCoreApplication, Qt
from qgis.utils import iface
from qgis.PyQt.QtCore import QVariant
import os
from ...utils.help_footer import social_links_footer
from vgrid.utils.geometry import geodesic_dggs_metrics
from ...settings import settings
from vgrid.utils.io import validate_coordinate
from qgis.core import QgsCoordinateTransform
from ...utils.latlon import epsg4326
from vgrid.utils.constants import DGGS_TYPES
from collections import deque
from shapely.geometry import box
import a5
from vgrid.conversion.dggs2geo.a52geo import a52geo_u64


class A5Gen(QgsProcessingAlgorithm):
    EXTENT = "EXTENT"
    RESOLUTION = "RESOLUTION"
    SEGMENTS = "SEGMENTS"
    SPLIT_ANTIMERIDIAN = "SPLIT_ANTIMERIDIAN"
    OUTPUT = "OUTPUT"

    LOC = QgsApplication.locale()[:2]

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

    def tr(self, *string):
        # Translate to Vietnamese: arg[0] - English (translate), arg[1] - Vietnamese
        if self.LOC == "vi":
            if len(string) == 2:
                return string[1]
            else:
                return self.translate(string[0])
        else:
            return self.translate(string[0])

    def createInstance(self):
        return A5Gen()

    def name(self):
        return "a5_gen"

    def icon(self):
        return QIcon(
            os.path.join(
                os.path.dirname(os.path.dirname(__file__)),
                "../images/generator/grid_a5.svg",
            )
        )

    def displayName(self):
        return self.tr("A5", "A5")

    def group(self):
        return self.tr("Generator", "Generator")

    def groupId(self):
        return "generator"

    def tags(self):
        return self.tr("DGGS, A5, generator").split(",")

    txt_en = "A5 DGGS Generator"
    txt_vi = "A5 DGGS Generator"
    figure = "../images/tutorial/grid_a5.png"

    def shortHelpString(self):
        footer = (
            '''<div align="center">
                      <img src="'''
            + os.path.join(os.path.dirname(os.path.dirname(__file__)), self.figure)
            + """">
                    </div>
                    <div align="right">
                      <p align="right">
                      <b>"""
            + self.tr("Author: Thang Quach", "Author: Thang Quach")
            + """</b>
                      </p>"""
            + social_links_footer()
            + """
                    </div>
                    """
        )
        return self.tr(self.txt_en, self.txt_vi) + footer

    def initAlgorithm(self, config=None):
        settings.readSettings()

        param = QgsProcessingParameterExtent(
            self.EXTENT, self.tr("Canvas extent"), optional=True
        )
        self.addParameter(param)
        min_res = DGGS_TYPES["a5"]["min_res"]
        max_res = DGGS_TYPES["a5"]["max_res"]
        param = QgsProcessingParameterNumber(
            self.RESOLUTION,
            self.tr(f"Resolution [{min_res}..{max_res}]"),
            QgsProcessingParameterNumber.Integer,
            defaultValue=1,
            minValue=min_res,
            maxValue=max_res,
            optional=False,
        )
        self.addParameter(param)

        param = QgsProcessingParameterNumber(
            self.SEGMENTS,
            self.tr("Segments"),
            QgsProcessingParameterNumber.Integer,
            defaultValue=settings.A5SgementsSpinBox,
            minValue=1,
            optional=False,
        )
        self.addParameter(param)

        param = QgsProcessingParameterBoolean(
            self.SPLIT_ANTIMERIDIAN,
            self.tr("Split at Antimeridian"),
            defaultValue=False,
        )
        self.addParameter(param)

        param = QgsProcessingParameterFeatureSink(self.OUTPUT, "A5")
        self.addParameter(param)

    def prepareAlgorithm(self, parameters, context, feedback):
        self.resolution = self.parameterAsInt(parameters, self.RESOLUTION, context)
        # Get the extent parameter
        self.canvas_extent = self.parameterAsExtent(parameters, self.EXTENT, context)
        self.split_antimeridian = self.parameterAsBoolean(
            parameters, self.SPLIT_ANTIMERIDIAN, context
        )
        self.segments = self.parameterAsInt(parameters, self.SEGMENTS, context)
        self.a5_options = {"segments": self.segments}
        if self.resolution > 8 and (
            self.canvas_extent is None or self.canvas_extent.isEmpty()
        ):
            feedback.reportError(
                "For performance reason, when resolution is greater than 8, the canvas extent must be set."
            )
            return False

        return True

    def outputFields(self):
        output_fields = QgsFields()
        output_fields.append(QgsField("a5", QVariant.String))
        output_fields.append(QgsField("resolution", QVariant.Int))
        output_fields.append(QgsField("center_lat", QVariant.Double))
        output_fields.append(QgsField("center_lon", QVariant.Double))
        output_fields.append(QgsField("avg_edge_len", QVariant.Double))
        output_fields.append(QgsField("cell_area", QVariant.Double))
        output_fields.append(QgsField("cell_perimeter", QVariant.Double))

        return output_fields

    def processAlgorithm(self, parameters, context, feedback):
        """
        Generate A5 DGGS polygons intersecting the requested extent.
        """
        fields = self.outputFields()
        (sink, dest_id) = self.parameterAsSink(
            parameters,
            self.OUTPUT,
            context,
            fields,
            QgsWkbTypes.Polygon,
            QgsCoordinateReferenceSystem("EPSG:4326"),
        )

        if not sink:
            raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))

        canvas_crs = QgsProject.instance().crs()

        if self.canvas_extent is None or self.canvas_extent.isEmpty():
            min_lon, min_lat, max_lon, max_lat = -180, -90, 180, 90
        else:
            try:
                min_lon, min_lat, max_lon, max_lat = (
                    self.canvas_extent.xMinimum(),
                    self.canvas_extent.yMinimum(),
                    self.canvas_extent.xMaximum(),
                    self.canvas_extent.yMaximum(),
                )
                # Transform extent to EPSG:4326 if needed
                if epsg4326 != canvas_crs:
                    trans_to_4326 = QgsCoordinateTransform(
                        canvas_crs, epsg4326, QgsProject.instance()
                    )
                    transformed_extent = trans_to_4326.transform(self.canvas_extent)
                    min_lon, min_lat, max_lon, max_lat = (
                        transformed_extent.xMinimum(),
                        transformed_extent.yMinimum(),
                        transformed_extent.xMaximum(),
                        transformed_extent.yMaximum(),
                    )
            except Exception:
                min_lon, min_lat, max_lon, max_lat = -180, -90, 180, 90

        # validate_coordinate expects (min_lon, min_lat, max_lon, max_lat)
        min_lon, min_lat, max_lon, max_lat = validate_coordinate(
            min_lon, min_lat, max_lon, max_lat
        )

        bbox_polygon = box(min_lon, min_lat, max_lon, max_lat)
        bbox_center_lon = bbox_polygon.centroid.x
        bbox_center_lat = bbox_polygon.centroid.y

        # Centroid-seed BFS: expand neighbors until no intersecting cells remain.
        seed_cell_id = a5.lonlat_to_cell(
            (bbox_center_lon, bbox_center_lat), self.resolution
        )
        seed_cell_polygon = a52geo_u64(
            seed_cell_id,
            options=self.a5_options,
            split_antimeridian=self.split_antimeridian,
        )
        if seed_cell_polygon is None:
            raise QgsProcessingException("Failed to generate seed A5 cell polygon.")

        intersecting_cells = {}  # {cell_u64: shapely_polygon}
        if seed_cell_polygon.contains(bbox_polygon):
            intersecting_cells[seed_cell_id] = seed_cell_polygon
        else:
            covered_cells = set()
            queue = deque([seed_cell_id])

            while queue:
                if feedback.isCanceled():
                    break

                current_cell_id = queue.popleft()
                if current_cell_id in covered_cells:
                    continue
                covered_cells.add(current_cell_id)

                cell_polygon = a52geo_u64(
                    current_cell_id,
                    options=self.a5_options,
                    split_antimeridian=self.split_antimeridian,
                )
                if cell_polygon is None:
                    continue

                if cell_polygon.intersects(bbox_polygon):
                    intersecting_cells[current_cell_id] = cell_polygon
                    neighbors = a5.uncompact(
                        a5.grid_disk_vertex(current_cell_id, 1), self.resolution
                    )
                    for neighbor_id in neighbors:
                        if neighbor_id not in covered_cells:
                            queue.append(neighbor_id)

        if not intersecting_cells:
            raise QgsProcessingException(
                "No A5 cells were generated for the provided extent."
            )

        items = list(intersecting_cells.items())
        total_cells = len(items)

        feedback.pushInfo(f"A5 DGGS generation: {total_cells} intersecting cells.")
        for idx, (cell_u64, cell_polygon) in enumerate(items):
            if feedback.isCanceled():
                break

            feedback.setProgress(int((idx / max(total_cells, 1)) * 100))
            cell_resolution = a5.get_resolution(cell_u64)
            num_edges = 5
            if cell_resolution == 1:
                num_edges = 3

            a5_hex = a5.u64_to_hex(cell_u64)
            (
                center_lat,
                center_lon,
                avg_edge_len,
                cell_area,
                cell_perimeter,
            ) = geodesic_dggs_metrics(cell_polygon, num_edges)

            cell_geometry = QgsGeometry.fromWkt(cell_polygon.wkt)
            a5_feature = QgsFeature()
            a5_feature.setGeometry(cell_geometry)
            a5_feature.setAttributes(
                [
                    a5_hex,
                    self.resolution,
                    center_lat,
                    center_lon,
                    avg_edge_len,
                    cell_area,
                    cell_perimeter,
                ]
            )
            sink.addFeature(a5_feature, QgsFeatureSink.FastInsert)

        feedback.pushInfo("A5 DGGS generation completed.")
        if context.willLoadLayerOnCompletion(dest_id):
            lineColor = settings.a5Color
            fontColor = QColor("#000000")
            context.layerToLoadOnCompletionDetails(dest_id).setPostProcessor(
                StylePostProcessor.create(lineColor, fontColor)
            )

        return {self.OUTPUT: dest_id}


class StylePostProcessor(QgsProcessingLayerPostProcessorInterface):
    instance = None
    line_color = None
    font_color = None

    def __init__(self, line_color, font_color):
        self.line_color = line_color
        self.font_color = font_color
        super().__init__()

    def postProcessLayer(self, layer, context, feedback):
        if not isinstance(layer, QgsVectorLayer):
            return
        sym = layer.renderer().symbol().symbolLayer(0)
        sym.setBrushStyle(Qt.BrushStyle.NoBrush)
        sym.setStrokeColor(self.line_color)
        if settings.gridLabel:
            label = QgsPalLayerSettings()
            label.fieldName = "a5"
            format = label.format()
            format.setColor(self.font_color)
            format.setSize(8)
            label.setFormat(format)
            labeling = QgsVectorLayerSimpleLabeling(label)
            layer.setLabeling(labeling)
            layer.setLabelsEnabled(True)

        iface.layerTreeView().refreshLayerSymbology(layer.id())

        root = QgsProject.instance().layerTreeRoot()
        layer_node = root.findLayer(layer.id())
        if layer_node:
            layer_node.setCustomProperty("showFeatureCount", True)
        # iface.mapCanvas().setExtent(layer.extent())
        iface.mapCanvas().refresh()

    # Hack to work around sip bug!
    @staticmethod
    def create(line_color, font_color) -> "StylePostProcessor":
        """
        Returns a new instance of the post processor, keeping a reference to the sip
        wrapper so that sip doesn't get confused with the Python subclass and call
        the base wrapper implementation instead... ahhh sip, you wonderful piece of sip
        """
        StylePostProcessor.instance = StylePostProcessor(line_color, font_color)
        return StylePostProcessor.instance
