# -*- 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 qgis.PyQt.QtCore import QCoreApplication
from qgis.PyQt.QtGui import QIcon
from qgis import processing
from qgis.core import (QgsProcessing,
                       QgsProcessingFeedback,
                       QgsProject,
                       QgsProcessingAlgorithm,
                       QgsProcessingParameterNumber,
                       QgsProcessingParameterFeatureSource,
                       QgsProcessingParameterVectorDestination,
                       QgsProcessingParameterRasterDestination,
                       QgsProcessingParameterEnum,
                       QgsVectorLayer,
                       QgsCoordinateReferenceSystem,
                       QgsVectorFileWriter)
from tempfile import TemporaryDirectory
from os import (chdir, path)
from numpy import zeros
from pandas import DataFrame

from .land_cover_functions import *


class DownloadFromLatLng(QgsProcessingAlgorithm):
    """
    LandCoverDownloader main processing algorithm
    """

    # Constants

    LAT = "LAT"
    LNG = "LNG"
    SEARCHRADIUS = "SEARCHRADIUS"
    YEAR = "YEAR"

    OUTPUT = "OUTPUT"
    AOI = "AOI"

    CODEFILENAME = 'land_cover_downloader_algorithm.py'

    YEARLIST = ['2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024']

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

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

        # Add input and output parameters
        # 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.addParameter(
            QgsProcessingParameterEnum(
                self.YEAR,
                self.tr('Data Collection Year (2017-2024)'),
                options=self.YEARLIST,
                defaultValue=7
            )
        )

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

        # Output land cover raster
        self.addParameter(
            QgsProcessingParameterRasterDestination(
                self.OUTPUT,
                self.tr('Land Cover Raster')
            )
        )

    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 = int(self.parameterAsString(parameters, self.YEAR, context))
        year_string = self.YEARLIST[year]

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

        """
        ------------------------ Perform initial setup ---------------------------------
        """

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        # Update progress
        feedback.setProgressText('Performing initial setup...')

        # Get location of current file
        current_path = __file__
        file_directory = current_path.replace(self.CODEFILENAME, '')
        chdir(file_directory)

        # Create layer of UTM grid (for finding land cover tiles)
        utm_filename = 'resources/World_UTM_Grid.shp'
        QgsVectorLayer(utm_filename, 'World_UTM_Grid')
        utm_layer = QgsVectorLayer(utm_filename, 'World_UTM_Grid')

        # Create temporary directory for downloads/other data
        temp_dir = TemporaryDirectory(delete=True, ignore_cleanup_errors=True)
        scratch_folder = temp_dir.name

        # Get project coordinate system and define model coordinate system
        model_coordinate_system_name = 'epsg:3857'
        model_coordinate_system = QgsCoordinateReferenceSystem(model_coordinate_system_name)

        # Create point feature from lat/lng values
        # Export lat/lng values to table
        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'])

        # Create and reproject POI layer
        with open(scratch_folder + '/coordinate_table.csv', "wb") as f:
            df.to_csv(f, index=False)
            poi_layer = processing.run("native:createpointslayerfromtable", {
                'INPUT': scratch_folder + '\\coordinate_table.csv',
                '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')

        """
        ------------------------ Download Data ---------------------------------
        """

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        # Update progress
        feedback.setProgressText('Downloading Land Cover Data...')

        # Select UTM zones that intersect input AOI
        processing.run("native:selectbylocation",
                       {'INPUT': utm_layer, 'PREDICATE': [0], 'INTERSECT': aoi_layer,
                        'METHOD': 0})
        QgsVectorFileWriter.writeAsVectorFormat(utm_layer,
                                                scratch_folder + '/selected_utm.shp', 'utf-8',
                                                driverName='ESRI Shapefile', onlySelected=True)
        temp_selection = QgsVectorLayer(scratch_folder + '/selected_utm.shp', 'temp_selection')
        temp_features = temp_selection.getFeatures()
        utm_zones = []
        for feature in temp_features:
            utm_zones.append([feature[1], feature[2]])

        # Download land cover data
        # Variable for output land cover filenames
        lc_return = []

        # Download necessary land cover files
        for i in range(len(utm_zones)):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break
            feedback.setProgressText('Downloading LULC Data ' + str(i + 1) + '/' + str(len(utm_zones)))
            temp_return = downloadLandCoverRaster(i, utm_zones, year_string, scratch_folder)
            lc_return.append(temp_return)

        """
        ------------------------ Clip and Merge ---------------------------------
        """

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        # Update progress
        feedback.setProgressText('Clipping and Merging Land Cover Data...')

        # Merge and clip rasters
        mosaicAndClipRasters(lc_return, aoi_layer, scratch_folder, output)

        # Update progress
        feedback.setProgressText('...Complete!')

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

    def name(self):
        """
        Returns the algorithm name, used for identifying the algorithm.
        """
        return 'Download Land Cover from Lat/Lng'

    def displayName(self):
        """
        Returns the translated algorithm name.
        """
        return self.tr(self.name())

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

    def createInstance(self):
        return DownloadFromLatLng()

    def icon(self):
        return QIcon(path.dirname(__file__) + "/resources/lat_lng_icon_svg.svg")

    def shortHelpString(self):
        str = """
        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.
        """

        return str

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

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


class DownloadFromPoint(QgsProcessingAlgorithm):
    """
    LandCoverDownloader main processing algorithm
    """

    # Constants

    POINT = "POINT"
    SEARCHRADIUS = "SEARCHRADIUS"
    YEAR = "YEAR"

    OUTPUT = "OUTPUT"
    AOI = "AOI"

    CODEFILENAME = 'land_cover_downloader_algorithm.py'

    YEARLIST = ['2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024']

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

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

        # Add input and output parameters
        # 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.addParameter(
            QgsProcessingParameterEnum(
                self.YEAR,
                self.tr('Data Collection Year (2017-2024)'),
                options=self.YEARLIST,
                defaultValue=7
            )
        )

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

        # Output land cover raster
        self.addParameter(
            QgsProcessingParameterRasterDestination(
                self.OUTPUT,
                self.tr('Land Cover Raster')
            )
        )

    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 = int(self.parameterAsString(parameters, self.YEAR, context))
        year_string = self.YEARLIST[year]

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

        """
        ------------------------ Perform initial setup ---------------------------------
        """

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        # Update progress
        feedback.setProgressText('Performing initial setup...')

        # Get location of current file
        current_path = __file__
        file_directory = current_path.replace(self.CODEFILENAME, '')
        chdir(file_directory)

        # Create layer of UTM grid (for finding land cover tiles)
        utm_filename = 'resources/World_UTM_Grid.shp'
        QgsVectorLayer(utm_filename, 'World_UTM_Grid')
        utm_layer = QgsVectorLayer(utm_filename, 'World_UTM_Grid')

        # Create temporary directory for downloads/other data
        temp_dir = TemporaryDirectory(delete=True, ignore_cleanup_errors=True)
        scratch_folder = temp_dir.name

        # Get project coordinate system and define model coordinate system
        model_coordinate_system_name = 'epsg:3857'
        model_coordinate_system = QgsCoordinateReferenceSystem(model_coordinate_system_name)

        # 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')

        """
        ------------------------ Download Data ---------------------------------
        """

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        # Update progress
        feedback.setProgressText('Downloading Land Cover Data...')

        # Select UTM zones that intersect input AOI
        processing.run("native:selectbylocation",
                       {'INPUT': utm_layer, 'PREDICATE': [0], 'INTERSECT': aoi_layer,
                        'METHOD': 0})
        QgsVectorFileWriter.writeAsVectorFormat(utm_layer,
                                                scratch_folder + '/selected_utm.shp', 'utf-8',
                                                driverName='ESRI Shapefile', onlySelected=True)
        temp_selection = QgsVectorLayer(scratch_folder + '/selected_utm.shp', 'temp_selection')
        temp_features = temp_selection.getFeatures()
        utm_zones = []
        for feature in temp_features:
            utm_zones.append([feature[1], feature[2]])

        # Download land cover data
        # Variable for output land cover filenames
        lc_return = []

        # Download necessary land cover files
        for i in range(len(utm_zones)):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break
            feedback.setProgressText('Downloading LULC Data ' + str(i + 1) + '/' + str(len(utm_zones)))
            temp_return = downloadLandCoverRaster(i, utm_zones, year_string, scratch_folder)
            lc_return.append(temp_return)

        """
        ------------------------ Clip and Merge ---------------------------------
        """

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        # Update progress
        feedback.setProgressText('Clipping and Merging Land Cover Data...')

        # Merge and clip rasters
        mosaicAndClipRasters(lc_return, aoi_layer, scratch_folder, output)

        # Update progress
        feedback.setProgressText('...Complete!')

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

    def name(self):
        """
        Returns the algorithm name, used for identifying the algorithm.
        """
        return 'Download Land Cover from Point'

    def displayName(self):
        """
        Returns the translated algorithm name.
        """
        return self.tr(self.name())

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

    def createInstance(self):
        return DownloadFromPoint()

    def icon(self):
        return QIcon(path.dirname(__file__) + "/resources/point_icon_svg.svg")

    def shortHelpString(self):
        str = """
        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.
        """

        return str

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

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


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

    Download data from AOI input.
    """

    # Constants

    AOI = "AOI"
    YEAR = "YEAR"

    OUTPUT = "OUTPUT"

    CODEFILENAME = 'land_cover_downloader_algorithm.py'

    YEARLIST = ['2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024']

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

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

        # Add input and output parameters
        # Input Point
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.AOI,
                self.tr('Input AOI'),
                [QgsProcessing.TypeVectorPolygon]
            )
        )

        # Input year
        self.addParameter(
            QgsProcessingParameterEnum(
                self.YEAR,
                self.tr('Data Collection Year (2017-2024)'),
                options=self.YEARLIST,
                defaultValue=7
            )
        )

        # Output land cover raster
        self.addParameter(
            QgsProcessingParameterRasterDestination(
                self.OUTPUT,
                self.tr('Land Cover Raster')
            )
        )

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

        # Assign inputs to variables
        aoi = self.parameterAsVectorLayer(parameters, self.AOI, context)
        year = int(self.parameterAsString(parameters, self.YEAR, context))
        year_string = self.YEARLIST[year]

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

        """
        ------------------------ Perform initial setup ---------------------------------
        """

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        # Update progress
        feedback.setProgressText('Performing initial setup...')

        # Get location of current file
        current_path = __file__
        file_directory = current_path.replace(self.CODEFILENAME, '')
        chdir(file_directory)

        # Create layer of UTM grid (for finding land cover tiles)
        utm_filename = 'resources/World_UTM_Grid.shp'
        QgsVectorLayer(utm_filename, 'World_UTM_Grid')
        utm_layer = QgsVectorLayer(utm_filename, 'World_UTM_Grid')

        # Create temporary directory for downloads/other data
        temp_dir = TemporaryDirectory(delete=True, ignore_cleanup_errors=True)
        scratch_folder = temp_dir.name

        # Get project coordinate system and define model coordinate system
        model_coordinate_system_name = 'epsg:3857'
        model_coordinate_system = QgsCoordinateReferenceSystem(model_coordinate_system_name)

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

        """
        ------------------------ Download Data ---------------------------------
        """

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        # Update progress
        feedback.setProgressText('Downloading Land Cover Data...')

        # Select UTM zones that intersect input AOI
        processing.run("native:selectbylocation",
                       {'INPUT': utm_layer, 'PREDICATE': [0], 'INTERSECT': aoi_layer,
                        'METHOD': 0})
        QgsVectorFileWriter.writeAsVectorFormat(utm_layer,
                                                scratch_folder + '/selected_utm.shp', 'utf-8',
                                                driverName='ESRI Shapefile', onlySelected=True)
        temp_selection = QgsVectorLayer(scratch_folder + '/selected_utm.shp', 'temp_selection')
        temp_features = temp_selection.getFeatures()
        utm_zones = []
        for feature in temp_features:
            utm_zones.append([feature[1], feature[2]])

        # Download land cover data
        # Variable for output land cover filenames
        lc_return = []

        # Download necessary land cover files
        for i in range(len(utm_zones)):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break
            feedback.setProgressText('Downloading LULC Data ' + str(i + 1) + '/' + str(len(utm_zones)) + "...")
            temp_return = downloadLandCoverRaster(i, utm_zones, year_string, scratch_folder)
            lc_return.append(temp_return)

        """
        ------------------------ Clip and Merge ---------------------------------
        """

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        # Update progress
        feedback.setProgressText('Clipping and Merging Land Cover Data...')

        # Merge and clip rasters
        mosaicAndClipRasters(lc_return, aoi_layer, scratch_folder, output)

        # Update progress
        feedback.setProgressText('...Complete!')

        return {
            self.OUTPUT: output
        }

    def name(self):
        """
        Returns the algorithm name, used for identifying the algorithm.
        """
        return 'Download Land Cover from AOI'

    def displayName(self):
        """
        Returns the translated algorithm name.
        """
        return self.tr(self.name())

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

    def createInstance(self):
        return DownloadFromAoi()

    def icon(self):
        return QIcon(path.dirname(__file__) + "/resources/aoi_icon_svg.svg")

    def shortHelpString(self):
        str = """
        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).
        """

        return str

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

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