/*
rkprogresscontol - This file is part of RKWard (https://rkward.kde.org). Created: Sun Sep 10 2006
SPDX-FileCopyrightText: 2006-2025 by Thomas Friedrichsmeier <thomas.friedrichsmeier@kdemail.net>
SPDX-FileContributor: The RKWard Team <rkward@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "rkprogresscontrol.h"

#include <QCloseEvent>
#include <QDialog>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QScreen>
#include <QScrollBar>
#include <QTextEdit>
#include <QTimer>
#include <QVBoxLayout>

#include <KColorScheme>
#include <KLocalizedString>

#include "../misc/rkcommonfunctions.h"
#include "../misc/rkstyle.h"
#include "../rbackend/rkrinterface.h"
#include "../settings/rksettingsmoduler.h"

#include "../debug.h"

/** This class provides the dialog shown as part of an RKProgressControl. Generally you should not use this class directly, but rather use RKProgressControl. */
class RKProgressControlDialog : public QDialog {
  public:
	/** constructor. */
	RKProgressControlDialog(const QString &text, const QString &caption, int mode_flags, bool modal);
	/** destructor. */
	~RKProgressControlDialog();

	QTextEdit *output_text;
	QWidget *detailsbox;

  public:
	void finished();
	void indicateError();
	void toggleDetails();

  protected:
	void closeEvent(QCloseEvent *e) override;

  private:
	/** Replace "Cancel" button text with "Close" (to be called, when underlying command has finished */
	void setCloseTextToClose();

	QLabel *error_indicator;
	QDialogButtonBox *buttons;

	ROutput::ROutputType last_output_type;
	bool prevent_close;
	bool is_done;
};

RKProgressControl::RKProgressControl(QObject *parent, const QString &text, const QString &caption, int mode_flags) : QObject(parent) {
	RK_TRACE(MISC);

	RKProgressControl::text = text;
	RKProgressControl::caption = caption;
	RKProgressControl::mode = mode_flags;

	dialog = nullptr;
	is_done = false;
	modal = false;
	autodelete = false;
}

RKProgressControl::~RKProgressControl() {
	RK_TRACE(MISC);

	if (!is_done) done();
}

void RKProgressControl::autoDeleteWhenDone() {
	RK_TRACE(MISC);
	autodelete = true;
	if (outstanding_commands.isEmpty()) deleteLater();
}

bool RKProgressControl::doModal(bool autodelete) {
	RK_TRACE(MISC);
	RK_ASSERT(!dialog);

	modal = true;
	createDialog();

	int res = dialog->exec();
	RK_ASSERT((res == QDialog::Accepted) || (res == QDialog::Rejected));

	bool ret = is_done; // for good style, we copy this onto the stack, as we are about to self destruct
	if (autodelete) deleteLater();

	return ret;
}

void RKProgressControl::doNonModal(bool autodelete) {
	RK_TRACE(MISC);
	RK_ASSERT(!dialog);

	RKProgressControl::autodelete = autodelete;
	if ((!dialog) && (mode & ShowAtOnce)) { // actually, dialog should always be 0 at this point
		createDialog();
		dialog->show();
	}
}

void RKProgressControl::addRCommand(RCommand *command, bool done_when_finished) {
	RK_TRACE(MISC);
	RK_ASSERT(command);

	outstanding_commands.append(command);
	connect(command->notifier(), &RCommandNotifier::commandOutput, this, &RKProgressControl::newOutput);
	command->whenFinished(this, [this, done_when_finished](const RCommand *command) {
		qDebug("finished");
		if (command->failed()) {
			qDebug("failed");
			if (!dialog) createDialog();
			dialog->indicateError();
		}
		outstanding_commands.removeAll(command);
		if (done_when_finished) done();
	});
}

void RKProgressControl::dialogDestroyed() {
	RK_TRACE(MISC);

	dialog = nullptr;
	if ((!is_done) && (mode & AllowCancel)) {
		is_done = true;
		if (mode & AutoCancelCommands) {
			for (auto it = outstanding_commands.cbegin(); it != outstanding_commands.cend(); ++it) {
				RInterface::instance()->cancelCommand(*it);
			}
		}
		Q_EMIT cancelled();
	}
}

void RKProgressControl::done() {
	RK_TRACE(MISC);

	is_done = true;
	if (dialog) {
		dialog->finished();
	}

	if ((!modal) && autodelete) {
		if (dialog) disconnect(dialog, &QObject::destroyed, this, &RKProgressControl::dialogDestroyed); // we're already dead
		deleteLater();
	}
}

