"""
***************************************************************************
    AlgorithmDialog.py
    ---------------------
    Date                 : August 2012
    Copyright            : (C) 2012 by Victor Olaya
    Email                : volayaf at gmail dot com
***************************************************************************
*                                                                         *
*   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.                                   *
*                                                                         *
***************************************************************************
"""

__author__ = "Victor Olaya"
__date__ = "August 2012"
__copyright__ = "(C) 2012, Victor Olaya"

import datetime
import time
from typing import Optional

from qgis.core import (
    Qgis,
    QgsApplication,
    QgsProcessingAlgorithm,
    QgsProcessingAlgRunnerTask,
    QgsProcessingFeatureSourceDefinition,
    QgsProcessingOutputHtml,
    QgsProxyProgressTask,
)
from qgis.gui import (
    QgsGui,
    QgsProcessingAlgorithmWidgetBase,
    QgsProcessingContextGenerator,
    QgsProcessingParametersGenerator,
)
from qgis.PyQt import sip
from qgis.PyQt.QtCore import QCoreApplication
from qgis.PyQt.QtGui import QColor, QPalette
from qgis.PyQt.QtWidgets import (
    QDialogButtonBox,
    QMainWindow,
    QMessageBox,
    QPushButton,
    QWidget,
)
from qgis.utils import iface

from processing.core.exceptions import InvalidOutputExtension, InvalidParameterValue
from processing.core.ProcessingConfig import ProcessingConfig
from processing.core.ProcessingResults import resultsList
from processing.gui.AlgorithmExecutor import execute, execute_in_place, executeIterating
from processing.gui.BatchAlgorithmDialog import BatchAlgorithmDialog
from processing.gui.ParametersPanel import ParametersPanel
from processing.gui.Postprocessing import handleAlgorithmResults
from processing.tools import dataobjects


