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

"""
***************************************************************************
*                                                                         *
*   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.                                   *
*                                                                         *
***************************************************************************
"""
from qgis.PyQt.QtCore import QCoreApplication
from qgis.PyQt.QtWidgets import QMessageBox
from qgis.core import (
    QgsFeatureSink,
    QgsSnappingConfig,
    QgsVectorLayer,
    QgsFeature,
    QgsPointXY,
    QgsMessageLog,
    QgsTolerance,
    QgsGeometry,
    QgsProject,
    QgsCoordinateReferenceSystem,    
    QgsProcessingException,
    QgsProcessingAlgorithm,
    QgsProcessingParameterVectorDestination,    
    QgsProcessingParameterCrs,
    QgsProcessingParameterNumber,
    QgsProcessingUtils,
    QgsProcessingLayerPostProcessorInterface
)
from qgis import processing
import os, inspect


class Renamer (QgsProcessingLayerPostProcessorInterface):
    def __init__(self, layer_name):
        self.name = layer_name
        super().__init__()
        
    def postProcessLayer(self, layer, context, feedback):
        layer.setName(self.name)


def _utmzone2epsg(utm, band):
    """Converts utm zone to EPSG code on the WGS84 geodetic datum

    Parameters
    ----------
    utm: int
        UTM zone number (1--60 starting from zero meridian)
    band: str
        UTC band letter (X at North Pole to C at south pole)

    Returns
    -------
    epsg: int
        EPSG integer id
    """
    # Get hemisphere and EPSG
    if band < "N":  # Southern Hemisphere before "N"
        epsg = 32700 + utm
    else:
        epsg = 32600 + utm
    return epsg


def _mgrs_from_latlon(lat, lon):
    """Get the MGRS zone (UTM and letters) from a provided latitude and longitude

    Parameters
    ----------
    lat: float
        Latitude of the site
    lon: float
        Longitude of the site

    Returns
    -------
    utm: int
        UTM zone number (1--60 starting from zero meridian)
    mgrs_letter : char
        Military Grid Reference System grid-zone designation letter
    """
    mgrs_letter = _get_mgrs_letter_from_lat(lat)
    utm = _get_utm_from_latlon(lat, lon)

    return utm, mgrs_letter


def _get_utm_from_latlon(lat, lon):
    """Get UTM zone from lat and lon coordinates

    Parameters
    ----------
    lat: float
        Latitude of the site
    lon: float
        Longitude of the site

    Returns
    -------
    int
        UTM zone number (1--60 starting from zero meridian)

    Notes
    -----
    Get the Longitude bounds for the box, standard grids are 6-degrees
    wide, however latitude bands V and X have special values
    """
    # V band is special for Norway
    if 56 <= lat < 64 and 3 <= lon < 12:
        return 32

    # X is special as it has 9-degree bands
    if 72 <= lat <= 84 and lon >= 0:
        if lon < 9:
            return 31
        elif lon < 21:
            return 33
        elif lon < 33:
            return 35
        elif lon < 42:
            return 37

    # Zones start at 1
    return int((lon + 180) / 6) + 1


def _get_mgrs_letter_from_lat(lat):
    """Gets Military Grid Reference System grid-zone designation letter

    Parameters
    ----------
    lat: float
        Latitude of the site

    Returns
    -------
    char
        Letter indicating mgrs grid-zone

    Raises
    ------
    ValueError
        If lat is outside MGRS bounds

    Notes
    -----
    Latitude bands are a constant 8 degrees except for zone x which is
    12 degrees. The Latitude bands cover the range of 80S to 84N.
    """
    alpha = [
        "C",
        "D",
        "E",
        "F",
        "G",
        "H",
        "J",
        "K",
        "L",
        "M",
        "N",
        "P",
        "Q",
        "R",
        "S",
        "T",
        "U",
        "V",
        "W",
        "X",
    ]
    if lat > 84 or lat < -80:
        raise ValueError("Latitude outside of MGRS bounds.")

    if lat > 72:
        return "X"
    else:
        return alpha[int((lat + 80) / 8)]


