package org.geogebra.common.plugin.script;

import java.util.ArrayList;

import org.geogebra.common.euclidian.SymbolicEditor;
import org.geogebra.common.kernel.arithmetic.MyDouble;
import org.geogebra.common.kernel.commands.AlgebraProcessor;
import org.geogebra.common.kernel.geos.inputbox.EditorContent;
import org.geogebra.common.kernel.kernelND.GeoElementND;
import org.geogebra.common.main.App;
import org.geogebra.common.plugin.Event;
import org.geogebra.common.plugin.EventType;
import org.geogebra.common.plugin.ScriptError;
import org.geogebra.common.plugin.ScriptType;
import org.geogebra.common.util.StringUtil;

/**
 * @author arno Script class for GgbScript scripts
 */
public class GgbScript extends Script {

	private AlgebraProcessor proc;

	/**
	 * @param app
	 *            the script's application
	 * @param scriptText
	 *            the script's source code
	 */
	public GgbScript(App app, String scriptText) {
		super(app, scriptText);
		this.proc = app.getKernel().getAlgebraProcessor();
	}

	@Override
	public String getText() {
		return script2LocalizedScript(app, text);
	}

	@Override
	public boolean run(Event evt) throws ScriptError {
		String scriptText;
		if (text == null) {
			return true;
		}
		scriptText = substitutePlaceholders(text, evt);
		String[] lines = scriptText.split("\n");
		boolean success = true;
		for (int i = 0; i < lines.length; i++) {
			String line = lines[i].trim();
			if ("".equals(line) || line.charAt(0) == '#') {
				continue;
			}
			try {
				GeoElementND[] res = proc
						.processAlgebraCommandNoExceptionHandling(line, false,
								new ScriptErrorHandler(app, evt, i), false,
								null);
				success = success && res != null;
			} catch (Throwable e) {
				// error handler should actually catch it
			}
		}
		return success;
	}

	private String substitutePlaceholders(String text, Event evt) {
		String scriptText = text;
		if (evt.argument != null) {
			scriptText = scriptText.replaceAll("%0", evt.argument);
		}
		if (evt.type == EventType.EDITOR_KEY_TYPED
				&& (scriptText.contains("%1") || scriptText.contains("%2"))) {
			SymbolicEditor editor = getEditor(evt.argument);
			if (editor != null) {
				scriptText = replacePlaceholdersFromEditor(scriptText, editor);
			}
		}
		return scriptText;
	}

	private String replacePlaceholdersFromEditor(String scriptText, SymbolicEditor editor) {
		EditorContent editorState = editor.getEditorStateWithQuestionMarks();
		StringBuilder content = new StringBuilder();
		boolean valid = editor.getGeoInputBox().validate(editorState, content);
		return scriptText.replace("%1", content.toString())
				.replace("%2", String.valueOf(valid));
	}

	private SymbolicEditor getEditor(String argument) {
		SymbolicEditor editor = app.getActiveEuclidianView().getSymbolicEditor();
		if (editor == null || editor.getGeoInputBox() == null
				|| !argument.equals(editor.getGeoInputBox().getLabelSimple())) {
			return null;
		}
		return editor;
	}

	/**
	 * Localize the script.
	 * 
	 * @param app
	 *            app
	 * @param st
	 *            script with internal commands
	 * @return localized script
	 */
	public static String script2LocalizedScript(App app, String st) {
		final String[] starr = splitScriptByCommands(st);
		boolean isAssignment = false;
		for (String string : starr) {
			if (":=".equals(string)) {
				isAssignment = true;
				break;
			}
		}
		final StringBuilder retone = new StringBuilder();
		for (int i = 0; i < starr.length; i++) {
			if ((i % 2) == 0 || isFunction(starr, i, app)) {
				retone.append(starr[i]);
				// app.getFunction("nroot")
			} else if (app.getParserFunctions().isTranslatableFunction(starr[i])) {
				retone.append(app.getLocalization().getFunction(starr[i]));
			} else {
				// Log.debug("NOT FUNCTION" + starr[i]);
				// do not translate name of function
				// see #4391
				if (i == 1 && isAssignment) {
					retone.append(starr[i]);
				} else {
					retone.append(app.getLocalization().getCommand(starr[i]));
				}
			}
		}
		return retone.toString();
	}

	private static boolean isFunction(String[] starr, int i, App app) {
		if (i >= starr.length - 1 || starr[i + 1].startsWith("[")) {
			return false;
		}
		return app.getKernel().lookupLabel(starr[i]) != null;
	}

	/**
	 * Delocalize a script
	 * 
	 * @param app
	 *            the application
	 * @param st
	 *            the script text
	 * @return the text of the delocalized script
	 */
	public static String localizedScript2Script(App app, String st) {
		final String[] starr = splitScriptByCommands(st);
		boolean isAssignment = false;
		for (String string : starr) {
			if (":=".equals(string)) {
				isAssignment = true;
				break;
			}
		}
		final StringBuilder retone = new StringBuilder();
		for (int i = 0; i < starr.length; i++) {
			if ((i % 2) == 0) {
				retone.append(starr[i]);
			} else {
				// allow English language command in French scripts
				if (isFunction(starr, i, app)) {
					retone.append(starr[i]);
				}
				// do not translate function name
				// e.g. for German panel in Gerade(t)
				// do not translate Gerade to Line
				// needed for #4391
				else if (app.getInternalCommand(starr[i]) != null) {
					if (i == 1 && isAssignment) {
						retone.append(starr[i]);
					} else {
						retone.append(app.getInternalCommand(starr[i]));
					}
				} else if (app.getParserFunctions()
						.getInternal(app.getLocalization(), starr[i]) != null) {
					retone.append(app.getParserFunctions()
							.getInternal(app.getLocalization(), starr[i]));
				} else {
					// fallback for wrong call in English already
					// or if someone writes an English command into an
					// other language script
					retone.append(starr[i]);
				}
			}
		}
		return retone.toString();
	}

