/* praat_script.cpp
 *
 * Copyright (C) 1993-2026 Paul Boersma
 *
 * 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 "praatP.h"
#include "praat_script.h"
#include "UiPause.h"
#include "DemoEditor.h"

static integer praat_findObjectFromString (Interpreter interpreter, conststring32 string) {
	try {
		integer IOBJECT;
		while (*string == U' ') string ++;
		if (*string >= U'A' && *string <= U'Z') {
			/*
				Find the object by its name.
			*/
			static MelderString buffer;
			MelderString_copy (& buffer, string);
			char32 *space = str32chr (buffer.string, U' ');
			if (! space)
				Melder_throw (U"Missing space in name.");
			*space = U'\0';
			char32 *className = & buffer.string [0], *givenName = space + 1;
			WHERE_DOWN (1) {
				Daata object = (Daata) OBJECT;
				if (str32equ (className, Thing_className (OBJECT)) && str32equ (givenName, object -> name.get()))
					return IOBJECT;
			}
			/*
				No object with that name. Perhaps the class name was wrong?
			*/
			ClassInfo klas = Thing_classFromClassName (className, nullptr);
			WHERE_DOWN (1) {
				Daata object = (Daata) OBJECT;
				if (str32equ (klas -> className, Thing_className (OBJECT)) && str32equ (givenName, object -> name.get()))
					return IOBJECT;
			}
			Melder_throw (U"No object with that name.");
		} else {
			/*
				Find the object by its ID.
			*/
			double value;
			Interpreter_numericExpression (interpreter, string, & value);
			integer id = (integer) value;
			WHERE (ID == id)
				return IOBJECT;
			Melder_throw (U"No object with number ", id, U".");
		}
	} catch (MelderError) {
		Melder_throw (U"Object \"", string, U"\" does not exist.");
	}
}

Editor praat_findEditorFromString (conststring32 string) {
	integer IOBJECT;
	while (*string == U' ')
		string ++;
	if (*string >= U'A' && *string <= U'Z') {
		WHERE_DOWN (1) {
			for (int ieditor = 0; ieditor < praat_MAXNUM_EDITORS; ieditor ++) {
				Editor editor = theCurrentPraatObjects -> list [IOBJECT]. editors [ieditor];
				if (editor) {
					Melder_assert (editor -> name);
					const char32 *space = str32chr (editor -> name.get(), U' ');   // editors tend to be called like "3. Sound kanweg"
					if (space) {   // but not all
						conststring32 name = space + 1;
						if (str32equ (name, string))
							return editor;
					}
				}
			}
		}
	} else {
		WHERE_DOWN (1) {
			for (int ieditor = 0; ieditor < praat_MAXNUM_EDITORS; ieditor ++) {
				Editor editor = theCurrentPraatObjects -> list [IOBJECT]. editors [ieditor];
				if (editor && str32equ (editor -> name.get(), string))
					return editor;
			}
		}
	}
	Melder_throw (U"Editor \"", string, U"\" does not exist.");
}

Editor praat_findEditorById (integer id) {
	integer IOBJECT;
	WHERE (1) {
		if (ID == id) {
			for (int ieditor = 0; ieditor < praat_MAXNUM_EDITORS; ieditor ++) {
				Editor editor = theCurrentPraatObjects -> list [IOBJECT]. editors [ieditor];
				if (editor)
					return editor;
			}
		}
	}
	Melder_throw (U"Editor ", id, U" does not exist.");
}

