/**
 *   This file is part of Skript.
 *
 *  Skript 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.
 *
 *  Skript 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 Skript.  If not, see <http://www.gnu.org/licenses/>.
 *
 *
 * Copyright 2011-2017 Peter Güttinger and contributors
 */
package ch.njol.skript.lang.function;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import ch.njol.skript.ScriptLoader;
import org.eclipse.jdt.annotation.Nullable;

import ch.njol.skript.Skript;
import ch.njol.skript.SkriptAPIException;
import ch.njol.skript.SkriptAddon;
import ch.njol.skript.classes.ClassInfo;
import ch.njol.skript.config.SectionNode;
import ch.njol.skript.lang.ParseContext;
import ch.njol.skript.lang.SkriptParser;
import ch.njol.skript.lang.Trigger;
import ch.njol.skript.lang.function.Namespace.Key;
import ch.njol.skript.log.ParseLogHandler;
import ch.njol.skript.log.SkriptLogger;
import ch.njol.skript.registrations.Classes;
import ch.njol.skript.util.Utils;
import ch.njol.util.NonNullPair;
import ch.njol.util.StringUtils;

/**
 * Static methods to work with functions.
 */
public abstract class Functions {

	private static final String INVALID_FUNCTION_DEFINITION =
			"Invalid function definition. Please check for " +
					"typos and make sure that the function's name " +
					"only contains letters and underscores. " +
					"Refer to the documentation for more information.";

	private Functions() {}
	
	final static class FunctionData {
		final Function<?> function;
		
		public FunctionData(final Function<?> function) {
			this.function = function;
		}
	}
	
	@Nullable
	public static ScriptFunction<?> currentFunction = null;
	
	/**
	 * Function namespaces.
	 */
	private static final Map<Namespace.Key, Namespace> namespaces = new HashMap<>();
	
	/**
	 * Namespace of Java functions.
	 */
	private static final Namespace javaNamespace;
		
	static {
		javaNamespace = new Namespace();
		namespaces.put(new Namespace.Key(Namespace.Origin.JAVA, "unknown"), javaNamespace);
	}
	
	/**
	 * Namespaces of functions that are globally available.
	 */
	private static final Map<String, Namespace> globalFunctions = new HashMap<>();
		
	static boolean callFunctionEvents = false;
	
	/**
	 * Registers a function written in Java.
	 * @param function
	 * @return The passed function
	 */
	public static JavaFunction<?> registerFunction(JavaFunction<?> function) {
		Skript.checkAcceptRegistrations();
		String name = function.getName();
		if (!name.matches(functionNamePattern))
			throw new SkriptAPIException("Invalid function name '" + name + "'");
		javaNamespace.addSignature(function.getSignature());
		javaNamespace.addFunction(function);
		globalFunctions.put(function.getName(), javaNamespace);
		
		return function;
	}
	
	public final static String functionNamePattern = "[\\p{IsAlphabetic}][\\p{IsAlphabetic}\\p{IsDigit}_]*";
	
	@SuppressWarnings("null")
	private final static Pattern functionPattern = Pattern.compile("function (" + functionNamePattern + ")\\((.*)\\)(?: :: (.+))?", Pattern.CASE_INSENSITIVE),
			paramPattern = Pattern.compile("\\s*(.+?)\\s*:(?=[^:]*$)\\s*(.+?)(?:\\s*=\\s*(.+))?\\s*");
	
	/**
	 * Loads a script function from given node.
	 * @param node Section node.
	 * @return Script function, or null if something went wrong.
	 */
	@Nullable
	public static Function<?> loadFunction(SectionNode node) {
		SkriptLogger.setNode(node);
		final String key = node.getKey();
		final String definition = ScriptLoader.replaceOptions(key == null ? "" : key);
		assert definition != null;
		final Matcher m = functionPattern.matcher(definition);
		if (!m.matches()) // We have checks when loading the signature, but matches() must be called anyway
			return error(INVALID_FUNCTION_DEFINITION);
		final String name = "" + m.group(1);
		
		Namespace namespace = globalFunctions.get(name);
		if (namespace == null) {
			return null; // Probably duplicate signature; reported before
		}
		Signature<?> sign = namespace.getSignature(name);
		if (sign == null) // Signature parsing failed, probably: null signature
			return null; // This has been reported before...
		final Parameter<?>[] params = sign.parameters;
		final ClassInfo<?> c = sign.returnType;
		
		if (Skript.debug() || node.debug())
			Skript.debug("function " + name + "(" + StringUtils.join(params, ", ") + ")"
					+ (c != null ? " :: " + (sign.isSingle() ? c.getName().getSingular() : c.getName().getPlural()) : "") + ":");
		
		final Function<?> f = new ScriptFunction<>(sign, node);
		
		// Register the function for signature
		namespace.addFunction(f);
		
		return f;
	}
	
