from PyQt4.QtCore import *
from PyQt4.QtGui import *
from qgis.core import *
import os
from PyQt4 import QtGui, uic

import csv
import datetime
from datetime import datetime, timedelta
from PyQt4.QtCore import pyqtSignal
import qgis.utils
from qgis.utils import iface
from freewat.freewat_utils import getVectorLayerNames,getModelsInfoLists

#Load the interface
FORM_CLASS, _ = uic.loadUiType(os.path.join( os.path.dirname(__file__), 'ui/ui_createInputFilesForOBS.ui') )
#
class CreateInputFilesForOBS(QDialog, FORM_CLASS):
    def __init__(self, iface):
        QDialog.__init__(self)
        self.iface = iface
        self.setupUi(self)

        self.manageGui()

        ################################Load input csv file################################
        #When clicking the browse_button button, the loadCSVfile function below is recalled
        self.browse_button.clicked.connect(self.loadCSVfile)

        ##############################   Create input files   #############################
        ############################## for head observations  #############################
        #The label of the 'OK' button is changed to 'Run'
        self.buttonBox_1.button(QDialogButtonBox.Ok).setText("Run")
        #If the 'Run' button is clicked, the runHeadObservations function below is recalled
        self.buttonBox_1.button(QDialogButtonBox.Ok).clicked.connect(self.runHeadObservations)
        #If the 'Cancel' button is clicked, the Dialog closes
        self.buttonBox_1.button(QDialogButtonBox.Cancel).clicked.connect(self.reject)


    def manageGui(self):

        self.cmbModelName.clear()
        self.cmbGrid.clear()
        self.layerNameList = getVectorLayerNames()
        self.layerNameList.sort()

        (modelNameList, pathList) =  getModelsInfoLists(self.layerNameList)

        self.cmbModelName.addItems(modelNameList)

        grid_layers = []
        for layer in self.layerNameList:
            if '_grid' in layer:
                grid_layers.append(layer)
        self.cmbGrid.addItems(grid_layers)


########################################Load input csv file########################################

    def loadCSVfile(self):

        #getOpenFileName is a method of the QFileDialog class which allows to open a dialog window with
        #the heading "Select input csv file".
        #The argument "" makes this dialog window to open always at the same path, while '*.csv' allows
        #to filter csv files.
        #The getOpenFileName method returns the csv file csv_file
        #I use self.csv_file (and not just csv_file) because I will need this variable in the functions below
        self.csv_file=QFileDialog.getOpenFileName(self,"Select input csv file", "", '*.csv')

        #The path_bar fills with the whole path of the selected csv file
        #(setText is a method of the QLabel class which holds the label's text)
        self.path_bar.setText(self.csv_file)

        #csv_content is a dictionary get from the readCSV function.
        #It is nothing but the content of the input csv file.
        #csv_headers is a list get from the readCSV function.
        #It is nothing but the list of headers of the input csv file.
        self.csv_content, self.csv_headers=self.readCSV(self.csv_file)

        #Fill the comboboxes with the csv_headers list (addItems is a method
        #of the QComboBox class which provides a list of options in a combobox)
        self.name.addItems(self.csv_headers)
        self.x.addItems(self.csv_headers)
        self.y.addItems(self.csv_headers)
        self.top.addItems(self.csv_headers)
        self.bottom.addItems(self.csv_headers)
        self.from_lay.addItems(self.csv_headers)
        self.to_lay.addItems(self.csv_headers)
        self.date.addItems(self.csv_headers)
        self.time.addItems(self.csv_headers)
        self.value.addItems(self.csv_headers)
        self.stat_value.addItems(self.csv_headers)
        self.stat_type.addItems(self.csv_headers)