static int parseCommaSeparatedArguments (Interpreter interpreter, char32 *arguments, structStackel *args) {
	int narg = 0, depth = 0;
	for (char32 *p = arguments; ; p ++) {
		bool endOfArguments = *p == U'\0';
		if (endOfArguments || (*p == U',' && depth == 0)) {
			if (narg == MAXIMUM_NUMBER_OF_FIELDS)
				Melder_throw (U"Cannot have more than ", MAXIMUM_NUMBER_OF_FIELDS, U" arguments");
			*p = U'\0';
			Formula_Result result;
			Interpreter_anyExpression (interpreter, arguments, & result);
			narg ++;
			/*
				First remove the old contents.
			*/
			args [narg]. reset();
			#if STACKEL_VARIANTS_ARE_PACKED_IN_A_UNION
				memset (& args [narg], 0, sizeof (structStackel));
			#endif
			/*
				Then copy in the new contents.
			*/
			switch (result. expressionType) {
				case kFormula_EXPRESSION_TYPE_NUMERIC: {
					args [narg]. which = Stackel_NUMBER;
					args [narg]. number = result. numericResult;
				} break; case kFormula_EXPRESSION_TYPE_STRING: {
					args [narg]. setString (result. stringResult.move());
				} break; case kFormula_EXPRESSION_TYPE_NUMERIC_VECTOR: {
					args [narg]. which = Stackel_NUMERIC_VECTOR;
					args [narg]. numericVector = result. numericVectorResult;
					args [narg]. owned = result. owned;
					result. owned = false;
				} break; case kFormula_EXPRESSION_TYPE_NUMERIC_MATRIX: {
					args [narg]. which = Stackel_NUMERIC_MATRIX;
					args [narg]. numericMatrix = result. numericMatrixResult;
					args [narg]. owned = result. owned;
					result. owned = false;
				} break; case kFormula_EXPRESSION_TYPE_STRING_ARRAY: {
					args [narg]. which = Stackel_STRING_ARRAY;
					args [narg]. stringArray = result. stringArrayResult;
					args [narg]. owned = result. owned;
					result. owned = false;
				} break;
			}
			arguments = p + 1;
		} else if (*p == U'(' || *p == U'[' || *p == U'{') {
			depth ++;
		} else if (*p == U')' || *p == U']' || *p == U'}') {
			depth --;
		} else if (*p == U'\"') {
			for (;;) {
				p ++;
				if (*p == U'\"') {
					if (p [1] == U'\"')
						p ++;
					else
						break;
				}
			}
		}
		if (endOfArguments)
			break;
	}
	return narg;
}