static void scrollDown(QTextEdit *log) {
	QScrollBar *bar = log->verticalScrollBar();
	if (bar) bar->setValue(bar->maximum());
}

static void addOutput(QTextEdit *log, const ROutput &output) {
	RK_TRACE(MISC);

	bool scroll_to_end = log->isVisible();
	if (scroll_to_end) {
		QScrollBar *bar = log->verticalScrollBar();
		if (bar && (bar->value() < bar->maximum())) scroll_to_end = false;
	}

	if (output.type == ROutput::Output) {
		auto font = log->font();
		if (font.bold()) {
			font.setBold(false);
			log->setFont(font);
		}
		//log->setTextColor(RKStyle::viewScheme()->foreground(KColorScheme::NormalText).color());
	} else {
		// We don't want output from install.packages() (which will always arrive as at "Warning" level)
		// to look too alarming. Yet we'd like to keep output types differentiated in other cases.
		auto font = log->font();
		if (!font.bold()) {
			font.setBold(true);
			log->setFont(font);
		}
		//log->setTextColor(RKStyle::viewScheme()->foreground(KColorScheme::NegativeText).color());
	}

	log->insertPlainText(output.output);

	// if previously at end, auto-scroll
	if (scroll_to_end) scrollDown(log);
}

void RKProgressControl::createDialog() {
	RK_TRACE(MISC);

	dialog = new RKProgressControlDialog(text, caption, mode, modal);
	connect(dialog, &QObject::destroyed, this, &RKProgressControl::dialogDestroyed);
	if (is_done) done();
	for (const auto &output : std::as_const(output_log)) {
		addOutput(dialog->output_text, output);
		if (output.type != ROutput::Output && !dialog->detailsbox->isVisible()) dialog->toggleDetails();
	}
}

void RKProgressControl::newOutput(RCommand *, const ROutput &output) {
	RK_TRACE(MISC);

	output_log.append(output);
	if (mode & RaiseOnOutput) {
		if (!dialog) createDialog();
		dialog->raise();
	}
	if (dialog) {
		addOutput(dialog->output_text, output);
		if (output.type != ROutput::Output && !dialog->detailsbox->isVisible()) dialog->toggleDetails();
	}
}

QString RKProgressControl::fullCommandOutput() {
	RK_TRACE(MISC);

	QString ret;
	for (const ROutput &out : std::as_const(output_log))
		ret.append(out.output);
	return ret;
}

//////////////////////////// RKProgressControlDialog ///////////////////////////////////////////7

#include <QPushButton>
#include <qlabel.h>
#include <qlayout.h>

#include <kstandardguiitem.h>

RKProgressControlDialog::RKProgressControlDialog(const QString &text, const QString &caption, int mode_flags, bool modal) : QDialog(nullptr) {
	RK_TRACE(MISC);

	setAttribute(Qt::WA_DeleteOnClose, true);
	setModal(modal);
	setWindowTitle(caption);

	QVBoxLayout *layout = new QVBoxLayout(this);

	QWidget *mainbox = new QWidget(this);
	QVBoxLayout *mainboxlayout = new QVBoxLayout(mainbox);
	mainbox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
	mainboxlayout->setContentsMargins(0, 0, 0, 0);

	mainboxlayout->addWidget(RKCommonFunctions::linkedWrappedLabel(text));

	error_indicator = new QLabel(i18n("<b>There has been an error while running the command. See below for output details</b>"), mainbox);
	QPalette palette = error_indicator->palette();
	palette.setColor(error_indicator->foregroundRole(), QColor(255, 0, 0));
	error_indicator->setPalette(palette);
	error_indicator->hide();
	mainboxlayout->addWidget(error_indicator);

	detailsbox = new QWidget(this);
	QVBoxLayout *detailsboxlayout = new QVBoxLayout(detailsbox);
	detailsboxlayout->setContentsMargins(0, 0, 0, 0);

	detailsboxlayout->addWidget(new QLabel(i18n("Output")));

	output_text = new QTextEdit(detailsbox);
	output_text->setReadOnly(true);
	output_text->setPlainText(QString());
	output_text->setUndoRedoEnabled(false);
	output_text->setLineWrapMode(QTextEdit::NoWrap);
	// default width depends on options("width"), but limit, in case users have set very large values
	const int min_width = qMin(QFontMetrics(output_text->font()).averageCharWidth() * RKSettingsModuleR::getDefaultWidth(),
	                           static_cast<int>(qApp->primaryScreen()->geometry().width() * .75));
	output_text->setMinimumWidth(min_width);
	detailsboxlayout->addWidget(output_text);
	detailsboxlayout->setStretchFactor(output_text, 10);

	buttons = new QDialogButtonBox(QDialogButtonBox::Cancel, this);
	connect(buttons->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &QDialog::reject);
	if ((mode_flags & RKProgressControl::ShowOutput) && !(mode_flags & RKProgressControl::OutputShownByDefault)) {
		QPushButton *button = buttons->addButton(i18n("Show Output"), QDialogButtonBox::HelpRole);
		connect(button, &QPushButton::clicked, this, &RKProgressControlDialog::toggleDetails);
	}
	if (mode_flags & RKProgressControl::AllowCancel) buttons->button(QDialogButtonBox::Cancel)->setText(i18n("Cancel"));
	else (setCloseTextToClose());

	detailsbox->setVisible(mode_flags & RKProgressControl::OutputShownByDefault);

	layout->addWidget(mainbox);
	layout->addWidget(detailsbox);
	layout->addWidget(buttons);

	prevent_close = (mode_flags & RKProgressControl::PreventClose);

	last_output_type = ROutput::Output;
	is_done = false;
}

