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

"""
/***************************************************************************
 LandCoverDownloader
                                 A QGIS plugin
 Downloads Sentinel-2 Land Cover Data from Esri's Living Atlas
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2026-01-11
        copyright            : (C) 2026 by CustomCartographix
        email                : john@customcartographix.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.                                   *
 *                                                                         *
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This script contains the main processing algorithms for the land      *
 *   cover downloader processing plugin.                                   *
 *                                                                         *
 ***************************************************************************/
"""

__author__ = 'CustomCartographix'
__date__ = '2026-01-11'
__copyright__ = '(C) 2026 by CustomCartographix'

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

__revision__ = '$Format:%H$'

# Import necessary modules
from os import path
from tempfile import TemporaryDirectory

from numpy import zeros
from pandas import DataFrame

from qgis.PyQt.QtCore import QCoreApplication
from qgis.PyQt.QtGui import QIcon
from qgis import processing
from qgis.core import (QgsProcessing,
                       QgsProject,
                       QgsProcessingAlgorithm,
                       QgsProcessingParameterNumber,
                       QgsProcessingParameterFeatureSource,
                       QgsProcessingParameterVectorDestination,
                       QgsProcessingParameterRasterDestination,
                       QgsProcessingParameterEnum,
                       QgsVectorLayer,
                       QgsCoordinateReferenceSystem)

from .land_cover_functions import (runLandCoverPipeline,
                                    SetLandCoverStylePostProcessor,
                                    SetAoiOutlineStylePostProcessor)


# Shared constants used by all three algorithms
YEARLIST = ['2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024']
MODEL_CRS = 'epsg:3857'


class _LandCoverAlgorithmBase(QgsProcessingAlgorithm):
    """
    Base class providing the boilerplate shared by all three
    Land Cover Downloader algorithms (translation, help URL, etc.).

    Subclasses implement ``initAlgorithm``, ``processAlgorithm``, ``name``,
    ``icon``, and ``shortHelpString`` / ``shortDescription``.
    """

    YEAR = "YEAR"
    OUTPUT = "OUTPUT"

    def displayName(self):
        return self.tr(self.name())

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

    def helpUrl(self):
        return "https://github.com/CustomCartographix/LandCoverDownloader"

    def _addYearParameter(self):
        self.addParameter(
            QgsProcessingParameterEnum(
                self.YEAR,
                self.tr('Data Collection Year (2017-2024)'),
                options=YEARLIST,
                defaultValue=7
            )
        )

    def _addOutputRasterParameter(self):
        self.addParameter(
            QgsProcessingParameterRasterDestination(
                self.OUTPUT,
                self.tr('Land Cover Raster')
            )
        )

    def _resolveYear(self, parameters, context):
        year_index = int(self.parameterAsString(parameters, self.YEAR, context))
        return YEARLIST[year_index]

    def _applyStyleOnLoad(self, context, layer_output, post_processor_cls):
        """
        Attach ``post_processor_cls`` to ``layer_output`` so its bundled QML
        style is applied when QGIS loads the layer. Safe to call whether or
        not the layer is set to auto-load.
        """
        if context.willLoadLayerOnCompletion(layer_output):
            context.layerToLoadOnCompletionDetails(layer_output).setPostProcessor(
                post_processor_cls.create())

    def _applyLandCoverStyleOnLoad(self, context, output):
        """Apply the Esri Living Atlas palette to the output raster on load."""
        self._applyStyleOnLoad(context, output, SetLandCoverStylePostProcessor)

    def _applyAoiOutlineStyleOnLoad(self, context, aoi):
        """Apply the red-outline / no-fill polygon style to the AOI on load."""
        self._applyStyleOnLoad(context, aoi, SetAoiOutlineStylePostProcessor)