bool praat_executeCommand (Interpreter interpreter, char32 *command) {
	if (interpreter)
		interpreter -> returnType = kInterpreter_ReturnType::VOID_;   // clear return type to its default

	static struct structStackel args [1 + MAXIMUM_NUMBER_OF_FIELDS];
	//trace (U"praat_executeCommand: ", Melder_pointer (interpreter), U": ", command);
	if (command [0] == U'\0' || command [0] == U'#' || command [0] == U'!' || command [0] == U';')
		/* Skip empty lines and comments. */;
	else if ((command [0] == U'.' || command [0] == U'+' || command [0] == U'-') && Melder_isAsciiUpperCaseLetter (command [1])) {   // selection?
		integer IOBJECT = praat_findObjectFromString (interpreter, command + 1);
		if (command [0] == '.')
			praat_deselectAll ();
		if (command [0] == '-')
			praat_deselect (IOBJECT);
		else
			praat_select (IOBJECT); 
		praat_show ();
	} else if (Melder_isLetter (command [0]) && ! Melder_isUpperCaseLetter (command [0])) {   // all directives start with an ASCII lower-case letter
		if (str32nequ (command, U"select ", 7)) {
			if (str32nequ (command + 7, U"all", 3) && (command [10] == U'\0' || command [10] == U' ' || command [10] == U'\t')) {
				praat_selectAll ();
				praat_show ();
			} else {
				integer IOBJECT = praat_findObjectFromString (interpreter, command + 7);
				praat_deselectAll ();
				praat_select (IOBJECT);
				praat_show ();
			}
		} else if (str32nequ (command, U"plus ", 5)) {
			integer IOBJECT = praat_findObjectFromString (interpreter, command + 5);
			praat_select (IOBJECT);
			praat_show ();
		} else if (str32nequ (command, U"minus ", 6)) {
			integer IOBJECT = praat_findObjectFromString (interpreter, command + 6);
			praat_deselect (IOBJECT);
			praat_show ();
		} else if (str32nequ (command, U"echo ", 5)) {
			MelderInfo_open ();
			MelderInfo_write (command + 5);
			MelderInfo_close ();
		} else if (str32nequ (command, U"clearinfo", 9)) {
			Melder_clearInfo ();
		} else if (str32nequ (command, U"print ", 6)) {
			MelderInfo_write (command + 6);
			MelderInfo_drain ();
		} else if (str32nequ (command, U"printtab", 8)) {
			MelderInfo_write (U"\t");
			MelderInfo_drain ();
		} else if (str32nequ (command, U"printline", 9)) {
			if (command [9] == ' ') MelderInfo_write (command + 10);
			MelderInfo_write (U"\n");
			MelderInfo_drain ();
		} else if (str32nequ (command, U"fappendinfo ", 12)) {
			if (theCurrentPraatObjects != & theForegroundPraatObjects)
				Melder_throw (U"The script command “fappendinfo” is not available inside pictures.");
			structMelderFile file { };
			Melder_relativePathToFile (command + 12, & file);
			MelderFile_appendText (& file, Melder_getInfo ());
		} else if (str32nequ (command, U"unix ", 5)) {
			Melder_require (praat_commandsWithExternalSideEffectsAreAllowed (),
				U"The script command “unix” is not available inside manuals.");
			try {
				Melder_runSystem (command + 5);
			} catch (MelderError) {
				Melder_throw (U"Unix command “", command + 5, U"” returned error status;\n"
					U"if you want to ignore this, use `unix_nocheck' instead of `unix'.");
			}
		} else if (str32nequ (command, U"unix_nocheck ", 13)) {
			Melder_require (praat_commandsWithExternalSideEffectsAreAllowed (),
				U"The script command “unix_nocheck” is not available inside manuals.");
			try {
				Melder_runSystem (command + 13);
			} catch (MelderError) {
				Melder_clearError ();
			}
		} else if (str32nequ (command, U"system ", 7)) {
			Melder_require (praat_commandsWithExternalSideEffectsAreAllowed (),
				U"The script command “system” is not available inside manuals.");
			try {
				Melder_runSystem (command + 7);
			} catch (MelderError) {
				Melder_throw (U"System command “", command + 7, U"” returned error status;\n"
					U"if you want to ignore this, use “system_nocheck” instead of “system”.");
			}
		} else if (str32nequ (command, U"system_nocheck ", 15)) {
			Melder_require (praat_commandsWithExternalSideEffectsAreAllowed (),
				U"The script command “system_nocheck” is not available inside manuals.");
			try {
				Melder_runSystem (command + 15);
			} catch (MelderError) {
				Melder_clearError ();
			}
		} else if (str32nequ (command, U"nowarn ", 7)) {
			autoMelderWarningOff nowarn;
			return praat_executeCommand (interpreter, command + 7);
		} else if (str32nequ (command, U"noprogress ", 11)) {
			autoMelderProgressOff noprogress;
			return praat_executeCommand (interpreter, command + 11);
		} else if (str32nequ (command, U"nocheck ", 8)) {
			try {
				return praat_executeCommand (interpreter, command + 8);
			} catch (MelderError) {
				Melder_clearError ();
				return false;
			}
		} else if (str32nequ (command, U"demo ", 5)) {
			autoDemoOpen demo;
			return praat_executeCommand (interpreter, command + 5);
		} else if (str32nequ (command, U"asynchronous ", 13)) {
			autoMelderAsynchronous asynchronous;
			return praat_executeCommand (interpreter, command + 13);
		} else if (str32nequ (command, U"pause ", 6) || str32equ (command, U"pause")) {
			if (theCurrentPraatApplication -> batch)
				return true;   // in batch we ignore pause statements
			const Editor optionalPauseWindowOwningEditor = interpreter -> optionalDynamicEnvironmentEditor();
			const GuiWindow parentShell = ( optionalPauseWindowOwningEditor ? optionalPauseWindowOwningEditor -> windowForm : theCurrentPraatApplication -> topShell );
			UiPause_begin (parentShell, optionalPauseWindowOwningEditor, U"stop or continue", interpreter);
			UiPause_comment (str32equ (command, U"pause") ? U"..." : command + 6);
			UiPause_end (1, 1, 0, U"Continue", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, interpreter);
		} else if (str32nequ (command, U"execute ", 8)) {
			praat_executeScriptFromFileNameWithArguments (interpreter, command + 8);
		} else if (str32nequ (command, U"editor", 6)) {   // deprecated
			Melder_require (praat_commandsWithExternalSideEffectsAreAllowed (),
				U"The script command “editor” is not available inside manuals.");
			if (command [6] == U' ' && Melder_isLetter (command [7])) {
				interpreter -> setDynamicEditorEnvironmentFromEditor (praat_findEditorFromString (command + 7));
			} else if (command [6] == U'\0') {
				if (interpreter && interpreter -> wasStartedFromEditorEnvironment())
					/*
						The script directive
							editor
						(without arguments) should set the interpreter's environment back to the editor that started the script.
					*/
					interpreter -> setDynamicFromOwningEditorEnvironment();   // TODO: what if the owener has been orphaned?
				else
					Melder_throw (U"The script command “editor” requires an argument when called from outside an editor.");
			} else {
				Interpreter_voidExpression (interpreter, command);
			}
		} else if (str32nequ (command, U"endeditor", 9)) {
			Melder_require (praat_commandsWithExternalSideEffectsAreAllowed (),
				U"The script command “endeditor” is not available inside manuals.");
			interpreter -> nullifyDynamicEditorEnvironment();
		} else if (str32nequ (command, U"filedelete ", 11)) {
			Melder_require (praat_commandsWithExternalSideEffectsAreAllowed (),
				U"The script command “filedelete” is not available inside manuals.");
			const char32 *p = command + 11;
			structMelderFile file { };
			while (*p == U' ' || *p == U'\t') p ++;
			if (*p == U'\0')
				Melder_throw (U"Missing file name after “filedelete”.");
			Melder_relativePathToFile (p, & file);
			MelderFile_delete (& file);
		} else if (str32nequ (command, U"fileappend ", 11)) {
			Melder_require (praat_commandsWithExternalSideEffectsAreAllowed (),
				U"The script command “fileappend” is not available inside manuals.");
			const char32 *p = command + 11;
			char32 path [kMelder_MAXPATH+1], *q = & path [0];
			while (*p == U' ' || *p == U'\t') p ++;
			if (*p == U'\0')
				Melder_throw (U"Missing file name after “fileappend”.");
			if (*p == '\"') {
				for (;;) {
					char32 kar = * ++ p;
					if (kar == U'\"') if (* ++ p == U'\"') *q ++ = U'\"'; else break;
					else if (kar == U'\0') break;
					else *q ++ = kar;
				}
			} else {
				for (;;) {
					char32 kar = * p;
					if (kar == U'\0' || kar == U' ' || kar == U'\t') break;
					*q ++ = kar;
					p ++;
				}
			}
			*q = U'\0';
			if (*p == U' ' || *p == U'\t') {
				structMelderFile file { };
				Melder_relativePathToFile (path, & file);
				MelderFile_appendText (& file, p + 1);
			}
		} else {
			/*
				This must be a formula command:
					proc (args)
			*/
			Interpreter_voidExpression (interpreter, command);
		}
	} else {   /* Simulate menu choice. */
		bool hasDots = false, hasColon = false;

 		/* Parse command line into command and arguments. */
		/* The separation is formed by the three dots or a colon. */

		char32 *arguments = & command [0];
		for (arguments = & command [0]; *arguments != U'\0'; arguments ++) {
			if (*arguments == U':') {
				hasColon = true;
				if (arguments [1] == U'\0') {
					arguments = & arguments [1];   // empty string
				} else {
					if (arguments [1] != U' ')
						Melder_throw (U"There should be a space after the colon.");
					arguments [1] = U'\0';   // new end of "command"
					arguments += 2;   // the arguments start after the space
				}
				break;
			}
			if (*arguments == U'.' && arguments [1] == U'.' && arguments [2] == U'.') {
				hasDots = true;
				arguments += 3;
				if (*arguments == U'\0') {
					// empty string
				} else {
					if (*arguments != U' ')
						Melder_throw (U"There should be a space after the three dots.");
					*arguments = U'\0';   // new end of "command"
					arguments ++;   // the arguments start after the space
				}
				break;
			}
		}

		/* See if command exists and is available; ignore separators. */
		/* First try loose commands, then fixed commands. */

		integer narg;
		char32 command2 [200];
		if (hasColon) {
			narg = parseCommaSeparatedArguments (interpreter, arguments, args);
			str32cpy (command2, command);
			char32 *colon = str32chr (command2, U':');
			colon [0] = colon [1] = colon [2] = U'.';
			colon [3] = U'\0';
		}
		if (theCurrentPraatObjects == & theForegroundPraatObjects && interpreter && interpreter -> hasDynamicEnvironmentEditor()) {
			if (hasColon)
				Editor_doMenuCommand (interpreter -> optionalDynamicEnvironmentEditor(), command2, narg, args, nullptr, interpreter);
			else
				Editor_doMenuCommand (interpreter -> optionalDynamicEnvironmentEditor(), command, 0, nullptr, arguments, interpreter);
		} else if (! praat_commandsWithExternalSideEffectsAreAllowed () &&
		    (str32nequ (command, U"Save ", 5) ||
			 str32nequ (command, U"Write ", 6) ||
			 str32nequ (command, U"Append to ", 10) ||
			 str32equ (command, U"Quit")))
		{
			Melder_throw (U"Commands that write files (including Quit) are not available inside manuals.");
		} else {
			bool theCommandIsAnExistingAction = false;
			try {
				if (hasColon)
					theCommandIsAnExistingAction = praat_doAction (command2, narg, args, interpreter);
				else
					theCommandIsAnExistingAction = praat_doAction (command, arguments, interpreter);
			} catch (MelderError) {
				/*
					We only get here if the command *was* an existing action.
					Anything could have gone wrong in its execution,
					but one invisible problem can be checked here.
				*/
				if (hasDots && arguments [0] != U'\0' && arguments [Melder_length (arguments) - 1] == U' ')
					Melder_throw (U"It may be helpful to remove the trailing spaces in \"", arguments, U"\".");
				else
					throw;
			}
			if (! theCommandIsAnExistingAction) {
				bool theCommandIsAnExistingMenuCommand = false;
				try {
					if (hasColon)
						theCommandIsAnExistingMenuCommand = praat_doMenuCommand (command2, narg, args, interpreter);
					else
						theCommandIsAnExistingMenuCommand = praat_doMenuCommand (command, arguments, interpreter);
				} catch (MelderError) {
					/*
						We only get here if the command *was* an existing menu command.
						Anything could have gone wrong in its execution,
						but one invisible problem can be checked here.
					*/
					if (hasDots && arguments [0] != U'\0' && arguments [Melder_length (arguments) - 1] == U' ')
						Melder_throw (U"It may be helpful to remove the trailing spaces in “", arguments, U"”.");
					else
						throw;
				}
				if (! theCommandIsAnExistingMenuCommand) {
					const integer length = Melder_length (command);
					if (str32nequ (command, U"ARGS ", 5))
						Melder_throw (U"Command “ARGS” no longer supported. Instead use “form” and “endform”.");
					else if (str32chr (command, U'='))
						if (command [0] == U'_')
							Melder_throw (U"Command “", command, U"” not recognized.\n"
								U"Probable cause: you are trying to use a variable name that starts with an underscore.");
						else
							Melder_throw (U"Command “", command, U"” not recognized.\n"
								U"Probable cause: you are trying to use a variable name that starts with a capital letter.");
					else if (length >= 1 && Melder_isHorizontalSpace (command [length - 1]))
						Melder_throw (U"Command “", command, U"” not available for current selection. "
							U"It may be helpful to remove the trailing spaces.");
					else if (length >= 2 && Melder_isHorizontalSpace (command [length - 2]) && command [length - 1] == U':')
						Melder_throw (U"Command “", command, U"” not available for current selection. "
							U"It may be helpful to remove the space before the colon.");
					else if (str32nequ (command, U"\"ooTextFile\"", 12))
						Melder_throw (U"Command “", command, U"” not available for current selection. "
							U"It is possible that this file is not a Praat script but a Praat data file that you can open with “Read from file...”.");
					else {
						if (interpreter && interpreter -> wasStartedFromEditorEnvironment())
							if (interpreter -> hasDynamicEnvironmentEditor())
								Melder_throw (U"Command “", command, U"” not available in ", interpreter -> optionalDynamicEditorEnvironmentClassName(), U".");
							else
								Melder_throw (U"Command “", command, U"” not available for current selection.\n"
										U"Perhaps this is a ", interpreter -> optionalOwningEditorEnvironmentClassName(), U" command?");
						else
							Melder_throw (U"Command “", command, U"” not available for current selection.");
					}
				}
			}
		}
		praat_updateSelection ();
	}
	return true;
}