RKProgressControlDialog::~RKProgressControlDialog() {
	RK_TRACE(MISC);
}

void RKProgressControlDialog::indicateError() {
	RK_TRACE(MISC);
	raise();
	error_indicator->show();
}

void RKProgressControlDialog::toggleDetails() {
	RK_TRACE(MISC);

	int new_height = height();
	if (detailsbox->isVisible()) {
		new_height -= detailsbox->height();
		detailsbox->hide();
	} else {
		int h = detailsbox->height();
		if (h <= 0) h = detailsbox->sizeHint().height();
		new_height += h;
		detailsbox->show();
		scrollDown(output_text);
	}

	layout()->activate();
	resize(width(), new_height);
}

void RKProgressControlDialog::setCloseTextToClose() {
	RK_TRACE(MISC);

	buttons->removeButton(buttons->button(QDialogButtonBox::Cancel));
	QPushButton *done_button = buttons->addButton(QDialogButtonBox::Ok);
	done_button->setText(i18n("Done"));
	connect(done_button, &QPushButton::clicked, this, &QDialog::reject);
}

void RKProgressControlDialog::finished() {
	RK_TRACE(MISC);

	is_done = true;
	setCloseTextToClose();
	if (!detailsbox->isVisible()) reject();
}

void RKProgressControlDialog::closeEvent(QCloseEvent *e) {
	RK_TRACE(DIALOGS);

	if (prevent_close && (!is_done)) {
		e->ignore();
	} else {
		QDialog::closeEvent(e);
	}
}

#include <KMessageBox>
#include <KMessageWidget>
#include <KStandardAction>

#include "../windows/rkmdiwindow.h"
#include "rkstandardicons.h"

RKInlineProgressControl::RKInlineProgressControl(QWidget *display_area, bool allow_cancel) : QObject(display_area),
                                                                                             autoclose(false),
                                                                                             allow_cancel(allow_cancel),
                                                                                             is_done(false),
                                                                                             any_failed(false),
                                                                                             display_area(display_area),
                                                                                             prevent_close_message(nullptr),
                                                                                             close_action(nullptr) {
	RK_TRACE(MISC);

	QWidget *logical_parent = display_area;
	while (logical_parent->parentWidget() && !(logical_parent->isWindow() || qobject_cast<RKMDIWindow *>(logical_parent))) {
		logical_parent = logical_parent->parentWidget();
	}
	logical_parent->installEventFilter(this);
	// display_area->installEventFilter(this);

	wrapper = new QWidget(display_area);
	wrapper->setAutoFillBackground(true);
	auto layout = new QVBoxLayout(wrapper);
	layout->setContentsMargins(0, 0, 0, 0);
	layout->setSpacing(0);
	message_widget = new KMessageWidget();
	message_widget->setPosition(KMessageWidget::Position::Header);
	message_widget->setWordWrap(true);
	message_widget->setCloseButtonVisible(false); // we want a button, instead, for consistency with cancel
	if (allow_cancel) {
		setCloseAction(i18n("Cancel"));
	}
	output_display = new QTextEdit();
	output_display->setProperty("_breeze_force_frame", false);
	layout->addWidget(message_widget);
	layout->addWidget(output_display);
	wrapper->resize(display_area->size());
}