	/**
	 * Loads the signature of function from given node.
	 * @param script Script file name (<b>might</b> be used for some checks).
	 * @param node Section node.
	 * @return Signature of function, or null if something went wrong.
	 */
	@Nullable
	public static Signature<?> loadSignature(String script, SectionNode node) {
		SkriptLogger.setNode(node);
		final String key = node.getKey();
		final String definition = ScriptLoader.replaceOptions(key == null ? "" : key);
		assert definition != null;
		final Matcher m = functionPattern.matcher(definition);
		if (!m.matches())
			return signError(INVALID_FUNCTION_DEFINITION);
		final String name = "" + m.group(1);
		
		// Ensure there are no duplicate functions
		if (globalFunctions.containsKey(name)) {
			Namespace namespace = globalFunctions.get(name);
			if (namespace == javaNamespace) { // Special messages for built-in functions
				return signError("Function name '" + name + "' is reserved by Skript");
			} else {
				Signature<?> sign = namespace.getSignature(name);
				assert sign != null : "globalFunctions points to a wrong namespace";
				return signError("A function named '" + name + "' already exists in script '" + sign.script + "'");
			}
		}
		
		final String args = m.group(2);
		final String returnType = m.group(3);
		final List<Parameter<?>> params = new ArrayList<>();
		int j = 0;
		for (int i = 0; i <= args.length(); i = SkriptParser.next(args, i, ParseContext.DEFAULT)) {
			if (i == -1)
				return signError("Invalid text/variables/parentheses in the arguments of this function");
			if (i == args.length() || args.charAt(i) == ',') {
				final String arg = args.substring(j, i);
				
				if (arg.isEmpty()) // Zero-argument function
					break;
				
				// One ore more arguments for this function
				final Matcher n = paramPattern.matcher(arg);
				if (!n.matches())
					return signError("The " + StringUtils.fancyOrderNumber(params.size() + 1) + " argument's definition is invalid. It should look like 'name: type' or 'name: type = default value'.");
				final String paramName = "" + n.group(1);
				for (final Parameter<?> p : params) {
					if (p.name.toLowerCase(Locale.ENGLISH).equals(paramName.toLowerCase(Locale.ENGLISH)))
						return signError("Each argument's name must be unique, but the name '" + paramName + "' occurs at least twice.");
				}
				ClassInfo<?> c;
				c = Classes.getClassInfoFromUserInput("" + n.group(2));
				final NonNullPair<String, Boolean> pl = Utils.getEnglishPlural("" + n.group(2));
				if (c == null)
					c = Classes.getClassInfoFromUserInput(pl.getFirst());
				if (c == null)
					return signError("Cannot recognise the type '" + n.group(2) + "'");
				String rParamName = paramName.endsWith("*") ? paramName.substring(0, paramName.length() - 3) +
									(!pl.getSecond() ? "::1" : "") : paramName;
				final Parameter<?> p = Parameter.newInstance(rParamName, c, !pl.getSecond(), n.group(3));
				if (p == null)
					return null;
				params.add(p);
				
				j = i + 1;
			}
			if (i == args.length())
				break;
		}
		
		// Parse return type if one exists
		ClassInfo<?> returnClass;
		boolean singleReturn;
		if (returnType == null) {
			returnClass = null;
			singleReturn = false; // Ignored, nothing is returned
		} else {
			returnClass = Classes.getClassInfoFromUserInput(returnType);
			NonNullPair<String, Boolean> p = Utils.getEnglishPlural(returnType);
			singleReturn = !p.getSecond();
			if (returnClass == null)
				returnClass = Classes.getClassInfoFromUserInput(p.getFirst());
			if (returnClass == null) {
				return signError("Cannot recognise the type '" + returnType + "'");
			}
		}
		
		@SuppressWarnings({"unchecked", "null"})
		Signature<?> sign = new Signature<>(script, name, params.toArray(new Parameter[params.size()]), (ClassInfo<Object>) returnClass, singleReturn);

		// Register this signature
		Namespace.Key namespaceKey = new Namespace.Key(Namespace.Origin.SCRIPT, script);
		Namespace namespace = namespaces.computeIfAbsent(namespaceKey, k -> new Namespace());
		namespace.addSignature(sign);
		globalFunctions.put(name, namespace);
		
		Skript.debug("Registered function signature: " + name);
		return sign;
	}
	