class DownloadFromLatLng(_LandCoverAlgorithmBase):
    """
    LandCoverDownloader main processing algorithm.

    Builds an AOI from a user-supplied latitude/longitude and search radius,
    then delegates to the shared download/clip/mosaic pipeline.
    """

    # Constants
    LAT = "LAT"
    LNG = "LNG"
    SEARCHRADIUS = "SEARCHRADIUS"
    AOI = "AOI"

    def initAlgorithm(self, config):
        """
        Define inputs and outputs
        """

        # Save reference to project instance
        self.instance = QgsProject.instance()

        # Input LAT
        self.addParameter(
            QgsProcessingParameterNumber(
                self.LAT,
                self.tr('Latitude (DDG)'),
                QgsProcessingParameterNumber.Double,
                minValue=-90.0,
                maxValue=90.0
            )
        )

        # Input LNG
        self.addParameter(
            QgsProcessingParameterNumber(
                self.LNG,
                self.tr('Longitude (DDG)'),
                QgsProcessingParameterNumber.Double,
                minValue=-180.0,
                maxValue=180.0
            )
        )

        # Input search radius
        self.addParameter(
            QgsProcessingParameterNumber(
                self.SEARCHRADIUS,
                self.tr('Search Radius (m)'),
                QgsProcessingParameterNumber.Integer,
                minValue=0,
                defaultValue=5000
            )
        )

        # Input year
        self._addYearParameter()

        # Output AOI
        self.addParameter(
            QgsProcessingParameterVectorDestination(
                self.AOI,
                self.tr('AOI Layer'),
                QgsProcessing.TypeVectorPolygon
            )
        )

        # Output land cover raster
        self._addOutputRasterParameter()

    def processAlgorithm(self, parameters, context, feedback):
        """
        Processing portion
        """

        # Assign inputs to variables
        lat = self.parameterAsDouble(parameters, self.LAT, context)
        lng = self.parameterAsDouble(parameters, self.LNG, context)
        search_radius = self.parameterAsInt(parameters, self.SEARCHRADIUS, context)
        year_string = self._resolveYear(parameters, context)

        output = self.parameterAsOutputLayer(parameters, self.OUTPUT, context)
        aoi = self.parameterAsOutputLayer(parameters, self.AOI, context)

        if feedback.isCanceled():
            return {}

        feedback.setProgressText('Performing initial setup...')

        model_coordinate_system = QgsCoordinateReferenceSystem(MODEL_CRS)

        # Context-managed scratch dir so cleanup is deterministic (fixes the
        # ResourceWarning: Implicitly cleaning up <TemporaryDirectory ...>).
        with TemporaryDirectory() as scratch_folder:

            # Create point feature from lat/lng values via a temporary CSV.
            coordinate_array = zeros((1, 3))
            coordinate_array[0][0] = 1
            coordinate_array[0][1] = lat
            coordinate_array[0][2] = lng
            df = DataFrame(coordinate_array, columns=['id', 'lat', 'lng'])

            csv_path = path.join(scratch_folder, 'coordinate_table.csv')
            with open(csv_path, "wb") as f:
                df.to_csv(f, index=False)

            poi_layer = processing.run("native:createpointslayerfromtable", {
                'INPUT': csv_path,
                'XFIELD': 'lng', 'YFIELD': 'lat', 'ZFIELD': '', 'MFIELD': '',
                'TARGET_CRS': QgsCoordinateReferenceSystem('EPSG:4326'),
                'OUTPUT': 'TEMPORARY_OUTPUT'})['OUTPUT']

            poi_projected_layer = processing.run("native:reprojectlayer", {
                'INPUT': poi_layer,
                'TARGET_CRS': model_coordinate_system,
                'OUTPUT': 'TEMPORARY_OUTPUT'})['OUTPUT']

            # Generate AOI
            processing.run("native:buffer", {'INPUT': poi_projected_layer, 'DISTANCE': search_radius,
                                             'SEGMENTS': 5, 'END_CAP_STYLE': 2, 'JOIN_STYLE': 1,
                                             'MITER_LIMIT': 2, 'DISSOLVE': False,
                                             'OUTPUT': aoi})

            aoi_layer = QgsVectorLayer(aoi, 'aoi_layer')

            # Shared download / clip / mosaic pipeline
            runLandCoverPipeline(aoi_layer, year_string, scratch_folder,
                                 output, feedback)

        # Apply the Esri land cover palette to the output raster and the
        # red-outline style to the generated AOI polygon on load.
        self._applyLandCoverStyleOnLoad(context, output)
        self._applyAoiOutlineStyleOnLoad(context, aoi)

        feedback.setProgressText('...Complete!')

        return {
            self.OUTPUT: output,
            self.AOI: aoi
        }

    def name(self):
        return 'Download Land Cover from Lat/Lng'

    def createInstance(self):
        return DownloadFromLatLng()

    def icon(self):
        return QIcon(path.join(path.dirname(__file__), 'resources', 'lat_lng_icon_svg.svg'))

    def shortHelpString(self):
        return """
        Downloads Sentinel-2 land cover data from Esri's Living Atlas based on user input latitude and longitude.

        Input 'Latitude' and 'Longitude' in decimal degrees format. Use +/- for N/S and E/W.

        'Search Radius' is input in meters (resulting area of interest (AOI) box will have a length and width 2x the search radius).

        'Data Collection Year' is the year the data represent.

        The model will output the land cover raster clipped to the AOI, as well as the calculated AOI vector file.
        """

    def shortDescription(self):
        return "Downloads Sentinel-2 land cover data using an input latitude/longitude point and a search radius"


