/* Gui_messages.cpp
 *
 * Copyright (C) 1992-2018,2020-2026 Paul Boersma,
 *               2008 Stefan de Konink, 2010 Franz Brausse, 2013 Tom Naughton
 *
 * This code 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 3 of the License, or (at
 * your option) any later version.
 *
 * This code is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this work. If not, see <http://www.gnu.org/licenses/>.
 */

#include <time.h>

#include <assert.h>
#ifdef _WIN32
	#include <windows.h>
#endif

#include "melder.h"
#include "Graphics.h"
#include "Gui.h"
#include "Interpreter.h"
#include "Script.h"
#include "Notebook.h"
#include "GuiTrust.h"

//#include <QuartzCore/CoreAnimation.h>

/********** Exported variable. **********/

static GuiWindow Melder_topShell;

/********** PROGRESS **********/

static bool theProgressCancelled = false;

static bool waitWhileProgress (double progress, conststring32 message, GuiDialog dia,
	GuiProgressBar scale, GuiLabel label1, GuiLabel label2, GuiButton cancelButton)
{
	#if gtk
		(void) cancelButton;   // the Interrupt button has its own callback
		// Wait for all pending events to be processed. If anybody knows how to inspect GTK's
		// event queue for specific events, dump the code here, please.
		// Until then, the button click attaches a g_object data key named "pressed" to the cancelButton
		// which this function reads out in order to tell whether interruption has occurred
		while (gtk_events_pending ()) {
			trace (U"event pending");
			gtk_main_iteration ();
		}
	#elif motif
		XEvent event;
		while (PeekMessage (& event, 0, 0, 0, PM_REMOVE)) {
			if (event. message == WM_KEYDOWN) {
				/*
				 * Ignore all key-down messages, except Escape.
				 */
				if (LOWORD (event. wParam) == VK_ESCAPE) {
					XtUnmanageChild (dia -> d_widget);
					return false;   // don't continue
				}
			} else if (event. message == WM_LBUTTONDOWN) {
				/*
				 * Ignore all mouse-down messages, except click in Interrupt button.
				 */
				GuiObject me = (GuiObject) GetWindowLongPtr (event. hwnd, GWLP_USERDATA);
				if (me == cancelButton -> d_widget) {
					XtUnmanageChild (dia -> d_widget);
					return false;   // don't continue
				}
			} else if (event. message != WM_SYSKEYDOWN) {
				/*
				 * Process paint messages etc.
				 */
				DispatchMessage (& event);
			}
		}
	#elif cocoa
		(void) cancelButton;   // the Interrupt button has its own callback
	#endif
	if (progress >= 1.0) {
		GuiThing_hide (dia);
	} else {
		if (progress <= 0.0)
			progress = 0.0;
		GuiThing_show (dia);   // TODO: prevent raising to the front
		const char32 *newline = str32chr (message, U'\n');
		if (newline) {
			static MelderString buffer;
			MelderString_copy (& buffer, message);
			buffer.string [newline - message] = U'\0';
			GuiLabel_setText (label1, buffer.string);
			buffer.string [newline - message] = U'\n';
			GuiLabel_setText (label2, buffer.string + (newline - message) + 1);
		} else {
			GuiLabel_setText (label1, message);
			GuiLabel_setText (label2, U"");
		}
		#if gtk
			trace (U"update the progress bar");
			GuiProgressBar_setValue (scale, progress);
			while (gtk_events_pending ()) {
				trace (U"event pending");
				gtk_main_iteration ();
			}
			trace (U"check whether the cancel button has the \"pressed\" key set");
			if (g_object_steal_data (G_OBJECT (cancelButton -> d_widget), "pressed")) {
				trace (U"the cancel button has been pressed");
				return false;   // don't continue
			}
		#elif motif
			GuiProgressBar_setValue (scale, progress);
			GdiFlush ();
		#elif cocoa
			GuiProgressBar_setValue (scale, progress);
			//[scale -> d_cocoaProgressBar   displayIfNeeded];
			//[CATransaction flush];
			GuiShell_drain (scale -> d_shell, true, true);
			if (theProgressCancelled) {
				theProgressCancelled = false;
				return false;
			}
		#endif
	}
	trace (U"continue");
	return true;
}

static GuiButton theProgressCancelButton = nullptr;

#if gtk || cocoa
	static void progress_dia_close (Thing /* boss */) {
		theProgressCancelled = true;
		#if gtk
			g_object_set_data (G_OBJECT (theProgressCancelButton -> d_widget), "pressed", (gpointer) 1);
		#endif
	}
	static void progress_cancel_btn_press (Thing /* boss */, GuiButtonEvent /* event */) {
		theProgressCancelled = true;
		#if gtk
			g_object_set_data (G_OBJECT (theProgressCancelButton -> d_widget), "pressed", (gpointer) 1);
		#endif
	}