	/**
	 * Creates an error and returns Function null.
	 * @param error Error message.
	 * @return Null.
	 */
	@Nullable
	private static Function<?> error(final String error) {
		Skript.error(error);
		return null;
	}
	
	/**
	 * Creates an error and returns Signature null.
	 * @param error Error message.
	 * @return Null.
	 */
	@Nullable
	private static Signature<?> signError(final String error) {
		Skript.error(error);
		return null;
	}
	
	/**
	 * Gets a function, if it exists. Note that even if function exists in scripts,
	 * it might not have been parsed yet. If you want to check for existance,
	 * then use {@link #getSignature(String)}.
	 * @param name Name of function.
	 * @return Function, or null if it does not exist.
	 */
	@Nullable
	public static Function<?> getFunction(String name) {
		Namespace namespace = globalFunctions.get(name);
		if (namespace == null) {
			return null;
		}
		return namespace.getFunction(name);
	}
	
	/**
	 * Gets a signature of function with given name.
	 * @param name Name of function.
	 * @return Signature, or null if function does not exist.
	 */
	@Nullable
	public static Signature<?> getSignature(String name) {
		Namespace namespace = globalFunctions.get(name);
		if (namespace == null) {
			return null;
		}
		return namespace.getSignature(name);
	}
	
	private final static Collection<FunctionReference<?>> toValidate = new ArrayList<>();
	
	/**
	 * Remember to call {@link #validateFunctions()} after calling this
	 * 
	 * @param script
	 * @return How many functions were removed
	 */
	public static int clearFunctions(String script) {
		// Get and remove function namespace of script
		Namespace namespace = namespaces.remove(new Namespace.Key(Namespace.Origin.SCRIPT, script));
		if (namespace == null) { // No functions defined
			return 0;
		}
		
		// Remove references to this namespace from global functions
		Iterator<Namespace> it = globalFunctions.values().iterator();
		while (it.hasNext()) {
			if (it.next() == namespace) {
				it.remove();
			}
		}
		
		// Queue references to signatures we have for revalidation
		// Can't validate here, because other scripts might be loaded soon
		for (Signature<?> sign : namespace.getSignatures()) {
			for (FunctionReference<?> ref : sign.calls) {
				if (!script.equals(ref.script)) {
					toValidate.add(ref);
				}
			}
		}
		return namespace.getSignatures().size();
	}
	
	public static void validateFunctions() {
		for (final FunctionReference<?> c : toValidate)
			c.validateFunction(false);
		toValidate.clear();
	}
	
	/**
	 * Clears all function calls and removes script functions.
	 */
	public static void clearFunctions() {
		// Keep Java functions, remove everything else		
		Iterator<Namespace> it = globalFunctions.values().iterator();
		while (it.hasNext()) {
			if (it.next() != javaNamespace) {
				it.remove();
			}
		}
		namespaces.clear();
		
		assert toValidate.isEmpty() : toValidate;
		toValidate.clear();
	}
	
	@SuppressWarnings({"unchecked"})
	public static Collection<JavaFunction<?>> getJavaFunctions() {
		// We know there are only Java functions in that namespace
		return (Collection<JavaFunction<?>>) (Object) javaNamespace.getFunctions();
	}
	
	/**
	 * Normally, function calls do not cause actual Bukkit events to be
	 * called. If an addon requires such functionality, it should call this
	 * method. After doing so, the events will be called. Calling this method
	 * many times will not cause any additional changes.
	 * <p>
	 * Note that calling events is not free; performance might vary
	 * once you have enabled that.
	 * 
	 * @param addon Addon instance.
	 */
	@SuppressWarnings({"null", "unused"})
	public static void enableFunctionEvents(SkriptAddon addon) {
		if (addon == null) {
			throw new SkriptAPIException("enabling function events requires addon instance");
		}
		
		callFunctionEvents = true;
	}
}