class DownloadFromPoint(_LandCoverAlgorithmBase):
    """
    LandCoverDownloader main processing algorithm.

    Builds an AOI from a user-supplied point layer and search radius, then
    delegates to the shared download/clip/mosaic pipeline.
    """

    # Constants
    POINT = "POINT"
    SEARCHRADIUS = "SEARCHRADIUS"
    AOI = "AOI"

    def initAlgorithm(self, config):
        """
        Define inputs and outputs
        """

        # Save reference to project instance
        self.instance = QgsProject.instance()

        # Input Point
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.POINT,
                self.tr('Input Point'),
                [QgsProcessing.TypeVectorPoint]
            )
        )

        # Input search radius
        self.addParameter(
            QgsProcessingParameterNumber(
                self.SEARCHRADIUS,
                self.tr('Search Radius (m)'),
                QgsProcessingParameterNumber.Integer,
                minValue=0,
                defaultValue=5000
            )
        )

        # Input year
        self._addYearParameter()

        # Output AOI
        self.addParameter(
            QgsProcessingParameterVectorDestination(
                self.AOI,
                self.tr('AOI Layer'),
                QgsProcessing.TypeVectorPolygon
            )
        )

        # Output land cover raster
        self._addOutputRasterParameter()

    def processAlgorithm(self, parameters, context, feedback):
        """
        Processing portion
        """

        # Assign inputs to variables
        point = self.parameterAsVectorLayer(parameters, self.POINT, context)
        search_radius = self.parameterAsInt(parameters, self.SEARCHRADIUS, context)
        year_string = self._resolveYear(parameters, context)

        output = self.parameterAsOutputLayer(parameters, self.OUTPUT, context)
        aoi = self.parameterAsOutputLayer(parameters, self.AOI, context)

        if feedback.isCanceled():
            return {}

        feedback.setProgressText('Performing initial setup...')

        model_coordinate_system = QgsCoordinateReferenceSystem(MODEL_CRS)

        with TemporaryDirectory() as scratch_folder:

            # Reproject point layer
            poi_projected_layer = processing.run("native:reprojectlayer", {
                'INPUT': point,
                'TARGET_CRS': model_coordinate_system,
                'OUTPUT': 'TEMPORARY_OUTPUT'})['OUTPUT']

            # Generate AOI
            processing.run("native:buffer", {'INPUT': poi_projected_layer, 'DISTANCE': search_radius,
                                             'SEGMENTS': 5, 'END_CAP_STYLE': 2, 'JOIN_STYLE': 1,
                                             'MITER_LIMIT': 2, 'DISSOLVE': False,
                                             'OUTPUT': aoi})

            aoi_layer = QgsVectorLayer(aoi, 'aoi_layer')

            # Shared download / clip / mosaic pipeline
            runLandCoverPipeline(aoi_layer, year_string, scratch_folder,
                                 output, feedback)

        # Apply the Esri land cover palette to the output raster and the
        # red-outline style to the generated AOI polygon on load.
        self._applyLandCoverStyleOnLoad(context, output)
        self._applyAoiOutlineStyleOnLoad(context, aoi)

        feedback.setProgressText('...Complete!')

        return {
            self.OUTPUT: output,
            self.AOI: aoi
        }

    def name(self):
        return 'Download Land Cover from Point'

    def createInstance(self):
        return DownloadFromPoint()

    def icon(self):
        return QIcon(path.join(path.dirname(__file__), 'resources', 'point_icon_svg.svg'))

    def shortHelpString(self):
        return """
        Downloads Sentinel-2 land cover data from Esri's Living Atlas based on user input point.

        'Input Point' is a vector point file (can contain multiple features).

        'Search Radius' is input in meters (resulting area of interest (AOI) box(es) will have a length and width 2x the search radius, centered on each point).

        'Data Collection Year' is the year the data represent.

        The model will output the land cover raster clipped to the AOI(s), as well as the calculated AOI vector file.
        """

    def shortDescription(self):
        return "Downloads Sentinel-2 land cover data using input point(s) and a search radius"


