'''
/***************************************************************************
Name	   :  composites_controller.py
Description:  Composites Controller for FEWSTools plugin, updated from QGIS2
copyright  :  (C) 2019-2023 by FEWS
email      :  minxuansun@contractor.usgs.gov
Created    :  12/31/2019 - CHOLEN
Modified   :  02/20/2020 - cholen - Updated dataset_selection_changed
              04/02/2020 - cholen - Fixed bug in dataset selction change to get
                                    correct baseline years
              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
              05/14/2020 - cholen - Update to fix sort on baseline years list
              06/29/2020 - cholen - Warn on switched baseline years
              07/09/2020 - cholen - Update color files, fix static analysis
                                    issues
              07/16/2020 - cholen - Log on errors
              08/24/2020 - cholen - Bugfix for cross years check, baseline year
              08/28/2020 - cholen - Notify on completion, general cleanup to
                                    match patterns in other tools
              10/08/2020 - cholen - Fix os sep on browse
              10/13/2020 - cholen - Change to display_map_layer function
              10/23/2020 - cholen - Tweak map layer load and reg_dic name
              11/02/2020 - cholen - Adjust check for missing files
              12/03/2020 - cholen - Handle OSError
              12/14/2020 - cholen - Output filenames fixed
              02/02/2021 - cholen - Keep composite years when changing analysis
              03/08/2021 - cholen - Check mask vs region adjusted
              08/30/2021 - cholen - Add __get_years_for_std_anom_seas_sums__
              01/06/2022 - cholen - Use new utilties, SCA cleanup.
              02/24/2022 - cholen - Codebase consistency changes
              06/23/2022 - cholen - Fix path for region mask and map files
              07/27/2022 - cholen - Bugfix in check for open outputs
              01/03/2023 - dhackman - Output Prefix Changes
              08/23/2023 - aeben - Load and save parameters to txt 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, QgsProject, Qgis
from qgis.utils import iface

from fews_tools.workers.composites_worker import CompositesWorker
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_Composites import Ui_Composites

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


class CompositesController(QDialog):
    '''
    Class for creating composite products
    '''

    COMPOSITES_ANALYSIS_LIST = [u'Average',
                                u'Percent of Average',
                                u'Anomaly',
                                u'Standardized Anomaly']

    def __init__(self):
        QDialog.__init__(self)
        self.is_processing = False  # Flag to indicate if running a process
        self.datasets_info = DatasetsModel()
        self.wrksp_setup = WorkspaceSetupModel()
        self.region_info = RegionModel()
        self.ui = Ui_Composites()
        self.ui.setupUi(self)
        self.current_progress = 0
        self.available_years_list = []  # overall
        self.main_yr_control_years_list = []  # for main year control
        self.std_anom_std_years_list = []
        self.beg_per = ''
        self.end_per = ''
        self.baseline_years_list = []
        self.comp1_years_list = []
        self.comp2_years_list = []
        self.input_files_list = []
        self.selected_periods_list = []
        self.ds_available_regions_list = []
        self.jul_2_jun = False
        self.jj_idx = 0
        self.baseline_start_year = ''
        self.baseline_end_year = ''
        self.comp1_dst_file = ''
        self.comp2_dst_file = ''
        self.baseline_avg = ''
        self.all_years_std = ''
        self.dst_filename = ''
        self.comp_diff_dst_file = ''
        self.ds_dic = None
        self.reg_dic = None
        self.mid_point = 7
        self.col_offset = 0
        self.row_offset = 0
        self.available_files_list = []
        self.missing_files_list = []
        self.thread = None
        self.composites_wrkr = None
        self.closed = False  # A flag to indicate if close_event is triggered
        self.close_event = None
        self.set_dic = {
            'dataset' : '',
            'region' : '',
            'analysis' : '',
            'july_to_june' : False,
            'periods' : [],
            'comp_1_years' : [],
            'comp_2_years' : [],
            'baseline_from' : None,
            'baseline_to' : None,
            'output_prefix' : '',
            'output_folder' : '',
            'settings_file_path' : ''
        }

        #  default widget loading
        util.fill_dataset_combo(self.ui.datasetComboBox)

        # make connections to methods
        self.ui.datasetComboBox.currentIndexChanged.connect(
            self.dataset_selection_changed)
        self.ui.jul2JunCheckBox.clicked.connect(self.update_periods)
        self.ui.analysisComboBox.currentIndexChanged['QString'].connect(
            self.analysis_changed)
        self.ui.allPeriodsButton.clicked[bool].connect(self.select_all_periods)
        self.ui.allYearsButton.clicked[bool].connect(self.select_all_years)
        self.ui.comp1AddButton.clicked.connect(self.add_comp1)
        self.ui.comp1DeleteButton.clicked.connect(self.delete_comp1)
        self.ui.comp1ClearButton.clicked.connect(self.clear_comp1)
        self.ui.comp2AddButton.clicked.connect(self.add_comp2)
        self.ui.comp2DeleteButton.clicked.connect(self.delete_comp2)
        self.ui.comp2ClearButton.clicked.connect(self.clear_comp2)
        self.ui.browseButton.clicked.connect(self.browse_output)
        self.ui.processButton.clicked.connect(self.start_composites)
        self.ui.loadButton.clicked.connect(self.load_param_file)
        self.ui.saveButton.clicked.connect(self.save_param_file)
        self.ui.blFromComboBox.currentIndexChanged['QString'].connect(
            self.baseline_selection_changed)
        self.ui.blToComboBox.currentIndexChanged['QString'].connect(
            self.baseline_selection_changed)
        self.ui.parameterLineEdit.setEnabled(False)
        self.ui.regionComboBox.currentIndexChanged.connect(
            self.setup_output_prefix)
        # initialize to workspace output, user can change
        self.output_path = self.wrksp_setup.get_output_path()
        self.ui.outputLineEdit.setText(self.output_path)
        self.ui.helpButton.clicked.connect(self.view_manual)

        self.region_no_space = self.ui.regionComboBox.currentText().replace(
            ' ', '')
        # don't use util function here, we don't want the sort
        self.ui.analysisComboBox.clear()
        for entry in self.COMPOSITES_ANALYSIS_LIST:
            self.ui.analysisComboBox.addItem(entry)
        self.ui.analysisComboBox.setCurrentIndex(0)
        self.analysis = self.ui.analysisComboBox.currentText()

        self.ui.allPeriodsButton.setCheckable(True)
        self.ui.allYearsButton.setCheckable(True)
        self.ui.cancelButton.setEnabled(False)
        self.ui.outputLineEdit.setEnabled(False)
        self.ui.settingsLineEdit.setEnabled(False)
        self.ui.outputPrefixLineEdit.setEnabled(True)

        self.ui.progressBar.setValue(0)
        QgsProject.instance().setCrs(qgs_util.CRS)

        self.dataset_selection_changed()
        self.open_file_info = None

    def __add_comp__(self, comp):
        '''
        Function to add years to a composite selection widget.
        params(integer) - comp - Composite widget to adjust.
        '''
        if comp == 1:
            comp_list = self.comp1_years_list
            comp_widget = self.ui.comp1ListWidget
        else:
            comp_list = self.comp2_years_list
            comp_widget = self.ui.comp2ListWidget

        for item in self.ui.yearListWidget.selectedItems():
            txt = item.text()
            # handle available years list
            if txt in self.main_yr_control_years_list:
                self.main_yr_control_years_list.remove(txt)
            # handle comp list
            if txt not in comp_list:
                comp_list.append(txt)
        # Update the controls
        util.fill_misc_widget(
            comp_widget, comp_list, sort_reverse=True)
        util.fill_misc_widget(
            self.ui.yearListWidget, self.main_yr_control_years_list,
            sort_reverse=True)

        if self.ui.allYearsButton.isChecked():
            self.ui.allYearsButton.click()
        self.ui.progressBar.setValue(0)

    def __check_input_files__(self):
        '''
        Checks input files. Identifies needed files from gui widgets as
        either available or missing.
        '''
        all_years_list = sorted(set(self.comp1_years_list +
                                    self.comp2_years_list +
                                    self.baseline_years_list))

        (_,
         self.available_files_list,
         self.missing_files_list) =\
            util.check_non_consecutive_process_inputs(
                self.ds_dic, all_years_list,
                self.selected_periods_list, self.jul_2_jun)

    def __get_years_for_std_anom_seas_sums__(self):
        '''
        Checks input files. Identifies needed files for getting all available
        seasonal sums. Will exclude years that don't have the needed periods
        '''
        self.std_anom_std_years_list = sorted(
            set(self.comp1_years_list +
                self.comp2_years_list +
                self.main_yr_control_years_list))
        remove_years = []
        for entry in self.std_anom_std_years_list:
            (_, _, missing_l) =\
                util.check_non_consecutive_process_inputs(
                    self.ds_dic, [entry],
                    self.selected_periods_list, self.jul_2_jun)
            if missing_l:
                remove_years.append(entry)
        if self.std_anom_std_years_list[0] in remove_years:
            remove_years.remove(self.std_anom_std_years_list[0])
            self.std_anom_std_years_list = self.std_anom_std_years_list[1:]

        if self.std_anom_std_years_list[-1] in remove_years:
            remove_years.remove(self.std_anom_std_years_list[-1])
            self.std_anom_std_years_list = self.std_anom_std_years_list[:-1]
        if remove_years:  # if there are other remove years, error per Diego
            QgsMessageLog.logMessage(u"ERROR: Missing needed data for:  "
                                     + str(remove_years),
                                     level=Qgis.Critical)
            raise IOError(u"ERROR: Missing needed data for:  "
                          + str(remove_years))

    def __clear_comp__(self, comp):
        '''
        Function to remove all items in a composite selection widget.
        params(integer) - comp - Composite widget to adjust.
        '''
        if comp == 1:
            comp_list = self.comp1_years_list
            comp_widget = self.ui.comp1ListWidget
        else:
            comp_list = self.comp2_years_list
            comp_widget = self.ui.comp2ListWidget
        for i in range(comp_widget.count()):
            txt = comp_widget.item(i).text()
            # handle available years
            if txt not in self.main_yr_control_years_list:
                self.main_yr_control_years_list.append(txt)
            # handle comp years list
            if txt in comp_list:
                comp_list.remove(txt)
        # Update the controls
        util.fill_misc_widget(
            comp_widget, comp_list, sort_reverse=True)
        util.fill_misc_widget(
            self.ui.yearListWidget, self.main_yr_control_years_list,
            sort_reverse=True)

        if self.ui.allYearsButton.isChecked():
            self.ui.allYearsButton.click()
        self.ui.progressBar.setValue(0)

    def __delete_comp__(self, comp):
        '''
        Function to remove years to a composite selection widget.
        params(integer) - comp - Composite widget to adjust.
        '''
        if comp == 1:
            comp_list = self.comp1_years_list
            comp_widget = self.ui.comp1ListWidget
        else:
            comp_list = self.comp2_years_list
            comp_widget = self.ui.comp2ListWidget

        for item in comp_widget.selectedItems():
            txt = item.text()
            # handle available years list
            if txt not in self.main_yr_control_years_list:
                self.main_yr_control_years_list.append(txt)
            # handle comp years list
            if txt in comp_list:
                comp_list.remove(txt)
        # Update the controls
        util.fill_misc_widget(
            comp_widget, comp_list, sort_reverse=True)
        util.fill_misc_widget(
            self.ui.yearListWidget, self.main_yr_control_years_list,
            sort_reverse=True)

        if self.ui.allYearsButton.isChecked():
            self.ui.allYearsButton.click()
        self.ui.progressBar.setValue(0)

    def add_comp1(self):
        '''
        Add selected years to comp1 widget.
        '''
        self.__add_comp__(1)

    def add_comp2(self):
        '''
        Add selected years to comp2 widget.
        '''
        self.__add_comp__(2)

    def analysis_changed(self):
        '''
        Function to adjust window dependent on the analysis combo box choice.
        '''
        self.analysis = self.ui.analysisComboBox.currentText()
        if self.analysis == self.COMPOSITES_ANALYSIS_LIST[0]:  # 'Average':
            self.ui.comp2ClearButton.click()
            self.ui.lblComp2.setVisible(False)
            self.ui.comp2ListWidget.setVisible(False)
            self.ui.comp2AddButton.setVisible(False)
            self.ui.comp2DeleteButton.setVisible(False)
            self.ui.comp2ClearButton.setVisible(False)
            self.ui.baseLineGroupBox.setVisible(False)
        else:
            self.ui.lblComp2.setVisible(True)
            self.ui.comp2ListWidget.setVisible(True)
            self.ui.comp2AddButton.setVisible(True)
            self.ui.comp2DeleteButton.setVisible(True)
            self.ui.comp2ClearButton.setVisible(True)
            self.ui.baseLineGroupBox.setVisible(True)
        self.ui.progressBar.setValue(0)

    def baseline_selection_changed(self):
        '''
        Function to reset progress bar if baseline selections change.
        params(boolean) - pressed - State of years push button.
        '''
        self.ui.progressBar.setValue(0)

    def browse_output(self):
        '''
        Browse to output directory and set line edit control.
        '''
        dir_name =\
            QFileDialog.getExistingDirectory(None,
                                             u'Select Output Directory',
                                             self.output_path)
        if dir_name:
            self.ui.outputLineEdit.setText(
                self.wrksp_setup.fix_os_sep_in_path(dir_name))
        self.ui.progressBar.setValue(0)

    def clear_comp1(self):
        '''
        Clear years in comp2 widget.
        '''
        self.__clear_comp__(1)

    def clear_comp2(self):
        '''
        Clear years in comp2 widget.
        '''
        self.__clear_comp__(2)

    def dataset_selection_changed(self):
        '''
        Function to update database object and dependent gui widgets.
        '''
        err = self.datasets_info.query_named_dataset(
            self.ui.datasetComboBox.currentText())
        if err:
            raise IOError('Database read failed')

        self.ds_dic = self.datasets_info.get_ds_dictionary()
        util.fill_ds_type_widget(self.ui.parameterLineEdit, self.ds_dic)
        self.input_files_list = util.get_input_file_list(self.ds_dic)

        if self.input_files_list == []:
            QMessageBox.information(self,
                                    u'Data missing!!',
                                    u'No data available for dataset!!',
                                    QMessageBox.Ok)
            self.ui.yearListWidget.clear()
            self.ui.blFromComboBox.clear()
            self.ui.blToComboBox.clear()
            self.ui.regionComboBox.clear()
            return
        util.fill_region_combo_for_dataset_selection(
            self.ui.regionComboBox, self.input_files_list[0])
        self.beg_per = util.extract_data_period(
            self.ds_dic, self.input_files_list[0])
        self.end_per = util.extract_data_period(
            self.ds_dic, self.input_files_list[-1])
        self.available_years_list =\
            util.extract_data_years(self.ds_dic, self.input_files_list)
        self.main_yr_control_years_list = util.extract_data_years(
            self.ds_dic, self.input_files_list)
        # must have the right available files before calling update_periods
        self.update_periods()
        if self.ui.allPeriodsButton.isChecked():
            self.ui.allPeriodsButton.click()
        if self.ui.allYearsButton.isChecked():
            self.ui.allYearsButton.click()
        self.ui.comp1ListWidget.clear()
        self.ui.comp2ListWidget.clear()
        self.ui.progressBar.setValue(0)

        self.mid_point = util.get_midpoint(self.ds_dic)

    def delete_comp1(self):
        '''
        Remove years in comp1 widget.
        '''
        self.__delete_comp__(1)

    def delete_comp2(self):
        '''
        Remove years in comp2 widget.
        '''
        self.__delete_comp__(2)

    def finish_composites(self, calc_result):
        '''
        Clean up the composites worker object and thread.
        params(list) - calc_result - Returned code and string from
                                 composites analysis object
        '''
        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.composites_wrkr.killed:
            self.ui.progressBar.setValue(0)
            msg = (u"Composites aborted by user")
            iface.messageBar().pushCritical("Composites", msg)
            QgsMessageLog.logMessage(msg, level=Qgis.Critical)
            iface.mainWindow().statusBar().showMessage(msg, 3000)
        elif calc_result is not None:
            # setup color files
            if self.analysis == self.COMPOSITES_ANALYSIS_LIST[0]:
                color_file = config.RF_2500_RASTER_COLOR_FILE
            elif self.analysis == self.COMPOSITES_ANALYSIS_LIST[1]:
                color_file = config.RF_ANOM_PCT_COLOR_FILE
            elif self.analysis == self.COMPOSITES_ANALYSIS_LIST[2]:
                color_file = config.RF_ANOM_DIFF_COLOR_FILE
            else:
                color_file = config.RF_ANOM_PCT_COLOR_FILE
            color_dir = self.wrksp_setup.get_colors_path()
            color_path = os.path.join(color_dir, color_file)
            # must empty the map layers before creating jpg
            # save loaded layers out to a temp project file
            open_file_names = [sublist[1] for sublist in self.open_file_info]
            qgs_util.map_panel_backup()

            # get the jpg created
            map_file = self.reg_dic['Map'].split(',')[0]
            input_file_list = [self.dst_filename, '', map_file]
            qgs_util.write_image(input_file_list, color_dir, color_file)
            iface.messageBar().clearWidgets()
            QgsProject.instance().removeAllMapLayers()

            # put map panel back the way it was
            qgs_util.map_panel_restore()
            # add new results to the map panel
            qgs_util.display_raster_layer(self.dst_filename, color_path)
            qgs_util.display_map_layer(self.reg_dic, open_file_names)
            util.remove_temp_files(self.output_path)
            log_util.add_log("Composites",
                             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]) +
                             log_util.log_string("July to June",
                                                 [self.jul_2_jun]) +
                             log_util.log_string("Selected Periods",
                                                 self.selected_periods_list) +
                             log_util.log_string("Composite 1 Years",
                                                 self.comp1_years_list) +
                             log_util.log_string("Composite 2 Years",
                                                 self.comp2_years_list) +
                             log_util.log_string("Baseline Years",
                                                 self.baseline_years_list) +
                             log_util.log_string("STD_Anom Years",
                                                 self.std_anom_std_years_list),
                             [self.comp1_dst_file,
                              self.comp2_dst_file,
                              self.baseline_avg,
                              self.comp_diff_dst_file])
            self.ui.progressBar.setValue(100)

            msg = u'Composites Analysis Completed'
            QgsMessageLog.logMessage(msg, level=Qgis.Info)
            iface.mainWindow().statusBar().showMessage(msg, 3000)
        else:
            # notify of an error
            msg = (u'Error: Composites analysis did not finish')
            iface.messageBar().pushCritical('Composites', msg)
            QgsMessageLog.logMessage(msg, level=Qgis.Critical)
            iface.mainWindow().statusBar().showMessage(msg, 3000)

        # disable cancel button and re-enable all other GUI widgets
        self.ui.cancelButton.setEnabled(False)
        self.ui.allPeriodsButton.setEnabled(True)
        self.ui.allYearsButton.setEnabled(True)
        self.ui.processButton.setEnabled(True)
        self.ui.closeButton.setEnabled(True)
        self.ui.regionComboBox.setEnabled(True)
        self.ui.outputPrefixLineEdit.setEnabled(True)

        self.ui.analysisComboBox.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.comp1ListWidget.setEnabled(True)
        self.ui.comp1ClearButton.setEnabled(True)
        self.ui.comp1AddButton.setEnabled(True)
        self.ui.comp1DeleteButton.setEnabled(True)
        if self.analysis != self.COMPOSITES_ANALYSIS_LIST[0]:
            self.ui.comp2DeleteButton.setEnabled(True)
            self.ui.comp2ListWidget.setEnabled(True)
            self.ui.comp2AddButton.setEnabled(True)
            self.ui.comp2ClearButton.setEnabled(True)
            self.ui.blToComboBox.setEnabled(True)
            self.ui.blFromComboBox.setEnabled(True)
        self.thread_worker_cleanup()
        all_done = util.notify_process_completed('Composites')
        if all_done:
            self.ui.closeButton.click()

    def region_selection_changed(self) -> bool:
        '''
        Function to update region object.
        :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)
        # when you clear the control it counts as a selection change
        # so if nothing is in the text, do nothing
        if not self.ui.regionComboBox.currentText() == '':
            err = self.region_info.query_named_region(
                self.ui.regionComboBox.currentText())
            if err:
                raise IOError('Database read failed')

            self.reg_dic = self.region_info.get_region_dictionary()
            self.col_offset, self.row_offset = qgs_util.get_offsets(
                self.input_files_list[0], self.reg_dic)
            self.region_no_space =\
                self.ui.regionComboBox.currentText().replace(' ', '')

            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)
                if self.ui.regionComboBox.count() == 1:
                    self.ui.regionComboBox.clear()
                else:
                    self.ui.regionComboBox.setCurrentIndex(0)
                return False

            msk_extent, _, _, _ =\
                qgs_util.extract_raster_file_params(self.reg_dic['Mask'])
            iface.messageBar().clearWidgets()
            if msk_extent:
                # 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, msk_extent, qgs_util.get_region_extent(self.reg_dic))

                if not mask_ok:
                    return False
        return True

    def select_all_periods(self, pressed):
        '''
        Function to select/deselect all available periods in the GUI.
        params(boolean) - pressed - State of the periods push button.
        '''
        source = self.sender()
        if pressed:
            source.setText(u'Clear All Periods')
            self.ui.periodListWidget.selectAll()
        else:
            source.setText(u'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.
        params(boolean) - pressed - State of years push button.
        '''
        source = self.sender()
        if pressed:
            source.setText(u'Clear All Years')
            self.ui.yearListWidget.selectAll()
        else:
            source.setText(u'Select All Years')
            self.ui.yearListWidget.clearSelection()
        self.ui.progressBar.setValue(0)

    def setup_output_prefix(self):
        '''
        Function to set the values for the output prefix as needed
        '''
        region_info = RegionModel()
        if self.ui.regionComboBox.currentText() != "":
            region_info.query_named_region(
                self.ui.regionComboBox.currentText())
            self.reg_dic = region_info.get_region_dictionary()
            region_text = self.ui.regionComboBox.currentText().replace(' ', '')
            param = self.ui.parameterLineEdit.text().replace(' ', '')
            prefix_string = region_text + '_' + param
            self.ui.outputPrefixLineEdit.setText(prefix_string)

    def view_manual(self):
        '''
        Function to browse to output directory and set line edit control.
        '''
        clim_manual_webpage_section = "chapter-6-climate-composites"
        view_manual(clim_manual_webpage_section)

    def setup_output_files(self):
        '''
        Setup output file names. Checks to see if any are in the map panel.
        '''
        self.comp1_dst_file = ''
        self.comp2_dst_file = ''
        self.baseline_avg = ''
        self.all_years_std = ''
        self.dst_filename = ''
        self.comp_diff_dst_file = ''
        prefix = self.ui.outputPrefixLineEdit.text()
        if self.analysis == self.COMPOSITES_ANALYSIS_LIST[0]:
            stat_prefix = prefix + '_avg_'
        elif self.analysis == self.COMPOSITES_ANALYSIS_LIST[1]:
            stat_prefix = prefix + '_pctofavg_'
        elif self.analysis == self.COMPOSITES_ANALYSIS_LIST[2]:
            stat_prefix = prefix + '_anomaly_'
        else:  # self.analysis == self.COMPOSITES_ANALYSIS_LIST[3]:
            stat_prefix = prefix + '_stdanom_'
            self.all_years_std = os.path.join(
                self.output_path,
                prefix + '_stdev_allyears' + self.ds_dic['DATASUFFIX'])
        self.comp1_dst_file = os.path.join(
            self.output_path,
            prefix + '_avg_comp1' + self.ds_dic['DATASUFFIX'])

        if self.analysis == self.COMPOSITES_ANALYSIS_LIST[0]:
            self.dst_filename = self.comp1_dst_file
        else:
            self.comp2_dst_file = os.path.join(
                self.output_path,
                prefix + '_avg_comp2' + self.ds_dic['DATASUFFIX'])
            self.baseline_avg = os.path.join(
                self.output_path,
                prefix + '_average_baseline' + self.ds_dic['DATASUFFIX'])
            if self.comp2_years_list:
                self.comp_diff_dst_file = os.path.join(
                    self.output_path,
                    stat_prefix + 'comp1_comp2' + self.ds_dic['DATASUFFIX'])
            else:
                self.comp_diff_dst_file = os.path.join(
                    self.output_path,
                    stat_prefix + 'comp1' + self.ds_dic['DATASUFFIX'])
            self.dst_filename = self.comp_diff_dst_file
        self.dst_filename = self.wrksp_setup.fix_os_sep_in_path(
            self.dst_filename)
        # any of these might be open in the  map panel, close them if so
        output_file_list = [self.comp1_dst_file,
                            self.comp2_dst_file,
                            self.baseline_avg,
                            self.comp_diff_dst_file]
        for dst_fl in output_file_list:
            out_string = os.path.basename(dst_fl)[:-4]
            for entry in self.open_file_info:
                if entry[1] in out_string:
                    qgs_util.notify_loaded_file(self, entry[1], entry[0])

    def start_composites(self):
        '''
        Run the analysis.
        '''
        if not self.region_selection_changed():
            return

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

        self.selected_periods_list = util.convert_gui_periods_to_file_periods(
            self.ui.periodListWidget, self.ds_dic)
        # verify that time selections are ok
        if not self.selected_periods_list:
            QMessageBox.information(self,
                                    u'No Periods Selected',
                                    u'Select at least one period.',
                                    QMessageBox.Ok)
            return

        if not self.comp1_years_list:
            QMessageBox.information(self,
                                    u'No Composite 1 Years Selected',
                                    u'Select at least one year.',
                                    QMessageBox.Ok)
            return

        # recheck output_path and create if necessary
        self.output_path = self.ui.outputLineEdit.text()
        if not os.path.exists(self.output_path):
            os.makedirs(self.output_path)
        self.baseline_years_list = []
        if self.analysis != self.COMPOSITES_ANALYSIS_LIST[0]:
            all_baseline_years =\
                sorted([self.ui.blFromComboBox.itemText(i)
                        for i in range(self.ui.blFromComboBox.count())])
            self.baseline_start_year = self.ui.blFromComboBox.currentText()
            self.baseline_end_year = self.ui.blToComboBox.currentText()
            if int(self.baseline_end_year[:4]) <\
                    int(self.baseline_start_year[:4]):
                QMessageBox.information(
                    self,
                    u'Baseline "From" year is later than "To" year',
                    u'Baseline "From" year is later than "To" year\n' +
                    u'Please correct the baseline years',
                    QMessageBox.Ok)
                return
            for entry in all_baseline_years:
                if entry == self.baseline_start_year:
                    beg_index = all_baseline_years.index(entry)
                if entry == self.baseline_end_year:
                    end_index = all_baseline_years.index(entry)
            self.baseline_years_list =\
                all_baseline_years[beg_index: (end_index + 1)]
        else:
            self.baseline_start_year = ''
            self.baseline_end_year = ''

        self.ui.progressBar.setValue(0)

        msg = u'Composites Analysis running...'
        iface.mainWindow().statusBar().showMessage(msg)

        self.__check_input_files__()
        if self.missing_files_list:
            self.ui.progressBar.setValue(0)
            QMessageBox.information(self,
                                    u'Missing files!!',
                                    str(self.missing_files_list),
                                    QMessageBox.Ok)
            QgsMessageLog.logMessage(u'Missing files!!',
                                     level=Qgis.Info)
            return
        if self.analysis == self.COMPOSITES_ANALYSIS_LIST[3]:
            self.__get_years_for_std_anom_seas_sums__()
        # check if outputs are in map panels
        self.open_file_info = qgs_util.get_open_files_info()
        # validate output prefix, only allow alpha, digits '_' or '-'
        if len(self.ui.outputPrefixLineEdit.text()) > 25:
            QMessageBox.warning(
                self,
                "Output Prefix too long",
                'The length of the output prefix should be less than 25 '
                'characters',
                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
        self.setup_output_files()

        self.current_progress = 5
        self.ui.progressBar.setValue(self.current_progress)
        if (self.analysis == '' or
                self.selected_periods_list == [] or
                self.comp1_years_list == []):
            QMessageBox.critical(self,
                                 u'Error, Missing parameters!!',
                                 u'Missing parameters, cannot run ' +
                                 'Composites Analysis. Exiting.',
                                 QMessageBox.Ok)
            return

        self.composites_wrkr = CompositesWorker(
            self.ds_dic, self.reg_dic, self.output_path,
            self.analysis, self.jul_2_jun,
            self.selected_periods_list, self.comp1_years_list,
            self.comp2_years_list, self.baseline_years_list,
            self.std_anom_std_years_list,
            self.dst_filename, self.comp1_dst_file,
            self.comp2_dst_file, self.comp_diff_dst_file,
            self.baseline_avg, self.all_years_std,
            self.COMPOSITES_ANALYSIS_LIST)

        self.ui.cancelButton.clicked.connect(self.composites_wrkr.kill)
        # enable the cancel button and disable all other gui widgets
        self.ui.cancelButton.setEnabled(True)
        self.ui.allPeriodsButton.setEnabled(False)
        self.ui.allYearsButton.setEnabled(False)
        self.ui.processButton.setEnabled(False)
        self.ui.closeButton.setEnabled(False)
        self.ui.regionComboBox.setEnabled(False)
        self.ui.analysisComboBox.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.comp1ListWidget.setEnabled(False)
        self.ui.comp1ClearButton.setEnabled(False)
        self.ui.comp1AddButton.setEnabled(False)
        self.ui.comp1DeleteButton.setEnabled(False)
        self.ui.outputPrefixLineEdit.setEnabled(False)
        if self.analysis != self.COMPOSITES_ANALYSIS_LIST[0]:
            self.ui.comp2DeleteButton.setEnabled(False)
            self.ui.comp2ListWidget.setEnabled(False)
            self.ui.comp2AddButton.setEnabled(False)
            self.ui.comp2ClearButton.setEnabled(False)
            self.ui.blToComboBox.setEnabled(False)
            self.ui.blFromComboBox.setEnabled(False)

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

    def update_periods(self):
        '''
        Function to adjust widgets per the July to June checkbox.
        '''
        self.jul_2_jun = self.ui.jul2JunCheckBox.isChecked()
        self.ui.comp1ClearButton.click()
        self.ui.comp2ClearButton.click()
        # handle years
        self.main_yr_control_years_list = util.fill_year_widget(
            self.ui.yearListWidget, self.jul_2_jun, self.available_years_list,
            self.beg_per, self.end_per, self.mid_point)
        util.fill_year_widget(
            self.ui.blFromComboBox, self.jul_2_jun, self.available_years_list,
            self.beg_per, self.end_per, self.mid_point)
        util.fill_year_widget(
            self.ui.blToComboBox, self.jul_2_jun, self.available_years_list,
            self.beg_per, self.end_per, self.mid_point)
        self.ui.blFromComboBox.setCurrentIndex(
            self.ui.blFromComboBox.count() - 1)
        self.ui.blToComboBox.setCurrentIndex(0)
        # handle periods
        util.fill_period_widget(
            self.ui.periodListWidget, self.jul_2_jun, self.ds_dic['PERIODICITY'])
        if self.ui.allPeriodsButton.isChecked():
            self.ui.allPeriodsButton.click()
        if self.ui.allYearsButton.isChecked():
            self.ui.allYearsButton.click()
        self.ui.progressBar.setValue(0)

    def composites_error(self, err, ex_str):
        '''
        Function to log any errors from the climatological analysis
            object and thread.
        params(int) - err  - Exception.
        params(string) - ex_string - Exception string.
        '''
        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.composites_wrkr:
            self.composites_wrkr.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 load_param_file(self):
        '''
        Load text file with previously saved params
        '''
        # get file
        file_name = QFileDialog.getOpenFileName(
            self,
            u'Select Filename',
            self.set_dic["settings_file_path"],
            '*.txt')

        if file_name[0]:
            # clear gui and set_dic
            self.ui.comp1ClearButton.click()
            self.ui.comp2ClearButton.click()
            self.ui.periodListWidget.clearSelection()
            self.set_dic['periods'] = []
            self.set_dic['comp_1_years'] = []
            self.set_dic['comp_2_years'] = []

            self.set_dic['settings_file_path'] = file_name[0]
            self.ui.settingsLineEdit.setText(file_name[0])

            # fill set_dic with params from file
            if self.wrksp_setup.fix_os_sep_in_path(file_name[0]):
                with open(file_name[0], 'r') as file:
                    for row in file:
                        row = row.strip()
                        if 'Dataset:' in row:
                            row = row.replace('Dataset: ', '')
                            self.set_dic['dataset'] = row
                        if 'Region:' in row:
                            row = row.replace('Region: ', '')
                            self.set_dic['region'] = row
                        elif 'Analysis Type:' in row:
                            row = row.replace('Analysis Type: ', '')
                            self.set_dic['analysis'] = row
                        elif 'July to June:' in row:
                            if 'True' in row:
                                self.set_dic['july_to_june'] = True
                            else:
                                self.set_dic['july_to_june'] = False
                        elif 'Periods:' in row:
                            row = row.replace('Periods: ', '')
                            if '[]' not in row: # do not process if list is empty
                                row = row.replace('\'', '')
                                row = row.replace('[', '')
                                row = row.replace(']', '')
                                for s in row.split(', '):
                                    self.set_dic['periods'].append(s)
                        elif 'Comp 1 years:' in row:
                            row = row.replace('Comp 1 years: ', '')
                            if '[]' not in row: # do not process if list is empty
                                row = row.replace('\'', '')
                                row = row.replace('[', '')
                                row = row.replace(']', '')
                                for s in row.split(', '):
                                    self.set_dic['comp_1_years'].append(s)
                        elif 'Comp 2 years:' in row:
                            row = row.replace('Comp 2 years: ', '')
                            if '[]' not in row: # do not process if list is empty
                                row = row.replace('\'', '')
                                row = row.replace('[', '')
                                row = row.replace(']', '')
                                for s in row.split(', '):
                                    self.set_dic['comp_2_years'].append(s)
                        elif 'Baseline (from year):' in row:
                            row = row.replace('Baseline (from year): ', '')
                            self.set_dic['baseline_from'] = row
                        elif 'Baseline (to year):' in row:
                            row = row.replace('Baseline (to year): ', '')
                            self.set_dic['baseline_to'] = row
                        elif 'Output Prefix:' in row:
                            row = row.replace('Output Prefix: ', '')
                            self.set_dic['output_prefix'] = row
                        elif 'Output Folder:' in row:
                            row = row.replace('Output Folder: ', '')
                            self.set_dic['output_folder'] = row

            # add params to ui
            self.ui.datasetComboBox.setCurrentText(self.set_dic['dataset'])
            self.ui.regionComboBox.setCurrentText(self.set_dic['region'])
            self.ui.analysisComboBox.setCurrentText(self.set_dic['analysis'])
            if (self.set_dic['july_to_june'] != self.ui.jul2JunCheckBox.isChecked()):
                self.ui.jul2JunCheckBox.setChecked(self.set_dic['july_to_june'])
                self.update_periods()
            if self.set_dic['periods']: # do not process if list is empty
                periods_list_regex = '|'.join(self.set_dic['periods'])
                periods_item_list = self.ui.periodListWidget.findItems(
                                    periods_list_regex, QtCore.Qt.MatchRegularExpression)
                for n in periods_item_list:
                    n.setSelected(True)
            if self.set_dic['comp_1_years']: # do not process if list is empty
                comp1_list_regex = '|'.join(self.set_dic['comp_1_years'])
                comp1_item_list = self.ui.yearListWidget.findItems(
                                comp1_list_regex, QtCore.Qt.MatchRegularExpression)
                for n in comp1_item_list:
                    n.setSelected(True)
                self.add_comp1()
            if self.set_dic['analysis'] != 'Average':
                if self.set_dic['comp_2_years']: # do not process if list is empty
                    comp2_list_regex = '|'.join(self.set_dic['comp_2_years'])
                    comp2_item_list = self.ui.yearListWidget.findItems(comp2_list_regex, QtCore.Qt.MatchRegularExpression)
                    for n in comp2_item_list:
                        n.setSelected(True)
                    self.add_comp2()
                self.ui.blFromComboBox.setCurrentText(self.set_dic['baseline_from'])
                self.ui.blToComboBox.setCurrentText(self.set_dic['baseline_to'])
            self.ui.outputPrefixLineEdit.setText(self.set_dic['output_prefix'])
            self.ui.outputLineEdit.setText(self.set_dic['output_folder'])

    def save_param_file(self):
        '''
        Save params to text file
        '''
        # fill set_dic with current params
        self.set_dic['dataset'] = self.ds_dic['DATASETNAME']
        self.set_dic['region'] = self.ui.regionComboBox.currentText()
        self.set_dic['analysis'] = self.analysis
        self.set_dic['july_to_june'] = self.ui.jul2JunCheckBox.isChecked()
        self.set_dic['periods'] = []
        for item in self.ui.periodListWidget.selectedItems():
            self.set_dic['periods'].append(item.text())
        self.set_dic['comp_1_years'] = self.comp1_years_list
        if self.set_dic['analysis'] != 'Average':
            self.set_dic['comp_2_years'] = self.comp2_years_list
            self.set_dic['baseline_from'] = self.ui.blFromComboBox.currentText()
            self.set_dic['baseline_to'] = self.ui.blToComboBox.currentText()
        self.set_dic['output_prefix'] = self.ui.outputPrefixLineEdit.text()
        self.set_dic['output_folder'] = self.ui.outputLineEdit.text()

        # get save file name
        save_file_name = QFileDialog.getSaveFileName(
            None,
            u'Select Filename',
            self.set_dic['output_folder'],
            '*.txt')

        if save_file_name[0]:
            self.set_dic['settings_file_path'] = save_file_name[0]
            self.ui.settingsLineEdit.setText(save_file_name[0])
            # write params to file
            with open(save_file_name[0], 'w') as set_file:
                set_file.write('Dataset: ' +
                            self.set_dic['dataset'] + config.CR)
                set_file.write('Region: ' +
                            self.set_dic['region'] + config.CR)
                set_file.write('Analysis Type: ' +
                            self.set_dic['analysis'] + config.CR)
                set_file.write('July to June: ' +
                            str(self.set_dic['july_to_june']) + config.CR)
                set_file.write('Periods: ' +
                            str(self.set_dic['periods']) + config.CR)
                set_file.write('Comp 1 years: ' +
                            str(self.set_dic['comp_1_years']) + config.CR)
                if self.set_dic['analysis'] != 'Average':
                    set_file.write('Comp 2 years: ' +
                                str(self.set_dic['comp_2_years']) + config.CR)
                    set_file.write('Baseline (from year): ' +
                                self.set_dic['baseline_from'] + config.CR)
                    set_file.write('Baseline (to year): ' +
                                self.set_dic['baseline_to'] + config.CR)
                set_file.write('Output Prefix: ' +
                            self.set_dic['output_prefix'] + config.CR)
                set_file.write('Output Folder: ' +
                            self.set_dic['output_folder'] + config.CR)
