"""
/***************************************************************************
Name       :  rainfall_summary_controller.py
Description:  A class used to handle the front-end behavior
copyright  :  (C) 2020-2023 by FEWS
email      :  minxuansun@contractor.usgs.gov
Created    :  1/22/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
              06/04/2020 cholen - Add warn on more than 10 inputs.
              07/09/2020 cholen - Update color files, fix static analysis
                                  issues
              07/14/2020 cholen - Adjust error
              08/28/2020 cholen - Notify on completion, util widget fills,
                                  changes for codebase uniformity
              10/08/2020 cholen - Fix os sep in browse
              10/13/2020 cholen - Change to display_map_layer function
              10/19/2020 msun - Open Clim Anal tool if averages file missing
              11/03/2020 cholen - Resample inputs changes
              11/02/2020 cholen - Add check for missing files
              03/08/2021 cholen - Check mask vs region adjusted
              01/05/2022 cholen - Changes for codebase uniformity
              04/20/2022 cholen - Add tiff support
              05/27/2022 cholen - Fix average file list problem
              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 import fews_tools_config as config
from fews_tools.utilities import geoclim_qgs_utilities as qgs_util
from fews_tools.utilities import geoclim_utilities as util
from fews_tools.utilities import logging_utilities as log_util
from fews_tools.utilities.help_utilities import view_manual

from fews_tools.controllers.climatological_analysis_controller import ClimatologicalAnalysisController
from fews_tools.models.datasets_model import DatasetsModel
from fews_tools.workers.rainfall_summary_worker import RainfallSummaryWorker
from fews_tools.models.region_model import RegionModel
from fews_tools.models.workspace_setup_model import WorkspaceSetupModel
from fews_tools.forms.Ui_RainfallSummaries import Ui_RainfallSummaries


class RainfallSummariesController(QDialog):
    """
    Rainfall Summary Controller class
    """

    def __init__(self):
        QDialog.__init__(self)
        # set up workspace, dataset and region
        self.is_processing = False  # Flag to indicate if running a process
        self.wrksp_setup = WorkspaceSetupModel()
        self.dataset_list = None
        self.input_file_list = []
        self.ds_dic = {}
        self.reg_dic = {}

        # set up other model
        self.region_text = ''
        self.available_year_list = []
        self.needed_basenames_list = []
        self.available_files_list = []
        self.missing_files_list = []
        self.from_year = None
        self.to_year = None
        self.start_period = None
        self.end_period = None
        self.year_end_period = None

        # set up output file
        color_directory = self.wrksp_setup.get_colors_path()
        self.average_and_sum_color_file = \
            os.path.join(color_directory, config.RF_2500_RASTER_COLOR_FILE)
        self.difference_color_file = \
            os.path.join(color_directory, config.RF_ANOM_DIFF_COLOR_FILE)
        self.percentage_color_file = \
            os.path.join(color_directory, config.RF_ANOM_PCT_COLOR_FILE)
        self.sum_file_path = ""
        self.average_file_path = ""
        self.difference_file_path = ""
        self.percentage_file_path = ""

        # set up UI
        self.ui = Ui_RainfallSummaries()
        self.ui.setupUi(self)
        self.ui.progressBar.setValue(0)
        self.ui.datasetComboBox.clear()
        self.ui.regionComboBox.clear()
        self.ui.outputLineEdit.setEnabled(False)
        self.ui.helpButton.clicked.connect(self.view_manual)
        self.closed = False  # A flag to indicate if close_event is triggered
        self.close_event = None

        # initialize to workspace output, user can change
        self.output_path = self.wrksp_setup.get_output_path()
        util.fill_dataset_combo_by_type(
            self.ui.datasetComboBox,
            config.DATA_TYPES[0][1])

        self.ui.outputLineEdit.setText(self.output_path)
        self.ui.cancelButton.setEnabled(False)

        # make connections to methods
        self.ui.datasetComboBox.currentIndexChanged.connect(
            self.dataset_selection_changed)
        self.ui.regionComboBox.currentIndexChanged.connect(
            self.region_selection_changed)
        self.ui.browseOutputButton.clicked.connect(self.browse_output)
        self.ui.processButton.clicked.connect(self.start_rainfall_summary)

        # set up worker
        self.worker = None
        self.thread = None

        # set up UI
        self.ui.checkBoxPeriodTotal.setChecked(True)
        self.ui.checkBoxAvgTotal.setChecked(True)
        self.ui.checkBoxDiffFrmAvg.setChecked(True)
        self.ui.checkBoxPctOfAvg.setChecked(True)

        self.dataset_selection_changed()
        self.open_file_info = None

    def check_input_files(self):
        '''
        Checks input files. Identifies needed files from gui widgets as
        either available or missing.
        '''
        from_interval = self.ui.frmPerComboBox.currentText()
        to_interval = self.ui.toPerComboBox.currentText()
        self.from_year = int(self.ui.frmYrComboBox.currentText())
        self.to_year = int(self.ui.toYrComboBox.currentText())
        self.start_period = util.get_period_string(self.ds_dic, from_interval)
        self.end_period = util.get_period_string(self.ds_dic, to_interval)

        (self.needed_basenames_list,
         self.available_files_list,
         self.missing_files_list) = util.check_consecutive_process_inputs(
             self.ds_dic,
             self.from_year, self.to_year,
             self.start_period, self.end_period)

    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.
        Arguments:
        basename -- 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 dataset_selection_changed(self):
        """
        Function to update dataset information.
        """
        self.ui.progressBar.setValue(0)
        ds_info = DatasetsModel()
        ds_info.query_named_dataset(self.ui.datasetComboBox.currentText())
        self.ds_dic = ds_info.get_ds_dictionary()
        self.input_file_list = \
            util.get_input_file_list(self.ds_dic)
        if not self.input_file_list:
            self.ui.regionComboBox.clear()
            self.ui.frmYrComboBox.clear()
            self.ui.toYrComboBox.clear()
            self.ui.frmPerComboBox.clear()
            self.ui.toPerComboBox.clear()
            QMessageBox.warning(
                self,
                'Data missing!!',
                'No data available for dataset!!',
                QMessageBox.Ok)
            return

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

        # fill the date's combo boxes
        self.available_year_list = \
            util.extract_data_years(
                self.ds_dic, self.input_file_list)
        util.fill_period_widget(self.ui.frmPerComboBox, False, self.ds_dic['PERIODICITY'])
        util.fill_period_widget(self.ui.toPerComboBox, False, self.ds_dic['PERIODICITY'])

        interval_dic = util.get_interval_dic(self.ds_dic['PERIODICITY'])
        self.year_end_period = len(interval_dic)
        mid_point = util.get_midpoint(self.ds_dic)
        util.fill_year_widget(self.ui.frmYrComboBox, False,
                              self.available_year_list,
                              1, self.year_end_period, mid_point)
        util.fill_year_widget(self.ui.toYrComboBox, False,
                              self.available_year_list,
                              1, self.year_end_period, mid_point)

    def region_selection_changed(self):
        '''
        Function to update region information.
        '''
        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(' ', '')
            prefix_string = region_text + '_rfsummary_'
            self.ui.outputPrefixLineEdit.setText(prefix_string)

    def check_region(self):
        '''
        Function to validate region information.

        Returns:
        boolean -- False means problem with the region, else True.
        '''
        self.ui.progressBar.setValue(0)
        if self.ui.regionComboBox.currentText() != "":
            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
            mask_params = 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_params[0],
                    qgs_util.get_region_extent(self.reg_dic))
            if not mask_ok:
                return False
        return True

    def browse_output(self):
        """
        Function to browse to an output location.
        """
        dir_name = QFileDialog.getExistingDirectory(
            None,
            'Select Output Directory',
            self.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-5-view-and-explore-rainfall-summaries"
        view_manual(clim_manual_webpage_section)

    def start_rainfall_summary(self):
        """
        Called by clicking OK button. Start calling worker to do data analysis.
        """
        # Don't start processing if invalid region selected
        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)

        # 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

        # 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.open_file_info = qgs_util.get_open_files_info()
        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
        # identifying the start & end date
        start_date = str(self.from_year) + self.start_period
        end_date = str(self.to_year) + self.end_period
        if start_date > end_date:  # string comparison
            # back to the date selection if start date is greater than end date
            QMessageBox.warning(
                self, 'Warning',
                'Top date should be smaller than bottom date. '
                'Re-select dates.', QMessageBox.Ok)
            return

        # get dates string list
        if "." in self.ds_dic['DATADATEFORMAT']:
            if self.ds_dic['PERIODICITY'] == config.PERIODICITY_LIST[0]:
                max_periods_per_month = 3
            elif self.ds_dic['PERIODICITY'] == config.PERIODICITY_LIST[2]:
                max_periods_per_month = 6
            else:
                max_periods_per_month = 1
            dates_list = util.get_date_string_list_per_month_format(
                start_date, end_date, max_periods_per_month)
        else:
            dates_list = util.get_date_string_list_per_year_format(
                start_date, end_date, self.year_end_period)
        if len(dates_list) > 10:
            reply = QMessageBox.question(
                self,
                'Large input set selected',
                'You have more than 10 inputs selected.\n' +
                'Processing large ' +
                'numbers of inputs will take several minutes.\n' +
                'Do you want to proceed?',
                QMessageBox.Yes, QMessageBox.No)
            if reply == QMessageBox.No:
                return
        # check for needed average files
        average_file_list, missing_average_file_list = \
            util.get_dataset_average_file_list(self.ds_dic)

        # get the needed period from the available files list
        avg_per_list = []
        for entry in self.available_files_list:
            avg_per_list.append(util.extract_data_period(self.ds_dic, entry))

        avg_per_list = sorted(set(avg_per_list))
        dig_ct = len(avg_per_list[0])
        # remove the average files that won't be used
        remove_average_list = []
        # this assumes that the period string minus the year portion,
        # for the data files is the same as the period string
        # for the average files
        # if that is not true, this will break
        for entry in average_file_list:
            ent_per = os.path.splitext(
                os.path.basename(entry))[0][-(dig_ct):]
            if ent_per not in avg_per_list:
                remove_average_list.append(entry)
        for entry in remove_average_list:
            average_file_list.remove(entry)

        if missing_average_file_list:
            reply = QMessageBox.warning(
                self,
                'Missing average files!!',
                'Missing average files: ' +
                str(missing_average_file_list) + '\n' +
                'Do you want to open Climatological Analysis tool to create '
                'averages files?',
                QMessageBox.Yes, QMessageBox.No)
            if reply == QMessageBox.Yes:
                # open climatological analysis tool
                climatological_analysis_dialog = ClimatologicalAnalysisController()
                climatological_analysis_dialog.exec_()
            return

        # check if all the info are ready
        if not self.ds_dic or not self.reg_dic:
            QMessageBox.information(self,
                                    "Error!!",
                                    "Missing information!",
                                    QMessageBox.Ok)
            return

        # Get four summary file name
        prefix = self.ui.outputPrefixLineEdit.text() + start_date + 'to' + end_date
        sum_basename = prefix + 'sum'
        average_basename = prefix + 'avg'
        difference_basename = prefix + 'dif'
        percentage_basename = prefix + 'pct'
        # generate absolute path
        self.sum_file_path = \
            self.wrksp_setup.fix_os_sep_in_path(
                os.path.join(self.output_path,
                             sum_basename + self.ds_dic['DATASUFFIX']))
        self.difference_file_path = \
            self.wrksp_setup.fix_os_sep_in_path(
                os.path.join(self.output_path,
                             difference_basename + self.ds_dic['DATASUFFIX']))
        self.average_file_path = \
            self.wrksp_setup.fix_os_sep_in_path(
                os.path.join(self.output_path,
                             average_basename + self.ds_dic['DATASUFFIX']))
        self.percentage_file_path = \
            self.wrksp_setup.fix_os_sep_in_path(
                os.path.join(self.output_path,
                             percentage_basename + self.ds_dic['DATASUFFIX']))
        for b_name in [sum_basename, average_basename,
                       difference_basename, percentage_basename]:
            self.check_loaded_map(b_name)

        # put info into worker
        self.worker = \
            RainfallSummaryWorker(
                self.wrksp_setup,
                self.ds_dic,
                self.reg_dic,
                self.sum_file_path,
                self.average_file_path,
                self.difference_file_path,
                self.percentage_file_path,
                self.available_files_list,
                average_file_list)

        # setup UI
        self.ui.cancelButton.clicked.connect(self.worker.kill)
        self.ui.cancelButton.setEnabled(True)
        self.ui.processButton.setEnabled(False)
        self.ui.closeButton.setEnabled(False)
        self.ui.frmPerComboBox.setEnabled(False)
        self.ui.frmYrComboBox.setEnabled(False)
        self.ui.toPerComboBox.setEnabled(False)
        self.ui.toYrComboBox.setEnabled(False)
        self.ui.browseOutputButton.setEnabled(False)
        self.ui.outputPrefixLineEdit.setEnabled(False)
        self.ui.datasetComboBox.setEnabled(False)
        self.ui.regionComboBox.setEnabled(False)
        # check box for summary outputs
        self.ui.checkBoxPeriodTotal.setEnabled(False)
        self.ui.checkBoxAvgTotal.setEnabled(False)
        self.ui.checkBoxDiffFrmAvg.setEnabled(False)
        self.ui.checkBoxPctOfAvg.setEnabled(False)

        message_text = "Rainfall summaries calculating..."
        iface.mainWindow().statusBar().showMessage(message_text)

        # 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.rainfall_summary_finished)
        self.worker.error.connect(self.rainfall_summary_error)
        self.worker.progress.connect(self.ui.progressBar.setValue)
        self.is_processing = True

    def rainfall_summary_finished(self, return_value):
        """
        Clean up the worker and thread.
        """
        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"Rainfall Summaries aborted by user")
            iface.messageBar().pushCritical("Rainfall Summaries", msg)
            QgsMessageLog.logMessage(msg, level=Qgis.Critical)
            iface.mainWindow().statusBar().showMessage(msg, 3000)
        elif return_value is not None:
            if self.ui.checkBoxPeriodTotal.isChecked():
                qgs_util.display_raster_layer(
                    self.sum_file_path, self.average_and_sum_color_file)
            if self.ui.checkBoxAvgTotal.isChecked():
                qgs_util.display_raster_layer(
                    self.average_file_path, self.average_and_sum_color_file)
            if self.ui.checkBoxDiffFrmAvg.isChecked():
                qgs_util.display_raster_layer(
                    self.difference_file_path, self.difference_color_file)
            if self.ui.checkBoxPctOfAvg.isChecked():
                qgs_util.display_raster_layer(
                    self.percentage_file_path, self.percentage_color_file)
            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("Rainfall Summary",
                             log_util.log_string("Dataset",
                                                 [self.ds_dic['DATASETNAME']]) +
                             log_util.log_string("Region",
                                                 [self.reg_dic['RegionName']]) +
                             log_util.log_string("Start Period",
                                                 [self.start_period]) +
                             log_util.log_string("End Period",
                                                 [self.end_period]) +
                             log_util.log_string("Start Year",
                                                 [self.from_year]) +
                             log_util.log_string("End Year",
                                                 [self.to_year]),
                             [self.sum_file_path,
                              self.average_file_path,
                              self.difference_file_path,
                              self.percentage_file_path])

            self.ui.progressBar.setValue(100)

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

        self.ui.cancelButton.setEnabled(False)
        self.ui.processButton.setEnabled(True)
        self.ui.closeButton.setEnabled(True)
        self.ui.frmPerComboBox.setEnabled(True)
        self.ui.frmYrComboBox.setEnabled(True)
        self.ui.toPerComboBox.setEnabled(True)
        self.ui.toYrComboBox.setEnabled(True)
        self.ui.browseOutputButton.setEnabled(True)
        self.ui.outputPrefixLineEdit.setEnabled(True)
        self.ui.datasetComboBox.setEnabled(True)
        self.ui.regionComboBox.setEnabled(True)
        self.ui.checkBoxPeriodTotal.setEnabled(True)
        self.ui.checkBoxAvgTotal.setEnabled(True)
        self.ui.checkBoxDiffFrmAvg.setEnabled(True)
        self.ui.checkBoxPctOfAvg.setEnabled(True)
        self.thread_worker_cleanup()
        all_done = util.notify_process_completed('Rainfall Summaries')
        if all_done:
            self.ui.closeButton.click()

    def rainfall_summary_error(self, err, ex_str):
        '''
        Function to log any errors from the object and thread.
        Arguments:
        err -- Not used
        ex_str -- 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()