void praat_executeCommandFromStandardInput (conststring32 programName) {
	char command8 [1000];   // can be recursive
	/*
		FIXME: implement for Windows.
	*/
	for (;;) {
		printf ("%s > ", Melder_peek32to8 (programName));
		if (! fgets (command8, 999, stdin))
			Melder_throw (U"Cannot read input.");
		char *newLine = strchr (command8, '\n');
		if (newLine)
			*newLine = '\0';
		autostring32 command32 = Melder_8to32 (command8);
		try {
			(void) praat_executeCommand (nullptr, command32.get());
		} catch (MelderError) {
			Melder_flushError (programName, U": Command “", Melder_peek8to32 (command8), U"” not executed.");
		}
	}
}

void praat_executeScriptFromFile (Interpreter optionalParentInterpreter, MelderFile file, conststring32 arguments, Editor optionalInterpreterOwningEditor) {
	try {
		autostring32 text = MelderFile_readText (file);
		{// scope
			autoMelderSaveCurrentFolder saveFolder;
			autoMelderFileSetCurrentFolder folder (file);   // so that script-relative file names can be used for including include files
			Melder_includeIncludeFiles (& text);
		}   // back to the default directory of the caller
		autoInterpreter interpreter = Interpreter_createFromEnvironment (optionalParentInterpreter, optionalInterpreterOwningEditor);
		if (arguments) {
			Interpreter_readParameters (interpreter.get(), text.get());
			Interpreter_getArgumentsFromString (interpreter.get(), arguments);   // interpret caller-relative paths for infile/outfile/folder arguments
		}
		autoMelderFileSetCurrentFolder folder (file);   // so that script-relative file names can be used inside the script
		Interpreter_run (interpreter.get(), text.move(), false);
	} catch (MelderError) {
		Melder_throw (U"Script ", file, U" not completed.");
	}
}