class AlgorithmWidget(QgsProcessingAlgorithmWidgetBase):
    """
    QgsProcessingAlgorithmWidgetBase with python specific logic
    """

    def __init__(
        self,
        alg: QgsProcessingAlgorithm,
        in_place: bool = False,
        parent: Optional[QMainWindow] = None,
        flags: QgsProcessingAlgorithmWidgetBase.WidgetFlags = QgsProcessingAlgorithmWidgetBase.WidgetFlags(),
        initialState: Qgis.DockableWidgetInitialState = Qgis.DockableWidgetInitialState.RestorePreviousState,
    ):
        super().__init__(
            parent or (iface and iface.mainWindow()),
            flags=flags,
            initialState=initialState,
        )

        self.feedback_dialog = None
        self.in_place = in_place
        self.active_layer = iface.activeLayer() if self.in_place else None

        self.context = None
        self.feedback = None
        self.history_log_id = None
        self.history_details = {}

        self._is_running = False

        self.setAlgorithm(alg)
        self.setMainWidget(self.getParametersPanel(alg, self))

        if not self.in_place:
            self.runAsBatchButton = QPushButton(
                QCoreApplication.translate("AlgorithmDialog", "Run as Batch Process…")
            )
            self.runAsBatchButton.clicked.connect(self.runAsBatch)
            self.buttonBox().addButton(
                self.runAsBatchButton, QDialogButtonBox.ButtonRole.ResetRole
            )  # reset role to ensure left alignment
        else:
            in_place_input_parameter_name = "INPUT"
            if hasattr(alg, "inputParameterName"):
                in_place_input_parameter_name = alg.inputParameterName()

            self.mainWidget().setParameters(
                {in_place_input_parameter_name: self.active_layer}
            )

            self.runAsBatchButton = None

            if self.active_layer:
                self.updateRunButtonText()
                self.active_layer.selectionChanged.connect(self.updateRunButtonText)
                self.active_layer.willBeDeleted.connect(self.close)
                self.setWindowTitle(
                    self.windowTitle() + " | " + self.active_layer.name()
                )

        self.updateRunButtonVisibility()

    def updateRunButtonText(self):
        has_selection = self.active_layer.selectedFeatureCount() > 0
        self.buttonBox().button(QDialogButtonBox.StandardButton.Ok).setText(
            QCoreApplication.translate("AlgorithmDialog", "Modify Selected Features")
            if has_selection
            else QCoreApplication.translate("AlgorithmDialog", "Modify All Features")
        )

    def getParametersPanel(self, alg, parent):
        panel = ParametersPanel(parent, alg, self.in_place, self.active_layer)
        return panel

    def runAsBatch(self):
        self.reject()
        dlg = BatchAlgorithmDialog(self.algorithm().create())
        dlg.exec()

    def resetAdditionalGui(self):
        if not self.in_place:
            self.runAsBatchButton.setEnabled(True)

    def blockAdditionalControlsWhileRunning(self):
        if not self.in_place:
            self.runAsBatchButton.setEnabled(False)

    def setParameters(self, parameters):
        self.mainWidget().setParameters(parameters)

    def flag_invalid_parameter_value(self, message: str, widget: QWidget):
        """
        Highlights a parameter with an invalid value
        """
        try:
            self.buttonBox().accepted.connect(lambda w=widget: w.setPalette(QPalette()))
            palette = widget.palette()
            palette.setColor(QPalette.ColorRole.Base, QColor(255, 255, 0))
            widget.setPalette(palette)
        except:
            pass
        self.messageBar().clearWidgets()
        self.messageBar().pushMessage(
            "",
            self.tr("Wrong or missing parameter value: {0}").format(message),
            level=Qgis.MessageLevel.Warning,
            duration=5,
        )

    def flag_invalid_output_extension(self, message: str, widget: QWidget):
        """
        Highlights a parameter with an invalid output extension
        """
        try:
            self.buttonBox().accepted.connect(lambda w=widget: w.setPalette(QPalette()))
            palette = widget.palette()
            palette.setColor(QPalette.ColorRole.Base, QColor(255, 255, 0))
            widget.setPalette(palette)
        except:
            pass
        self.messageBar().clearWidgets()
        self.messageBar().pushMessage(
            "", message, level=Qgis.MessageLevel.Warning, duration=5
        )

    def createProcessingParameters(
        self, flags=QgsProcessingParametersGenerator.Flags()
    ):
        if self.mainWidget() is None:
            return {}

        try:
            return self.mainWidget().createProcessingParameters(flags)
        except InvalidParameterValue as e:
            self.flag_invalid_parameter_value(e.parameter.description(), e.widget)
        except InvalidOutputExtension as e:
            self.flag_invalid_output_extension(e.message, e.widget)
        return {}

    def processingContext(self):
        if self.context is None:
            self.feedback = self.createFeedback()
            self.context = dataobjects.createContext(self.feedback)

        self.applyContextOverrides(self.context)
        return self.context

    def runAlgorithm(self):
        self.feedback = self.createFeedback()
        self.context = dataobjects.createContext(self.feedback)
        self.applyContextOverrides(self.context)
        self.algorithmAboutToRun.emit(self.context)

        checkCRS = ProcessingConfig.getSetting(ProcessingConfig.WARN_UNMATCHING_CRS)
        try:
            # messy as all heck, but we don't want to call the dialog's implementation of
            # createProcessingParameters as we want to catch the exceptions raised by the
            # parameter panel instead...
            parameters = (
                {}
                if self.mainWidget() is None
                else self.mainWidget().createProcessingParameters()
            )

            if checkCRS and not self.algorithm().validateInputCrs(
                parameters, self.context
            ):
                reply = QMessageBox.question(
                    self,
                    self.tr("Unmatching CRS's"),
                    self.tr(
                        "Parameters do not all use the same CRS. This can "
                        "cause unexpected results.\nDo you want to "
                        "continue?"
                    ),
                    QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                    QMessageBox.StandardButton.No,
                )
                if reply == QMessageBox.StandardButton.No:
                    return
            ok, msg = self.algorithm().checkParameterValues(parameters, self.context)
            if not ok:
                QMessageBox.warning(self, self.tr("Unable to execute algorithm"), msg)
                return

            self.blockControlsWhileRunning()
            self.setExecutedAnyResult(True)
            self.cancelButton().setEnabled(False)

            self.iterateParam = None
            self._is_running = True

            for param in self.algorithm().parameterDefinitions():
                if (
                    isinstance(
                        parameters.get(param.name(), None),
                        QgsProcessingFeatureSourceDefinition,
                    )
                    and parameters[param.name()].flags
                    & QgsProcessingFeatureSourceDefinition.Flag.FlagCreateIndividualOutputPerInputFeature
                ):
                    self.iterateParam = param.name()
                    break

            self.clearProgress()
            self.feedback.pushVersionInfo(self.algorithm().provider())
            if (
                self.algorithm().provider()
                and self.algorithm().provider().warningMessage()
            ):
                self.feedback.reportError(self.algorithm().provider().warningMessage())

            self.feedback.pushInfo(
                QCoreApplication.translate(
                    "AlgorithmDialog", "Algorithm started at: {}"
                ).format(datetime.datetime.now().replace(microsecond=0).isoformat())
            )

            self.setInfo(
                QCoreApplication.translate(
                    "AlgorithmDialog", "<b>Algorithm '{0}' starting&hellip;</b>"
                ).format(self.algorithm().displayName()),
                escapeHtml=False,
            )

            self.feedback.pushInfo(self.tr("Input parameters:"))
            display_params = []
            for k, v in parameters.items():
                display_params.append(
                    "'"
                    + k
                    + "' : "
                    + self.algorithm()
                    .parameterDefinition(k)
                    .valueAsPythonString(v, self.context)
                )
            self.feedback.pushCommandInfo("{ " + ", ".join(display_params) + " }")
            self.feedback.pushInfo("")
            start_time = time.time()

            def elapsed_time(start_time) -> str:
                delta_t = time.time() - start_time
                hours = int(delta_t / 3600)
                minutes = int((delta_t % 3600) / 60)
                seconds = delta_t - hours * 3600 - minutes * 60

                str_hours = [self.tr("hour"), self.tr("hours")][hours > 1]
                str_minutes = [self.tr("minute"), self.tr("minutes")][minutes > 1]
                str_seconds = [self.tr("second"), self.tr("seconds")][seconds != 1]

                if hours > 0:
                    elapsed = f"{delta_t:0.2f} {str_seconds} ({hours} {str_hours} {minutes} {str_minutes} {seconds:0.0f} {str_seconds})"
                elif minutes > 0:
                    elapsed = f"{delta_t:0.2f} {str_seconds} ({minutes} {str_minutes} {seconds:0.0f} {str_seconds})"
                else:
                    elapsed = f"{delta_t:0.2f} {str_seconds}"

                return elapsed

            if self.iterateParam:
                # Make sure the Log tab is visible before executing the algorithm
                try:
                    self.showLog()
                    self.repaint()
                except:
                    pass

                self.cancelButton().setEnabled(
                    self.algorithm().flags() & QgsProcessingAlgorithm.Flag.FlagCanCancel
                )
                if executeIterating(
                    self.algorithm(),
                    parameters,
                    self.iterateParam,
                    self.context,
                    self.feedback,
                ):
                    self.feedback.pushInfo(
                        self.tr("Execution completed in {}").format(
                            elapsed_time(start_time)
                        )
                    )
                    self.cancelButton().setEnabled(False)
                    self.finish(True, parameters, self.context, self.feedback)
                else:
                    self.cancelButton().setEnabled(False)
                    self.resetGui()
            else:
                self.history_details = {
                    "python_command": self.algorithm().asPythonCommand(
                        parameters, self.context
                    ),
                    "algorithm_id": self.algorithm().id(),
                    "parameters": self.algorithm().asMap(parameters, self.context),
                }
                process_command, command_ok = self.algorithm().asQgisProcessCommand(
                    parameters, self.context
                )
                if command_ok:
                    self.history_details["process_command"] = process_command
                self.history_log_id, _ = QgsGui.historyProviderRegistry().addEntry(
                    "processing", self.history_details
                )

                QgsGui.instance().processingRecentAlgorithmLog().push(
                    self.algorithm().id()
                )
                self.cancelButton().setEnabled(
                    self.algorithm().flags() & QgsProcessingAlgorithm.Flag.FlagCanCancel
                )

                def on_complete(ok, results):
                    if sip.isdeleted(self):
                        return

                    if ok:
                        self.feedback.pushInfo(
                            self.tr("Execution completed in {}").format(
                                elapsed_time(start_time)
                            )
                        )
                        self.feedback.pushFormattedResults(
                            self.algorithm(), self.context, results
                        )
                    else:
                        self.feedback.reportError(
                            self.tr("Execution failed after {}").format(
                                elapsed_time(start_time)
                            )
                        )
                    self.feedback.pushInfo("")

                    if self.history_log_id is not None:
                        # can't deepcopy this!
                        self.history_details["results"] = {
                            k: v for k, v in results.items() if k != "CHILD_INPUTS"
                        }
                        self.history_details["log"] = self.feedback.htmlLog()

                        QgsGui.historyProviderRegistry().updateEntry(
                            self.history_log_id, self.history_details
                        )

                    if self.feedback_dialog is not None:
                        self.feedback_dialog.close()
                        self.feedback_dialog.deleteLater()
                        self.feedback_dialog = None

                    self.cancelButton().setEnabled(False)

                    self.finish(
                        ok, results, self.context, self.feedback, in_place=self.in_place
                    )

                    self.feedback = None
                    self.context = None

                if not self.in_place and not (
                    self.algorithm().flags()
                    & QgsProcessingAlgorithm.Flag.FlagNoThreading
                ):
                    # Make sure the Log tab is visible before executing the algorithm
                    self.showLog()

                    task = QgsProcessingAlgRunnerTask(
                        self.algorithm(), parameters, self.context, self.feedback
                    )
                    if task.isCanceled():
                        on_complete(False, {})
                    else:
                        task.executed.connect(on_complete)
                        self.setCurrentTask(task)
                else:
                    self.proxy_progress = QgsProxyProgressTask(
                        QCoreApplication.translate(
                            "AlgorithmDialog", "Executing “{}”"
                        ).format(self.algorithm().displayName())
                    )
                    QgsApplication.taskManager().addTask(self.proxy_progress)
                    self.feedback.progressChanged.connect(
                        self.proxy_progress.setProxyProgress
                    )
                    self.feedback_dialog = self.createProgressDialog()
                    self.feedback_dialog.show()
                    if self.in_place:
                        ok, results = execute_in_place(
                            self.algorithm(), parameters, self.context, self.feedback
                        )
                    else:
                        ok, results = execute(
                            self.algorithm(), parameters, self.context, self.feedback
                        )
                    self.feedback.progressChanged.disconnect()
                    self.proxy_progress.finalize(ok)
                    on_complete(ok, results)

        except InvalidParameterValue as e:
            self.flag_invalid_parameter_value(e.parameter.description(), e.widget)
        except InvalidOutputExtension as e:
            self.flag_invalid_output_extension(e.message, e.widget)

    def finish(self, successful, result, context, feedback, in_place=False):
        keepOpen = not successful or ProcessingConfig.getSetting(
            ProcessingConfig.KEEP_DIALOG_OPEN
        )
        generated_html_outputs = False

        self._is_running = False

        if not in_place and self.iterateParam is None:
            # add html results to results dock
            for out in self.algorithm().outputDefinitions():
                if (
                    isinstance(out, QgsProcessingOutputHtml)
                    and out.name() in result
                    and result[out.name()]
                ):
                    resultsList.addResult(
                        icon=self.algorithm().icon(),
                        name=out.description(),
                        timestamp=time.localtime(),
                        result=result[out.name()],
                    )
                    generated_html_outputs = True
            if not handleAlgorithmResults(self.algorithm(), context, feedback, result):
                self.resetGui()
                return

        self.setExecuted(True)
        self.setResults(result)
        self.setInfo(
            self.tr("Algorithm '{0}' finished").format(self.algorithm().displayName()),
            escapeHtml=False,
        )
        self.algorithmFinished.emit(successful, result)

        if not in_place and not keepOpen:
            self.close()
        else:
            self.resetGui()
            if generated_html_outputs:
                self.setInfo(
                    self.tr(
                        "HTML output has been generated by this algorithm."
                        "\nOpen the results dialog to check it."
                    ),
                    escapeHtml=False,
                )

    def isRunning(self):
        return self._is_running
