# -*- coding: utf-8 -*-
"""
/***************************************************************************
 HruDelinDockWidget
                                 A QGIS plugin
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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, shutil, sys, time, platform
from pathlib import Path
import tempfile, configparser
from zipfile import ZipFile
from collections import defaultdict
import numpy as np
from osgeo import gdal, ogr, osr
from osgeo.gdalnumeric import *
from osgeo.gdalconst import *

# resolve path inside plugin directory (to get included data like map color legends for example)
def resolve(name, basepath=None):
    if not basepath:
        basepath = os.path.dirname(os.path.realpath(__file__))
    return os.path.join(basepath, name)

from PyQt5 import QtGui, QtWidgets, uic, QtCore
from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import QFileDialog, QApplication, QMessageBox, QStyle
from PyQt5.QtCore import pyqtSignal, QFileInfo, pyqtRemoveInputHook

from qgis.core import *
from qgis._gui import *
import processing

from hrudelin.pluginUtils import layerstools
from hrudelin.pluginUtils.tools import isWindows, isMac, which, prepareGrassEnv

from multiprocessing import cpu_count
import multiprocessing
#if platform.system() == 'Windows':
if isWindows():
    path = os.path.abspath(os.path.join(sys.exec_prefix, '../../bin/pythonw3.exe'))
    multiprocessing.set_executable(path)
    sys.argv = [None]
    print('fix multiprocess for windows in plugin %s'%os.path.abspath(os.path.join(sys.exec_prefix, '../../bin/pythonw3.exe')))
elif isMac():
    path = os.path.abspath(os.path.join(sys.exec_prefix, 'bin/python3'))
    multiprocessing.set_executable(path)
    sys.argv = [None]
    print('fix multiprocess for Mac in plugin %s'%os.path.abspath(os.path.join(sys.exec_prefix, 'bin/python3')))

prepareGrassEnv()
from hrudelin.hrudelinCore.modules.hrudelin_1_init import main as main1
from hrudelin.hrudelinCore.modules.hrudelin_2_basins import main as main2
from hrudelin.hrudelinCore.modules.hrudelin_3_hrugen import main as main3
from hrudelin.hrudelinCore.modules.hrudelin_parms_J2000 import main as main4

try:
    import dbf
    DBF=True
except Exception as e:
    DBF=False

# this exception is used by the QgisTasks
class CancelException(Exception):
    pass

FORM_CLASS, _ = uic.loadUiType(os.path.join(
    os.path.dirname(__file__), 'hrudelin_dockwidget_base.ui'))

class HruDelinDockWidget(QtWidgets.QDockWidget, FORM_CLASS):

    closingPlugin = pyqtSignal()

    # here we initialize everything
    def __init__(self, parent, iface):
        """Constructor."""
        super(HruDelinDockWidget, self).__init__(parent)
        # Qgis interface, used to get main window, manipulate messageBar etc...
        self.iface = iface
        # can be set within the interface
        self.DEBUG = False
        # define strings used for layout groups in the interface
        self.groupLabels = {
            'input': self.tr('[hru-delin] Input data'),
            'step1':   self.tr('[hru-delin] Step1'),
            'step2':   self.tr('[hru-delin] Step2'),
            'step3':   self.tr('[hru-delin] Step3'),
            'step4':   self.tr('[hru-delin] Step4'),
            'results': self.tr('[hru-delin] Results'),
        }

        self.setupUi(self)

        # GUI initialization:
        # * element visibility
        # * connect events with methods)
        # * set button styles
        # * set most english strings
        # * create temp directory
        self.groupBoxMnt.setVisible(True)
        self.resetButton.setVisible(False)
        self.exportFrame.setVisible(False)
        self.exportDataFrame.setVisible(False)
        self.exportDataResultsCheck.setVisible(False)

        self.projectPathTitleLabel.setVisible(True)
        self.projectPathLabel.setVisible(True)
        self.changeProjectPathButton.setVisible(True)
        self.changeProjectPathButton.pressed.connect(self.changeProjectPath)
        self.mQgsFileDEM.fileChanged.connect(self.checkDEM)
        #self.mQgsFileStudyArea.fileChanged.connect(self.checkStudyArea)
        #self.mQgsFileSubcatchment.fileChanged.connect(self.checkUserSubcatchment)
        self.resetButton.clicked.connect(self.resetProject)
        self.loadButton.clicked.connect(self.loadProject)
        self.debugCheck.stateChanged.connect(self.debugChanged)
        self.exportButton.clicked.connect(self.exportProjectConfig)
        self.exportDataButton.clicked.connect(self.exportProjectData)
        self.step1Check.clicked.connect(self.stepClicked)
        self.step2Check.clicked.connect(self.stepClicked)
        self.step3Check.clicked.connect(self.stepClicked)
        self.step4Check.clicked.connect(self.stepClicked)
        self.stepClicked()
        # help buttons
        style = self.projectPathHelpButton.style()
        self.projectPathHelpButton.setIcon(style.standardIcon(QStyle.SP_MessageBoxQuestion))
        self.exportHelpButton.setIcon(style.standardIcon(QStyle.SP_MessageBoxQuestion))
        self.exportDataHelpButton.setIcon(style.standardIcon(QStyle.SP_MessageBoxQuestion))
        #self.subcatchmentHelpButton.setIcon(style.standardIcon(QStyle.SP_MessageBoxQuestion))
        #self.subcatchmentHelpButton.setToolTip(self.tr('Manually provide area where computations are done'))
        #self.studyHelpButton.setIcon(style.standardIcon(QStyle.SP_MessageBoxQuestion))
        #self.studyHelpButton.setToolTip(self.tr('Outlet area to determine subcatchment where computations are done'))
        #self.subcatchmentHelpButton.clicked.connect(self.help)
        #self.studyHelpButton.clicked.connect(self.help)
        self.exportHelpButton.clicked.connect(self.help)
        self.projectPathHelpButton.clicked.connect(self.help)
        self.exportDataHelpButton.clicked.connect(self.help)

        # style
        self.inputScrollArea.setBackgroundRole(QPalette.Light)
        self.mQgsFileDEM.setBackgroundRole(QPalette.Light)
        #self.mQgsFileStudyArea.setBackgroundRole(QPalette.Light)

        # translations
        self.tabWidget.setTabText(0, self.tr('Input files'))
        self.tabWidget.setTabText(1, self.tr('Export'))
        self.projectPathTitleLabel.setText(self.tr('Output directories location'))
        self.projectBox.setTitle(self.tr('Project'))
        self.groupBoxMnt.setTitle(self.tr('Digital elevation model'))
        self.demFileLabel.setText(self.tr('DEM'))
        #self.studyAreaLabel.setText(self.tr('Study area'))
        #self.subcatchmentLabel.setText(self.tr('Subcatchment'))
        self.changeProjectPathButton.setText(self.tr('Change'))
        self.resetButton.setText(self.tr('Reset project'))
        self.loadButton.setText(self.tr('Load HRU-delin config file (.cfg)'))
        self.debugCheck.setText(self.tr('See all intermediate files\n(debug mode)'))
        self.debugCheck.setVisible(False)
        self.exportDataResultsCheck.setText('Include results in exported archive')
        self.exportButton.setText('Export cfg file')
        self.exportDataButton.setText('Export portable config with input data')

        # default values
        self.step1Check.setChecked(True)
        self.step2Check.setChecked(True)
        self.step3Check.setChecked(True)
        self.step4Check.setChecked(True)
        self.nbProcessSpin.setValue(cpu_count())
        self.nbProcessSpin.setMinimum(1)
        self.nbProcessSpin.setMaximum(cpu_count())

        self.layers = defaultdict(list)

        self.tempDir = tempfile.TemporaryDirectory()
        # set new default temp dir
        self.projectPath = os.path.join(self.tempDir.name, self.tr('temporary'))
        self.projectPathLabel.setText(self.projectPath.replace('\\', '/'))

    # called when closing the dock widget
    def closeEvent(self, event):
        self.closingPlugin.emit()
        event.accept()
        self.resetProject()

    # called when clicking on help buttons
    # just display an alert message box with help
    def help(self):
        oName = self.sender().objectName()
        title = self.tr('Help')
        message = self.tr('No help found')
        if oName == 'subcatchmentHelpButton':
            title = self.tr('Help for subcatchment area')
            message = self.tr('Set this if you already know the area where you want to make the computations.\n\nIf you provide a value here, study area field will be ignored.')
        elif oName == 'studyHelpButton':
            title = self.tr('Help for study area')
            message = self.tr('If you set this, HRU-delin will compute the related subcatchment area which is the area flowing to the study area.\n\nIf you provide a value here, subcatchment field will be ignored.')
        elif oName == 'exportHelpButton':
            title = self.tr('Help for export')
            message = self.tr('You can export project configuration (file paths) to a config (.cfg) file. You can then load this file to run HRU-delin again with this configuration.\n\nThis is usefull to perform the same analysis multiple times or to change just one input file and re-run an analysis.')
        elif oName == 'exportDataHelpButton':
            title = self.tr('Help for export data')
            message = self.tr('This button allows you to export an archive containing all input data files and a config (.cfg) file.\n\nYou can then send this archive to someone who will directly be able to run the analysis with HRU-delin plugin.')
        if oName == 'projectPathHelpButton':
            title = self.tr('Help for project path')
            message = self.tr('This is the path where result directories will be created (results, indicators, tmp and work)')
        if oName == 'areaThrsHelpButton':
            title = self.tr('Help for area threshold')
            message = self.tr('Minimum drained area size considered to produce transfer and accumulation indicators')
        if oName == 'nullCurveHelpButton':
            title = self.tr('Help for ')
            message = self.tr('Until which value a curve is considered as plane?')

        QMessageBox.question(
            self.iface.mainWindow(),
            title,
            message,
            QMessageBox.Ok
        )

    def debugChanged(self):
        self.DEBUG = self.debugCheck.isChecked()

    def stepClicked(self):
        print('plop')
        checkBoxes = [self.step1Check, self.step2Check, self.step3Check, self.step4Check]
        checkedIndexes = []
        for i, cb in enumerate(checkBoxes):
            if cb.isChecked():
                checkedIndexes.append(i)
            checkBoxes[i].setDisabled(False)
        if len(checkedIndexes) >= 2:
            print('range %s %s' % (checkedIndexes[0] + 1, checkedIndexes[-1]))
            print(list(range(checkedIndexes[0] + 1, checkedIndexes[-1])))
            for i in range(checkedIndexes[0] + 1, checkedIndexes[-1]):
                checkBoxes[i].setChecked(True)
                checkBoxes[i].setDisabled(True)
        elif len(checkedIndexes) == 1:
            checkBoxes[checkedIndexes[0]].setDisabled(True)

    ########## manage layers ###########

    def createGroup(self, gid, project):
        root = project.layerTreeRoot()
        label = self.groupLabels[gid]
        groupNode = root.findGroup(label)
        # create if it does not exist
        if groupNode is None:
            groupNode = root.insertGroup(0, self.groupLabels[gid])
            #groupNode.setExpanded(False)

    def deleteGroup(self, gid):
        root = QgsProject.instance().layerTreeRoot()
        label = self.groupLabels[gid]
        groupNode = root.findGroup(label)
        if groupNode is not None:
            root.removeChildNode(groupNode)

    def addToGroup(self, layer, project, gid, expanded=True):
        root = project.layerTreeRoot()

        # add but not to the legend
        project.addMapLayer(layer, False)

        label = self.groupLabels[gid]
        groupNode = root.findGroup(label)
        if groupNode is None:
            self.createGroup(gid, project)
        groupNode = root.findGroup(label)

        layerNode = groupNode.insertLayer(0, layer)
        if not expanded:
            layerNode.setExpanded(False)

    def removeFromGroup(self, layer, gid):
        root = QgsProject.instance().layerTreeRoot()
        label = self.groupLabels[gid]
        groupNode = root.findGroup(label)
        if groupNode is not None:
            groupNode.removeLayer(layer)
        if isWindows() or isMac():
            # thanks to windows file locks, we need to do that to make sure we free the lock
            # and shp* files can be deleted by Python
            QgsProject.instance().addMapLayer(layer)
        # anyway try to remove it the usual way
        QgsProject.instance().removeMapLayer(layer)

    def removeLayersByTag(self, tag):
        #if tag in ['dem', 'study', 'subcatchment']:
        #    gid = 'general'
        #elif tag in ['landuse']:
        #    gid = 'landuse'
        #elif tag in ['soil']:
        #    gid = 'soil'
        #elif tag in ['linear']:
        #    gid = 'linear'
        #elif tag in ['indicator']:
        #    gid = 'indicator'

        gid = tag

        for layer in self.layers[tag]:
            # just in case layer has already been manually removed
            # underlying c++ object does not exist anymore and we get a runtime exception
            try:
                self.removeFromGroup(layer, gid)
                del layer
            except Exception as e:
                print('Exception when removing layer')
                print('%s'%e)
        self.layers[tag] = []

    def displayLayer(self, params, targetProject=None):
        if targetProject is None:
            project = QgsProject.instance()
        else:
            project = targetProject

        layerType = params['type']
        layerPath = params['path']
        layerName = params['name']
        expanded = params['expanded'] if 'expanded' in params else False
        zoom = params['zoom'] if 'zoom' in params else False
        # default tag is 'various'
        tag = params['tag'] if 'tag' in params else 'various'
        # if checked not specified : True
        layerChecked = params['checked'] if 'checked' in params else True

        if layerType == 'vector':
            self.iface.mainWindow().blockSignals(True)
            layer = QgsVectorLayer(layerPath, layerName)
            self.iface.mainWindow().blockSignals(False)
            layer.setCrs(self.projObj)
            if 'style' in params:
                layer.loadNamedStyle(params['style'])
        else:
            # let's temporarily shut down the warnings when loading a raster
            # because we set a projection anyway
            self.iface.mainWindow().blockSignals(True)
            layer = QgsRasterLayer(layerPath, layerName)
            self.iface.mainWindow().blockSignals(False)

            if 'palette' in params:
                classes = QgsPalettedRasterRenderer.classDataFromFile(params['palette'])
                for c in classes:
                    c.label = self.tr(c.label)
                renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 1, classes)
                layer.setRenderer(renderer)

            layer.setCrs(self.projObj)

        # put it in the related group
        self.addToGroup(layer, project, tag, expanded)

        if not layerChecked:
            item = project.layerTreeRoot().findLayer(layer.id())
            if item:
                item.setItemVisibilityChecked(False)
            else:
                QMessageBox.critical(
                    self.iface.mainWindow(),
                    self.tr('item not found'),
                    self.tr('item %s not found'%layerName)
                )

        if zoom:
            self.iface.setActiveLayer(layer)
            self.iface.zoomToActiveLayer()

        if targetProject is not None:
            # we store the layers by tag to be able to remove them later
            self.layers[tag].append(layer)

        return layer

    # save main QGIS project or a custom one
    def saveProject(self, layerDictList, savePath):
        # if no layers provided, just save current main QGIS project
        if layerDictList is None:
            project = QgsProject.instance()
        # if some layers provided, save a project displaying them
        else:
            project = QgsProject()
            for lp in layerDictList:
                self.displayLayer(lp, project)

        project.write(savePath)

    ############## END manage layers #############

    # load a config file and launch a full run
    def loadProject(self):
        self.resetProject()

        projectFilePath, filter = QFileDialog.getOpenFileName(self, self.tr('Project config file'), None, 'Config files (*.cfg)')
        if projectFilePath == '':
            return
        self.loadProjectStartTime = time.time()

        projectFileDir = os.path.dirname(projectFilePath)
        self.projectFileDir = projectFileDir
        config = configparser.ConfigParser()
        config.read(projectFilePath)
        self.projectFilePath = projectFilePath

        # dir_in is absolute or relative to config file parent directory
        dir = config['dir_in']['dir']
        if not os.path.isabs(dir):
            dir = os.path.join(projectFileDir, dir)
        cfgDemPath = os.path.join(dir, config['files_in']['dem'])

        ## studyarea is absolute or relative to dir_in
        #cfgStudyareaPath = config['files_in']['studyarea'] if ('studyarea' in config['files_in']) else None
        #if cfgStudyareaPath != None and not os.path.isabs(cfgStudyareaPath):
        #    cfgStudyareaPath = os.path.join(dir, cfgStudyareaPath)
        #cfgStudyareaField = config['studyarea_parms']['studyarea_id'] if ('studyarea_parms' in config and 'studyarea_id' in config['studyarea_parms']) else None

        self.cfgFilesOutPath = config['dir_out']['files']
        if not os.path.isabs(self.cfgFilesOutPath):
            self.cfgFilesOutPath = os.path.join(projectFileDir, self.cfgFilesOutPath)

        self.cfgResultsOutPath = config['dir_out']['results']
        if not os.path.isabs(self.cfgResultsOutPath):
            self.cfgResultsOutPath = os.path.join(projectFileDir, self.cfgResultsOutPath)

        self.projectPath = os.path.dirname(self.cfgResultsOutPath)
        self.projectPathLabel.setText(self.projectPath.replace('\\', '/'))

        # now get the job done
        self.mQgsFileDEM.setFilePath(cfgDemPath)

        self.autoLaunch()

    # export a config file from current form state
    def exportProjectConfig(self):
        exportPath, filter = QFileDialog.getSaveFileName(self, self.tr('Export project config file'), None, '.cfg config files (*.cfg)')
        if not exportPath.endswith('.cfg'):
            exportPath = '%s.cfg'%exportPath

        exportDir = os.path.dirname(exportPath)
        if not os.access(exportDir, os.W_OK):
            QMessageBox.critical(
                self.iface.mainWindow(),
                self.tr('Export failed'),
                self.tr('Can''t write to directory %s'%exportDir)
            )
            return

        outDir = self.projectPath
        demPath = self.mQgsFileDEM.filePath()

        # export absolute path only
        config = configparser.ConfigParser()
        config['dir_in'] = {'dir': '/not.used.because.every.path.is.absolute'}
        config['dir_out'] = {'results': os.path.abspath(outDir)}
        config['files_in'] = {'dem': os.path.abspath(demPath)}

        with open(exportPath, 'w') as exportFile:
            config.write(exportFile)
        self.iface.messageBar().pushSuccess('HRU delin', self.tr('Config file successfully exported to %s' % exportPath))

    # save an archive with config file and input data
    # optionnally include result data
    def exportProjectData(self):
        exportArchivePath, filter = QFileDialog.getSaveFileName(self, self.tr('Export project data+config archive'), None, 'ZIP archives (*.zip)')
        if not exportArchivePath.endswith('.zip'):
            exportArchivePath = '%s.zip'%exportArchivePath

        exportDir = os.path.dirname(exportArchivePath)
        if not os.access(exportDir, os.W_OK):
            QMessageBox.critical(
                self.iface.mainWindow(),
                self.tr('Export failed'),
                self.tr('Can''t write to directory %s'%exportDir)
            )
            return

        outDir = self.projectPath
        demPath = self.mQgsFileDEM.filePath()

        # export relative path only
        config = configparser.ConfigParser()
        input_dir = 'input_data'
        output_dir = 'results_HRU-delin_plugin'

        config['dir_in'] = {'dir': input_dir}
        config['dir_out'] = {'results': output_dir}
        config['files_in'] = {'dem': os.path.basename(demPath)}

        tmpConfigPath = os.path.join(exportDir, 'tmpconfig.cfg')
        with open(tmpConfigPath, 'w') as tmpConfigFile:
            config.write(tmpConfigFile)

        # now produce the zip archive
        exportName = os.path.basename(exportArchivePath)
        inside_dir = '.'.join(exportName.split('.')[:-1])
        with ZipFile(exportArchivePath, 'w') as zipObj:
            zipObj.write(tmpConfigPath, os.path.join(inside_dir, 'hrudelin_config.cfg'))
            zipObj.write(demPath, os.path.join(inside_dir, input_dir, os.path.basename(demPath)))

            #if studyPath:
            #    zipObj.write(studyPath, os.path.join(inside_dir, input_dir, os.path.basename(studyPath)))
            #    if studyPath.split('.')[-1] == 'shp':
            #        studyPathNoExt = '.'.join(studyPath.split('.')[:-1])
            #        for ext in ['dbf', 'prj', 'shx', 'qpj']:
            #            shapeBonusPath = studyPathNoExt+'.'+ext
            #            if os.path.exists(shapeBonusPath):
            #                zipObj.write(shapeBonusPath, os.path.join(inside_dir, input_dir, os.path.basename(shapeBonusPath)))
            #else:
            #    zipObj.write(subcatchmentPath, os.path.join(inside_dir, input_dir, os.path.basename(subcatchmentPath)))
            #    if subcatchmentPath.split('.')[-1] == 'shp':
            #        subcatchmentPathNoExt = '.'.join(subcatchmentPath.split('.')[:-1])
            #        for ext in ['dbf', 'prj', 'shx', 'qpj']:
            #            shapeBonusPath = subcatchmentPathNoExt+'.'+ext
            #            if os.path.exists(shapeBonusPath):
            #                zipObj.write(shapeBonusPath, os.path.join(inside_dir, input_dir, os.path.basename(shapeBonusPath)))

            # here we also put the results directory in the archive
            # TODO adjust that
            if self.exportDataResultsCheck.isChecked():
                result_dir = self.tr('plugin_results')
                flist = [os.path.join(outDir, f) for f in os.listdir(outDir) if os.path.isfile(os.path.join(outDir, f))]
                for fPath in flist:
                    zipObj.write(fPath, os.path.join(inside_dir, result_dir, os.path.basename(fPath)))
                for dirName in ['tmp', 'indicators', 'results', 'work']:
                    flist = [os.path.join(outDir, dirName, f) for f in os.listdir(os.path.join(outDir, dirName)) if os.path.isfile(os.path.join(outDir, dirName, f))]
                    for fPath in flist:
                        zipObj.write(fPath, os.path.join(inside_dir, result_dir, dirName, os.path.basename(fPath)))
        # delete temporary config file
        os.remove(tmpConfigPath)

        self.iface.messageBar().pushSuccess('HRU delin', self.tr('Data+config archive successfully exported to %s'%exportArchivePath))

    def autoLaunch(self, previous=0):

        if previous == 0 and self.step1Check.isChecked():
            self.doStep1(True, self.step1FinishedAuto)
        elif previous < 2 and self.step2Check.isChecked():
            self.doStep2(True, self.step2FinishedAuto)
        elif previous < 3 and self.step3Check.isChecked():
            self.doStep3(False, self.step3FinishedAuto)
        elif previous < 4 and self.step4Check.isChecked():
            self.doStep4(False, self.step4FinishedAuto)
        else:
            # we must have finished now
            wholeProcessEndTime = time.time()
            print()
            print('[FULL PROCESS] %.2f'%(wholeProcessEndTime - self.loadProjectStartTime))
            print()

    def step1FinishedAuto(self):
        self.step1Finished()
        self.autoLaunch(1)

    def step2FinishedAuto(self):
        self.step2Finished()
        self.autoLaunch(2)

    def step3FinishedAuto(self):
        self.step3Finished()
        self.autoLaunch(3)

    def step4FinishedAuto(self):
        self.step4Finished()
        self.autoLaunch(4)

    # reset all layers and data related to the plugin
    def resetProject(self):
        self.exportFrame.setVisible(False)
        self.exportDataFrame.setVisible(False)
        self.exportDataResultsCheck.setVisible(False)
        # project
        self.changeProjectPathButton.setVisible(True)
        self.projectPathLabel.setText('')
        self.resetButton.setVisible(False)

        # mnt
        self.mQgsFileDEM.blockSignals(True)
        self.mQgsFileDEM.setFilePath('')
        self.mQgsFileDEM.blockSignals(False)

        # sub reset parts
        #self.resetMapButtons()

        self.tempDir = tempfile.TemporaryDirectory()

        # remove all layers?
        if len(QgsProject.instance().layerTreeRoot().layerOrder()) > 0:
            reply = QMessageBox.question(self.iface.mainWindow(), self.tr('Reset QGIS project'),
                self.tr('Do you want to reset the current QGIS project (remove all loaded layers)'),
                QMessageBox.Yes, QMessageBox.No
            )
            if reply == QMessageBox.Yes:
                QgsProject.instance().clear()

        # set new default dir
        self.projectPath = os.path.join(self.tempDir.name, self.tr('temporary'))
        self.projectPathLabel.setText(self.projectPath.replace('\\', '/'))

    # when click on "change"
    # allow user to choose where to write output files
    def changeProjectPath(self):
        hostDir = QFileDialog.getExistingDirectory(
            self,
            self.tr('Select where to write the result files')
        )
        if hostDir:
            self.projectPath = hostDir
            self.projectPathLabel.setText(self.projectPath.replace('\\', '/'))

    # create all needed directories in output directory
    # and create saga CMD output files
    def buildProjectEnvironment(self):
        self.resetButton.setVisible(True)
        #self.projectPath = QFileDialog.getExistingDirectory(self, self.tr('Select a folder')) + '/' + self.lineEditProjectName.text()
        # remove result directories if present
        if not os.path.exists(self.projectPath):
            os.mkdir(self.projectPath)

    #    self.intermDir = os.path.join(self.projectPath, 'hrudelin_intermediate')
     #   if os.path.exists(self.intermDir):
     #       shutil.rmtree(self.intermDir)
     #   os.mkdir(self.intermDir)

      #  self.finalDir = os.path.join(self.projectPath, 'hrudelin_final')
       # if os.path.exists(self.finalDir):
        #    shutil.rmtree(self.finalDir)
       # os.mkdir(self.finalDir)

    # called after having changed the DEM
    # reset all further HRU-delin "chapters"
    # get project projection from the DEM file
    def checkDEM(self):
        self.changeProjectPathButton.setVisible(False)

        # reset study and subcatchment
        # TODO hide doStep* buttons
        #self.doStep1Btn.setVisible(False)
        #self.mQgsFileStudyArea.blockSignals(True)
        #self.mQgsFileSubcatchment.blockSignals(True)
        #self.mQgsFileStudyArea.setFilePath('')
        #self.mQgsFileSubcatchment.setFilePath('')
        #self.mQgsFileStudyArea.blockSignals(False)
        #self.mQgsFileSubcatchment.blockSignals(False)

        for tag in ['step1', 'step2', 'step3', 'step4', 'results']:
            self.removeLayersByTag(tag)
            self.deleteGroup(tag)

        # sub resets
        #self.resetLanduse()

        self.buildProjectEnvironment()

        self.demPath = self.mQgsFileDEM.filePath()
        self.demName = os.path.basename(self.demPath)
        if os.path.exists(self.demPath):
            # we just get the projection
            fd = gdal.Open(self.demPath)
            osrProj = osr.SpatialReference(wkt=fd.GetProjection())
            if str(osrProj.GetName()) == 'None':
               QMessageBox.critical(
                    self.iface.mainWindow(),
                    self.tr('Error'),
                    self.tr('Input Dem as no SCR. Please add one')
                    )




            self.projNum = int(osrProj.GetAttrValue('AUTHORITY', 1))
            self.proj = 'EPSG:%s' % self.projNum
#           self.projObj = QgsCoordinateReferenceSystem(self.projNum, QgsCoordinateReferenceSystem.EpsgCrsId)
            self.projObj = QgsCoordinateReferenceSystem.fromEpsgId(self.projNum) 
            self.demLayer = self.displayLayer({
                'type': 'raster',
                'path': self.demPath,
                'name': self.demName,
                'tag': 'input',
                'checked': True
            })

            ## new way to read projection
            #dem_rs   = gdal.Open(self.demPath)
            #self.gdalProj = dem_rs.GetProjection()

            #print("proj " + str(self.proj))
            #print("projNum " + str(self.projNum))

    # when changing study area, potentially convert it
    def checkStudyArea(self):
        self.removeLayersByTag('study')
        self.removeLayersByTag('subcatchment')
        self.resetLanduse()
        self.resetSoil()
        self.resetLinears()
        self.resetMapButtons()

        self.studyAreaPath = self.mQgsFileStudyArea.filePath()
        self.studyAreaName = os.path.basename(self.studyAreaPath)
        ## TODO adjust all that...
        #if not os.path.exists(self.studyAreaPath):
        #    self.generateCatchmentBtn.setVisible(False)
        #else:
        #    # in any case, we get rid of SUBCATCHMENT if study area is set
        #    self.mQgsFileSubcatchment.setFilePath('')

        #    if self.studyAreaName.split('.')[-1] in ['shp', 'gpkg']:
        #        self.studyAreaLayer = self.displayLayer({
        #            'type': 'vector',
        #            'path': self.studyAreaPath,
        #            'name': self.studyAreaName,
        #            'tag': 'study',
        #            'checked': True
        #        })
        #        self.generateCatchmentBtn.setVisible(True)
        #    elif self.studyAreaName.split('.')[-1] in ['tif', 'map']:
        #        self.studyAreaLayer = self.displayLayer({
        #            'type': 'raster',
        #            'path': self.studyAreaPath,
        #            'name': self.studyAreaName,
        #            'tag': 'study',
        #            'checked': True
        #        })
        #        self.generateCatchmentBtn.setVisible(True)
        #    else:
        #        self.generateCatchmentBtn.setVisible(False)

    # create the QgsTask and launch it
    # successMethod will be called after the QgsTask has successfully finished
    def launchTask(self, taskName, processMethodList, successMethod=None, errorMethod=None):
        task = HruDelinTask(taskName, self, processMethodList)
        task.nbProcess = self.nbProcessSpin.value()
        self.task = task

        # configure the QgsMessageBar
        messageBar = self.iface.messageBar().createMessage(self.tr('Running %s...' % taskName), )
        progressBar = QtWidgets.QProgressBar()
        progressBar.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
        cancelButton = QtWidgets.QPushButton()
        cancelButton.setText(self.tr('Cancel'))
        cancelButton.clicked.connect(task.cancel)
        messageBar.layout().addWidget(progressBar)
        messageBar.layout().addWidget(cancelButton)
        self.iface.messageBar().pushWidget(messageBar, Qgis.Info)
        self.messageBar = messageBar

        # start the worker in a new thread
        if successMethod != None:
            task.taskCompleted.connect(successMethod)
        if errorMethod != None:
            task.taskTerminated.connect(errorMethod)
        task.progressChanged.connect(progressBar.setValue)
        task.displayLayer.connect(self.displayLayer)

        QgsApplication.taskManager().addTask(task)
        # WOW this "was" necessary to avoid a crash
        #print('task ADDED')

    # receive on click event OR is called by load process
    def doStep1(self, uselessBool, successMethod=None):
        #self.generateCatchmentBtn.setVisible(False)
        #self.resetLanduse()
        #self.resetLinears()
        #self.resetSoil()
        self.removeLayersByTag('step1')

        if successMethod == None:
            taskSuccessMethod = self.step1Finished
        else:
            taskSuccessMethod = successMethod
        self.launchTask('Step 1', [self.processStep1], taskSuccessMethod, self.step1Error)

    # called when the task has finished with success in a manual run
    def step1Finished(self):
        self.iface.messageBar().popWidget(self.messageBar)
        self.iface.messageBar().pushSuccess('HRU delin', self.tr('Step 1 success'))

        ## show next GUI elements
        #self.groupBoxLanduse.setVisible(True)
        #self.groupBoxHRUdelinPlus.setVisible(True)
        #self.landuseLegend1Frame.setVisible(False)
        #self.landuseLegend2Frame.setVisible(False)

    # called when the task returns False
    # determine if the task was canceled or if it crashed
    def step1Error(self):
        self.iface.messageBar().popWidget(self.messageBar)
        QgsMessageLog.logMessage('Exception: {}'.format(self.task.exception),
                                 'HRU delin', Qgis.Critical)
        try:
            raise self.task.exception
        except CancelException as e:
            self.iface.messageBar().pushMessage(self.tr('Step 1 task TERMINATED because it was canceled'))
        except Exception as e:
            self.iface.messageBar().pushCritical('HRU delin', self.tr('Step 1 task problem. See StackTrace for more details'))
            raise e

    def doStep2(self, uselessBool, successMethod=None):
        self.removeLayersByTag('step2')

        if successMethod == None:
            taskSuccessMethod = self.step2Finished
        else:
            taskSuccessMethod = successMethod
        self.launchTask('Step 2', [self.processStep2], taskSuccessMethod, self.step2Error)

    # called when the task has finished with success in a manual run
    def step2Finished(self):
        self.iface.messageBar().popWidget(self.messageBar)
        self.iface.messageBar().pushSuccess('HRU delin', self.tr('Step 2 success'))

        # TODO we could add layers here...

        ## show next GUI elements
        #self.groupBoxLanduse.setVisible(True)
        #self.groupBoxHRUdelinPlus.setVisible(True)
        #self.landuseLegend1Frame.setVisible(False)
        #self.landuseLegend2Frame.setVisible(False)

    # called when the task returns False
    # determine if the task was canceled or if it crashed
    def step2Error(self):
        self.iface.messageBar().popWidget(self.messageBar)
        QgsMessageLog.logMessage('Exception: {}'.format(self.task.exception),
                                 'HRU delin', Qgis.Critical)
        try:
            raise self.task.exception
        except CancelException as e:
            self.iface.messageBar().pushMessage(self.tr('Step 2 task TERMINATED because it was canceled'))
        except Exception as e:
            self.iface.messageBar().pushCritical('HRU delin', self.tr('Step 2 task problem. See StackTrace for more details'))
            raise e

    def doStep3(self, uselessBool, successMethod=None):
        #self.generateCatchmentBtn.setVisible(False)
        #self.resetLanduse()
        #self.resetLinears()
        #self.resetSoil()
        self.removeLayersByTag('step3')

        if successMethod == None:
            taskSuccessMethod = self.step3Finished
        else:
            taskSuccessMethod = successMethod
        self.launchTask('Step 3', [self.processStep3], taskSuccessMethod, self.step3Error)

    # called when the task has finished with success in a manual run
    def step3Finished(self):
        self.iface.messageBar().popWidget(self.messageBar)
        self.iface.messageBar().pushSuccess('HRU delin', self.tr('Step 3 success'))

        # TODO we could add layers here...

        ## show next GUI elements
        #self.groupBoxLanduse.setVisible(True)
        #self.groupBoxHRUdelinPlus.setVisible(True)
        #self.landuseLegend1Frame.setVisible(False)
        #self.landuseLegend2Frame.setVisible(False)

    # called when the task returns False
    # determine if the task was canceled or if it crashed
    def step3Error(self):
        self.iface.messageBar().popWidget(self.messageBar)
        QgsMessageLog.logMessage('Exception: {}'.format(self.task.exception),
                                 'HRU delin', Qgis.Critical)
        try:
            raise self.task.exception
        except CancelException as e:
            self.iface.messageBar().pushMessage(self.tr('Step 3 task TERMINATED because it was canceled'))
        except Exception as e:
            self.iface.messageBar().pushCritical('HRU delin', self.tr('Step 3 task problem. See StackTrace for more details'))
            raise e

    def doStep4(self, uselessBool, successMethod=None):
        #self.generateCatchmentBtn.setVisible(False)
        #self.resetLanduse()
        #self.resetLinears()
        #self.resetSoil()
        self.removeLayersByTag('step4')

        if successMethod == None:
            taskSuccessMethod = self.step4Finished
        else:
            taskSuccessMethod = successMethod
        self.launchTask('Step 4', [self.processStep4], taskSuccessMethod, self.step4Error)

    def step4Finished(self):
        self.exportDataResultsCheck.setVisible(True)
        self.iface.messageBar().popWidget(self.messageBar)
        self.iface.messageBar().pushSuccess('HRU delin', self.tr('Step 4 success'))

        # add result layers here

        # TODO adjust QGIS project saving
        ## save a small QGIS project with result maps only
        #resultsParamList = [
        #    {
        #        'type': 'vector',
        #        'path': self.resultHruPath,
        #        'name': self.tr('HRUs'),
        #        'tag': 'results',
        #        'expanded': False,
        #    },
        #    {
        #        'type': 'vector',
        #        'path': self.resultReachPath,
        #        'name': self.tr('Reachs'),
        #        'tag': 'results',
        #        'expanded': False,
        #    },
        #]
        #self.saveProject(resultsParamList, os.path.join(self.resultsDir, 'hru-delin_final.qgz'))

        ## save a big QGIS project file in results dir (to keep palettes)
        #paramList = []
        #paramList.append({
        #    'type': 'vector',
        #    'path': self.catchmentVectorPath,
        #    'name': self.tr('catchment vector'),
        #    'tag': 'subcatchment'
        #})

        #paramList.extend(resultsParamList)
        #self.saveProject(paramList, os.path.join(self.projectPath, 'hru-delin_all_results.qgz'))

    def step4Error(self):
        self.iface.messageBar().popWidget(self.messageBar)
        QgsMessageLog.logMessage('Exception: {}'.format(self.task.exception),
                                 'HRU delin', Qgis.Critical)
        try:
            raise self.task.exception
        except CancelException as e:
            self.iface.messageBar().pushMessage(self.tr('Step 4 task TERMINATED because it was canceled'))
        except Exception as e:
            self.iface.messageBar().pushCritical('HRU delin', self.tr('Step 4 task problem. See StackTrace for more details'))
            raise e

    # here we get serious
    def processStep1(self, task):
        task.setProgress(0)

        # do the same job as hrudelin bash script which launch python modules
        if os.path.exists(self.cfgFilesOutPath):
            shutil.rmtree(self.cfgFilesOutPath)
        os.mkdir(self.cfgFilesOutPath)

        if os.path.exists(self.cfgResultsOutPath):
            shutil.rmtree(self.cfgResultsOutPath)
        os.mkdir(self.cfgResultsOutPath)

        tmpPath = os.path.join(self.projectFileDir, 'tmp')
        if os.path.exists(tmpPath):
            shutil.rmtree(tmpPath)
        os.mkdir(tmpPath)

        # run the mzfc
        main1(self.projectFilePath)

        # display layers
        for fPath in Path(self.cfgFilesOutPath).rglob('*step1*.tif'):
            strPath = str(fPath)
            task.displayLayer.emit({
                'type': 'raster',
                'path': strPath,
                'name': os.path.basename(strPath).replace('step1_', ''),
                'tag': 'step1',
                'zoom': (os.path.basename(strPath) == 'step1_dem_cut.tif')
            })
        for fPath in Path(self.cfgFilesOutPath).rglob('*step1*.shp'):
            strPath = str(fPath)
            task.displayLayer.emit({
                'type': 'vector',
                'path': strPath,
                'name': os.path.basename(strPath).replace('step1_', ''),
                'tag': 'step1'
            })

        return True

    def processStep2(self, task):
        task.setProgress(0)

        for fPath in Path(self.cfgFilesOutPath).rglob('step2*.tif'):
            os.remove(str(fPath))
        for fPath in Path(self.cfgFilesOutPath).rglob('step3*.tif'):
            os.remove(str(fPath))

        if os.path.exists(self.cfgResultsOutPath):
            shutil.rmtree(self.cfgResultsOutPath)
        os.mkdir(self.cfgResultsOutPath)

        # run the mzfc
        for progress in main2(self.projectFilePath, task.nbProcess, True):
            task.setProgress(progress)

        # display layers
        for fPath in Path(self.cfgFilesOutPath).rglob('*step2*.tif'):
            strPath = str(fPath)
            task.displayLayer.emit({
                'type': 'raster',
                'path': strPath,
                'name': os.path.basename(strPath).replace('step2_', ''),
                'tag': 'step2'
            })
        for fPath in Path(self.cfgFilesOutPath).rglob('*step2*.shp'):
            strPath = str(fPath)
            task.displayLayer.emit({
                'type': 'vector',
                'path': strPath,
                'name': os.path.basename(strPath).replace('step2_', ''),
                'tag': 'step2'
            })

        return True

    def processStep3(self, task):
        task.setProgress(0)

        for fPath in Path(self.cfgFilesOutPath).rglob('step3*.tif'):
            os.remove(str(fPath))

        if os.path.exists(self.cfgResultsOutPath):
            shutil.rmtree(self.cfgResultsOutPath)
        os.mkdir(self.cfgResultsOutPath)

        # run the mzfc
        for progress in main3(self.projectFilePath, task.nbProcess, True):
            task.setProgress(progress)

        # display layers
        for fPath in Path(self.cfgFilesOutPath).rglob('*step3*.tif'):
            strPath = str(fPath)
            task.displayLayer.emit({
                'type': 'raster',
                'path': strPath,
                'name': os.path.basename(strPath).replace('step3_', ''),
                'tag': 'step3'
            })
        for fPath in Path(self.cfgFilesOutPath).rglob('*step3*.shp'):
            strPath = str(fPath)
            task.displayLayer.emit({
                'type': 'vector',
                'path': strPath,
                'name': os.path.basename(strPath).replace('step3_', ''),
                'tag': 'step3'
            })

        return True

    def processStep4(self, task):
        task.setProgress(0)

        if os.path.exists(self.cfgResultsOutPath):
            shutil.rmtree(self.cfgResultsOutPath)
        os.mkdir(self.cfgResultsOutPath)

        for fPath in Path(os.path.join(self.projectFileDir, 'tmp')).rglob('topolog*'):
            os.remove(str(fPath))

        # run the mzfc
        for progress in main4(self.projectFilePath, task.nbProcess, True):
            task.setProgress(progress)

        # display layers
        task.displayLayer.emit({
            'type': 'vector',
            'path': os.path.join(self.cfgResultsOutPath, 'hru.shp'),
            'name': 'hru.shp',
            'tag': 'results',
            'zoom': True
        })
        task.displayLayer.emit({
            'type': 'vector',
            'path': os.path.join(self.cfgResultsOutPath, 'reach.shp'),
            'name': 'reach.shp',
            'tag': 'results'
        })

        return True


class HruDelinTask(QgsTask):
    displayLayer = QtCore.pyqtSignal(object)
    def __init__(self, desc, dockwidget, methodsToCall):
        QgsTask.__init__(self, desc, QgsTask.CanCancel)
        self.dockwidget = dockwidget
        self.methodsToCall = methodsToCall
        self.exception = None
        self.desc = desc

    def run(self):
        #print('task starting')
        ret = True
        try:
            self.setProgress(2)
            for method in self.methodsToCall:
                ret = method(self)
        except Exception as e:
            self.exception = e
            return False
        #print('task finishing')
        return ret

    # this method is not always called, we catch the signals anyway
    def finished(self, result):
        #print('task finished method, result: %s'%result)
        pass

    def cancel(self):
        self.dockwidget.iface.messageBar().pushMessage(
            self.desc + ': ' + self.dockwidget.tr('Task canceled, killing it might take some time. Close QGIS to kill it.')
        )
        super().cancel()