void praat_runScript (Interpreter parentInterpreter, conststring32 fileName, integer narg, Stackel args, Editor optionalInterpreterOwningEditor) {
	structMelderFile file { };
	Melder_relativePathToFile (fileName, & file);
	try {
		autostring32 text = MelderFile_readText (& file);
		/*
			We switch between default directories no fewer than four times:
			1. runScript() tends to be called from a script that we call the "caller";
			   when we enter runScript(), the default directory is the caller's folder,
			   as was appropriate for the use of file names in the caller before runScript(),
			   which had to be interpreted relative to the caller's folder.
			2. runScript() will call a script that we call the "callee";
			   include files have to be included from the callee's folder.
			3. For expanding any infile/outfile/folder arguments to runScript(),
			   we have to be back in the caller's folder.
			4. Inside the callee, file names will have to be interpreted relative to the callee's folder.
			5. After runScript() finishes, we will have to be back in the caller's folder,
			   so that the use of file names in the caller after runScript()
			   will be interpreted relative to the caller's folder again.
		*/
		{// scope
			autoMelderFileSetCurrentFolder folder (& file);   // so that callee-relative file names can be used for including include files
			Melder_includeIncludeFiles (& text);
		}   // back to the default directory of the caller
		autoInterpreter interpreter = Interpreter_createFromEnvironment (parentInterpreter, optionalInterpreterOwningEditor);
		Interpreter_readParameters (interpreter.get(), text.get());
		Interpreter_getArgumentsFromArgs (interpreter.get(), narg, args);   // interpret caller-relative paths for infile/outfile/folder arguments
		autoScript script = Script_createFromFile (& file);
		Script_rememberDuringThisAppSession_move (script.move());
		interpreter -> scriptReference = Script_find (MelderFile_peekPath (& file));
		{// scope
			autoMelderFileSetCurrentFolder folder (& file);   // so that callee-relative file names can be used inside the script
			Interpreter_run (interpreter.get(), text.move(), false);
		}   // back to the default directory of the caller
	} catch (MelderError) {
		Melder_throw (U"Script ", & file, U" not completed.");   // don't refer to 'fileName', because its contents may have changed
	}
}