###########################Create input files for head observations##########################

    def runHeadObservations(self):

        self.progressBar.setMinimum(0)
        self.progressBar.setMaximum(0)
        self.progressBar.setValue(0)

        QApplication.processEvents()

        #Check if modeltable, timetable and lpf table are in the Layers Panel
        self.checkMandatoryTables()

        #Check if the path_bar is empty or not
        self.checkPath()

        ###Read the content of the comboboxes and check if the mandatory comboboxes are empty or not

        #Read the header of the name field
        self.name_field = self.name.currentText()
        #Check if the name combobox is correctly filled
        self.checkContent(self.name,'A field for head observation points\' names is mandatory!')

        #Read the header of the X coordinate field
        self.x_field = self.x.currentText()
        #Check if the x combobox is correctly filled
        self.checkContent(self.x,'A field for X coordinates of head observation points is mandatory!')

        #Read the header of the Y coordinate field
        self.y_field = self.y.currentText()
        #Check if the y combobox is correctly filled
        self.checkContent(self.y,'A field for Y coordinates of head observation points is mandatory!')

        #If both the top_bot_elev and the from_to_lay checkboxes are checked...
        if self.top_bot_elev.isChecked() and self.from_to_lay.isChecked():
            #...then a warning appears
            QMessageBox.warning(self, self.tr('Warning!'), self.tr('Options "Top and Bottom elevations of wells screens" and "Extension of wells screens" are alternative!'))
            #...and nothing is done
            return

        #If the top_bot_elev checkbox is checked, the User has to provide
        #top and bottom elevations of screens at each head observation point
        if self.top_bot_elev.isChecked():
            #Read the header of the top elevation field
            self.top_field = self.top.currentText()
            #Check if the top combobox is correctly filled
            self.checkContent(self.top,'A field for top elevations of screens is mandatory!')
            #Read the header of the bottom elevation field
            self.bottom_field = self.bottom.currentText()
            #Check if the bottom combobox is correctly filled
            self.checkContent(self.bottom,'A field for bottom elevations of screens is mandatory!')

        #If the from_to_lay checkbox is checked, the User has to provide
        #uppermost and deepest layers to which each head observation point is screened
        if self.from_to_lay.isChecked():
            #Read the header of the uppermost layer field
            self.first_lay = self.from_lay.currentText()
            #Check if the from_lay combobox is correctly filled
            self.checkContent(self.from_lay,'A field for uppermost layers to which the head observation points are screened is mandatory!')
            #Read the header of the deepest layer field
            self.last_lay = self.to_lay.currentText()
            #Check if the to_lay combobox is correctly filled
            self.checkContent(self.to_lay,'A field for deepest layers to which the head observation points are screened is mandatory!')

        #Read the header of the date field
        self.date_field = self.date.currentText()
        #Check if the date combobox is correctly filled
        self.checkContent(self.date,'A field for dates observations is mandatory!')

        #Read the header of the time field
        self.time_field = self.time.currentText()

        #Read the header of the value field
        self.value_field = self.value.currentText()
        #Check if the value combobox is correctly filled
        self.checkContent(self.value,'A field for observations values is mandatory!')

        #Read the header of the statistics value field
        self.stat_value_field = self.stat_value.currentText()

        #Read the header of the statistics type field
        self.stat_type_field = self.stat_type.currentText()

        #If one between the variables stat_value_field and stat_type_field is not equal to 'Select a field...', but the other is)...
        if self.stat_value_field!='Select a field...' and self.stat_type_field=='Select a field...':
            #...then a warning appears
            QMessageBox.warning(self, self.tr('Warning!'), self.tr('Statistics values and Statistics types fields must be both provided!'))
            #...and nothing is done
            return
        elif self.stat_value_field=='Select a field...' and self.stat_type_field!='Select a field...':
            #...then a warning appears
            QMessageBox.warning(self, self.tr('Warning!'), self.tr('Statistics values and Statistics types fields must be both provided!'))
            #...and nothing is done
            return

        ###Create the input_files_for_head_obs subfolder

        #Get the modeltable from the legend
        layerMap = QgsMapLayerRegistry.instance().mapLayers()
        for name, layer in layerMap.iteritems():
            if layer.type() == QgsMapLayer.VectorLayer and layer.name() == 'modeltable_'+str(self.cmbModelName.currentText()):
                modeltable=layer

        #f iterates over the rows of the modeltable
        for f in modeltable.getFeatures():
            #The path of the working directory is read
            working_dir=str(f['working_dir'])

        #Create a subfolder (UCODE_input_files) within the working
        #directory (if it does not exist yet, otherwise it is rewritten)
        self.path = working_dir+'/input_files_for_head_obs/'
        if not os.path.exists(self.path):
            os.makedirs(self.path)

        ###Create a new point shapefile with head observation points
        self.createHeadObsShp()

        ###Create csv files with head observation values (one for each head observation point)
        self.createHeadCsvFiles()

        self.progressBar.setMaximum(100)

        #A message appears
        QMessageBox.information(self, self.tr('Information'), self.tr('Input files for head observations have been created in the '+self.path+' folder.'))

        #The Create input files for Observations dialog closes automatically when done
        self.reject()


    #The readCSV function allows to read the content of the input csv file and to make a dictionary
    def readCSV(self,csv_file):

        #Get a dictionary which is nothing but the content of the input csv file read by columns
        columns = []
        with open(csv_file,'rU') as csvfile:
            reader = csv.reader(csvfile, delimiter=';')
            for row in reader:
                if columns:
                    for i, value in enumerate(row):
                        columns[i].append(value)
                else:
                    columns = [[value] for value in row]
        dictionary = {c[0] : c[1:] for c in columns}

        #Get a list of the headers of each column of the input csv file
        headers=[]
        for col in columns:
            headers.append(col[0])
        #Sort headers alphabetically
        headers=sorted(headers)
        #Insert the string 'Select a field...' at the beginning of the list headers
        headers.insert(0,'Select a field...')

        #dictionary and headers are the outputs of the readCSV function
        return dictionary, headers


    #The checkMandatoryTables function checks if the modeltable, timetable
    #and lpf table of the selected flow model are present in the Layers Panel
    def checkMandatoryTables(self):

        modeltable='modeltable_'+str(self.cmbModelName.currentText())
        timetable='timetable_'+str(self.cmbModelName.currentText())
        lpftable='lpf_'+str(self.cmbModelName.currentText())

        if modeltable not in self.layerNameList:
            #...then a warning appears
            QMessageBox.warning(self, self.tr('Warning!'), self.tr('Table %s is not present in the Layers Panel!' %str(modeltable)))
            #...and nothing is done
            return

        if timetable not in self.layerNameList:
            #...then a warning appears
            QMessageBox.warning(self, self.tr('Warning!'), self.tr('Table %s is not present in the Layers Panel!' %str(timetable)))
            #...and nothing is done
            return

        if lpftable not in self.layerNameList:
            #...then a warning appears
            QMessageBox.warning(self, self.tr('Warning!'), self.tr('Table %s is not present in the Layers Panel!' %str(lpftable)))
            #...and nothing is done
            return


    #The checkPath function checks if the path_bar is empty or not
    #(no input nor output parameters)
    def checkPath(self):

        #If the path_bar is empty...
        if len(str(self.path_bar.text()))==0:
            #...then a warning appears
            QMessageBox.warning(self, self.tr('Warning!'), self.tr('You must select an input csv file first!'))
            #...and nothing is done
            return


    #The checkContent function checks if the mandatory comboboxes are correctly filled
    def checkContent(self,combobox,message):

        #If the content of the combobox is 'Select a field...'...
        if combobox.currentText()=='Select a field...':
            #...then a warning appears
            QMessageBox.warning(self, self.tr('Warning!'), self.tr(message))
            #...and nothing is done
            return


    #The createHeadObsShp function creates a new point shapefile with head observation points
    def createHeadObsShp(self):

        #Create a new point shapefile
        #(its name will be head_observation_points.shp; no CRS is defined)
        vl = QgsVectorLayer("Point", "head_observation_points", "memory")
        pr = vl.dataProvider()

        #Start editing mode
        vl.startEditing()

        #Add fields
        pr.addAttributes([QgsField("Name", QVariant.String), QgsField("X",  QVariant.Double), QgsField("Y",  QVariant.Double), QgsField("Top_screen", QVariant.Double), QgsField("Bot_screen", QVariant.Double)])

        #Add features
        points_names=[]
        #i iterates over the length of the list which is the value of the
        #csv_content dictionary corresponding to the key identified by name_field
        for i in range(len(self.csv_content[self.name_field])):
            #Check if the head observation points has been already taken into account (this is needed to avoid repetitions)
            #If not...
            if self.csv_content[self.name_field][i] not in points_names:
                #Add the name of the head observation point in the points_names list
                points_names.append(self.csv_content[self.name_field][i])
                #Add a new empty feature
                feat = QgsFeature()
                #The new point will have X and Y coordinates read from the csv_content dictionary
                feat.setGeometry(QgsGeometry.fromPoint(QgsPoint(float(self.csv_content[self.x_field][i]),float(self.csv_content[self.y_field][i]))))
                #Fill the Attribute Table of the observation_points.shp
                #file with info read from the csv_content dictionary
                #If the top_bot_elev combobox is checked...
                if self.top_bot_elev.isChecked():
                    #...top and bottom values read in the input csv file are used
                    #as Top_screen and Bot_screen values
                    feat.setAttributes([str(self.csv_content[self.name_field][i]),float(self.csv_content[self.x_field][i]),float(self.csv_content[self.y_field][i]),float(self.csv_content[self.top_field][i]),float(self.csv_content[self.bottom_field][i])])
                #If the from_to_lay combobox is checked...
                elif self.from_to_lay.isChecked():
                    top_screen, bot_screen=self.getTopBottomScreens()
                    feat.setAttributes([str(self.csv_content[self.name_field][i]),float(self.csv_content[self.x_field][i]),float(self.csv_content[self.y_field][i]),top_screen[str(self.csv_content[self.name_field][i])],bot_screen[str(self.csv_content[self.name_field][i])]])

                #Add the feature to the Attribute Table of the observation_points.shp file
                pr.addFeatures([feat])
            #If yes...
            else:
                #...go ahead
                continue

        #Commit changes
        vl.commitChanges()

        #Save the observation_points.shp file in the UCODE_input_files sub-folder
        QgsVectorFileWriter.writeAsVectorFormat(vl,self.path+'head_observation_points.shp','utf8',vl.crs(),'ESRI Shapefile')


    #The createHeadCsvFiles function creates csv files with head observation values
    #(one csv file for each head observation point)
    def createHeadCsvFiles(self):

        #Get a list of names for the head observation points, without repetitions
        self.names_list=set(self.csv_content[self.name_field])

        #Get initial and final dates and times of model time steps, and initial and
        #final dates and times of the simulation, recalling the getModelTimeDiscr
        #function below. The resulting time_discr is a dictionary whose keys are strings
        #like 'sp_ts', and whose values are lists like [initial date and time, final date and time].
        #model_start and model_end are dates in the format %Y-%m-%d %H:%M:%S
        time_discr,model_start,model_end=self.getModelTimeDiscr()

        #Save and fill new csv files (this is saved in the same directory where the point shapefile
        #with head observation points is located; one csv file for each head observation point)
        #Headers for fields in csv files
        self.first_row=['time','data','obsname','weight','statistics']
        #name iterates over the names_list list
        for name in self.names_list:
            #Create the csv file in the UCODE_input_files sub-folder
            out = csv.writer(open(self.path+name+'.csv',"wb"), delimiter=';')
            #Add the first row with headers
            out.writerow(self.first_row)
            #Add the other rows
            row=[]
            #index and n iterate over the list of the csv_content dictionary
            #corresponding to the name_field key
            for index,n in enumerate(self.csv_content[self.name_field]):

                #if n is equal to the name of the current head observaion point
                if n==name:

                    #Prepare date and time in the format YYYY-mm-dd hh-mm-ss
                    if self.time_field == 'Select a field...':
                        date_time=self.csv_content[self.date_field][index]+' 12:00:00'
                    else:
                        if self.csv_content[self.time_field][index] == '':
                            date_time=self.csv_content[self.date_field][index]+' 12:00:00'
                        else:
                            date_time=self.csv_content[self.date_field][index]+' '+self.csv_content[self.time_field][index]

                    #Get a name for the observation. This name will be a string like pointname_sp_ts

                    #Convert date_time in a datetime object to be compared with model_start,
                    #model_end and values of the dictionary time_discr above
                    date_time2=datetime.strptime(date_time,'%Y-%m-%d %H:%M:%S')

                    #If the date and time of the measurement is before the initial
                    #or after the final dates and times of the simulation...
                    if date_time2<model_start or date_time2>model_end:
                        #...then a warning appears
                        QMessageBox.warning(self, self.tr('Warning!'), self.tr('Measurement date and time '+str(date_time2)+' for head observation point '+str(name)+' is not within the simulation period!'))
                        #...and nothing is done
                        return

                    #If statistics values and types are not provided...
                    if self.stat_value_field=='Select a field...' and self.stat_type_field=='Select a field...':
                        #k and v iterate over the dictionary time_discr
                        for k,v in time_discr.iteritems():
                            #If the date and time of the measurement is between the initial
                            #and final dates and times of a certain time step...
                            if date_time2>=v[0] and date_time2<=v[1]:
                                #...then a name for the observation is set.
                                #It will be a string like 'pointname_sp_ts'
                                name2 = name + '_' + k
                                row=[date_time,self.csv_content[self.value_field][index],name2,'0.1','SD']
                    #If statistics values and types are provided...
                    else:
                        if self.csv_content[self.stat_value_field][index] == '' or self.csv_content[self.stat_type_field][index] == '':
                            QMessageBox.warning(self, self.tr('Warning!'), self.tr('Statistics value or type for '+name+' on '+date_time+' is not provided! Please, check your input csv file!'))
                            return
                        else:
                            #k and v iterate over the dictionary time_discr
                            for k,v in time_discr.iteritems():
                                #If the date and time of the measurement is between the initial
                                #and final dates and times of a certain time step...
                                if date_time2>=v[0] and date_time2<=v[1]:
                                    #...then a name for the observation is set.
                                    #It will be a string like 'pointname_sp_ts'
                                    name2 = name + '_' + k
                                    row=[date_time,self.csv_content[self.value_field][index],name2,self.csv_content[self.stat_value_field][index],self.csv_content[self.stat_type_field][index]]
                    out.writerow(row)


    #The getTopBottomScreens function allows to infer top
    #and bottom screens elevations from layers numbers
    def getTopBottomScreens(self):

        layerMap = QgsMapLayerRegistry.instance().mapLayers()
        #layer iterates over the layers in the Legend
        for name, layer in layerMap.iteritems():
            #If the name of the current layer is equal to the content of the cmbGrid combobox...
            if layer.type() == QgsMapLayer.VectorLayer and layer.name() == str(self.cmbGrid.currentText()):
                #...then that layer is stored in the variable grid
                grid=layer
            #If the name of the current layer is equal to 'lpf_flowmodel'...
            elif layer.type() == QgsMapLayer.VectorLayer and layer.name() == 'lpf_'+str(self.cmbModelName.currentText()):
                #...then that layer is stored in the variable lpf_table
                lpf_table=layer

        #model_layers is the dictionary of model layers
        model_layers={}
        #nrows is the number of rows in the lpf table
        nrows=0
        #f iterates over the rows of the lpf table
        for f in lpf_table.getFeatures():
            nrows=nrows+1
            #The model_layers dictionary is filled
            model_layers[nrows]=str(f['name'])

        dict={}
        #i iterates over the length of the list name_field in the
        #csv_content dictionary (i.e., in the csv input file)
        for i in range(len(self.csv_content[self.name_field])):
            #Get a temporary point with X and Y coordinates of a specific head measurement point
            point = QgsFeature()
            point.setGeometry(QgsGeometry.fromPoint(QgsPoint(float(self.csv_content[self.x_field][i]),float(self.csv_content[self.y_field][i]))))
            #Get row and col indexes of the grid cell intersected by a specific head measurement point
            for cell in grid.getFeatures():
                if point.geometry().intersects(cell.geometry()):
                    row_index=int(cell['ROW'])
                    col_index=int(cell['COL'])

            #Create the list value with tuples (x_coord,y_coord) and (row_index,col_index) for that specific head measurement point
            value=[(self.csv_content[self.x_field][i],self.csv_content[self.y_field][i]),(row_index,col_index)]

            #k and v iterate over keys and values of the model_layers dictionary
            for k, v in model_layers.iteritems():
                #Get each model layer from the legend
                for name, layer3 in layerMap.iteritems():
                    if layer3.type() == QgsMapLayer.VectorLayer and layer3.name() == v:
                        model_layer=layer3
                #f iterates over the rows of the attributes table of a model layer
                for f in model_layer.getFeatures():
                    #Get the grid cell intersected by that specific head measurement point
                    if int(f['ROW'])==row_index and int(f['COL'])==col_index:
                        #Check if the thickness of the cell is null, higher or lower that 0.1 m
                        #and calculate corresponding elevations of top and bottom screens
                        if float(f['TOP'])-float(f['BOTTOM'])>0.1:
                            top_screen=float(f['TOP'])-0.1
                            bot_screen=float(f['BOTTOM'])+0.1
                        elif float(f['TOP'])==float(f['BOTTOM']):
                            #...then a warning appears
                            QMessageBox.warning(self, self.tr('Warning!'), self.tr('Point %s falls in a grid cell with null thickness!' %str(self.csv_content[self.name_field][i])))
                            #...and nothing is done
                            return
                        else:
                            top_screen=float(f['TOP'])-((float(f['TOP'])-float(f['BOTTOM']))/2.0)
                            bot_screen=(float(f['TOP'])+3*float(f['BOTTOM']))/4.0
                        #Update the value list with top_screen and bot_screen values for that model layer
                        value.append((top_screen,bot_screen))

            #Update the dict dictionary (the keys are the names of head measurement points)
            dict[str(self.csv_content[self.name_field][i])]=value

        top_elev={}
        #i iterates over the length of the list first_lay in the
        #csv_content dictionary (i.e., in the csv input file)
        for i in range(len(self.csv_content[self.first_lay])):
            #The dictionary dict is something like {'well_name1':[(X,Y),(row,col),(top_lay1,bot_lay1),(top_lay2,bot_lay2),(top_lay3,bot_lay3),...];'well_name2':[(X,Y),(row,col),(top_lay1,bot_lay1),(top_lay2,bot_lay2),(top_lay3,bot_lay3),...]}
            #To read the top elevation of the correct first_lay, the correct couple (top_lay*,bot_lay*) must be selected.
            #Whatever * is, the couples (X,Y) and (row,col) must be skipped, so the couple (top_lay*,bot_lay*) for layer 1 will be found in position 2,
            #the couple (top_lay*,bot_lay*) for layer 2 will be found in position 3, and so on. That's why index is increased by 1
            index=int(self.csv_content[self.first_lay][i])+1
            #top_elev is the top_screen calculated for a certain measurement point
            #(it is the first element of the third tuple (top_screen,bot_screen))
            top_elev[str(self.csv_content[self.name_field][i])]=dict[str(self.csv_content[self.name_field][i])][index][0]

        bot_elev={}
        #i iterates over the length of the list last_lay in the
        #csv_content dictionary (i.e., in the csv input file)
        for i in range(len(self.csv_content[self.last_lay])):
            #The dictionary dict is something like {'well_name1':[(X,Y),(row,col),(top_lay1,bot_lay1),(top_lay2,bot_lay2),(top_lay3,bot_lay3),...];'well_name2':[(X,Y),(row,col),(top_lay1,bot_lay1),(top_lay2,bot_lay2),(top_lay3,bot_lay3),...]}
            #To read the bot elevation of the correct first_lay, the correct couple (top_lay*,bot_lay*) must be selected.
            #Whatever * is, the couples (X,Y) and (row,col) must be skipped, so the couple (top_lay*,bot_lay*) for layer 1 will be found in position 2,
            #the couple (top_lay*,bot_lay*) for layer 2 will be found in position 3, and so on. That's why index is increased by 1
            index=int(self.csv_content[self.last_lay][i])+1
            #bot_elev is the bot_screen calculated for a certain measurement point
            #(it is the second element of the third tuple (top_screen,bot_screen))
            bot_elev[str(self.csv_content[self.name_field][i])]=dict[str(self.csv_content[self.name_field][i])][index][1]

        return top_elev, bot_elev


    #The getModelTimeDiscr function allows to get starting and ending dates and
    #times of each time step implemented in the model.
    #ATTENTION! One modeltable and one timetable must be loaded in the Legend!
    def getModelTimeDiscr(self):

        layerMap = QgsMapLayerRegistry.instance().mapLayers()
        #layer iterates over the layers in the Legend
        for name, layer in layerMap.iteritems():
            #If the name of the current layer is equal to 'modeltable_flowmodel'...
            if layer.type() == QgsMapLayer.VectorLayer and layer.name() == 'modeltable_'+str(self.cmbModelName.currentText()):
                #...then that layer is stored in the variable modeltable
                self.modeltable=layer
            #If the name of the current layer is equal to 'timetable_flowmodel'...
            elif layer.type() == QgsMapLayer.VectorLayer and layer.name() == 'timetable_'+str(self.cmbModelName.currentText()):
                #...then that layer is stored in the variable timetable
                self.timetable=layer

        #Get initial date and time of simulation

        #f iterates over the only row of the modeltable
        for f in self.modeltable.getFeatures():
            #Join start_model_date and start_model_time
            model_start = str(f['initial_date']) + ' ' + str(f['initial_time'])
            #Convert model_start from string to datetime format
            model_start=datetime.strptime(model_start,'%Y-%m-%d %H:%M:%S')

        #Get the length of the simulation

        simulation_length=0.0
        #f iterates over the rows of the timetable
        for f in self.timetable.getFeatures():
            simulation_length=simulation_length+float(f['length'])
        #Convert simulation_length in days if needed
        simulation_length=self.timeConversion(simulation_length)

        #Get final date and time of simulation

        model_end=model_start+timedelta(days=simulation_length)

        #Get lengths of each time step in each stress period

        #length_ts is an empty dictionary. It will be filled with the leghts of each time step.
        #Its keys will be strings like 'sp_ts'
        length_ts={}
        #lengths is a list which will be filled with the leghts of each
        #time step and it will be useful to fill the dictionary length_ts
        lengths=[]
        #f iterates over the rows of the timetable
        for f in self.timetable.getFeatures():
            #ts iterates over the number of time steps
            for ts in range(int(f['ts'])):
                #The list lengths is updated with 0.0
                lengths.append(0.0)

            #Calculate the length of the first time step

            #If there is only 1 time step and the multiplier is equal to 1...
            #(in such case, in the formula from MODFLOW user manual a null value would result in the denominator)
            if int(f['ts'])==1 and float(f['multiplier'])==1:
                #...the length of this time step is equal to the length of the whole stress period, expressed in days
                lengths[0]=self.timeConversion(float(f['length']))
                #lengths[0]=float(f['length'])
            #If there are more than 1 time steps and the multiplier is equal to 1...
            #(in such case, in the formula from MODFLOW user manual a null value would result in the denominator)
            elif int(f['ts'])>1 and float(f['multiplier'])==1:
                #...the length of this time step is equal to the length of the whole stress period divided by the
                #number of stress periods, expressed in days
                lengths[0]=self.timeConversion(float(f['length'])/int(f['ts']))
            #If the multiplier is not equal to 1...
            elif float(f['multiplier'])!=1:
                #...use the formula from MODFLOW user manual and express it in days
                lengths[0]=self.timeConversion(float(f['length'])*((float(f['multiplier'])-1)/((float(f['multiplier'])**int(f['ts']))-1)))

            #If there are more than 1 time steps...
            if int(f['ts'])>1:
                #i iterates over the length of the list lengths
                #(e.g., over the number of time steps), starting from 1
                for i in range(1,len(lengths)):
                    #The list lengths is updated with the length of the following time steps,
                    #expressed in days, using the formula from MODFLOW user manual
                    lengths[i]=self.timeConversion(lengths[i-1]*float(f['multiplier']))

            #index and l iterate over the elements of the list lengths
            #ATTENTION! index starts from 0!!!
            for index, l in enumerate(lengths):
                #The key of the dictionary length_ts is a string like 'sp_ts'
                length_ts[str(f['sp']) + '_' + str(index+1)]=l

            #The list lengths is initiated again
            lengths=[]

        #Get initial and final dates and times of each time step in each stress period

        #ts_dates is a dictionary which will be filled with initial and final dates
        #and times of each time step.
        #Its keys will be the same as the dictionary length_ts. At the beginning,
        #its values are empy lists made of two defaults date and time
        ts_dates={}
        for k in length_ts.keys():
            default=datetime.strptime('2000-01-01 00:00:00', '%Y-%m-%d %H:%M:%S')
            ts_dates[k]=[default,default]

        #Get an ordered list of keys of the dictionary ts_dates
        keys=[]
        #k and v iterate over the sorted dictionary ts_dates. Actually,
        #sorted(ts_dates.items()) is a list of tuples (key,value), sorted by key
        for k,v in sorted(ts_dates.items()):
            #The list keys is updated
            keys.append(k)

        #index and k iterate over the elements of the list keys
        for index, k in enumerate(keys):
            #Add initial and final dates and times of the first time step
            if k=='1_1':
                #Initial date and time is equal to starting date and time of the simulation
                ts_dates[k][0]=model_start
                #Final date and time is equal to the initial date and time plus the length of the time step
                ts_dates[k][1]=ts_dates[k][0]+timedelta(days=length_ts[k])
            #Add initial and final dates and times of the following time steps
            else:
                #Initial date and time is equal to the final date and time of the previous time step
                ts_dates[keys[index]][0]=ts_dates[keys[index-1]][1]
                #Final date and time is equal to the initial date and time plus the length of that time step
                ts_dates[keys[index]][1]=ts_dates[keys[index]][0]+timedelta(days=length_ts[keys[index]])

        return ts_dates, model_start, model_end


    #The timeConversion function allows to convert time variables in days.
    #ATTENTION! One modeltable must be loaded in the Legend!
    def timeConversion(self,to_be_converted):

        #f iterates over the only row of the modeltable
        for f in self.modeltable.getFeatures():
            #Conversion from seconds to days
            if str(f['time_unit'])=='sec':
                 return to_be_converted/86400
            #Conversion from minutes to days
            elif str(f['time_unit'])=='min':
                 return to_be_converted/1440
            #Conversion from hours to days
            elif str(f['time_unit'])=='hour':
                 return to_be_converted/24
            #Conversion from days to days not needed
            elif str(f['time_unit'])=='day':
                 return to_be_converted
            #If months, years or undefined are used as time units...
            else:
                #...then a warning appears
                QMessageBox.warning(self, self.tr('Warning!'), self.tr('Please use days as time units!'))
                #...and nothing is done
                #(conversion from month or year to days is not unique!)
                return
