'''
/***************************************************************************
Name       :  changes_in_averages_controller.py
Description:  Changes in averages for climate data
copyright  :  (C) 2020-2023 by FEWS
email      :  minxuansun@contractor.usgs.gov
Author     :  Austin Christianson
Modified   :  mm/dd/yyyy - descripton - Description
              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 error
              08/24/2020 - cholen - Bugfix for cross years check
              08/28/2020 - cholen - Notify on completion, general cleanup to
                                    match patterns in other tools
              10/08/2020 - cholen - Fix os sep in browse
              10/13/2020 - cholen - Change to display_map_layer function
              10/23/2020 - cholen - Tweak map layer load and reg_dic name
              11/03/2020 - cholen - Resample inputs changes
              11/04/2020 - cholen - Adjust input check
              03/08/2021 - cholen - Check mask vs region adjusted
              01/06/2022 - cholen - Use new utilities, SCA cleanup
              03/18/2022 - cholen - Update form control names for consistency
              06/23/2022 - cholen - Fix paths for region mask and map file
              01/03/2023 - dhackman - Output prefix changes

 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.workers.changes_in_averages_worker import ChangesInAveragesWorker
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_ChangesInAverages import Ui_ChangesInAverages

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 ChangesInAveragesController(QDialog):
    '''
    Class for calculating changes in averages in a series.
    '''

    def __init__(self):
        QDialog.__init__(self)
        self.is_processing = False  # Flag to indicate if running a process
        self.datasets_info = DatasetsModel()
        self.region_info = RegionModel()
        self.wrksp_setup = WorkspaceSetupModel()
        # setup Ui
        self.ui = Ui_ChangesInAverages()
        self.ui.setupUi(self)
        # defining variables
        self.available_years_list = []
        self.seasonal_sums_ts1 = []
        self.seasonal_sums_ts2 = []
        self.season_list_all = []
        self.avg_ts1_filename = ''
        self.avg_ts2_filename = ''
        self.output_filename = ''
        self.series_years_list_ts1 = []
        self.series_years_list_ts2 = []
        self.jul_2_jun = False
        self.color = config.RF_TREND_COLOR_FILE
        self.interval_list = []  # i.e. [['Jan', '01'], ] adjusts for Jul2Jun
        self.selected_periods_list = []  # i.e. [['12'],['01'],['02']]
        self.decad_conv_factor = 1.0
        # used for jun2jul check
        self.beg_per = ''
        self.end_per = ''
        self.mid_point = 7
        # used to check files
        self.needed_files_list = []
        self.missing_files_list = []
        self.available_files_list = []

        self.average_worker = None
        self.thread = None
        self.ds_dic = None
        self.reg_dic = None
        self.col_offset = 0
        self.row_offset = 0

        self.periodicty = self.ui.intervalLineEdit.text()
        self.available_data_files = []

        self.closed = False  # A flag to indicate if close_event is triggered
        self.close_event = None

        # fill widget combo boxes with default values
        util.fill_dataset_combo(self.ui.datasetComboBox)
        self.region_no_space = self.ui.regionComboBox.currentText().replace(
            ' ', '')

        # Make connections to methods
        self.ui.datasetComboBox.currentIndexChanged.connect(
            self.dataset_change)
        self.ui.jul2JunCheckBox.stateChanged.connect(self.update_intervals)
        self.ui.processButton.clicked.connect(self.start_changes_in_averages)
        self.ui.browseOutputButton.clicked.connect(self.browse_output)
        self.ui.regionComboBox.currentIndexChanged.connect(
            self.setup_output_prefix)
        # Users cannot edit these parameters
        self.ui.intervalLineEdit.setEnabled(False)
        self.ui.datatypeLineEdit.setEnabled(False)
        self.ui.outputLineEdit.setEnabled(False)
        self.ui.cancelButton.setEnabled(False)
        self.ui.helpButton.clicked.connect(self.view_manual)
        # initialize to workspace output, user can change
        self.output_path = self.wrksp_setup.fix_os_sep_in_path(
            self.wrksp_setup.get_output_path())
        self.ui.outputLineEdit.setText(self.output_path)
        self.ui.outputPrefixLineEdit.setEnabled(True)
        self.dataset_change()
        self.open_file_info = None

    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.series_years_list_ts1 +
                                    self.series_years_list_ts2))
        (self.needed_files_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 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(' ', '')
            dt_str = self.ds_dic["DATATYPE"].lower()
            prefix_string = region_text + '_' + dt_str
            self.ui.outputPrefixLineEdit.setText(prefix_string)

    def setup_output_files(self):
        '''
        Setup output file names. Checks to see if any are in the map panel.
        '''
        # update outputPath and create if necessary
        self.output_path = self.wrksp_setup.fix_os_sep_in_path(
            self.ui.outputLineEdit.text())
        if not os.path.exists(self.output_path):
            os.makedirs(self.output_path)

        year_string_ts1 = (self.series_years_list_ts1[0] + 'to' +
                           self.series_years_list_ts1[-1])
        year_string_ts2 = (self.series_years_list_ts2[0] + 'to' +
                           self.series_years_list_ts2[-1])

        # setup the ts average filenames
        item_list =\
            [x.text() for x in self.ui.periodListWidget.selectedItems()]
        idx_list =\
            [x.row() for x in self.ui.periodListWidget.selectedIndexes()]
        temp_list = []
        for i in enumerate(idx_list):
            temp_list.append([idx_list[i[0]], item_list[i[0]]])
        temp_list = sorted(temp_list)
        start_period = temp_list[0][1]
        end_period = temp_list[-1][1]
        out_string1 = self.ui.outputPrefixLineEdit.text()\
            + ('_avg_' +
               start_period + '_to_' + end_period + '_' +
               year_string_ts1)
        self.avg_ts1_filename = self.wrksp_setup.fix_os_sep_in_path(
            os.path.join(self.output_path,
                         out_string1 + self.ds_dic['DATASUFFIX']))

        out_string2 = self.ui.outputPrefixLineEdit.text()\
            + ('_avg_' +
               start_period + '_to_' + end_period + '_' +
               year_string_ts2)
        self.avg_ts2_filename = self.wrksp_setup.fix_os_sep_in_path(
            os.path.join(self.output_path,
                         out_string2 + self.ds_dic['DATASUFFIX']))

        # setup the final output filename
        out_string3 = self.ui.outputPrefixLineEdit.text()\
            + ('avgs_chg' +
               year_string_ts2 + 'minus' +
               year_string_ts1 + start_period + '_to_' + end_period)
        self.output_filename = self.wrksp_setup.fix_os_sep_in_path(
            os.path.join(self.output_path,
                         out_string3 + self.ds_dic['DATASUFFIX']))
        output_layer_list = [out_string1, out_string2, out_string3]
        for entry in self.open_file_info:
            if entry[1] in output_layer_list:
                qgs_util.notify_loaded_file(self, entry[1], entry[0])

    def view_manual(self):
        '''
        Function to browse to output directory and set line edit control.
        '''
        clim_manual_webpage_section = "chapter-8-calculate-long-term-changes-in-averages"
        view_manual(clim_manual_webpage_section)

    def dataset_change(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')
        # available_data_files = users available dataset files
        self.ds_dic = self.datasets_info.get_ds_dictionary()
        self.available_data_files = util.get_input_file_list(self.ds_dic)

        if self.available_data_files == []:
            self.available_years_list = []
            self.ui.ts1FrmYrComboBox.clear()
            self.ui.ts1ToYrComboBox.clear()
            self.ui.ts2FrmYrComboBox.clear()
            self.ui.ts2ToYrComboBox.clear()
            QMessageBox.information(self,
                                    u'Data missing!!',
                                    u'No data available for dataset!!',
                                    QMessageBox.Ok)
            # clears out widgets if dataset files do not exist
            self.ui.regionComboBox.clear()
            return
        # Fills widgets with available region, data type, and interval data
        util.fill_region_combo_for_dataset_selection(
            self.ui.regionComboBox, self.available_data_files[0])
        self.beg_per = util.extract_data_period(
            self.ds_dic, self.available_data_files[0])
        self.end_per = util.extract_data_period(
            self.ds_dic, self.available_data_files[-1])
        util.fill_ds_type_widget(self.ui.datatypeLineEdit, self.ds_dic)
        util.fill_interval_widget(self.ui.intervalLineEdit, self.ds_dic)
        self.available_years_list =\
            util.extract_data_years(self.ds_dic, self.available_data_files)
        # updates ts1 and ts2 groupboxes to available year data
        self.update_intervals()
        self.mid_point = util.get_midpoint(self.ds_dic)

        self.ui.progressBar.setValue(0)

    def region_change(self):
        '''
        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.
        '''
        # when you clear the control it counts as a selection change
        # so if nothing is in the text, do nothing
        self.ui.progressBar.setValue(0)
        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.available_data_files[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'])

            # 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 update_intervals(self):
        '''
        Function to adjust widgets per the July to June checkbox.
        '''
        self.ui.ts1FrmYrComboBox.clear()
        self.ui.ts1ToYrComboBox.clear()
        self.ui.ts2FrmYrComboBox.clear()
        self.ui.ts2ToYrComboBox.clear()
        self.ui.periodListWidget.clear()

        self.jul_2_jun = self.ui.jul2JunCheckBox.isChecked()
        util.fill_year_widget(
            self.ui.ts1FrmYrComboBox, self.jul_2_jun,
            self.available_years_list, self.beg_per, self.end_per,
            self.mid_point)
        util.fill_year_widget(
            self.ui.ts1ToYrComboBox, self.jul_2_jun,
            self.available_years_list, self.beg_per, self.end_per,
            self.mid_point)
        util.fill_year_widget(
            self.ui.ts2FrmYrComboBox, self.jul_2_jun,
            self.available_years_list, self.beg_per, self.end_per,
            self.mid_point)
        util.fill_year_widget(
            self.ui.ts2ToYrComboBox, self.jul_2_jun,
            self.available_years_list, self.beg_per, self.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 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 start_changes_in_averages(self):
        '''
        Run the analysis.
        '''
        if not self.region_change():
            return

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

        self.open_file_info = qgs_util.get_open_files_info()
        self.selected_periods_list = util.convert_gui_periods_to_file_periods(
            self.ui.periodListWidget, self.ds_dic)
        ts1_frm_str = self.ui.ts1FrmYrComboBox.currentText()
        ts1_to_str = self.ui.ts1ToYrComboBox.currentText()
        ts2_frm_str = self.ui.ts2FrmYrComboBox.currentText()
        ts2_to_str = self.ui.ts2ToYrComboBox.currentText()

        ts1_error = (int(ts1_to_str[:4]) < int(ts1_frm_str[:4]))
        ts1_select_error = (int(ts1_to_str[:4]) < (int(ts1_frm_str[:4]) + 2))
        ts2_error = (int(ts2_to_str[:4]) < int(ts2_frm_str[:4]))
        ts2_select_error = (int(ts2_to_str[:4]) < (int(ts2_frm_str[:4]) + 2))
        # 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 ts1_error:
            QMessageBox.critical(self,
                                 u'Selection Error!!',
                                 u'Series 1 "From" year ' +
                                 'must be before "To" year',
                                 QMessageBox.Ok)
            return
        if ts1_select_error:
            QMessageBox.critical(self,
                                 u'Selection Error!!',
                                 u'Series 1 must span at least ' +
                                 'three years.',
                                 QMessageBox.Ok)
            return
        if ts2_error:
            QMessageBox.critical(self,
                                 u'Selection Error!!',
                                 u'Series 2 "From" year ' +
                                 'must be before "To" year',
                                 QMessageBox.Ok)
            return

        if ts2_select_error:
            QMessageBox.critical(self,
                                 u'Selection Error!!',
                                 u'Series 2 must span at least ' +
                                 'three years.',
                                 QMessageBox.Ok)
            return

        self.series_years_list_ts1 = []
        self.series_years_list_ts2 = []

        if not self.jul_2_jun:
            for i in range(int(ts1_frm_str[:4]), (int(ts1_to_str[-4:]) + 1)):
                self.series_years_list_ts1.append(str(i))
            for i in range(int(ts2_frm_str[:4]), (int(ts2_to_str[-4:]) + 1)):
                self.series_years_list_ts2.append(str(i))
        else:
            for i in range(int(ts1_frm_str[:4]), int(ts1_to_str[-4:])):
                self.series_years_list_ts1.append(str(i) + '-' + str(i + 1))
            for i in range(int(ts2_frm_str[:4]), int(ts2_to_str[-4:])):
                self.series_years_list_ts2.append(str(i) + '-' + str(i + 1))

        self.ui.progressBar.setValue(0)

        msg = u'Changes in averages 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

        # 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
        # checks output files before processing
        self.setup_output_files()
        self.average_worker = ChangesInAveragesWorker(
            self.ds_dic, self.reg_dic, self.jul_2_jun,
            self.series_years_list_ts1, self.series_years_list_ts2,
            self.selected_periods_list,
            self.avg_ts1_filename, self.avg_ts2_filename,
            self.output_path, self.output_filename)

        self.ui.cancelButton.clicked.connect(self.average_worker.kill)
        # enable the cancel button and disable all other gui widgets
        self.ui.cancelButton.setEnabled(True)
        self.ui.processButton.setEnabled(False)
        self.ui.closeButton.setEnabled(False)
        self.ui.regionComboBox.setEnabled(False)
        self.ui.intervalLineEdit.setEnabled(False)
        self.ui.browseOutputButton.setEnabled(False)
        self.ui.periodListWidget.setEnabled(False)
        self.ui.datasetComboBox.setEnabled(False)
        self.ui.jul2JunCheckBox.setEnabled(False)
        self.ui.outputPrefixLineEdit.setEnabled(False)

        # start the climate averages in a new thread
        iface.messageBar().clearWidgets()
        self.thread = QtCore.QThread(self)
        self.average_worker.moveToThread(self.thread)
        self.thread.started.connect(self.average_worker.run)
        self.thread.start()
        self.average_worker.finished.connect(self.changes_in_averages_finished)
        self.average_worker.error.connect(self.averages_error)
        self.average_worker.progress.connect(self.ui.progressBar.setValue)
        self.is_processing = True

    def changes_in_averages_finished(self, avg_result):
        '''
        Clean up the averages worker object and thread.
        params(tuple) - avg_result - Returned code and string from
                                 averages 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.average_worker.killed:
            self.ui.progressBar.setValue(0)
            msg = (u"Changes in Averages aborted by user")
            iface.messageBar().pushCritical("Averages", msg)
            QgsMessageLog.logMessage(msg, level=Qgis.Critical)
            iface.mainWindow().statusBar().showMessage(msg, 3000)
        elif avg_result is not None:
            if self.ui.datatypeLineEdit.text() == config.DATA_TYPES[0][0]:
                self.color = config.RF_TREND_COLOR_FILE
            else:
                self.color = config.SPI_COLOR_FILE
            msg = u'Exit code: ' + str(avg_result)
            open_file_names = [sublist[1] for sublist in self.open_file_info]

            # save off the map panel
            qgs_util.map_panel_backup()

            # write out image
            map_file = self.reg_dic['Map'].split(',')[0]
            input_file_list = [self.output_filename, '', map_file]
            qgs_util.write_image(input_file_list, self.color)
            iface.messageBar().clearWidgets()

            # restore the map panel
            qgs_util.map_panel_restore()
            # add new outputs to map panel
            color_dir = self.wrksp_setup.get_colors_path()
            color_path = os.path.join(color_dir, self.color)
            qgs_util.display_raster_layer(self.output_filename, color_path)
            qgs_util.display_map_layer(self.reg_dic, open_file_names)
            util.remove_temp_files(self.output_path)
            self.ui.progressBar.setValue(100)

            msg = u'Changes in Averages completed'
            QgsMessageLog.logMessage(msg, level=Qgis.Info)
            iface.mainWindow().statusBar().showMessage(msg, 3000)
        else:
            # notify of any other error
            msg = (u'Error: Changes in Averages did not finish')
            iface.messageBar().pushCritical('Averages', 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.processButton.setEnabled(True)
        self.ui.closeButton.setEnabled(True)
        self.ui.regionComboBox.setEnabled(True)
        self.ui.periodListWidget.setEnabled(True)
        self.ui.browseOutputButton.setEnabled(True)
        self.ui.datasetComboBox.setEnabled(True)
        self.ui.jul2JunCheckBox.setEnabled(True)
        self.ui.outputPrefixLineEdit.setEnabled(True)
        self.thread_worker_cleanup()

        log_util.add_log("Changes in Averages",
                         log_util.log_string("Dataset",
                                             [self.ds_dic['DATASETNAME']]) +
                         log_util.log_string("Region",
                                             [self.reg_dic['RegionName']]) +
                         log_util.log_string("Periods to Process",
                                             self.selected_periods_list) +
                         log_util.log_string("Series 1 Years",
                                             self.series_years_list_ts1) +
                         log_util.log_string("Series 2 Years",
                                             self.series_years_list_ts2) +
                         log_util.log_string("July to June",
                                             [self.jul_2_jun]),
                         [self.output_filename])

        all_done = util.notify_process_completed('Changes in Averages')
        if all_done:
            self.ui.closeButton.click()

    def averages_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.average_worker:
            self.average_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()
