# -*- coding: utf-8 -*-
"""
/***************************************************************************
 GraphabProject
                                 A QGIS plugin
 All functions that set and create a Graphab Project
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2020-04-20
        git sha              : $Format:%H$
        copyright            : (C) 2020 by ThéMA
        email                : robin.marlinl@gmail.com
        author               : Robin Marlin-Lefebvre
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/
"""

import os # This is is needed in the pyqgis console also
import subprocess

from qgis import processing
from qgis.core import (
    QgsProject,
    QgsVectorLayer,
    QgsRasterLayer,
    QgsSingleBandGrayRenderer,
    QgsRasterBandStats,
    QgsContrastEnhancement,
    QgsVectorLayerJoinInfo,
    QgsProcessingException,
    QgsFillSymbol,
    QgsField
)

from PyQt5.QtCore import QFileInfo, QVariant

# Module to convert graphab xml file to python object
from .xml_to_python.xmlToPython import convertXmlToPy

from ..qgis_lib_mc import qgsUtils

# Lib use for read correctly path for join using uri path
import urllib


class GraphabProject:
    """QGIS Plugin Implementation."""

    def __init__(self, plugin, projectFile, projectGroup = None):
        """It make some verifications, get the XML file
                    of a Graphab project, transform it in Python object and load all required files like :
                    patches, linkset..."""
        # Link the object to the main object (Graphab) for shared variables
        self.plugin = plugin
        self.projectFile = os.path.normcase(projectFile)
        self.projectDir = os.path.dirname(self.projectFile)
        bname = os.path.basename(self.projectFile)

        # creation of a python object named 'project' using my lib 'xmlToPython'
        self.project = convertXmlToPy(projectFile)
        self.projectGroup = projectGroup
        self.projectGroupName = bname[:-4]

        if projectGroup is None:

            # creation of a layers group with the name of the Graphab project
            root = QgsProject.instance().layerTreeRoot()
            self.projectGroup = root.addGroup(self.project.name)
            self.projectGroup.setCustomProperty("GraphabProject", projectFile)

            csvGroup = self.projectGroup.addGroup("Tabular data")
            csvGroup.setExpanded(False)

            # try to load the source.tif file in 'projectGroup' layers group
            path_to_sourcetif = os.path.join(self.projectDir, "source.tif")
            self.setSource(path_to_sourcetif)

            # try to load the patches.shp file in 'projectGroup' layers group
            path_to_patchesshp = os.path.join(self.projectDir, "patches.shp")
            self.setPatches(path_to_patchesshp)

            # get all graphab linksets and add them in qgis layers group 'Linksets'
            linksetsGroup = self.projectGroup.insertGroup(0, 'Linksets')
            for linkset in self.project.costLinks:
                self.setLinkset(linkset.name, linkset, linksetsGroup)

            # get all graphab graph and add them in qgis layers group 'Graphs' with for each a new layers group
            graphsGroup = self.projectGroup.insertGroup(0, 'Graphs')

            #when you active one graph group; others graph group are disabled
            graphsGroup.setIsMutuallyExclusive(True)

            #load each graph
            for graph in self.project.graphs:
                if graph.type == 1 or graph.type == 2:
                    graphGroup = graphsGroup.addGroup(graph.name)
                    QgsProject.instance().layerTreeRoot().findGroup(graphGroup.name()).setItemVisibilityChecked(False)
                    self.setGraph(graphGroup, graph)

            # loads patches.csv and link to all patches layer
            self.loadCSV('patches.csv')

            for linkset in self.project.costLinks:
                self.reloadLinksetCSV(linkset.name)


    #--------------------------------------------------------------------------
    
    def getProjectGroup(self):
        # print("getProjectGroup %s"%(self.projectGroupName))
        pName = os.path.normcase(self.projectGroupName)
        if self.projectGroupName:
            root = QgsProject.instance().layerTreeRoot()
            # print(str(self.projectGroupName))
            # print(str([g.name() for g in root.findGroups(recursive=True)]))
            groups = [g for g in qgsUtils.findGroupsAll() if os.path.normcase(g.name()) == pName]
            if len(groups) == 1:
                return groups[0]
        # print("NO")
        return self.projectGroup
    def getLinksetGroup(self):
        projGroup = self.getProjectGroup()
        for c in projGroup.children():
            if c.name() == 'Linksets':
                return c
        raise QgsProcessingException("Could not find 'Linksets' subgroup in project '{}'".format(self.projectGroupName))
    def getGraphGroup(self):
        projGroup = self.getProjectGroup()
        for c in projGroup.children():
            if c.name() == 'Graphs':
                return c
        raise QgsProcessingException("Could not find 'Graphs' subgroup in project '{}'".format(self.projectGroupName))
    
    #--------------------------------------------------------------------------

    def setSource(self, raster):
        """Load the source.tif file of a loaded Project and change its symbology."""
        
        # Get the path and the name of the file to be sure the file can be load
        fileInfo = QFileInfo(raster)
        path = fileInfo.filePath()
        baseName = fileInfo.baseName()

        # Load the file and add it to a specific group to have the same structure of a Graphab project
        layer = QgsRasterLayer(path, baseName)
        QgsProject.instance().addMapLayer(layer, False)
        self.getProjectGroup().insertLayer(0, layer)

        # change color of layer that was add
        myGrayRenderer = QgsSingleBandGrayRenderer(layer.dataProvider(), 1)
        layer.setRenderer(myGrayRenderer)

        renderer = layer.renderer()
        provider = layer.dataProvider()

        layer_extent = layer.extent()
        uses_band = renderer.usesBands()

        myType = renderer.dataType(uses_band[0])

        stats = provider.bandStatistics(uses_band[0],
                                        QgsRasterBandStats.All,
                                        layer_extent,
                                        0)

        myEnhancement = QgsContrastEnhancement(myType)

        contrast_enhancement = QgsContrastEnhancement.StretchToMinimumMaximum

        myEnhancement.setContrastEnhancementAlgorithm(contrast_enhancement,True)
        myEnhancement.setMinimumValue(stats.minimumValue)
        myEnhancement.setMaximumValue(stats.maximumValue)

        layer.renderer().setContrastEnhancement(myEnhancement)
        # Refresh the symbology
        layer.triggerRepaint()

        # Don't set visible the layer such as the loading of a Graphab project
        QgsProject.instance().layerTreeRoot().findLayer(layer.id()).setItemVisibilityChecked(False)


    #--------------------------------------------------------------------------

    def setVoronoi(self, voronoi, group):
        """Laod the Graph voronoi file and change its symbology."""
        
        # Get the path and the name of the file to be sure the file can be load
        path = QFileInfo(voronoi).filePath()

        # Load the file and add it to a specific group to have the same structure of a Graphab project
        layer = QgsVectorLayer(path, "Components")
        QgsProject.instance().addMapLayer(layer, False)
        group.addLayer(layer)
            
        # Create a new symbology
        symbol = QgsFillSymbol.createSimple({'line_style': 'solid', 'style': 'no'})

        # apply symbol to layer renderer
        layer.renderer().setSymbol(symbol)

        # repaint the layer
        layer.triggerRepaint()
        self.plugin.iface.layerTreeView().refreshLayerSymbology(layer.id())

        # Don't set visible the layer such as the loading of a Graphab project
        QgsProject.instance().layerTreeRoot().findLayer(layer.id()).setItemVisibilityChecked(False)

    
    #--------------------------------------------------------------------------
    
    def setGraph(self, group, graph):
        """Load every file that compose a Graph and change symboly of them."""

        # Get the path and the name of the file to be sure the file can be load
        path_to_patchesshp = os.path.join(self.projectDir, "patches-topo.shp")
        if not os.path.isfile(path_to_patchesshp):
            self.createTopoPatches()
        pathPatches = QFileInfo(path_to_patchesshp).filePath()
        
        # Load the file and add it to a specific group to have the same structure of a Graphab project
        layer = QgsVectorLayer(pathPatches, "Nodes")
        QgsProject.instance().addMapLayer(layer, False)
        group.addLayer(layer)

        # If the file is loaded
        if not layer.isValid():
            return None
            
        topoLinkset = os.path.join(self.projectDir, graph.name + '-topo_links.shp')
        if not os.path.isfile(topoLinkset):
            self.createTopoLinkset(graph)

        # Load Linkset with filter or not depending of the type of the Graph loaded
        if graph.type == 1:
            self.setLinkset("Edges", graph.costLink, group, graph.name, True)
            self.setLinkset("Paths", graph.costLink, group, None,  False)

        if graph.type == 2:
            self.setLinkset("Edges", graph.costLink, group, graph.name, True)
            self.setLinkset("Paths", graph.costLink, group, None, False, graph.threshold)

        # Load voronoi file
        path_to_voronoi = os.path.join(self.projectDir, graph.name+"-voronoi.shp")
        self.setVoronoi(path_to_voronoi, group)

        # Set default symbology of the current Graph
        self.plugin.GraphabStyle.prepareStyle('Capacity', layer, self.plugin.stylesTabUnabled[1])

        # Don't set visible the layer such as the loading of a Graphab project
        QgsProject.instance().layerTreeRoot().findLayer(layer.id()).setItemVisibilityChecked(True)

        # Collapse the layer group
        group.setExpanded(False)

        return layer
            

    #--------------------------------------------------------------------------
    
    def setPatches(self, vector):
        """Load patches.shp file and change its symbology."""

        # Get the path and the name of the file to be sure the file can be load
        fileInfo = QFileInfo(vector)
        path = fileInfo.filePath()
        baseName = fileInfo.baseName()

        # Load the file and add it to a specific group to have the same structure of a Graphab project
        layer = QgsVectorLayer(path, baseName)
        QgsProject.instance().addMapLayer(layer, False)
        self.getProjectGroup().insertLayer(0, layer)

        # If the file is loaded
        if not layer.isValid():
            return None
            
        # Change the symbology in a green
        self.plugin.GraphabStyle.colorizeFill(layer, '#65A252')

        return layer


    #--------------------------------------------------------------------------
    
    def setLinkset(self, name, linkset, group, graphName = None, visible = False, threshold = 0):
        """Load Linkset and change symbology depending on their type."""

        if graphName:
            linksetFile = graphName+"-topo_links.shp"
        else:
            linksetFile = linkset.name+"-links.shp"

        # Get the path and the name of the file to be sure the file can be load
        fileInfo = QFileInfo(os.path.join(self.projectDir, linksetFile))
        path = fileInfo.filePath()

        # Load the file and add it to a specific group to have the same structure of a Graphab project
        layer = QgsVectorLayer(path, name, "ogr")
        QgsProject.instance().addMapLayer(layer, False)
        group.addLayer(layer)
                
        # Set up of the color which depends of the type of the linkset
        if(linkset.type) == 1:
            self.plugin.GraphabStyle.colorizeLine(layer, '#B8C45D')
        elif(linkset.type) == 2:
            self.plugin.GraphabStyle.colorizeLine(layer, '#25372B')
        else:
            self.plugin.GraphabStyle.colorizeLine(layer, '#BCC3AC')

        if threshold > 0:
            #set filter ("Dist"<Graph.threshold)
            equation = '"Dist" <= %d' % (threshold,)
            layer.setSubsetString(equation)

        # Don't set visible the layer such as the loading of a Graphab project
        QgsProject.instance().layerTreeRoot().findLayer(layer.id()).setItemVisibilityChecked(visible)

            
    #--------------------------------------------------------------------------
    
    def addConcatenateField(self, layer, fieldName, firstField, secondField, separator):
        """Create a virtual field on a layer to allow to join csv with concatenate ID"""
        # add apostrophes at the beginning and the end of the separator to be sure it was count as string
        separator = "'" + separator + "'"
        
        # create virtual field to concatenate 2 fields
        field = QgsField( fieldName, QVariant.String )
        layer.addExpressionField( 'concat("' + firstField + '", ' + separator + ', "' + secondField + '")', field )


    # --------------------------------------------------------------------------

    def loadCSV(self, csvName):

        isPatch = csvName == 'patches.csv'

        # get the project
        csvGroup = self.getProjectGroup().findGroup("Tabular data")

        # find and delete the csv file and remove automatically all joins
        for layerGraphTree in csvGroup.children():
            if layerGraphTree.name() == csvName:
                QgsProject.instance().removeMapLayer(layerGraphTree.layer())
                break

        # get the path to the directory of the project and the csv path for patches
        path_to_csv = os.path.join(self.projectDir, csvName)

        if not os.path.isfile(path_to_csv):
            return

        # To make the uri path correct for special character we use urllib.parse.quote() function
        csv_file_path = urllib.parse.quote(path_to_csv)

        # Find and load the CSV file
        csv_uri = 'file:///' + csv_file_path + '?watchFile=no'
        csvLayer = QgsVectorLayer(csv_uri, csvName, "delimitedtext")

        if not isPatch:
            self.addConcatenateField(csvLayer, 'Id', 'ID1', 'ID2', '-')

        # Add it to the QGIS layer window
        QgsProject.instance().addMapLayer(csvLayer, False)
        csvGroup.addLayer(csvLayer)

        # Create the link between the CSV file and the layer based on a specific field ('Id' in general)
        joinObject = QgsVectorLayerJoinInfo()
        joinObject.setJoinLayerId(csvLayer.id())
        joinObject.setJoinFieldName('Id')
        joinObject.setTargetFieldName('Id')
        joinObject.setUsingMemoryCache(True)
        if isPatch:
            joinObject.setJoinFieldNamesBlackList(['Area', 'Perim'])
        else:
            joinObject.setJoinFieldNamesBlackList(['ID1', 'ID2', 'Dist', 'DistM'])
        joinObject.setPrefix(self.plugin.prefix)
        joinObject.setJoinLayer(csvLayer)

        shpName = csvName.replace('.csv', '.shp')
        shpNameTopo = []
        if shpName.endswith('-links.shp'):
            for graph in self.project.graphs:
                if graph.costLink.name == csvName.replace('-links.csv', ''):
                    shpNameTopo.append(graph.name + '-topo_links.shp')
        if shpName == "patches.shp":
            shpNameTopo.append("patches-topo.shp")

        for l in self.getProjectGroup().findLayers():
            layer = l.layer()
            if layer.source().endswith(shpName) or (shpName + '|') in layer.source():
                layer.addJoin(joinObject)
            # for graph topo layers
            for name in shpNameTopo:
                if layer.source().endswith(name):
                    layer.addJoin(joinObject)


    # --------------------------------------------------------------------------
    def reloadLinksetCSV(self, linksetName):
        self.loadCSV(linksetName + '-links.csv')

    #--------------------------------------------------------------------------

    def reloadPatchCSV(self):
        """Reload project csv layers using for patches by deleting and reloading them and return
        the project name to confirm or if it doesn't exist just return None"""

        self.loadCSV('patches.csv')

    
    #--------------------------------------------------------------------------
    
    def addGraphProject(self, graphName):
        """This function load a Graph that had been created when the project is already loaded"""

        # replace the previous stocked project by the new one
        self.upgradeLoadedProject()

        # get 'Graphs' group
        graphsGroup = self.getGraphGroup()

        # try to find the new graph with its name and load it
        for graph in self.project.graphs:
            if graph.name == graphName:
                graphGroup = graphsGroup.addGroup(graph.name)
                self.setGraph(graphGroup, graph)
                self.reloadPatchCSV()
                self.reloadLinksetCSV(graph.costLink.name)

    
    #--------------------------------------------------------------------------
    
    def addLinksetProject(self, linksetName):
        """This function load a Linkset that had been created when the project is already loaded"""

        # replace the previous stocked project by the new one
        self.upgradeLoadedProject()

        # get 'Linksets' group
        linksetsGroup = self.getLinksetGroup()

        # try to find the new linkset with its name and load it
        for linkset in self.project.costLinks:
            if linkset.name == linksetName:
                self.setLinkset(linksetName, linkset, linksetsGroup, visible=True)

        self.reloadLinksetCSV(linksetName)
    
    #--------------------------------------------------------------------------
    
    def upgradeLoadedProject(self):
        """This function replace old project by new project after a project modif"""

        # reload project
        self.project = convertXmlToPy(self.projectFile)
        
        return self.project

    def createTopoPatches(self):
        processing.run("native:centroids", {'INPUT': os.path.join(self.projectDir, 'patches.shp'),
                                         'ALL_PARTS': False,
                                         'OUTPUT': os.path.join(self.projectDir, 'patches-topo.shp')})

    def createTopoLinkset(self, graph):
        cmd = self.plugin.graphabProvider.getGraphabCommand() + ['--project', self.projectFile, '--usegraph', graph.name, '--topo']
        return subprocess.call(cmd) == 0

    def removeLinkset(self, linksetName):
        self.plugin.updateLoadedProject()
        cmd = self.plugin.graphabProvider.getGraphabCommand() + ['--project', self.projectFile, '--removelinkset', linksetName]
        ret = subprocess.call(cmd)
        if ret == 0:
            self.upgradeLoadedProject()
            linksetGroup = self.getLinksetGroup()
            for layer in linksetGroup.children():
                if layer.name() == linksetName:
                    # print("REMOVE LINKSET " + str(linksetName))
                    linksetGroup.removeChildNode(layer)
                    break
            return True
        else:
            return False

    def removeGraph(self, graphName):
        self.plugin.updateLoadedProject()
        cmd = self.plugin.graphabProvider.getGraphabCommand() + ['--project', self.projectFile, '--removegraph', graphName]
        ret = subprocess.call(cmd)
        if ret == 0:
            self.upgradeLoadedProject()
            graphGroup = self.getGraphGroup()
            for layer in graphGroup.children():
                if layer.name() == graphName:
                    graphGroup.removeChildNode(layer)
                    break
            return True
        else:
            return False