void praat_runNotebook (conststring32 fileName, integer narg, Stackel args, Editor optionalInterpreterOwningEditor) {
	structMelderFile file { };
	Melder_relativePathToFile (fileName, & file);
	try {
		autostring32 text = MelderFile_readText (& file);
		if (! Melder_startsWith (text.get(), U"\""))
			Melder_throw (U"File ", & file, U" is not a Praat notebook.");
		/*
			We switch between default directories no fewer than four times:
			1. runScript() tends to be called from a script that we call the "caller";
			   when we enter runScript(), the default directory is the caller's folder,
			   as was appropriate for the use of file names in the caller before runScript(),
			   which had to be interpreted relative to the caller's folder.
			2. runScript() will call a script that we call the "callee";
			   include files have to be included from the callee's folder.
			3. For expanding any infile/outfile/folder arguments to runScript(),
			   we have to be back in the caller's folder.
			4. Inside the callee, file names will have to be interpreted relative to the callee's folder.
			5. After runScript() finishes, we will have to be back in the caller's folder,
			   so that the use of file names in the caller after runScript()
			   will be interpreted relative to the caller's folder again.
		*/
		{// scope
			autoMelderSaveCurrentFolder saveFolder;
			autoMelderFileSetCurrentFolder folder (& file);   // so that callee-relative file names can be used for including include files
			Melder_includeIncludeFiles (& text);
		}   // back to the default directory of the caller
		autoInterpreter interpreter = Interpreter_createFromEnvironment (nullptr, optionalInterpreterOwningEditor);
		Interpreter_readParameters (interpreter.get(), text.get());
		autoMelderReadText readText = MelderReadText_createFromText (text.move());
		autoManPages manPages = ManPages_createFromText (readText.get(), & file);
		ManPage firstPage = manPages -> pages.at [1];
		autoManual manual = Manual_create (firstPage -> title.get(), interpreter.get(), manPages.releaseToAmbiguousOwner(), true, true);
		manual.releaseToUser ();
	} catch (MelderError) {
		Melder_throw (U"Notebook ", & file, U" not completed.");   // don't refer to 'fileName', because its contents may have changed
	}
}

