# -*- coding: utf-8 -*-
"""
/***************************************************************************
 FragScape
                                 A QGIS plugin
 Computes ecological continuities based on environments permeability
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                             -------------------
        begin                : 2018-04-12
        git sha              : $Format:%H$
        copyright            : (C) 2018 by IRSTEA
        email                : mathieu.chailloux@irstea.fr
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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 csv, sys

from qgis.core import (Qgis,
                       QgsMapLayerProxyModel,
                       QgsProcessingException,
                       QgsFieldProxyModel,
                       QgsVectorLayer)
from ..qgis_lib_mc import utils, abstract_model, qgsUtils, feedbacks, qgsTreatments
from ..algs import FragScape_algs 
from . import params

        
class LanduseFieldItem(abstract_model.DictItem):

    VALUE_FIELD = "value"
    DESCR_FIELD = "description"
    TO_SELECT_FIELD = "toSelect"
    FIELDS = [VALUE_FIELD,
              DESCR_FIELD,
              TO_SELECT_FIELD]

    @classmethod
    def fromValues(self,val,descr="",toSelect=False):
        # dict = { self.VALUE_FIELD : str(val),
        #          self.DESCR_FIELD : descr,
        #          self.TO_SELECT_FIELD : toSelect }
        return super().fromValues([str(val),descr,toSelect])
        
    @staticmethod
    def fromDict(dict,feedback=None):
        if LanduseFieldItem.DESCR_FIELD not in dict:
            dict[LanduseFieldItem.DESCR_FIELD] = ""
        return LanduseFieldItem(dict,feedback=feedback)
        
    def equals(self,other):
        return (self.dict[self.VALUE_FIELD] == other.dict[self.VALUE_FIELD])
        
    def updateFromItem(self,other):
        self.dict[self.DESCR_FIELD] = other.dict[self.DESCR_FIELD]
        self.dict[self.TO_SELECT_FIELD] = other.dict[self.TO_SELECT_FIELD]
        
        
class LanduseModel(abstract_model.DictModel):

    INPUT_FIELD = "in_layer"
    SELECT_MODE_FIELD = "select_mode"
    SELECT_FIELD_FIELD = "select_field"
    SELECT_DESCR_FIELD = "select_field"
    SELECT_EXPR_FIELD = "select_expr"
    
    ALG_INPUT = FragScape_algs.PrepareLanduseAlgorithm.INPUT
    ALG_CLIP_LAYER = FragScape_algs.PrepareLanduseAlgorithm.CLIP_LAYER
    ALG_SELECT_EXPR = FragScape_algs.PrepareLanduseAlgorithm.SELECT_EXPR
    ALG_OUTPUT = FragScape_algs.PrepareLanduseAlgorithm.OUTPUT
    
    SELECT_FIELD_MODE = 0
    SELECT_EXPR_MODE = 1

    def __init__(self,fragScapeModel):
        self.fsModel = fragScapeModel
        self.landuseLayer = None
        self.landuseLayerType = 'Vector'
        self.select_field = None
        self.descr_field = None
        self.dataClipFlag = False
        self.select_mode = self.SELECT_FIELD_MODE
        self.select_expr = ""
        itemClass = getattr(sys.modules[__name__], LanduseFieldItem.__name__)
        super().__init__(itemClass,
            feedback=fragScapeModel.feedback,
            fields=LanduseFieldItem.FIELDS)
        self.parser_name = "Landuse"
                        
    # def mkItemFromDict(self,dict):
        # return LanduseFieldItem(dict)
        # v = dict[LanduseFieldItem.VALUE_FIELD]
        # if LanduseFieldItem.DESCR_FIELD in dict:
            # d = dict[LanduseFieldItem.DESCR_FIELD]
        # else:
            # d = ""
        # i = (dict[LanduseFieldItem.TO_SELECT_FIELD] == "True")
        # return LanduseFieldItem(v,d,i)
        
    def changeLayer(self,path):
        feedbacks.debug("changeLayer " + str(path))
        if not path:
            self.landuseLayer = path
            self.setSelectExpr("")
            return
        if not utils.pathEquals(path,self.landuseLayer):
            self.landuseLayer = path
            self.setSelectExpr("")
        loaded_layer, layer_type = qgsUtils.loadLayerGetType(path)
        self.landuseLayerType = layer_type
        if layer_type == 'Vector':
            if self.select_field not in loaded_layer.fields().names():
                self.setSelectField(None)
            if self.descr_field not in loaded_layer.fields().names():
                self.setDescrField(None)
        else:
            self.setSelectField(None)
            self.setDescrField(None)
        
    def setSelectField(self,fieldname):
        feedbacks.debug("Setting select_field to " + str(fieldname))
        self.select_field = fieldname
        
    def setDescrField(self,fieldname):
        feedbacks.debug("Setting descr to " + str(fieldname))
        self.descr_field = fieldname
        
    def setSelectExpr(self,expr):
        self.select_expr = expr
        
    def checkLayerSelected(self):
        if not self.landuseLayer:
            utils.user_error("No layer selected")
            
    def checkFieldSelected(self):
        if not self.select_field:
            utils.user_error("No field selected")
            
    def getDissolveLayer(self):
        return self.fsModel.mkOutputFile("landuseSelectionDissolve.gpkg")
            
    def getOutputRaster(self):
        return self.fsModel.mkOutputFile("landuseSelectionWarp.tif")
        
    def getOutputLayer(self):
        if self.fsModel.paramsModel.modeIsVector():
            return self.getDissolveLayer()
        else:
            return self.getOutputRaster()
        
    def getSelectedValues(self):
        vals = [i.dict[LanduseFieldItem.VALUE_FIELD] for i in self.items if i.dict[LanduseFieldItem.TO_SELECT_FIELD]]
        return vals
            
    def mkRasterFormula(self,feedback):
        values = self.getSelectedValues()
        loaded_layer = qgsUtils.loadVectorLayer(self.landuseLayer)
        input_vals = qgsTreatments.getRasterUniqueVals(loaded_layer,feedback)
        feedbacks.debug("values = " + str(values))
        feedbacks.debug("input_vals = " + str(input_vals))
        input_vals_str = [str(v) for v in input_vals]
        existing_vals = [v for v in values if v in input_vals_str]
        nb_vals = len(existing_vals)
        if not existing_vals:
            utils.user_error("No existing values selected for landuse layer")
        # form = "logical_or(A==" + str(existing_vals[0])
        # for v in existing_vals[1:]:
            # form += ",A==" + str(v)
        # form += ")"
        form = ""
        for v in existing_vals[:-1]:
            form += "logical_or(A==" + str(v) + ","
        form += "A==" + str(existing_vals[-1]) + (")" * (nb_vals - 1))
        return form

    def mkSelectionExpr(self):
        self.checkFieldSelected()
        expr = ""
        for item in self.items:
            to_select = item.dict[LanduseFieldItem.TO_SELECT_FIELD]
            boolVal = to_select in ("True",True)
            feedbacks.debug("to_select {} = {}".format(to_select,boolVal))
            if boolVal:
                if expr != "":
                    expr += " + "
                field_val = item.dict[LanduseFieldItem.VALUE_FIELD].replace("'","''")
                expr += "(\"" + self.select_field + "\" = '" + field_val + "')"
        feedbacks.debug("selectionExpr = " + expr)
        return expr
        
    def getSelectionExpr(self):
        feedbacks.debug("select mode = " + str(self.select_mode))
        feedbacks.debug("items = " + str(self.items))
        if self.select_mode == self.SELECT_FIELD_MODE:
            expr = self.mkSelectionExpr()
            if not expr:
                utils.user_error("No value selected")
        elif self.select_mode == self.SELECT_EXPR_MODE:
            expr = self.select_expr
        else:
            utils.internal_error("Unexpected selection mode : " + str(self.select_mode))
        return expr
        
    def applyItemsWithContext(self,context,feedback,indexes):
        feedbacks.beginSection("Landuse classification")
        self.fsModel.checkWorkspaceInit()
        self.checkLayerSelected()
        step_feedback = feedbacks.ProgressMultiStepFeedback(3,feedback)
        vector_mode = self.fsModel.paramsModel.modeIsVector()
        if vector_mode:
            output = self.getDissolveLayer()
            qgsUtils.removeVectorLayer(output)
        else:
            output = self.getOutputRaster()
            qgsUtils.removeRaster(output)
        clipped_path = self.fsModel.paramsModel.clipByExtent(self.landuseLayer,
            name="landuse",clip_raster=False,context=context,feedback=step_feedback)
        clipped_layer, clipped_type = qgsUtils.loadLayerGetType(clipped_path)
        step_feedback.setCurrentStep(1)
        input_vector = clipped_type == 'Vector'
        crs, extent, resolution = self.fsModel.getRasterParams()
        if input_vector:
            reproj_path = params.mkTmpLayerPath("landuseReproject.gpkg")
            clipped_layer = qgsTreatments.applyReprojectLayer(clipped_layer,crs,
                reproj_path,context=context,feedback=feedback)
        if input_vector and vector_mode:
            expr = self.getSelectionExpr()
            selected_path = params.mkTmpLayerPath('landuseSelection.gpkg')
            qgsTreatments.selectGeomByExpression(clipped_layer,expr,selected_path,'landuseSelection')
            step_feedback.setCurrentStep(2)
            res = qgsTreatments.dissolveLayer(selected_path,output,
                context=context,feedback=step_feedback)
        elif input_vector and not vector_mode:
            expr = self.getSelectionExpr()
            # crs, extent, resolution = self.fsModel.getRasterParams()
            self.fsModel.checkExtentInit()
            self.fsModel.checkResolutionInit()
            selected_path = params.mkTmpLayerPath('landuseSelection.gpkg')
            qgsTreatments.classifByExpr(clipped_layer,expr,selected_path,'landuseSelection')
            res = qgsTreatments.applyRasterization(selected_path,output,
                extent=extent,resolution=resolution,
                out_type=Qgis.Byte,nodata_val=255,field='Value',
                all_touch=False,context=context,feedback=step_feedback)
        else:
            selected_path = params.mkTmpLayerPath('landuseSelection.tif')
            formula = self.mkRasterFormula(step_feedback)
            qgsTreatments.applyRasterCalc(clipped_layer,selected_path,formula,
                nodata_val=255,out_type=Qgis.Byte,context=context,feedback=step_feedback)
            step_feedback.setCurrentStep(2)
            if vector_mode:
                raise QgsProcessingException("Raster mode with vector input not yet implemented")
            else:
                # crs, extent, resolution = self.fsModel.getRasterParams()
                self.fsModel.checkExtentInit()
                self.fsModel.checkResolutionInit()
                res = qgsTreatments.applyWarpReproject(selected_path,output,
                    resampling_mode='mode',dst_crs=crs,
                    extent=extent,extent_crs=crs,resolution=resolution,
                    nodata_val=255,context=context,feedback=step_feedback)
        step_feedback.setCurrentStep(3)
        res_layer = qgsUtils.loadLayer(res,loadProject=True)
        feedbacks.endSection()
        return res_layer
        
    def toXML(self,indent=" "):
        if not self.landuseLayer:
            utils.warn("No layer selected")
            return ""
        layerRelPath = self.fsModel.normalizePath(self.landuseLayer)
        attribs_dict = { self.INPUT_FIELD : layerRelPath }
        if self.landuseLayerType == 'Vector':
            if not self.select_field:
                utils.warn("No field selected")
                return ""
            attribs_dict[self.SELECT_MODE_FIELD] : self.select_mode
            if self.select_field:
                utils.warn("select_field : " + str(self.select_field))
                attribs_dict[self.SELECT_FIELD_FIELD] = self.select_field
            if self.descr_field:
                attribs_dict[self.SELECT_DESCR_FIELD] = self.descr_field
        if self.fsModel.modeIsVector():  
            if self.select_mode == self.SELECT_EXPR_MODE:
                attribs_dict[self.SELECT_EXPR_FIELD] = self.select_expr
        xmlStr = super().toXML(indent,attribs_dict)
        return xmlStr
        
    def fromXMLAttribs(self,attribs):
        feedbacks.debug("attribs = " + str(attribs))
        if self.INPUT_FIELD in attribs:
            abs_layer = self.fsModel.getOrigPath(attribs[self.INPUT_FIELD])
            feedbacks.debug("abs_layer = " + str(abs_layer))
            self.landuseLayer = abs_layer
        if self.SELECT_MODE_FIELD in attribs:
            self.select_mode = int(attribs[self.SELECT_MODE_FIELD])
        if self.SELECT_FIELD_FIELD in attribs:
            feedbacks.debug("sf1 = " + str(self.select_field))
            feedbacks.debug("sf2 = " + str(attribs[self.SELECT_FIELD_FIELD]))
            self.setSelectField(attribs[self.SELECT_FIELD_FIELD])
        if self.SELECT_DESCR_FIELD in attribs:
            self.descr_field = attribs[self.SELECT_DESCR_FIELD]
        if self.SELECT_EXPR_FIELD in attribs:
            self.select_expr = attribs[self.SELECT_EXPR_FIELD]
        feedbacks.debug("select_field : " + str(self.select_field))
        
    def fromXMLRoot(self,root):
        self.fromXMLAttribs(root.attrib)
        self.items = []
        for parsed_item in root:
            dict = parsed_item.attrib
            item = self.mkItemFromDict(dict)
            self.addItem(item)
        self.layoutChanged.emit()
        
    # Loads field values from CSV file 'fname' into model.
    def fromCSVUpdate(self,fname):
        with open(fname,"r") as f:
            try:
                reader = csv.DictReader(f,fieldnames=self.fields,delimiter=';')
            except UnicodeDecodeError as e:
                utils.user_error("Encoding error : " + str(e)
                    + ", try to save file as UTF-8")
            first_line = next(reader)
            for row in reader:
                item = self.mkItemFromDict(row)
                matching_item = self.getMatchingItem(item)
                if matching_item:
                    matching_item.updateFromItem(item)
                else:
                    self.addItem(item)
        self.layoutChanged.emit()

        
class LanduseConnector(abstract_model.AbstractConnector):

    def __init__(self,dlg,landuseModel):
        self.dlg = dlg
        self.parser_name = "Landuse"
        delegate = abstract_model.CheckBoxDelegate(self.dlg.landuseView,landuseModel.feedback)
        self.dlg.landuseView.setItemDelegateForColumn(2,delegate)
        super().__init__(landuseModel,self.dlg.landuseView,
                         addButton=None,removeButton=self.dlg.landuseRemove,
                         runButton=self.dlg.landuseRun)
        
    def initGui(self):
        self.dlg.landuseInputLayerCombo.setFilters(QgsMapLayerProxyModel.VectorLayer)
        self.dlg.landuseDescrField.setFilters(QgsFieldProxyModel.String)
        
    def connectComponents(self):
        super().connectComponents()
        self.dlg.landuseInputLayerCombo.layerChanged.connect(self.setLayer)
        # self.dlg.landuseInputLayer.fileChanged.connect(self.loadLayer)
        self.layerComboDlg = qgsUtils.LayerComboDialog(self.dlg,
                                                       self.dlg.landuseInputLayerCombo,
                                                       self.dlg.landuseInputLayer)
        self.dlg.landuseSelectionMode.currentIndexChanged.connect(self.switchSelectionMode)
        self.dlg.landuseSelectField.fieldChanged.connect(self.model.setSelectField)
        self.dlg.landuseDescrField.fieldChanged.connect(self.model.setDescrField)
        self.dlg.landuseSelectExpr.fieldChanged.connect(self.model.setSelectExpr)
        self.dlg.landuseLoadFields.clicked.connect(self.loadFields)
        self.dlg.landuseOpenTable.clicked.connect(self.importFields)
        self.dlg.landuseSaveTable.clicked.connect(self.saveFields)
        
    def applyItems(self):
        super().applyItems()
        res_layer = qgsUtils.loadLayer(self.model.getOutputLayer())
        self.dlg.fragmInputLayerCombo.setLayer(res_layer)
        
    def setLayerUI(self,layer):
        feedbacks.debug("setlayerUI")
        self.dlg.landuseSelectField.setLayer(layer)
        self.dlg.landuseDescrField.setLayer(layer)
        self.dlg.landuseSelectExpr.setLayer(layer)
        
    def setLayer(self,layer):
        feedbacks.debug("setLayer " + str(layer))
        layer_is_vector = isinstance(layer,QgsVectorLayer)
        if layer:
            layer_path = qgsUtils.pathOfLayer(layer)
            self.model.changeLayer(layer_path)
            vector_widgets = self.getVectorWidgets()
            feedbacks.debug("nb widgets " + str(len(vector_widgets)))
            for w in vector_widgets:
                feedbacks.debug("setting " + str(layer_is_vector))
                w.setEnabled(layer_is_vector)
            if layer_is_vector:
                self.dlg.landuseSelectField.setLayer(layer)
                self.dlg.landuseDescrField.setLayer(layer)
                self.setLayerUI(layer)
            else:
                self.setLayerUI(None)
        else:
            feedbacks.debug("Noooone")
            self.model.changeLayer(None)
            self.setLayerUI(layer)
    def getVectorWidgets(self):
        widgets = [self.dlg.landuseSelectionMode,
            self.dlg.landuseSelectField,
            self.dlg.landuseDescrField]
        return widgets
        
    def loadVectorFields(self,layer):
        if not self.model.select_field:
            utils.user_error("No selection field selected")
        new_items = []
        if self.model.descr_field and self.model.descr_field != self.model.select_field:
            keepDescr = False
            fieldsAssoc = qgsUtils.getLayerAssocs(layer,self.model.select_field,self.model.descr_field)
            new_items = [ LanduseFieldItem.fromValues(v,str(l)) for v, l in fieldsAssoc.items() ]
        else:
            keepDescr = True
            fieldVals = qgsUtils.getLayerFieldUniqueValues(layer,self.model.select_field)
            new_items = [LanduseFieldItem.fromValues(v) for v in fieldVals]
        # for new_item in new_items:
            # old_item = self.model.getMatchingItem(new_item)
            # if old_item:
                # new_item.dict[LanduseFieldItem.TO_SELECT_FIELD] = old_item.dict[LanduseFieldItem.TO_SELECT_FIELD]
                # if keepDescr:
                   # new_item.dict[LanduseFieldItem.DESCR_FIELD] = old_item.dict[LanduseFieldItem.DESCR_FIELD]
        return (new_items, keepDescr)
        
    def loadRasterFields(self,layer):
        feedback = feedbacks.progressFeedback
        vals = qgsTreatments.getRasterUniqueVals(layer,feedback)
        new_items = [LanduseFieldItem.fromValues(v) for v in vals]
        return new_items
        
    # Load field values from input layer
    def loadFields(self,fieldname):
        feedbacks.debug("loadField")
        feedbacks.beginSection("Field values load")
        curr_layer = self.model.landuseLayer
        if not curr_layer:
            utils.user_error("No layer selected in landuse tab")
        loaded_layer, layer_type = qgsUtils.loadLayerGetType(curr_layer)
        if layer_type == 'Vector':
            new_items, keepDescr = self.loadVectorFields(loaded_layer)
        else:
            new_items = self.loadRasterFields(loaded_layer)
            keepDescr = True
        # Update items from old ones
        for new_item in new_items:
            old_item = self.model.getMatchingItem(new_item)
            if old_item:
                new_item.dict[LanduseFieldItem.TO_SELECT_FIELD] = old_item.dict[LanduseFieldItem.TO_SELECT_FIELD]
                if keepDescr:
                   new_item.dict[LanduseFieldItem.DESCR_FIELD] = old_item.dict[LanduseFieldItem.DESCR_FIELD]
        # Update model
        self.model.items = new_items
        self.model.layoutChanged.emit()
        feedbacks.endSection()
        
    def importFields(self):
        fname = qgsUtils.openFileDialog(parent=self.dlg,msg="Open field values CSV file",filter="*.csv")
        if fname:
            self.model.fromCSVUpdate(fname)
            
    def saveFields(self):
        fname = qgsUtils.saveFileDialog(parent=self.dlg,
                                      msg="Save field values as CSV file",
                                      filter="*.csv")
        if fname:
            self.model.saveCSV(fname)
        
    def switchSelectionMode(self,index):
        feedbacks.debug("switchSelectMode : " + str(index))
        if index == 0:
            self.dlg.landuseSelectionStack.setCurrentIndex(0)
            self.model.select_mode = LanduseModel.SELECT_FIELD_MODE
        elif index == 1:
            self.dlg.landuseSelectionStack.setCurrentIndex(1)
            self.model.select_mode = LanduseModel.SELECT_EXPR_MODE
        else:
            utils.internal_error("Unexpected index for landuse selection mode : " + str(index))

    def updateUI(self):
        feedbacks.debug("landuseLayer : " + str(self.model.landuseLayer))
        if self.model.landuseLayer:
            loaded_layer = qgsUtils.loadLayer(self.model.landuseLayer,loadProject=True)
            self.dlg.landuseInputLayerCombo.setLayer(loaded_layer)
        feedbacks.debug("select_field : " + str(self.model.select_field))
        if self.model.select_field:
            feedbacks.debug("setting select_field : " + str(self.model.select_field))
            self.dlg.landuseSelectField.setField(self.model.select_field)
        if self.model.descr_field:
            self.dlg.landuseDescrField.setField(self.model.descr_field)
        if self.model.select_expr:
            self.dlg.landuseSelectExpr.setExpression(self.model.select_expr)
        if self.model.select_mode is not None:
            self.switchSelectionMode(self.model.select_mode)
        
    def updateFromXML(self,root,feedback=None):
        self.model.fromXMLRoot(root)
        feedbacks.debug("select_field : " + str(self.model.select_field))
        self.updateUI()
        
    def toXML(self):
        return self.model.toXML()
    