class CreateBoundingBox(QgsProcessingAlgorithm):
    """
    This scripts extracts a roughness map from polygons 
    and saves it as a WAsP .map file
    """

    # Constants used to refer to parameters and outputs. They will be
    # used when calling the algorithm from another algorithm, or when
    # calling from the QGIS console.
    dest_id = None  # Save a reference to the output layer id
    LAT = "LAT"
    LON = "LON"
    EXTENT = "EXTENT"
    OUTPUT_CRS = "OUTPUT_CRS"
    INPUT_CRS = "INPUT_CRS"
    OUTPUT = "OUTPUT"
    
    def tr(self, string):
        """
        Returns a translatable string with the self.tr() function.
        """
        return QCoreApplication.translate("Processing", string)

    def createInstance(self):
        return CreateBoundingBox()

    def name(self):
        """
        Returns the algorithm name, used for identifying the algorithm. This
        string should be fixed for the algorithm, and must not be localised.
        The name should be unique within each provider. Names should contain
        lowercase alphanumeric characters only and no spaces or other
        formatting characters.
        """
        return "create_bounding_box"

    def displayName(self):
        """
        Returns the translated algorithm name, which should be used for any
        user-visible display of the algorithm name.
        """
        return self.tr("Create bounding box from lat/lon")

    def group(self):
        """
        Returns the name of the group this algorithm belongs to. This string
        should be localised.
        """
        return self.tr("WAsP scripts")

    def groupId(self):
        """
        Returns the unique ID of the group this algorithm belongs to. This
        string should be fixed for the algorithm, and must not be localised.
        The group id should be unique within each provider. Group id should
        contain lowercase alphanumeric characters only and no spaces or other
        formatting characters.
        """
        return "wasp_scripts"

    def shortHelpString(self):
        """
        Returns a localised short helper string for the algorithm. This string
        should provide a basic description about what the algorithm does and the
        parameters and outputs associated with it..
        """
        return self.tr(
            """Creates a bounding box from lat/lon position

            The inputs are a latitude (between -80 to 84, the validity of the MGRS system) and longitude (-180 to 180 deg.). The output projection has to be metric and available in the WAsP GUI. 
            
            The extent denotes the minimum distance to the square surrounding the point given in the lat/lon input boxes given in the chosen output coordinate reference system (CRS). The maximum extent is 30 km, because using larger maps is generally not needed and will make the software very slow. The minimum extent can be determined roughly by multiplying the height of your turbines or met mast by 100 and adding a buffer of about 50%.
            """
        )

    def initAlgorithm(self, config=None):
        """
        Here we define the inputs and output of the algorithm, along
        with some other properties.
        """

        self.addParameter(
            QgsProcessingParameterNumber(
                name=self.LAT,
                description=self.tr("Latitude (deg)"),
                type=QgsProcessingParameterNumber.Double,
                defaultValue=55.694219,
                optional=False,
                minValue=-80,
                maxValue=84,
            )
        )

        self.addParameter(
            QgsProcessingParameterNumber(
                name=self.LON,
                description=self.tr("Longitude (deg)"),
                type=QgsProcessingParameterNumber.Double,
                defaultValue=12.088316,
                optional=False,
                minValue=-180,
                maxValue=180,
            )
        )
        
        self.addParameter(
            QgsProcessingParameterCrs(
            self.OUTPUT_CRS,
            'Output coordinate reference system', 
            defaultValue='EPSG:32633'
            )
        )

        self.addParameter(
            QgsProcessingParameterNumber(
                name=self.EXTENT,
                description=self.tr("Extent (m)"),
                type=QgsProcessingParameterNumber.Double,
                defaultValue=20000.0,
                optional=False,
                minValue=1,
                maxValue=30000,
            )
        )

        self.addParameter(
            QgsProcessingParameterVectorDestination(
                self.OUTPUT, self.tr("Output layer")
            )
        )

    def checkParameterValues(self, parameters, context):
        out_crs = self.parameterAsCrs(parameters, self.OUTPUT_CRS, context)

        # open file from map editor to find usuable projections
        dir = os.path.split(inspect.getfile(inspect.currentframe()))[0]
        #dir = "/home/rofl/OneDrive/WAsP/QGIS_plugin/wasp_scripts/processing_provider/"
        with open(os.path.join(dir,"EPSG-WAsP-LookUpTable.dat"), "r") as f:
            lines = f.read().splitlines()        
        valid_epsg = ["EPSG:" + l.split("|")[1].strip() for l in lines] # get the epsg code from the file
        str_valid_epsg = ", ".join(valid_epsg[4:]) # first four items are lat/lon

        # chosen projection must be metric
        if out_crs.mapUnits() != 0:
            return False, self.tr(f"WAsP can only deal with metric and non-geographic projection. Please choose a different projection")

        # chosen projection not supported in the WAsP GUI
        if out_crs.authid() not in valid_epsg:
            return False, self.tr(f"You have chosen a projection that is not supported in the WAsP GUI! You can choose from the following codes: {str_valid_epsg}")

        # give warning if the UTM zone that is chosen is not the expected one based on the lat/lon
        lat = self.parameterAsDouble(parameters, self.LAT, context)
        lon = self.parameterAsDouble(parameters, self.LON, context)        
        expected_epsg = _utmzone2epsg(*_mgrs_from_latlon(lat, lon))

        if out_crs.description().startswith("WGS 84 / UTM zone") and out_crs.authid() != f"EPSG:{expected_epsg}":
            QMessageBox.warning(None, "Warning", f"The chosen projection {out_crs.authid()} is not appropriate for the given lat/lon. We recommend using EPSG:{expected_epsg} instead")

        return super().checkParameterValues(parameters, context)
            

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

        lat = self.parameterAsDouble(parameters, self.LAT, context)
        lon = self.parameterAsDouble(parameters, self.LON, context)
        extent = self.parameterAsDouble(parameters, self.EXTENT, context)
        out_crs = self.parameterAsCrs(parameters, self.OUTPUT_CRS, context)

        point_layer = QgsVectorLayer('Point', 'point', 'memory')
        point_layer.setCrs(QgsCoordinateReferenceSystem("EPSG:4326"))
        pr = point_layer.dataProvider()
        # add a point at lon=7.8, lat=47.5
        point_layer.startEditing()
        feature = QgsFeature()
        feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(lon,lat)))
        pr.addFeatures([feature])
        point_layer.updateExtents()
        point_layer.commitChanges()
        feedback.pushInfo(f"Creating layer with CRS {point_layer.crs().authid()} at point {point_layer.extent().xMinimum()}, {point_layer.extent().yMinimum()}")

        clipping_layer_reproj = processing.run("native:reprojectlayer", {
        'INPUT': point_layer,
        'OUTPUT': 'TEMPORARY_OUTPUT',
        'TARGET_CRS' : QgsCoordinateReferenceSystem(out_crs.authid())
        }, context=context, feedback=feedback)['OUTPUT']
        feedback.pushInfo(f"Reprojecting layer to CRS {clipping_layer_reproj.crs().authid()} (xmin, ymin = {clipping_layer_reproj.extent().xMinimum()}, {clipping_layer_reproj.extent().yMinimum()})")

        buffered_layer = processing.run(
        "native:buffer",
        {
            "DISSOLVE": False,
            "DISTANCE": extent,
            "END_CAP_STYLE": 2,
            "INPUT": clipping_layer_reproj,
            "JOIN_STYLE": 0,
            "MITER_LIMIT": 2,
            "OUTPUT": "TEMPORARY_OUTPUT",
            "SEGMENTS": 5,
        },
        context=context,
        feedback=feedback,
        )["OUTPUT"]

        (sink, self.dest_id) = self.parameterAsSink(
            parameters,
            self.OUTPUT,
            context,
            buffered_layer.fields(),
            buffered_layer.wkbType(),
            buffered_layer.sourceCrs(),
        )
        
        if sink is None:
            raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))

        # Compute the number of steps to display within the progress bar and
        # get features from source
        total = 100.0 / buffered_layer.featureCount() if buffered_layer.featureCount() else 0
        features = buffered_layer.getFeatures()
        feedback.pushInfo("Number of roughness lines to write: " + str(total))
        for current, feature in enumerate(features):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break

            # Add a feature in the sink
            sink.addFeature(feature, QgsFeatureSink.FastInsert)
            feedback.setProgress(int(current * total))

        # Return the results of the algorithm. In this case our only result is
        # the feature sink which contains the processed features, but some
        # algorithms may return multiple feature sinks, calculated numeric
        # statistics, etc. These should all be included in the returned
        # dictionary, with keys matching the feature corresponding parameter
        # or output names.
        return {self.OUTPUT: self.dest_id}
    
    def postProcessAlgorithm(self, context, feedback):
        # post process to give our desired style to the output object

        processed_layer = QgsProcessingUtils.mapLayerFromString(self.dest_id, context)

        dir = os.path.split(inspect.getfile(inspect.currentframe()))[0]
        processing.run("native:setlayerstyle", {'INPUT':processed_layer,'STYLE':os.path.join(dir, "styles", "bb_style.qml")})
        
        # make sure that the table has the name 'landcover_table' so that pywasp can import it
        global renamer
        renamer = Renamer('Bounding box')
        context.layerToLoadOnCompletionDetails(self.dest_id).setPostProcessor(renamer)

        return {self.OUTPUT: self.dest_id}
            
    # def prepareAlgorithm(self, parameters, context, feedback):
    #     lat = self.parameterAsDouble(parameters, self.LAT, context)
    #     lon = self.parameterAsDouble(parameters, self.LON, context)
    #     out_crs = self.parameterAsCrs(parameters, self.OUTPUT_CRS, context)

    #     epsg = _utmzone2epsg(*_mgrs_from_latlon(lat, lon))
    #     if out_crs.isGeographic():
    #         feedback.reportError('Layer CRS must be a projected metric CRS for this algorithm')
    #         return False
    #     return super().prepareAlgorithm(parameters, context, feedback)