	/**
	 * This method should split a GeoGebra script into the following format: ""
	 * or "something"; "command"; "something"; "command"; "something"; ...
	 * 
	 * @param st
	 *            String GeoGebra script
	 * @return String [] the GeoGebra script split into and array
	 */
	private static String[] splitScriptByCommands(final String st) {

		StringBuilder retone = new StringBuilder();
		final ArrayList<String> ret = new ArrayList<>();

		// as the other algorithms would be too complicated,
		// just go from the end of the string and advance character by character

		// at first count the number of "s to decide how to start the algorithm
		int countapo = 0;
		for (int j = 0; j < st.length(); j++) {
			if (st.charAt(j) == '"') {
				countapo++;
			}
		}

		boolean in_string = false;
		if (MyDouble.isOdd(countapo)) {
			in_string = true;
		}

		boolean before_bracket = false;
		boolean just_before_bracket = false;
		for (int i = st.length() - 1; i >= 0; i--) {
			if (in_string) {
				if (st.charAt(i) == '"') {
					in_string = false;
				}
			} else if (just_before_bracket) {
				if (StringUtil.isLetterOrDigitOrUnderscore(st.charAt(i))) {
					ret.add(0, retone.toString());
					retone = new StringBuilder();
					just_before_bracket = false;
					before_bracket = true;
				} else if (!bracketAt(st, i) && (st.charAt(i) != ' ')) {
					just_before_bracket = false;
					before_bracket = false;
					if (st.charAt(i) == '"') {
						in_string = true;
					}
				}
			} else if (before_bracket) {
				if (!StringUtil.isLetterOrDigitOrUnderscore(st.charAt(i))) {
					ret.add(0, retone.toString());
					retone = new StringBuilder();
					before_bracket = false;
					if (st.charAt(i) == '"') {
						in_string = true;
					} else if (bracketAt(st, i)) {
						just_before_bracket = true;
					}
				}
			} else {
				if (st.charAt(i) == '"') {
					in_string = true;
				} else if (bracketAt(st, i)) {
					just_before_bracket = true;
				}
			}
			retone.insert(0, st.charAt(i));
		}
		ret.add(0, retone.toString());
		if (before_bracket) {
			ret.add(0, "");
		}
		final String[] ex = { "" };
		return ret.toArray(ex);
	}

	private static boolean bracketAt(String st, int i) {
		return (st.charAt(i) == '[') || (st.charAt(i) == '(');
	}

	@Override
	public ScriptType getType() {
		return ScriptType.GGBSCRIPT;
	}

	@Override
	public Script copy() {
		return new GgbScript(app, text);
	}

	/**
	 * The text of this script is modified by changing every whole word oldLabel
	 * to newLabel.
	 * 
	 * @return whether any renaming happened
	 */
	@Override
	public boolean renameGeo(String oldLabel, String newLabel) {
		if (oldLabel == null || "".equals(oldLabel) || newLabel == null
				|| "".equals(newLabel)) {
			return false;
		}
		ArrayList<String> work = StringUtil.wholeWordTokenize(text);
		boolean ret = false;
		int numChars = 0, lengthChars;
		String forLength1, forLength2;
		for (int i = 1; i < work.size(); i += 2) {
			if (work.get(i - 1) != null) {
				// this is even, so will not be changed,
				// because only odd places are checked and replaced
				numChars += work.get(i - 1).length();
			}
			if (oldLabel.equals(work.get(i))) {
				// in theory, i+1 is always less than work.size(),
				// because it is an odd number, and in theory,
				// there is at least a non-null element there
				// but better to check...
				if (i + 1 < work.size() && work.get(i + 1) != null) {
					if ((work.get(i + 1).length() > 0)
							&& '[' == work.get(i + 1).charAt(0)) {
						// Now it's still possible that oldLabel
						// is used as a command name here,
						// so we have to rule out that possibility first.
						// Luckily, command names are always followed
						// by a [, as far as we know, so it is easy.
						numChars += oldLabel.length();
						continue;
					}
				}

				// We also have to rule out the case when
				// the string is used as a "string",
				// or used as e.g. "blabla !!* string ---+ ".
				// The easiest way to do that is to count the
				// number of " signs in the string until our
				// string, except the \" signs, and if it's odd,
				// then we're probably in a string.
				// For this computation, we use numChars.

				forLength1 = text.substring(0, numChars).replaceAll("\\\"", "");
				forLength2 = forLength1.replaceAll("\"", "");
				lengthChars = forLength1.length() - forLength2.length();

				if (MyDouble.isOdd(lengthChars)) {
					// there is an odd number of living " signs before this
					// whole-word token, and this means that we're in a
					// string, so this whole-word token doesn't matter
					numChars += oldLabel.length();
					continue;
				}

				// If the string equals to "e" or "i",
				// and we get here, then they mean their
				// geo labels, but only after they get
				// them, and if they get them in the script,
				// then some original "e" or "i" constants
				// may be renamed as well... however, we
				// guess it's no problem as in the second
				// running of the script they should all mean
				// the geos, and it's the bug of the code elsewhere

				// numChars should be reassigned after it is used,
				// but still before "work" is redefined
				if (work.get(i) != null) {
					numChars += work.get(i).length();
				}
				// this is really something to be renamed!
				// ...or not? TODO: check
				work.set(i, newLabel);
				ret = true;
			} else if (work.get(i) != null) {
				numChars += work.get(i).length();
			}
		}
		text = StringUtil.joinTokens(work, null);
		return ret;
	}
}