class DownloadFromAoi(_LandCoverAlgorithmBase):
    """
    LandCoverDownloader main processing algorithm.

    Uses a user-supplied AOI polygon layer directly and delegates to the
    shared download/clip/mosaic pipeline.
    """

    # Constants
    AOI = "AOI"

    def initAlgorithm(self, config):
        """
        Define inputs and outputs
        """

        # Save reference to project instance
        self.instance = QgsProject.instance()

        # Input AOI
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.AOI,
                self.tr('Input AOI'),
                [QgsProcessing.TypeVectorPolygon]
            )
        )

        # Input year
        self._addYearParameter()

        # Output land cover raster
        self._addOutputRasterParameter()

    def processAlgorithm(self, parameters, context, feedback):
        """
        Processing portion
        """

        # Assign inputs to variables
        aoi = self.parameterAsVectorLayer(parameters, self.AOI, context)
        year_string = self._resolveYear(parameters, context)

        output = self.parameterAsOutputLayer(parameters, self.OUTPUT, context)

        if feedback.isCanceled():
            return {}

        feedback.setProgressText('Performing initial setup...')

        model_coordinate_system = QgsCoordinateReferenceSystem(MODEL_CRS)

        with TemporaryDirectory() as scratch_folder:

            # Reproject aoi layer
            aoi_layer = processing.run("native:reprojectlayer", {
                'INPUT': aoi,
                'TARGET_CRS': model_coordinate_system,
                'OUTPUT': 'TEMPORARY_OUTPUT'})['OUTPUT']

            # Shared download / clip / mosaic pipeline
            runLandCoverPipeline(aoi_layer, year_string, scratch_folder,
                                 output, feedback)

        # Apply the Esri land cover palette to the output raster on load
        self._applyLandCoverStyleOnLoad(context, output)

        feedback.setProgressText('...Complete!')

        return {
            self.OUTPUT: output
        }

    def name(self):
        return 'Download Land Cover from AOI'

    def createInstance(self):
        return DownloadFromAoi()

    def icon(self):
        return QIcon(path.join(path.dirname(__file__), 'resources', 'aoi_icon_svg.svg'))

    def shortHelpString(self):
        return """
        Downloads Sentinel-2 land cover data from Esri's Living Atlas based on user input area of interest (AOI).

        'Input AOI' is a vector polygon file (can contain multiple features).

        'Data Collection Year' is the year the data represent.

        The model will output the land cover raster clipped to the AOI(s).
        """

    def shortDescription(self):
        return "Downloads Sentinel-2 land cover data using input AOI(s)"