#endif

static void _Melder_dia_init (GuiDialog *dia, GuiProgressBar *scale, GuiLabel *label1, GuiLabel *label2, GuiButton *cancelButton, bool hasMonitor) {
	trace (U"creating the dialog");
	*dia = GuiDialog_create (Melder_topShell, 200, 100, 500, hasMonitor ? 530 : 200, U"Work in progress",
		#if gtk || cocoa
			progress_dia_close, nullptr,
		#else
			nullptr, nullptr,
		#endif
		GuiDialog_Modality::MODELESS
	);

	trace (U"creating the labels");
	*label1 = GuiLabel_createShown (*dia, 3, 503, 0, Gui_LABEL_HEIGHT, U"label1", 0);
	*label2 = GuiLabel_createShown (*dia, 3, 503, 30, 30 + Gui_LABEL_HEIGHT, U"label2", 0);

	trace (U"creating the scale");
	*scale = GuiProgressBar_createShown (*dia, 3, -3, 70, 110, 0);

	trace (U"creating the cancel button");
	*cancelButton = GuiButton_createShown (*dia, 0, 500, 170, 170 + Gui_PUSHBUTTON_HEIGHT,
		U"Interrupt",
		#if gtk
			progress_cancel_btn_press, nullptr,
		#elif cocoa
			progress_cancel_btn_press, nullptr,
		#else
			nullptr, nullptr,
		#endif
		0
	);
	trace (U"end");
}

static void gui_progress (double progress, conststring32 message) {
	static double lastTime;
	static GuiDialog dia = nullptr;
	static GuiProgressBar scale = nullptr;
	static GuiLabel label1 = nullptr, label2 = nullptr;
	const double now = Melder_clock ();
	if (progress <= 0.0 || progress >= 1.0 ||
		now - lastTime > 0.25)   // this time step must be much longer than the null-event waiting time
	{
		if (! dia)
			_Melder_dia_init (& dia, & scale, & label1, & label2, & theProgressCancelButton, false);
		if (! waitWhileProgress (progress, message, dia, scale, label1, label2, theProgressCancelButton))
			Melder_throw (U"Interrupted!");
		lastTime = now;
	}
}

static autoGraphics graphics;

static void gui_drawingarea_cb_expose (Thing /* boss */, GuiDrawingArea_ExposeEvent /* event */) {
	if (! graphics)
		return;
	Graphics_play (graphics.get(), graphics.get());
}

static void * gui_monitor (double progress, conststring32 message) {
	static double lastTime;
	static GuiDialog dia = nullptr;
	static GuiProgressBar scale = nullptr;
	static GuiDrawingArea drawingArea = nullptr;
	static GuiButton cancelButton = nullptr;
	static GuiLabel label1 = nullptr, label2 = nullptr;
	const double now = Melder_clock ();
	if (progress <= 0.0 || progress >= 1.0 ||
		now - lastTime > 0.25)   // this time step must be much longer than the null-event waiting time
	{
		if (! dia) {
			_Melder_dia_init (& dia, & scale, & label1, & label2, & cancelButton, true);
			drawingArea = GuiDrawingArea_createShown (dia, 0, 500, 230, 530,
					gui_drawingarea_cb_expose, nullptr, nullptr, nullptr, nullptr, nullptr, 0);
			GuiThing_show (dia);
			graphics = Graphics_create_xmdrawingarea (drawingArea);
		}
		if (progress <= 0.0 && graphics) {
			Graphics_clearRecording (graphics.get());
			Graphics_startRecording (graphics.get());
			Graphics_clearWs (graphics.get());
		}
		if (! waitWhileProgress (progress, message, dia, scale, label1, label2, cancelButton))
			Melder_throw (U"Interrupted!");
		lastTime = now;
		if (progress == 0.0)
			return graphics.get();
	}
	return nullptr;
}

