"""
/***************************************************************************
Name	   :  climatological_analysis_controller.py
Description:  climatological analysis Controller for FEWSTools plugin,
              updated from QGIS2
copyright  :  (C) 2019-2023 by FEWS
email      :  minxuansun@contractor.usgs.gov
Created    :  02/07/2020 - Minxuan Sun
Modified   :  04/10/2020 - cholen - Updated to not clear map panel and fix
                                    problem with notify of loaded file.
              05/08/2020 - cholen - Update removal of open file
              07/09/2020 - cholen - Update color files, fix static analysis
                                    issues
              07/14/2020 - cholen - Adjust reclassify error
              08/11/2020 - cholen - Label fix, no load of averages, cleanup
              08/28/2020 - cholen - Notify on completion, cleanup to match
              patterns from other tools
              10/01/2020 - cholen - Don't load outputs if update ds averages
              10/08/2020 - cholen - Fix os sep on browse
              10/13/2020 - cholen - Change to display_map_layer function
              11/02/2020 - msun - Resample input raster based on region extents
                                  and pixels size before running the analysis
              11/04/2020 - cholen - Add check for missing files
              12/02/2020 - cholen - Remove trend ppt_int from map panel display
              12/03/2020 - cholen - Handle OSError
              01/05/2021 - cholen - No SPI option for non-PPT datasets, add
                                  seasonal averages for temperature datasets
              01/13/2021 - cholen - Handle regex issues
              03/08/2021 - cholen - Check mask vs region adjusted
              03/24/2021 - cholen - SPI require minimum PDF years, remove
                                    unnecessary class attributes
              01/13/2022 - cholen - New gdal utils, refactor.
              03/18/2022 - cholen - Updates for codebase consistency
              06/23/2022 - cholen - Fix path for region mask file

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

from PyQt5 import QtCore, QtGui
from PyQt5.QtWidgets import QMessageBox, QDialog, QFileDialog

from qgis.core import QgsMessageLog, Qgis
from qgis.utils import iface

from fews_tools.models.datasets_model import DatasetsModel
from fews_tools.models.region_model import RegionModel
from fews_tools.models.workspace_setup_model import WorkspaceSetupModel
from fews_tools.forms.Ui_ClimAnalysis import Ui_ClimAnalysis

from fews_tools import fews_tools_config as config
from fews_tools.utilities import geoclim_utilities as util
from fews_tools.utilities import geoclim_qgs_utilities as qgs_util
from fews_tools.utilities import logging_utilities as log_util
from fews_tools.utilities.help_utilities import view_manual
from fews_tools.workers.climatological_analysis_worker import ClimatologicalAnalysisWorker as wrkr


class ClimatologicalAnalysisController(QDialog):
    '''
    class for Climatological analysis
    '''

    def __init__(self):
        QDialog.__init__(self)
        self.is_processing = False  # Flag to indicate if running a process

        # set up UI and models
        self.datasets_info = DatasetsModel()
        self.wrksp_setup = WorkspaceSetupModel()
        self.region_info = RegionModel()
        self.ui = Ui_ClimAnalysis()
        self.ui.setupUi(self)
        # attributes used to set default values on controls
        self.min_r2 = 0.1
        self.percentile = 25
        self.min_frequency = 0
        self.max_frequency = 300
        # set up instance variables
        self.output_path = self.wrksp_setup.get_output_path()
        self.input_file_list = []  # files available in workspace dataset fldr
        self.ds_dic = {}
        self.available_year_list = []
        self.jul_2_jun = False
        self.reg_dic = {}
        self.ds_beg_per = None
        self.ds_end_per = None
        self.sel_beg_per = None
        self.sel_end_per = None
        self.analysis_method = ''
        self.seasons_total = False
        self.season_type = 'sum'
        self.dataset_average = False
        self.smart_trend = None
        self.selected_period_list = []
        self.selected_year_list = []
        self.selected_spi_year_list = []
        self.mid_point = 7
        self.good_files_list = []
        self.ppt_spi_list = []
        self.output_file_list = []
        self.missing_files_list = []
        self.thread = None
        self.worker = None
        self.closed = False  # A flag to indicate if close_event is triggered
        self.close_event = None
        self.is_consecutive_year = True

        # handle UI
        self.reg_str =\
            self.ui.regionComboBox.currentText().replace(' ', '')
        self.ui.progressBar.setValue(0)
        self.output_prefix = ''
        # set availability of UI
        self.ui.parameterLineEdit.setEnabled(False)
        # force user to set output path by folder explorer
        self.ui.outputLineEdit.setEnabled(False)
        self.ui.seasonalCheckBox.setChecked(False)
        self.ui.jul2JunCheckBox.setVisible(False)
        self.ui.updateCheckBox.setChecked(False)
        self.ui.periodButton.setCheckable(True)
        self.ui.periodButton.setText('Select All Periods')
        self.ui.yearButton.setCheckable(True)
        self.ui.outputLineEdit.setText(self.output_path)
        self.ui.lblSPIYears.setVisible(False)
        self.ui.yearSPIListWidget.setVisible(False)
        self.ui.yearSPIButton.setVisible(False)
        self.ui.yearSPIButton.setCheckable(True)
        self.ui.smartTrendCheckBox.setVisible(False)
        self.ui.lblMinR2.setVisible(False)
        self.ui.minR2SpinBox.setVisible(False)
        self.ui.minR2SpinBox.setValue(self.min_r2)
        self.ui.lblPercentile.setVisible(False)
        self.ui.percentileSpinBox.setVisible(False)
        self.ui.percentileSpinBox.setValue(self.percentile)
        self.ui.lblFreqMin.setVisible(False)
        self.ui.lblFreqMax.setVisible(False)
        self.ui.freqMinSpinBox.setVisible(False)
        self.ui.freqMaxSpinBox.setVisible(False)
        self.ui.freqMinSpinBox.setValue(self.min_frequency)
        self.ui.freqMaxSpinBox.setValue(self.max_frequency)
        self.ui.cancelButton.setEnabled(False)
        # close is handled by QDialog

        util.fill_dataset_combo(self.ui.datasetComboBox)
        util.fill_misc_widget_no_sort(self.ui.analysisComboBox,
                                      wrkr.CLIM_ANAL_METHODS)

        # make connection to method
        self.ui.datasetComboBox.currentIndexChanged.connect(
            self.dataset_selection_changed)
        self.ui.regionComboBox.currentIndexChanged['QString'].connect(
            self.region_selection_changed)
        self.ui.seasonalCheckBox.stateChanged.connect(
            self.update_season_totals)
        self.ui.jul2JunCheckBox.stateChanged.connect(self.update_intervals)
        self.ui.analysisComboBox.currentIndexChanged['QString'].connect(
            self.analysis_changed)
        self.ui.periodButton.clicked[bool].connect(self.select_all_periods)
        self.ui.yearButton.clicked[bool].connect(self.select_all_years)
        self.ui.yearSPIButton.clicked[bool].connect(
            self.select_all_spi_years)
        self.ui.smartTrendCheckBox.stateChanged.connect(
            self.update_smart_trend)
        self.ui.browseButton.clicked.connect(self.browse_output)
        self.ui.helpButton.clicked.connect(self.view_manual)
        self.ui.updateCheckBox.stateChanged.connect(
            self.update_average_changed)
        self.ui.processButton.clicked.connect(
            self.start_climatological_analysis)
        self.dataset_selection_changed()
        self.open_file_info = []

    def __check_loaded_map__(self, basename):
        '''
        Check a basename to see if it's in the map panel and notify
        if it is.  Will remove the specified map layer.
        Args:  basename(string) - Basename as a string
        '''
        for entry in self.open_file_info:
            if basename in entry[1]:
                qgs_util.notify_loaded_file(self, entry[1], entry[0])

    def __check_spi_gui_selections__(self):
        '''
        Check GUI selections for SPI
        '''
        err = False
        # added 3/25/2021 per Greg Husak
        # test if user has selected enough data
        if 20 < len(self.selected_year_list) < 30:
            reply = QMessageBox.question(
                self, 'Warning',
                ('It is recommended that you select at least 30 years ' +
                 'for the PDF calculation when running SPI.\n' +
                 'Do you want to continue?'),
                QMessageBox.Yes, QMessageBox.No)
            if reply == QMessageBox.No:
                err = True
        elif len(self.selected_year_list) < 20:
            # don't allow run with less than 20 years selected
            QMessageBox.critical(
                self, 'Error',
                ('You must select a minimum of 20 years of data ' +
                 'for the PDF calculation when running SPI.\nExiting!'),
                QMessageBox.Ok)
            err = True
        elif not self.seasons_total:
            QMessageBox.warning(
                self,
                'Error, Invalid parameter!!',
                'SPI analysis requires seasonal totals. Exiting.',
                QMessageBox.Ok)
            self.ui.seasonalCheckBox.setChecked(True)
            err = True
        elif self.ui.parameterLineEdit.text() != config.DATA_TYPES[0][0]:
            QMessageBox.warning(
                self,
                'Error, Invalid parameter!!',
                'SPI analysis requires Rainfall datasets. Exiting '
                'SPI analysis.',
                QMessageBox.Ok)
            err = True
        self.ui.progressBar.setValue(0)
        return err

    def dataset_selection_changed(self):
        '''
        called by changing dataset combo box index.
        '''
        self.datasets_info.query_named_dataset(
            self.ui.datasetComboBox.currentText())
        self.ds_dic = self.datasets_info.get_ds_dictionary()
        self.input_file_list = \
            util.get_input_file_list(self.ds_dic)
        self.mid_point = util.get_midpoint(self.ds_dic)
        if not self.input_file_list:
            # if no data files available, warn user.
            QMessageBox.warning(self,
                                'Data Missing!!',
                                'No data available for dataset!!',
                                QMessageBox.Ok)
            self.ui.periodListWidget.clear()
            self.ui.yearListWidget.clear()
            self.ui.yearSPIListWidget.clear()
            self.ui.regionComboBox.clear()
            self.ui.outputPrefixLineEdit.clear()
            return

        self.ds_beg_per = util.extract_data_period(
            self.ds_dic, self.input_file_list[0])
        self.ds_end_per = util.extract_data_period(
            self.ds_dic, self.input_file_list[-1])

        #  Fill region combo box
        util.fill_region_combo_for_dataset_selection(
            self.ui.regionComboBox, self.input_file_list[0])
        iface.messageBar().clearWidgets()

        # extents from dataset can be used to fill in region combo box
        self.ui.freqMinSpinBox.setValue(30)
        self.ui.freqMaxSpinBox.setValue(50)
        util.fill_ds_type_widget(self.ui.parameterLineEdit, self.ds_dic)
        self.ui.seasonalCheckBox.setText('Seasonal totals')
        if self.ds_dic['DATATYPE'] == config.DATA_TYPES[0][1]:
            self.ui.freqMinSpinBox.setValue(0)
            self.ui.freqMaxSpinBox.setValue(300)
            util.fill_misc_widget_no_sort(
                self.ui.analysisComboBox,
                wrkr.CLIM_ANAL_METHODS)
        elif self.ds_dic['DATATYPE'] == config.DATA_TYPES[4][1]:  # PET
            self.ui.freqMinSpinBox.setValue(0)
            self.ui.freqMaxSpinBox.setValue(100)
            util.fill_misc_widget_no_sort(
                self.ui.analysisComboBox,
                wrkr.CLIM_ANAL_METHODS[0:-1])  # no SPI
        else:  # temperature datasets
            self.ui.seasonalCheckBox.setText('Seasonal averages')
            util.fill_misc_widget_no_sort(
                self.ui.analysisComboBox,
                wrkr.CLIM_ANAL_METHODS[0:-1])  # no SPI

        self.available_year_list = \
            util.extract_data_years(self.ds_dic, self.input_file_list)
        self.update_intervals()
        if self.ds_dic["DATATYPE"].upper() in ["TAV", "TMN", "TMX"]:
            self.season_type = "avg"
        if self.ui.periodButton.isChecked():
            self.ui.periodButton.click()
        if self.ui.yearButton.isChecked():
            self.ui.yearButton.click()
        if self.ui.yearSPIButton.isChecked():
            self.ui.yearSPIButton.click()

    def region_selection_changed(self):
        '''
        Function to update region information
        '''
        self.reg_str = \
            self.ui.regionComboBox.currentText().replace(' ', '')
        self.output_prefix = \
            self.reg_str + '_' + \
            self.ds_dic['DATATYPE'].lower() + '_'
        self.ui.outputPrefixLineEdit.setText(self.output_prefix)
        self.region_info.query_named_region(
            self.ui.regionComboBox.currentText())
        self.reg_dic = self.region_info.get_region_dictionary()

    def check_region(self):
        '''
        Function to validate region information.
        :return: Boolean. False means invalid region selected.
        True means valid region selected. It's used like a flag to indicate
        if the region is able to be used.
        '''
        self.ui.progressBar.setValue(0)
        if not os.path.exists(self.reg_dic['Mask']):
            QMessageBox.warning(
                self,
                'Missing Mask File for region ' +
                self.ui.regionComboBox.currentText(),
                'Missing mask file: ' + self.reg_dic['Mask'],
                QMessageBox.Ok)
            return False

        mask_extent, _, _, _ = \
            qgs_util.extract_raster_file_params(self.reg_dic['Mask'])

        # this should have been done when region was created, but just in
        # case someone has messed with the mask file afterwards
        mask_ok = qgs_util.check_dataset_vs_region_extents(
            self, mask_extent,
            qgs_util.get_region_extent(self.reg_dic))

        if not mask_ok:
            return False
        return True

    def update_season_totals(self):
        '''
        Function to adjust widgets per the Seasonal totals checkbox.
        '''
        if self.ui.seasonalCheckBox.isChecked():
            self.ui.jul2JunCheckBox.setVisible(True)
            self.ui.updateCheckBox.setVisible(False)
        else:
            self.ui.jul2JunCheckBox.setChecked(False)
            self.ui.jul2JunCheckBox.setVisible(False)
            if self.ui.analysisComboBox.currentText() == "Average":
                self.ui.updateCheckBox.setVisible(True)
        self.ui.progressBar.setValue(0)

    def update_intervals(self):
        '''
        Update year and period widget depends on the periodicity, the available
        input list and if jul to jun box is checked
        '''
        self.jul_2_jun = self.ui.jul2JunCheckBox.isChecked()
        util.fill_year_widget(self.ui.yearListWidget, self.jul_2_jun,
                              self.available_year_list,
                              self.ds_beg_per, self.ds_end_per,
                              self.mid_point)
        util.fill_year_widget(self.ui.yearSPIListWidget, self.jul_2_jun,
                              self.available_year_list,
                              self.ds_beg_per, self.ds_end_per,
                              self.mid_point)
        util.fill_period_widget(
            self.ui.periodListWidget, self.jul_2_jun, self.ds_dic['PERIODICITY'])
        self.ui.progressBar.setValue(0)

    def analysis_changed(self, value_string: str) -> None:
        '''
        Function to adjust window dependent on the analysis combo box choice.
        Args: value_string: The current contents of the combo box.
        '''
        self.ui.lblSPIYears.setVisible(False)
        self.ui.yearSPIListWidget.setVisible(False)
        self.ui.yearSPIButton.setVisible(False)
        self.ui.updateCheckBox.setChecked(False)
        self.ui.updateCheckBox.setVisible(False)
        self.ui.seasonalCheckBox.setVisible(True)
        self.ui.smartTrendCheckBox.setVisible(False)
        self.ui.lblMinR2.setVisible(False)
        self.ui.minR2SpinBox.setVisible(False)
        self.ui.lblPercentile.setVisible(False)
        self.ui.percentileSpinBox.setVisible(False)
        self.ui.lblFreqMin.setVisible(False)
        self.ui.freqMinSpinBox.setVisible(False)
        self.ui.lblFreqMax.setVisible(False)
        self.ui.freqMaxSpinBox.setVisible(False)
        self.ui.lblYears.setText('Select Years to Analyze')
        if value_string == wrkr.CLIM_ANAL_METHODS[0]:  # Average
            self.ui.updateCheckBox.setVisible(True)
        elif value_string == wrkr.CLIM_ANAL_METHODS[5]:  # Trend
            self.ui.smartTrendCheckBox.setVisible(True)
            if self.ui.smartTrendCheckBox.isChecked():
                self.ui.lblMinR2.setVisible(True)
                self.ui.minR2SpinBox.setVisible(True)
        elif value_string == wrkr.CLIM_ANAL_METHODS[6]:  # Pctl
            self.ui.lblPercentile.setVisible(True)
            self.ui.percentileSpinBox.setVisible(True)
        elif value_string == wrkr.CLIM_ANAL_METHODS[7]:  # Freq
            self.ui.seasonalCheckBox.setChecked(True)
            self.ui.lblFreqMin.setVisible(True)
            self.ui.freqMinSpinBox.setVisible(True)
            self.ui.lblFreqMax.setVisible(True)
            self.ui.freqMaxSpinBox.setVisible(True)
        elif value_string == wrkr.CLIM_ANAL_METHODS[8]:  # SPI
            self.ui.lblYears.setText('Select Years for PDF')
            self.ui.seasonalCheckBox.setChecked(True)
            self.ui.jul2JunCheckBox.setVisible(True)
            self.ui.lblSPIYears.setVisible(True)
            self.ui.yearSPIListWidget.setVisible(True)
            self.ui.yearSPIButton.setVisible(True)
        self.ui.progressBar.setValue(0)

    def select_all_periods(self, pressed: bool) -> None:
        '''
        Function to select/deselect all available periods
        in the GUI.
        Args: pressed: State of the period push button.
        '''
        source = self.sender()
        if pressed:
            source.setText("Clear All Periods")
            self.ui.periodListWidget.selectAll()
        else:
            source.setText("Select All Periods")
            self.ui.periodListWidget.clearSelection()
        self.ui.progressBar.setValue(0)

    def select_all_years(self, pressed):
        '''
        Function to select/deselect all available years in the GUI.
        Args: pressed(boolean) - State of years push button.
        '''
        source = self.sender()
        if pressed:
            source.setText("Clear All Years")
            self.ui.yearListWidget.selectAll()
        else:
            source.setText("Select All Years")
            self.ui.yearListWidget.clearSelection()
        self.ui.progressBar.setValue(0)

    def select_all_spi_years(self, pressed):
        '''
        Function to select/deselect all SPI years in the GUI.
        Args: pressed(boolean) - State of SPI years push button.
        '''
        source = self.sender()
        if pressed:
            source.setText("Clear All SPI Years")
            self.ui.yearSPIListWidget.selectAll()
        else:
            source.setText("Select All SPI Years")
            self.ui.yearSPIListWidget.clearSelection()
        self.ui.progressBar.setValue(0)

    def update_smart_trend(self):
        '''
        Function to adjust widgets per the Smart trends checkbox.
        '''
        if self.ui.smartTrendCheckBox.isChecked():
            self.ui.lblMinR2.setVisible(True)
            self.ui.minR2SpinBox.setVisible(True)
        else:
            self.ui.lblMinR2.setVisible(False)
            self.ui.minR2SpinBox.setVisible(False)
        self.ui.progressBar.setValue(0)

    def browse_output(self):
        '''
        Function to browse to output directory and set line edit control.
        '''
        dir_name = \
            QFileDialog.getExistingDirectory(
                None,
                "Select Output Directory",
                self.wrksp_setup.get_output_path())
        if dir_name:
            self.ui.outputLineEdit.setText(
                self.wrksp_setup.fix_os_sep_in_path(dir_name))

    def view_manual(self):
        '''
        Function to browse to output directory and set line edit control.
        '''
        clim_manual_webpage_section = "chapter-4-climatological-analysis"
        view_manual(clim_manual_webpage_section)

    def update_average_changed(self):
        '''
        Function to adjust widgets per the update dataset averages checkbox.
        '''
        if self.ui.updateCheckBox.isChecked():
            self.ui.seasonalCheckBox.setChecked(False)
            self.ui.jul2JunCheckBox.setChecked(False)
            self.ui.seasonalCheckBox.setVisible(False)
            self.ui.jul2JunCheckBox.setVisible(False)
        else:
            self.ui.seasonalCheckBox.setVisible(True)

    def get_selected_years(self):
        '''
        Function to get the selected years from the GUI.
        '''
        self.selected_year_list = []
        selected_items = self.ui.yearListWidget.selectedItems()
        for item in selected_items:
            self.selected_year_list.append(item.text()[:4])
        self.selected_year_list = sorted(self.selected_year_list)

    def get_selected_spi_years(self):
        '''
        Function to get the selected SPI years from the GUI.
        '''
        self.selected_spi_year_list = []
        selected_items = self.ui.yearSPIListWidget.selectedItems()
        for item in selected_items:
            self.selected_spi_year_list.append(item.text()[:4])
        self.selected_spi_year_list = sorted(self.selected_spi_year_list)

    def get_seasonal_output_names(self, prefix, yr_string):
        '''
        Function to get seasonal output names(not spi).
        Arguments:
            prefix
            year_string
        Returns:
            file_list(list(string)) -  List of output files.
        '''
        file_list = []
        if self.jul_2_jun is True:
            _, self.sel_beg_per, self.sel_end_per =\
                util.get_input_info_for_cross_year_seasonals(
                    self.ds_dic, self.selected_year_list[0],
                    self.selected_period_list)
        else:
            _, self.sel_beg_per, self.sel_end_per =\
                util.get_input_info_for_seasonals(
                    self.ds_dic, self.selected_year_list[0],
                    self.selected_period_list)
        if len(self.selected_period_list) == 1:
            # only one period selected
            output_base = prefix + self.sel_beg_per + yr_string
        else:
            # multiple periods selected
            output_base = (prefix + self.sel_beg_per +
                           '_to_' + self.sel_end_per + yr_string)
        output_file_name = os.path.join(
            self.output_path, output_base + self.ds_dic['DATASUFFIX'])
        file_list.append(output_file_name)
        if self.analysis_method == wrkr.CLIM_ANAL_METHODS[5]:  # Trend
            intcp_file_name = output_file_name.replace("slp", "int")
            r2_file_name = output_file_name.replace("slp", "r2")
            file_list.append(intcp_file_name)
            file_list.append(r2_file_name)
        elif self.analysis_method == wrkr.CLIM_ANAL_METHODS[7]:  # Frequency
            count_file_name = os.path.splitext(output_file_name)[0] + '_count' + self.ds_dic['DATASUFFIX']
            file_list.append(count_file_name)
            file_list[0] = os.path.splitext(output_file_name)[0] + '_percent' + self.ds_dic['DATASUFFIX']
        return file_list

    def get_spi_seasonal_output_names(self, output_analysis_prefix):
        '''
        Function to get seasonal output names for SPI, these are different
        from the other seasonal file names.
        Arguments:
            output_analysis_prefix - the output file prefix
        Returns:
            file_list(list(string)) -  List of spi output files.
        '''
        if self.jul_2_jun is True:
            _, self.sel_beg_per, self.sel_end_per =\
                util.get_input_info_for_cross_year_seasonals(
                    self.ds_dic, self.selected_spi_year_list[0],
                    self.selected_period_list)
        else:
            _, self.sel_beg_per, self.sel_end_per =\
                util.get_input_info_for_seasonals(
                    self.ds_dic, self.selected_spi_year_list[0],
                    self.selected_period_list)
        temp_list = util.get_seasonal_file_names(
            self.ds_dic, self.reg_str, self.selected_spi_year_list,
            [self.sel_beg_per, self.sel_end_per], self.output_path)
        file_list = []
        for entry in temp_list:
            file_list.append(
                entry.replace(
                    self.reg_str + '_' + self.ds_dic["DATATYPE"].lower() + self.season_type, output_analysis_prefix))
        return file_list

    def start_climatological_analysis(self):
        '''
        Function to run the analysis.
        '''
        if not self.check_region():
            return

        # Remove temp files before running to make sure output area is clean
        util.remove_temp_files(self.output_path)

        self.ui.progressBar.setValue(0)
        self.open_file_info = qgs_util.get_open_files_info()

        # collect all the info from GUI controls
        # update outputPath and create if necessary
        # update output path and create it if necessary
        self.output_path = self.ui.outputLineEdit.text()
        if not os.path.exists(self.output_path):
            try:
                os.makedirs(self.output_path)
            except OSError as err:
                QMessageBox.warning(
                    self,
                    "Can't create the output folder",
                    str(err),
                    QMessageBox.Ok)
                return
        self.analysis_method = self.ui.analysisComboBox.currentText()
        self.seasons_total = self.ui.seasonalCheckBox.isChecked()
        self.jul_2_jun = self.ui.jul2JunCheckBox.isChecked()
        self.dataset_average = self.ui.updateCheckBox.isChecked()
        self.smart_trend = self.ui.smartTrendCheckBox.isChecked()
        self.min_r2 = self.ui.minR2SpinBox.value()
        self.percentile = self.ui.percentileSpinBox.value()
        self.min_frequency = self.ui.freqMinSpinBox.value()
        self.max_frequency = self.ui.freqMaxSpinBox.value()
        # verify that time selections are ok
        self.selected_period_list = util.convert_gui_periods_to_file_periods(
            self.ui.periodListWidget, self.ds_dic, sort=False)

        self.get_selected_years()
        if not self.selected_period_list:
            QMessageBox.warning(self,
                                "No Periods Selected",
                                "Please select at least one period.",
                                QMessageBox.Ok)
            return

        if self.analysis_method == wrkr.CLIM_ANAL_METHODS[8]:
            err = self.__check_spi_gui_selections__()
            if err:
                return
            self.get_selected_spi_years()  # if SPI is the analysis method
        else:
            self.selected_spi_year_list = []  # not using SPI, empty the list
            # warning - bad selection
            if len(self.selected_year_list) < 2:
                QMessageBox.warning(self,
                                    "Not enough Years Selected",
                                    "Please select at least two years.",
                                    QMessageBox.Ok)
                return

        err = util.check_line_edit(self.ui.outputPrefixLineEdit)
        if err is True:
            QMessageBox.warning(
                self,
                "Invalid Output Prefix - " + self.ui.outputPrefixLineEdit.text(),
                'Should only contains alpha, digit, underscore and '
                'hyphen symbol',
                QMessageBox.Ok)
            return

        iface.mainWindow().statusBar().showMessage(
            'Climatological Analysis running...')
        period_list = self.selected_period_list
        # for dataset averages we need all periods
        if self.dataset_average:
            period_list = util.get_all_period_string(self.ds_dic)
        # this will have the cross years together if necessary
        temp_spi_year_list =\
            [item.text()
             for item in self.ui.yearSPIListWidget.selectedItems()]
        # this will have the cross years together if necessary
        temp_year_list =\
            [item.text()
             for item in self.ui.yearListWidget.selectedItems()]

        (_, self.good_files_list, missing_files_list1) =\
            util.check_non_consecutive_process_inputs(
                self.ds_dic, temp_year_list,
                period_list, self.jul_2_jun)

        (_, self.ppt_spi_list, missing_files_list2) =\
            util.check_non_consecutive_process_inputs(
                self.ds_dic, temp_spi_year_list,
                period_list, self.jul_2_jun)

        missing_files_list =\
            sorted(set(missing_files_list1 + missing_files_list2))

        if missing_files_list:
            self.ui.progressBar.setValue(0)
            QMessageBox.information(self,
                                    "Missing files!!",
                                    str(missing_files_list),
                                    QMessageBox.Ok)
            QgsMessageLog.logMessage("Missing files!!",
                                     level=Qgis.Info)
            return

        self.output_prefix = self.ui.outputPrefixLineEdit.text()

        # logic for different analysis method
        output_analysis_prefix = ''
        output_analysis_prefix_int = ''
        output_analysis_prefix_r2 = ''
        if self.analysis_method == wrkr.CLIM_ANAL_METHODS[0]:
            # Average
            output_analysis_prefix = self.output_prefix + 'avg'
        elif self.analysis_method == wrkr.CLIM_ANAL_METHODS[1]:
            # Median
            output_analysis_prefix = self.output_prefix + 'med'
        elif self.analysis_method == wrkr.CLIM_ANAL_METHODS[2]:
            # standard deviation
            output_analysis_prefix = self.output_prefix + 'stddev'
        elif self.analysis_method == wrkr.CLIM_ANAL_METHODS[3]:
            # count
            output_analysis_prefix = self.output_prefix + 'count'
        elif self.analysis_method == wrkr.CLIM_ANAL_METHODS[4]:
            # coefficient of variation
            output_analysis_prefix = self.output_prefix + 'cv'
        elif self.analysis_method == wrkr.CLIM_ANAL_METHODS[7]:
            # Frequency
            if not self.seasons_total:
                QMessageBox.warning(
                    self,
                    'Error, Invalid parameter!!',
                    'Frequency analysis requires seasonal totals. Exiting',
                    QMessageBox.Ok)
                self.ui.progressBar.setValue(0)
                self.ui.seasonalCheckBox.setChecked(True)
                return
            if not self.is_consecutive_year:
                QMessageBox.warning(self,
                                    'Error, Need consecutive years!',
                                    'Consecutive years required',
                                    QMessageBox.Ok)
                self.ui.progressBar.setValue(0)
                return
            output_analysis_prefix = (
                self.output_prefix + 'freq' + str(self.min_frequency) + '-' +
                str(self.max_frequency))
        elif self.analysis_method == wrkr.CLIM_ANAL_METHODS[6]:
            # Percentiles
            output_analysis_prefix = self.output_prefix + \
                str(self.percentile) + '_pctl'
        elif self.analysis_method == wrkr.CLIM_ANAL_METHODS[5]:
            # Trend
            if self.smart_trend:
                self.min_r2 *= 100
            output_analysis_prefix = self.output_prefix + 'slp'
            output_analysis_prefix_int = self.output_prefix + 'int'
            output_analysis_prefix_r2 = self.output_prefix + 'r2'
        elif self.analysis_method == wrkr.CLIM_ANAL_METHODS[8]:
            output_analysis_prefix = self.output_prefix + 'SPI_'
        # check for consecutive years
        self.is_consecutive_year = util.is_consecutive(self.selected_year_list)
        year_string = self.build_year_string()

        if not self.seasons_total:
            self.output_file_list = []

            # Seasonal total is unchecked
            for interval in self.selected_period_list:
                interval_string = \
                    util.get_key_from_period_value(self.ds_dic, interval)
                output_base = \
                    output_analysis_prefix + interval_string + '_' + year_string
                output_file_name = os.path.join(
                    self.output_path,
                    output_base + self.ds_dic['DATASUFFIX'])
                self.output_file_list.append(output_file_name)
                self.__check_loaded_map__(output_base)

                if self.analysis_method == wrkr.CLIM_ANAL_METHODS[5]:
                    # Trend has two extra files
                    output_base = \
                        output_analysis_prefix_int + interval_string + '_' + year_string
                    output_file_name = \
                        os.path.join(self.output_path,
                                     output_base + self.ds_dic['DATASUFFIX'])
                    self.output_file_list.append(output_file_name)
                    self.__check_loaded_map__(output_base)

                    output_base = \
                        output_analysis_prefix_r2 + interval_string + '_' + year_string
                    output_file_name = \
                        os.path.join(self.output_path,
                                     output_base + self.ds_dic['DATASUFFIX'])
                    self.output_file_list.append(output_file_name)
                    self.__check_loaded_map__(output_base)
        else:
            if self.analysis_method != wrkr.CLIM_ANAL_METHODS[8]:
                self.output_file_list = self.get_seasonal_output_names(
                    output_analysis_prefix, year_string)
            else:  # for SPI
                self.output_file_list = self.get_spi_seasonal_output_names(output_analysis_prefix)

        for entry in self.output_file_list:
            output_base = os.path.splitext(os.path.basename(entry))[0]
            self.__check_loaded_map__(output_base)
        # verify parameters before setting up the thread
        if (not self.ds_dic or not self.reg_dic or
                not self.analysis_method or not self.selected_period_list or
                not self.selected_year_list or not self.good_files_list or
                not self.output_file_list):
            QMessageBox.critical(
                self,
                'Error, Missing parameters!!',
                'Missing parameters, cannot run Climatological Analysis. '
                'Exiting.',
                QMessageBox.Ok)
            return

        self.worker = wrkr(
            self.ds_dic,
            self.reg_dic,
            self.output_path,
            self.analysis_method,
            self.seasons_total,
            self.season_type,
            self.jul_2_jun,
            self.dataset_average,
            self.smart_trend,
            self.min_r2,
            self.percentile,
            self.min_frequency,
            self.max_frequency,
            self.selected_period_list,
            self.selected_year_list,
            self.selected_spi_year_list,
            self.good_files_list,
            self.output_file_list)

        # lock UI
        self.ui.cancelButton.clicked.connect(
            self.worker.kill)
        self.ui.cancelButton.setEnabled(True)
        self.ui.periodButton.setEnabled(False)
        self.ui.yearButton.setEnabled(False)
        self.ui.outputPrefixLineEdit.setEnabled(False)
        self.ui.processButton.setEnabled(False)
        self.ui.closeButton.setEnabled(False)
        self.ui.regionComboBox.setEnabled(False)
        self.ui.analysisComboBox.setEnabled(False)
        self.ui.seasonalCheckBox.setEnabled(False)
        self.ui.updateCheckBox.setEnabled(False)
        self.ui.browseButton.setEnabled(False)
        self.ui.periodListWidget.setEnabled(False)
        self.ui.yearListWidget.setEnabled(False)
        self.ui.datasetComboBox.setEnabled(False)
        self.ui.jul2JunCheckBox.setEnabled(False)
        self.ui.yearSPIListWidget.setEnabled(False)
        self.ui.yearSPIButton.setEnabled(False)
        self.ui.smartTrendCheckBox.setEnabled(False)
        self.ui.minR2SpinBox.setEnabled(False)
        self.ui.percentileSpinBox.setEnabled(False)
        self.ui.freqMinSpinBox.setEnabled(False)
        self.ui.freqMaxSpinBox.setEnabled(False)

        # start the process in a new thread
        iface.messageBar().clearWidgets()
        self.thread = QtCore.QThread(self)
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.run)
        self.thread.start()
        self.worker.finished.connect(self.finish_clim_analysis)
        self.worker.error.connect(self.clim_analysis_error)
        self.worker.progress.connect(self.ui.progressBar.setValue)
        self.is_processing = True

    def finish_clim_analysis(self, return_value):
        '''
        Clean up the climatological analysis worker object and thread.
         return_value - Returned code and string from
                                climatological analysis worker
        '''
        self.is_processing = False
        self.thread.quit()
        self.thread.wait()
        if self.closed:
            self.thread_worker_cleanup()
            self.close_event.accept()
            QDialog.closeEvent(self, self.close_event)
            return
        if self.worker.killed:
            self.ui.progressBar.setValue(0)
            msg = (u"Climatological analysis aborted by user")
            iface.messageBar().pushCritical("Climatological analysis", msg)
            QgsMessageLog.logMessage(msg, level=Qgis.Critical)
            iface.mainWindow().statusBar().showMessage(msg, 3000)
        elif return_value is not None:
            # set up color file
            color_dir = self.wrksp_setup.get_colors_path()
            color_file = ''
            color_file_r2 = ''
            color_file_frequency_count = ''
            if self.analysis_method in wrkr.CLIM_ANAL_METHODS[0:2]:
                # If Average or Median
                color_file = os.path.join(
                    color_dir, config.RF_2500_RASTER_COLOR_FILE)
            elif self.analysis_method in wrkr.CLIM_ANAL_METHODS[2]:
                color_file = os.path.join(
                    color_dir, config.RF_600_RASTER_COLOR_FILE)  # If Std Dev
            elif self.analysis_method in wrkr.CLIM_ANAL_METHODS[3]:
                color_file = os.path.join(
                    color_dir, config.COUNT_COLOR_FILE)  # If Count
            elif self.analysis_method == wrkr.CLIM_ANAL_METHODS[4]:
                color_file = os.path.join(
                    color_dir, config.RF_CV_COLOR_FILE)  # Coefficient of Variation
            elif self.analysis_method == wrkr.CLIM_ANAL_METHODS[5]:
                # Trend
                if self.ds_dic['DATATYPE'].lower() in [
                        config.DATA_TYPES[0][1].lower(),
                        config.DATA_TYPES[4][1].lower()]:
                    # If dataset type if ppt or pet
                    color_file = os.path.join(
                        color_dir, config.RF_TREND_COLOR_FILE)
                else:
                    color_file = os.path.join(
                        color_dir, config.RF_TREND_COLOR_FILE)  # still need value
                color_file_r2 = os.path.join(
                    color_dir, config.RF_TREND_COLOR_FILE)
            elif self.analysis_method == wrkr.CLIM_ANAL_METHODS[6]:
                color_file = os.path.join(
                    color_dir, config.RF_600_RASTER_COLOR_FILE)  # Percentiles
            elif self.analysis_method == wrkr.CLIM_ANAL_METHODS[7]:
                color_file = os.path.join(
                    color_dir, config.RF_FREQUENCY_COLOR_FILE)  # Frequency
                color_file_frequency_count = os.path.join(
                    color_dir, config.RF_FREQUENCY_COUNT_COLOR_FILE)
            elif self.analysis_method == wrkr.CLIM_ANAL_METHODS[8]:
                color_file = os.path.join(
                    color_dir, config.SPI_COLOR_FILE)  # SPI
            # don't load outputs if we are updating ds averages
            if self.analysis_method == wrkr.CLIM_ANAL_METHODS[0] and\
                    self.dataset_average:
                pass
            elif self.analysis_method not in [wrkr.CLIM_ANAL_METHODS[5], wrkr.CLIM_ANAL_METHODS[7]]:
                # if NOT Trend and Frequency
                for entry in self.output_file_list:
                    qgs_util.display_raster_layer(entry, color_file)
            else:
                if self.analysis_method == wrkr.CLIM_ANAL_METHODS[5]:
                    # if Trend, int is not displayed per Diego
                    for entry in self.output_file_list:
                        if '_slp' in entry:
                            qgs_util.display_raster_layer(entry, color_file)
                        elif '_r2' in entry:
                            qgs_util.display_raster_layer(entry, color_file_r2)
                elif self.analysis_method == wrkr.CLIM_ANAL_METHODS[7]:
                    # if Frequency
                    qgs_util.display_raster_layer(self.output_file_list[0], color_file)
                    qgs_util.display_raster_layer(self.output_file_list[1], color_file_frequency_count)
            open_file_names = [sublist[1] for sublist in self.open_file_info]
            qgs_util.display_map_layer(self.reg_dic, open_file_names)
            util.remove_temp_files(self.output_path)
            log_util.add_log("Climatological Analysis",
                             log_util.log_string("Dataset",
                                                 [self.ds_dic['DATASETNAME']]) +
                             log_util.log_string("Region",
                                                 [self.reg_dic['RegionName']]) +
                             log_util.log_string("Analysis Method",
                                                 [self.analysis_method]) +
                             log_util.log_string("Seasonal Totals",
                                                 [self.seasons_total]) +
                             log_util.log_string("July to June",
                                                 [self.jul_2_jun]) +
                             log_util.log_string("Selected Periods",
                                                 self.selected_period_list) +
                             log_util.log_string("Selected Years",
                                                 self.selected_year_list) +
                             log_util.log_string("Selected SPI Years",
                                                 self.selected_spi_year_list) +
                             log_util.log_string("Smart Trend",
                                                 [self.smart_trend]) +
                             log_util.log_string("Percentile",
                                                 [self.percentile]) +
                             log_util.log_string("Smart Trend",
                                                 [self.smart_trend]) +
                             log_util.log_string("Min Frequency",
                                                 [self.min_frequency]) +
                             log_util.log_string("Max Frequency",
                                                 [self.max_frequency]),
                             self.output_file_list)
            self.ui.progressBar.setValue(100)
            msg = 'Climatological Analysis Completed'
            QgsMessageLog.logMessage(msg, level=Qgis.Info)
            iface.mainWindow().statusBar().showMessage(msg, 3000)
        else:
            # notify of an error
            msg = (u'Error: Climatological Analysis did not finish')
            iface.messageBar().pushCritical('Climatological Analysis', msg)
            QgsMessageLog.logMessage(msg, level=Qgis.Critical)
            iface.mainWindow().statusBar().showMessage(msg, 3000)

        # Unlock UI
        self.ui.cancelButton.setEnabled(False)
        self.ui.periodButton.setEnabled(True)
        self.ui.yearButton.setEnabled(True)
        self.ui.outputPrefixLineEdit.setEnabled(True)
        self.ui.processButton.setEnabled(True)
        self.ui.closeButton.setEnabled(True)
        self.ui.regionComboBox.setEnabled(True)
        self.ui.analysisComboBox.setEnabled(True)
        self.ui.seasonalCheckBox.setEnabled(True)
        self.ui.updateCheckBox.setEnabled(True)
        self.ui.browseButton.setEnabled(True)
        self.ui.periodListWidget.setEnabled(True)
        self.ui.yearListWidget.setEnabled(True)
        self.ui.datasetComboBox.setEnabled(True)
        self.ui.jul2JunCheckBox.setEnabled(True)
        self.ui.yearSPIListWidget.setEnabled(True)
        self.ui.yearSPIButton.setEnabled(True)
        self.ui.smartTrendCheckBox.setEnabled(True)
        self.ui.minR2SpinBox.setEnabled(True)
        self.ui.percentileSpinBox.setEnabled(True)
        self.ui.freqMinSpinBox.setEnabled(True)
        self.ui.freqMaxSpinBox.setEnabled(True)
        self.thread_worker_cleanup()
        all_done = util.notify_process_completed('Climatological Analysis')
        if all_done:
            self.ui.closeButton.click()

    def clim_analysis_error(self, err, ex_str):
        '''
        Function to log any errors from the object and thread.
        Args:  err - not used
               ex_str string: Exception info
        '''
        self.is_processing = False
        QgsMessageLog.logMessage(ex_str, level=Qgis.Critical)
        QMessageBox.critical(self, 'Error!', ex_str, QMessageBox.Ok)
        iface.mainWindow().statusBar().showMessage(ex_str)

    def closeEvent(self, a_0: QtGui.QCloseEvent) -> None:
        '''
        Close event used to handle a case where user closes the form
        while the process is running.
        '''
        self.closed = True
        if self.worker:
            self.worker.kill()
        self.close_event = a_0
        if self.is_processing:
            self.close_event.ignore()
        else:
            self.close_event.accept()

    def thread_worker_cleanup(self):
        """
        Cleanup thread if it exists.
        """
        if self.thread is not None:
            self.thread.deleteLater()

    def build_year_string(self):
        """
        Build the year string for output files

        Returns
        -------
        year_string : TYPE
            DESCRIPTION.

        """
        if self.is_consecutive_year:
            year_string = \
                self.selected_year_list[0] + 'to' + self.selected_year_list[-1]
        else:
            year_string = self.selected_year_list[0]
            for year in self.selected_year_list[1:]:
                year_string += '_' + year
        return year_string