RKInlineProgressControl::~RKInlineProgressControl() {
	RK_TRACE(MISC);

	delete wrapper;
}

void RKInlineProgressControl::setCloseAction(const QString &label) {
	RK_TRACE(MISC);

	if (!close_action) {
		close_action = KStandardAction::create(KStandardAction::Close, this, &RKInlineProgressControl::cancelAndClose, this);
		message_widget->addAction(close_action);
	}
	close_action->setText(label);
}

void RKInlineProgressControl::cancelAndClose() {
	RK_TRACE(MISC);

	is_done = true;
	for (int i = 0; i < unfinished_commands.size(); ++i) {
		RInterface::instance()->cancelCommand(unfinished_commands[i]);
	}
	deleteLater();
}

void RKInlineProgressControl::addRCommand(RCommand *command) {
	RK_TRACE(MISC);
	unfinished_commands.append(command);
	connect(command->notifier(), &RCommandNotifier::commandFinished, this, [this](const RCommand *c) {
		unfinished_commands.removeAll(c);
		if (c->failed()) {
			any_failed = true;
		}
		if (unfinished_commands.isEmpty()) {
			done();
		}
	});
	connect(command->notifier(), &RCommandNotifier::commandOutput, this, [this](const RCommand *, const ROutput &o) {
		addOutput(output_display, o);
	});
}

void RKInlineProgressControl::done() {
	RK_TRACE(MISC);
	if (autoclose && !any_failed) {
		deleteLater();
	} else {
		message_widget->setMessageType(any_failed ? KMessageWidget::Error : KMessageWidget::Positive);
		message_widget->setText(text + u' ' + (any_failed ? i18n("<b>An error occurred</b> (see below for details)") : i18n("<b>Done</b>")));
		message_widget->setIcon(QIcon::fromTheme(any_failed ? u"emblem-error"_s : u"emblem-success"_s));
		message_widget->animatedShow(); // to force an update of geometry
		setCloseAction(i18n("Close"));
	}
	is_done = true;
}

void RKInlineProgressControl::setText(const QString &_text) {
	RK_TRACE(MISC);
	text = _text;
	message_widget->setText(text);
}

void RKInlineProgressControl::show(int delay_ms) {
	RK_TRACE(MISC);
	if (delay_ms > 0) {
		QTimer::singleShot(delay_ms, wrapper, &QWidget::show);
	} else {
		wrapper->show();
	}
	RKStandardIcons::busyAnimation(this, [this](const QIcon &icon) {
		if (!is_done) message_widget->setIcon(icon);
	});
}

bool RKInlineProgressControl::eventFilter(QObject *w, QEvent *e) {
	RK_TRACE(MISC);
	Q_UNUSED(w); // we only installed filter on one object (logical_parent)
	if (e->type() == QEvent::Resize || e->type() == QEvent::Show) {
		QTimer::singleShot(0, this, [this]() {
			wrapper->resize(display_area->size());
		});
		return false;
	}
	if ((e->type() == QEvent::Close) && !is_done) {
		if (allow_cancel) {
			bool autoclose_save = autoclose; // must prevent self-destruction while dialog below is active (the operation might complete, while it exec's)
			autoclose = false;

			bool ignore = (KMessageBox::warningContinueCancel(display_area, i18n("Closing this window will cancel the current operation. Are you sure?"), i18n("Cancel operation"), KGuiItem(i18n("Keep waiting")), KGuiItem(i18n("Cancel && Close"))) == KMessageBox::Continue);

			autoclose = autoclose_save;
			if (ignore) {
				e->ignore();
				return true;
			}
			cancelAndClose();
			return false;
		}
		// TODO
		if (!prevent_close_message) {
			prevent_close_message = new KMessageWidget(i18n("An operation is still running, please wait."), display_area->window());
		}
		QSize s = prevent_close_message->sizeHint();
		prevent_close_message->resize(display_area->window()->width(), s.height());
		prevent_close_message->setMessageType(KMessageWidget::Error);
		prevent_close_message->animatedShow();
		QTimer::singleShot(5000, prevent_close_message, &KMessageWidget::animatedHide);
		e->ignore();
		return true;
	}
	return false;
}