#if cocoa
	static void mac_message (NSAlertStyle macAlertType, conststring32 message32) {
		static char16 message16 [4000];
		const integer messageLength = Melder_length (message32);
		uinteger j = 0;
		for (int i = 0; i < messageLength && j <= 4000 - 3; i ++) {
			char32 kar = message32 [i];
			if (kar <= 0x00'FFFF) {
				message16 [j ++] = (char16) kar;
			} else if (kar <= 0x10'FFFF) {
				kar -= 0x01'0000;
				message16 [j ++] = (char16) (0x00'D800 | (kar >> 10));
				message16 [j ++] = (char16) (0x00'DC00 | (kar & 0x00'03FF));
			}
		}
		message16 [j] = u'\0';   // append null byte because we are going to search this string

		/*
			Split up the message between header (will appear in bold) and rest.
			The split is done at the first line break, except if the first line ends in a colon,
			in which case the split is done at the second line break.
		*/
		const char16 *lineBreak = & message16 [0];
		for (; *lineBreak != u'\0'; lineBreak ++)
			if (*lineBreak == u'\n')
				break;
		if (*lineBreak == u'\n' && lineBreak - message16 > 0 && lineBreak [-1] == u':')
			for (lineBreak ++; *lineBreak != u'\0'; lineBreak ++)
				if (*lineBreak == u'\n')
					break;
		uinteger lengthOfFirstSentence = (uinteger) (lineBreak - message16);
		/*
			Create an alert dialog with an icon that is appropriate for the level.
		*/
		NSAlert *alert = [[NSAlert alloc] init];
		[alert setAlertStyle: macAlertType];
		if (macAlertType == NSCriticalAlertStyle)
			[alert addButtonWithTitle: @"Crash"];
		/*
			Add the header in bold.
		*/
		NSString *header = [[NSString alloc] initWithCharacters: (const unichar *) & message16 [0]   length: lengthOfFirstSentence];   // note: init can change the object pointer!
		if (header) {   // make this very safe, because we can be at error time or at fatal time
			[alert setMessageText: header];
			[header release];
		}
		/*
			Add the rest of the message in small type.
		*/
		if (lengthOfFirstSentence < j) {
			NSString *rest = [[NSString alloc] initWithCharacters: (const unichar *) & lineBreak [1]   length: j - 1 - lengthOfFirstSentence];
			if (rest) {   // make this very safe, because we can be at error time or at crash time
				[alert setInformativeText: rest];
				[rest release];
			}
		}
		/*
			Display the alert dialog and synchronously wait for the user to click OK.
		*/
		[alert runModal];
		[alert release];
	}
#endif

#define theMessageFund_SIZE  100'000
static char * theMessageFund = nullptr;

static void gui_crash (conststring32 message) {
	free (theMessageFund);
	Melder_casual (U"PRAAT CRASH MESSAGE:\n", message, U"\n(END OF PRAAT CRASH MESSAGE)");
	#if gtk
		GuiObject dialog = gtk_message_dialog_new (GTK_WINDOW (Melder_topShell -> d_gtkWindow), GTK_DIALOG_DESTROY_WITH_PARENT,
			GTK_MESSAGE_ERROR, GTK_BUTTONS_NONE, "%s", Melder_peek32to8 (message));
		gtk_dialog_add_buttons (GTK_DIALOG (dialog), "Crash", GTK_RESPONSE_OK, nullptr);
		gtk_dialog_run (GTK_DIALOG (dialog));
		gtk_widget_destroy (GTK_WIDGET (dialog));
	#elif motif
		MessageBox (nullptr, Melder_peek32toW (message), L"Crashing bug", MB_OK | MB_TOPMOST | MB_ICONSTOP);
	#elif cocoa
		mac_message (NSAlertStyleCritical, message);
		SysError (11);
	#endif
}

static void gui_error (conststring32 message) {
	const bool memoryIsLow = str32str (message, U"Out of memory");
	if (memoryIsLow)
		free (theMessageFund);
	/*
		It is not impossible that the program crashes during `gtk_dialog_run` or `MessageBox` or `runModal`,
		especially if these are called at expose time.
		Write the message to stdout just in case.
	*/
	Melder_casual (U"PRAAT ERROR MESSAGE:\n", message, U"(END OF PRAAT ERROR MESSAGE)");
	#if gtk
		trace (U"create dialog");
		GuiObject dialog = gtk_message_dialog_new (GTK_WINDOW (Melder_topShell -> d_gtkWindow), GTK_DIALOG_DESTROY_WITH_PARENT,
			GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, "%s", Melder_peek32to8 (message));
		trace (U"run dialog");
		gtk_dialog_run (GTK_DIALOG (dialog));
		trace (U"destroy dialog");
		gtk_widget_destroy (GTK_WIDGET (dialog));
	#elif motif
		MessageBox (nullptr, Melder_peek32toW (message), L"Message", MB_OK | MB_TOPMOST | MB_ICONWARNING);   // or (HWND) XtWindow ((GuiObject) Melder_topShell)
	#elif cocoa
		mac_message (NSAlertStyleWarning, message);
	#endif
	if (memoryIsLow) {
		theMessageFund = (char *) malloc (theMessageFund_SIZE);
		if (! theMessageFund) {
			#if gtk
				GuiObject dialog = gtk_message_dialog_new (GTK_WINDOW (Melder_topShell -> d_gtkWindow), GTK_DIALOG_DESTROY_WITH_PARENT,
					GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "Praat is very low on memory.\nSave your work and quit Praat.\nIf you don't do that, Praat may crash.");
				gtk_dialog_run (GTK_DIALOG (dialog));
				gtk_widget_destroy (GTK_WIDGET (dialog));
			#elif motif
				MessageBox (nullptr, L"Praat is very low on memory.\nSave your work and quit Praat.\nIf you don't do that, Praat may crash.", L"Message", MB_OK);
			#elif cocoa
				mac_message (NSAlertStyleCritical, U"Praat is very low on memory.\nSave your work and quit Praat.\nIf you don't do that, Praat may crash.");
			#endif
		}
	}
}