void praat_executeScriptFromCommandLine (conststring32 fileName, integer argc, char **argv) {
	structMelderFile file { };
	Melder_relativePathToFile (fileName, & file);
	try {
		autostring32 text = MelderFile_readText (& file);
		{// scope
			autoMelderSaveCurrentFolder saveFolder;
			autoMelderFileSetCurrentFolder folder (& file);   // so that script-relative file names can be used for including include files
			Melder_includeIncludeFiles (& text);
		}   // back to the default directory of the caller
		autoInterpreter interpreter = Interpreter_createFromEnvironment (nullptr, nullptr);
		Interpreter_readParameters (interpreter.get(), text.get());
		Interpreter_getArgumentsFromCommandLine (interpreter.get(), argc, argv);   // interpret caller-relative paths for infile/outfile/folder arguments
		autoMelderFileSetCurrentFolder folder (& file);   // so that script-relative file names can be used inside the script
		Interpreter_run (interpreter.get(), text.move(), false);
	} catch (MelderError) {
		Melder_throw (U"Script ", & file, U" not completed.");   // don't refer to 'fileName', because its contents may have changed
	}
}

void praat_executeScriptFromFileNameWithArguments (Interpreter optionalParentInterpreter, conststring32 nameAndArguments) {
	char32 path [256];
	const char32 *p, *arguments;
	structMelderFile file { };
	/*
		Split into file name and arguments.
	*/
	p = nameAndArguments;
	while (*p == U' ' || *p == U'\t')
		p ++;
	if (*p == U'\"') {
		char32 *q = path;
		p ++;   // skip quote
		while (*p != U'\"' && *p != U'\0')
			* q ++ = * p ++;
		*q = U'\0';
		arguments = p;
		if (*arguments == U'\"')
			arguments ++;
		if (*arguments == U' ')
			arguments ++;
	} else {
		char32 *q = path;
		while (*p != U' ' && *p != U'\0')
			* q ++ = * p ++;
		*q = U'\0';
		arguments = p;
		if (*arguments == U' ')
			arguments ++;
	}
	Melder_relativePathToFile (path, & file);
	praat_executeScriptFromFile (optionalParentInterpreter, & file, arguments, nullptr);
}

extern "C" void praatlib_executeScript (const char *text8) {
	try {
		autoInterpreter interpreter = Interpreter_create ();
		autostring32 string = Melder_8to32 (text8);
		Interpreter_run (interpreter.get(), string.move(), false);
	} catch (MelderError) {
		Melder_throw (U"Script not completed.");
	}
}

void praat_executeScriptFromText (conststring32 text) {
	try {
		autoInterpreter interpreter = Interpreter_create ();
		autostring32 string = Melder_dup (text);   // copy, because Interpreter will change it (UGLY)
		Interpreter_run (interpreter.get(), string.move(), false);
	} catch (MelderError) {
		Melder_throw (U"Script not completed.");
	}
}

static void secondPassThroughScript (UiForm sendingForm, integer /* narg */, Stackel /* args */,
	conststring32 /* sendingString_dummy */, Interpreter /* interpreter_dummy */,
	conststring32 /* invokingButtonTitle */, bool /* modified */, void * /* closure */, Editor optionalInterpreterOwningEditor)
{
	structMelderFile file { };
	Melder_pathToFile (sendingForm -> scriptFilePath.get(), & file);
	autostring32 text = MelderFile_readText (& file);
	autoMelderFileSetCurrentFolder folder (& file);
	Melder_includeIncludeFiles (& text);
	autoInterpreter interpreter = Interpreter_createFromEnvironment (nullptr, optionalInterpreterOwningEditor);
	Interpreter_readParameters (interpreter.get(), text.get());
	Interpreter_getArgumentsFromDialog (interpreter.get(), sendingForm);
	autoPraatBackground background;
	Interpreter_run (interpreter.get(), text.move(), false);
}