static void gui_warning (conststring32 message) {
	Melder_casual (U"PRAAT WARNING MESSAGE:\n", message, U"\n(END OF PRAAT WARNING MESSAGE)");
	#if gtk
		GuiObject dialog = gtk_message_dialog_new (GTK_WINDOW (Melder_topShell -> d_gtkWindow), GTK_DIALOG_DESTROY_WITH_PARENT,
			GTK_MESSAGE_INFO, GTK_BUTTONS_OK, "%s", Melder_peek32to8 (message));
		gtk_dialog_run (GTK_DIALOG (dialog));
		gtk_widget_destroy (GTK_WIDGET (dialog));
	#elif motif
		MessageBox (nullptr, Melder_peek32toW (message), L"Warning", MB_OK | MB_TOPMOST | MB_ICONINFORMATION);
	#elif cocoa
		mac_message (NSAlertStyleInformational, message);
	#endif
}

static void gui_trust (void *void_interpreter, conststring32 action) {
	Interpreter interpreter = (Interpreter) void_interpreter;
	if (interpreter) {
		Script script = interpreter -> scriptReference;
		Notebook notebook = interpreter -> notebookReference;
		if (script && script -> trusted || notebook && notebook -> trusted)
			return;   // the request should be granted
		if (Melder_appVersion() < 7000)
			return;   // no trust checking before Praat 7.0
		conststring32 paragraphs [1+5] = { };
		if (script) {
			paragraphs [1] = U"The script";
			paragraphs [2] = Melder_cat (U"“", script -> string.get(), U"”");
		} else if (notebook) {
			paragraphs [1] = U"The notebook";
			paragraphs [2] = Melder_cat (U"“", notebook -> string.get(), U"”");
		} else
			paragraphs [1] =  U"Your untitled script or notebook";
		paragraphs [3] = U"requests permission to:";
		paragraphs [4] = action;
		conststring32 option1 = U"CANCEL\n(because I don’t want the requested action to happen)";
		conststring32 option2 =
			script ?
				U"Yes, I allow this script to perform the action that it requests\n(and ask me again next time)"
			: notebook ?
				U"Yes, I allow this notebook to perform the action that it requests\n(and ask me again next time)"
			:
				U"Yes, I allow this script or notebook to perform the action that it requests\n(and ask me again next time)";
		conststring32 option3 =
			script ?
				U"Yes, and I even allow this script to CONTROL MY COMPUTER from now on (i.e. to perform any action,\n"
				"including saving, deleting, calling system commands, and internetting), because I FULLY TRUST\n"
				"the skills and intentions of the authors of the script AND of all the scripts called by it."
			: notebook ?
				U"Yes, and I even allow this notebook to CONTROL MY COMPUTER from now on (i.e. to perform any action,\n"
				"including saving, deleting, calling system commands, and internetting), because I FULLY TRUST\n"
				"the skills and intentions of the authors of the notebook AND of all the scripts and notebooks called by it."
			:
				U"(The option to allow this script or notebook to control your computer will be available\n"
				"after you save the script or notebook. Until then, clicking here will allow only this one action.)";
		const integer buttonClicked = GuiTrust_get (nullptr, nullptr,
			paragraphs [1], paragraphs [2], paragraphs [3], paragraphs [4], paragraphs [5],
			option1, option2, option3, nullptr, nullptr, interpreter
		);
		if (buttonClicked == 2)
			return;   // allow this one action
		Melder_assert (buttonClicked == 3);
		if (script)
			script -> trusted = true;
		else if (notebook)
			notebook -> trusted = true;
	}
}

void Gui_injectMessageProcs (GuiWindow parent) {
	theMessageFund = (char *) malloc (theMessageFund_SIZE);
	assert (theMessageFund);
	Melder_topShell = parent;
	Melder_setCrashProc (gui_crash);
	Melder_setErrorProc (gui_error);
	Melder_setWarningProc (gui_warning);
	Melder_setProgressProc (gui_progress);
	Melder_setMonitorProc (gui_monitor);
	Melder_setTrustProc (gui_trust);
}

/* End of file Gui_messages.cpp */