static void firstPassThroughScript (MelderFile file, Editor optionalInterpreterOwningEditor, EditorCommand optionalCommand) {
	UiPause_cleanUp ();
	try {
		autostring32 text = MelderFile_readText (file);
		/*
			We switch between default directories no fewer than four times:
			1. runScript() tends to be called from a script that we call the "caller";
			   when we enter runScript(), the default directory is the caller's folder,
			   as was appropriate for the use of file names in the caller before runScript(),
			   which had to be interpreted relative to the caller's folder.
			2. runScript() will call a script that we call the "callee";
			   include files have to be included from the callee's folder.
			3. For expanding any infile/outfile/folder arguments to runScript(),
			   we have to be back in the caller's folder.
			4. Inside the callee, file names will have to be interpreted relative to the callee's folder.
			5. After runScript() finishes, we will have to be back in the caller's folder,
			   so that the use of file names in the caller after runScript()
			   will be interpreted relative to the caller's folder again.
		*/
		{// scope
			autoMelderFileSetCurrentFolder folder (file);   // so that callee-relative file names can be used for including include files
			Melder_includeIncludeFiles (& text);
		}   // back to the default directory of the caller

		static autoInterpreter interpreter;   // SMELL
		interpreter = Interpreter_createFromEnvironment (nullptr, optionalInterpreterOwningEditor);

		autoScript script = Script_createFromFile (file);
		Script_rememberDuringThisAppSession_move (script.move());
		interpreter -> scriptReference = Script_find (MelderFile_peekPath (file));

		const integer numberOfParameters = Interpreter_readParameters (interpreter.get(), text.get());
		if (numberOfParameters > 0) {
			const GuiWindow parentShell = ( optionalInterpreterOwningEditor ? optionalInterpreterOwningEditor -> windowForm : theCurrentPraatApplication -> topShell );
			autoUiForm form = Interpreter_createForm (interpreter.get(), parentShell, optionalInterpreterOwningEditor,
					MelderFile_peekPath (file), secondPassThroughScript, nullptr, false);
			UiForm_do (form.get(), false);
			if (optionalCommand) {
				optionalCommand -> d_uiform = form.move();
			} else {
				UiForm_destroyWhenUnmanaged (form.get());
				form. releaseToUser();
			}
		} else {
			autoPraatBackground background;
			//praat_executeScriptFromFile (nullptr, file, nullptr, optionalInterpreterOwningEditor);
			{// scope
				autoMelderFileSetCurrentFolder folder (file);   // so that callee-relative file names can be used inside the script
				Interpreter_run (interpreter.get(), text.move(), false);
			}
		}
	} catch (MelderError) {
		Melder_throw (U"Script ", file, U" not completed.");
	}
}

void DO_RunTheScriptFromAnyAddedMenuCommand (UiForm /* sendingForm_dummy */, integer /* narg */, Stackel /* args */,
	conststring32 scriptPath, Interpreter /* interpreter */,
	conststring32 /* invokingButtonTitle */, bool /* modified */, void *, Editor optionalInterpreterOwningEditor)
{
	structMelderFile file { };
	Melder_relativePathToFile (scriptPath, & file);
	firstPassThroughScript (& file, optionalInterpreterOwningEditor, nullptr);
}

void praat_runScriptWithForm (conststring32 fileName) {
	structMelderFile file { };
	Melder_relativePathToFile (fileName, & file);
	firstPassThroughScript (& file, nullptr, nullptr);
}

void praat_executeScriptFromEditorCommand (Editor interpreterOwningEditor, EditorCommand command, conststring32 scriptPath) {
	Melder_assert (interpreterOwningEditor);
	Melder_assert (command);
	Melder_assert (command -> d_editor == interpreterOwningEditor);
	structMelderFile file { };
	Melder_relativePathToFile (scriptPath, & file);
	firstPassThroughScript (& file, interpreterOwningEditor, command);
}

/* End of file praat_script.cpp */
