/*******************************************************************************
 * Copyright (c) 2000, 2013 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Sergey Prigogin (Google) - Bug 421375
 *******************************************************************************/
package org.eclipse.core.internal.expressions;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleListener;

import org.w3c.dom.Element;

import org.eclipse.core.expressions.Expression;
import org.eclipse.core.expressions.ICountable;
import org.eclipse.core.expressions.IIterable;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdapterManager;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.Platform;

public class Expressions {

	/**
	 * Cache to optimize instanceof computation. Weak Map of Class->Map(String, Boolean). Avoid
	 * conflicts caused by multiple classloader contributions with the same class name. It's a rare
	 * occurrence but is supported by the OSGi classloader.
	 */
	private static WeakHashMap<Class<?>, Map<String, Boolean>> fgKnownClasses;

	/**
	 * Cache to optimize loading of classes for evaluation of adapt expressions. Keys are class
	 * loaders. Values are sets of qualified class names that the corresponding class loader was
	 * not able to find.
	 */
	private static WeakHashMap<ClassLoader, Set<String>> fgNotFoundClasses;

	/* debugging flag to enable tracing */
	public static final boolean TRACING= "true".equalsIgnoreCase(Platform.getDebugOption("org.eclipse.core.expressions/tracePropertyResolving")); //$NON-NLS-1$ //$NON-NLS-2$


	private Expressions() {
		// no instance
	}

	public static boolean isInstanceOf(Object element, String type) {
		// null isn't an instanceof of anything.
		if (element == null)
			return false;
		return isSubtype(element.getClass(), type);
	}

	private static synchronized boolean isSubtype(Class<?> clazz, String type) {
		WeakHashMap<Class<?>, Map<String, Boolean>> knownClassesMap= getKnownClasses();
		Map<String, Boolean> nameMap = knownClassesMap.get(clazz);
		if (nameMap != null) {
			Object obj = nameMap.get(type);
			if (obj != null)
				return ((Boolean)obj).booleanValue();
		}
		if (nameMap == null) {
			nameMap = new HashMap<>();
			knownClassesMap.put(clazz, nameMap);
		}
		boolean isSubtype = uncachedIsSubtype(clazz, type);
		nameMap.put(type, isSubtype ? Boolean.TRUE : Boolean.FALSE);
		return isSubtype;
	}

	/**
	 * Loads the given class using the given class loader. Uses {@link #fgNotFoundClasses} for
	 * performance.
	 *
	 * @param classLoader the class loader to use
	 * @param className the qualified name of the class
	 * @return the loaded class, or {@code null} if the class was not found
	 */
	static Class<?> loadClass(ClassLoader classLoader, String className) {
		/*
		 * Class.forName is pretty slow when it throws a ClassNotFoundException. Since expression
		 * evaluation is done very often, we use a cache of names of classes that failed to load.
		 */
		WeakHashMap<ClassLoader, Set<String>> cache;
		synchronized (Expressions.class) {
			cache = getNotFoundClasses();
			Set<String> classNames= cache.get(classLoader);
			if (classNames != null && classNames.contains(className)) {
				return null;
			}
		}

		try {
			return Class.forName(className, false, classLoader);
		} catch (ClassNotFoundException e) {
			synchronized (Expressions.class) {
				Set<String> classNames= cache.get(classLoader);
				if (classNames == null) {
					classNames= new HashSet<>();
					cache.put(classLoader, classNames);
				}
				classNames.add(className);
			}
		}
		return null;
	}

	private static WeakHashMap<Class<?>, Map<String, Boolean>> getKnownClasses() {
		createClassCaches();
		return fgKnownClasses;
	}

	private static WeakHashMap<ClassLoader, Set<String>> getNotFoundClasses() {
		createClassCaches();
		return fgNotFoundClasses;
	}

	private static void createClassCaches() {
		if (fgKnownClasses == null) {
			fgKnownClasses= new WeakHashMap<>();
			fgNotFoundClasses = new WeakHashMap<>();
			BundleContext bundleContext= ExpressionPlugin.getDefault().getBundleContext();
			BundleListener listener= new BundleListener() {
				@Override
				public void bundleChanged(BundleEvent event) {
					// Invalidate the caches if any of the bundles is stopped
					if (event.getType() == BundleEvent.STOPPED) {
						synchronized (Expressions.class) {
							fgKnownClasses.clear();
							fgNotFoundClasses.clear();
						}
					}
				}
			};
			ExpressionPlugin.fgBundleListener = listener;
			bundleContext.addBundleListener(listener);
		}
	}

	public static boolean uncachedIsSubtype(Class<?> clazz, String type) {
		if (clazz.getName().equals(type))
			return true;
		Class<?> superClass= clazz.getSuperclass();
		if (superClass != null && uncachedIsSubtype(superClass, type))
			return true;
		Class<?>[] interfaces= clazz.getInterfaces();
		for (Class<?> interfaze : interfaces) {
			if (uncachedIsSubtype(interfaze, type))
				return true;
		}
		return false;
	}

	public static void checkAttribute(String name, String value) throws CoreException {
		if (value == null) {
			throw new CoreException(new ExpressionStatus(
				ExpressionStatus.MISSING_ATTRIBUTE,
				Messages.format(ExpressionMessages.Expression_attribute_missing, name)));
		}
	}

	public static void checkAttribute(String name, String value, String[] validValues) throws CoreException {
		checkAttribute(name, value);
		for (String validValue : validValues) {
			if (value.equals(validValue))
				return;
		}
		throw new CoreException(new ExpressionStatus(
			ExpressionStatus.WRONG_ATTRIBUTE_VALUE,
			Messages.format(ExpressionMessages.Expression_attribute_invalid_value, value)));
	}

	public static void checkCollection(Object var, Expression expression) throws CoreException {
		if (var instanceof Collection)
			return;
		throw new CoreException(new ExpressionStatus(
			ExpressionStatus.VARIABLE_IS_NOT_A_COLLECTION,
			Messages.format(ExpressionMessages.Expression_variable_not_a_collection, expression.toString())));
	}

	public static void checkList(Object var, Expression expression) throws CoreException {
		if (var instanceof List)
			return;
		throw new CoreException(new ExpressionStatus(
			ExpressionStatus.VARIABLE_IS_NOT_A_LIST,
			Messages.format(ExpressionMessages.Expression_variable_not_a_list, expression.toString())));
	}

	/**
	 * Converts the given variable into an <code>IIterable</code>. If a corresponding adapter can't be found an
	 * exception is thrown. If the corresponding adapter isn't loaded yet, <code>null</code> is returned.
	 *
	 * @param var the variable to turn into an <code>IIterable</code>
	 * @param expression the expression referring to the variable
	 *
	 * @return the <code>IIterable</code> or <code>null<code> if a corresponding adapter isn't loaded yet
	 *
	 * @throws CoreException if the var can't be adapted to an <code>IIterable</code>
	 */
	public static IIterable<?> getAsIIterable(Object var, Expression expression) throws CoreException {
		if (var instanceof IIterable) {
			return (IIterable<?>)var;
		} else {
			IAdapterManager manager= Platform.getAdapterManager();
			IIterable<?> result= manager.getAdapter(var, IIterable.class);
			if (result != null)
				return result;

			if (manager.queryAdapter(var, IIterable.class.getName()) == IAdapterManager.NOT_LOADED)
				return null;

			throw new CoreException(new ExpressionStatus(
				ExpressionStatus.VARIABLE_IS_NOT_A_COLLECTION,
				Messages.format(ExpressionMessages.Expression_variable_not_iterable, expression.toString())));
		}
	}

	/**
	 * Converts the given variable into an <code>ICountable</code>. If a corresponding adapter can't be found an
	 * exception is thrown. If the corresponding adapter isn't loaded yet, <code>null</code> is returned.
	 *
	 * @param var the variable to turn into an <code>ICountable</code>
	 * @param expression the expression referring to the variable
	 *
	 * @return the <code>ICountable</code> or <code>null<code> if a corresponding adapter isn't loaded yet
	 *
	 * @throws CoreException if the var can't be adapted to an <code>ICountable</code>
	 */
	public static ICountable getAsICountable(Object var, Expression expression) throws CoreException {
		if (var instanceof ICountable) {
			return (ICountable)var;
		} else {
			IAdapterManager manager= Platform.getAdapterManager();
			ICountable result= manager.getAdapter(var, ICountable.class);
			if (result != null)
				return result;

			if (manager.queryAdapter(var, ICountable.class.getName()) == IAdapterManager.NOT_LOADED)
				return null;

			throw new CoreException(new ExpressionStatus(
				ExpressionStatus.VARIABLE_IS_NOT_A_COLLECTION,
				Messages.format(ExpressionMessages.Expression_variable_not_countable, expression.toString())));
		}
	}

	public static boolean getOptionalBooleanAttribute(IConfigurationElement element, String attributeName) {
		String value= element.getAttribute(attributeName);
		if (value == null)
			return false;
		return Boolean.valueOf(value).booleanValue();
	}

	public static boolean getOptionalBooleanAttribute(Element element, String attributeName) {
		String value= element.getAttribute(attributeName);
		if (value.length() == 0)
			return false;
		return Boolean.valueOf(value).booleanValue();
	}

	//---- Argument parsing --------------------------------------------

	public static final Object[] EMPTY_ARGS= new Object[0];

	public static Object[] getArguments(IConfigurationElement element, String attributeName) throws CoreException {
		String args= element.getAttribute(attributeName);
		if (args != null) {
			return parseArguments(args);
		} else {
			return EMPTY_ARGS;
		}
	}

	public static Object[] getArguments(Element element, String attributeName) throws CoreException {
		String args= element.getAttribute(attributeName);
		if (args.length() > 0) {
			return parseArguments(args);
		} else {
			return EMPTY_ARGS;
		}
	}

	public static Object[] parseArguments(String args) throws CoreException {
		List<Object> result= new ArrayList<>();
		int start= 0;
		int comma;
		while ((comma= findNextComma(args, start)) != -1) {
			result.add(convertArgument(args.substring(start, comma).trim()));
			start= comma + 1;
		}
		result.add(convertArgument(args.substring(start).trim()));
		return result.toArray();
	}

	private static int findNextComma(String str, int start) throws CoreException {
		boolean inString= false;
		for (int i= start; i < str.length(); i++) {
			char ch= str.charAt(i);
			if (ch == ',' && ! inString)
				return i;
			if (ch == '\'') {
				if (!inString) {
					inString= true;
				} else {
					if (i + 1 < str.length() && str.charAt(i + 1) == '\'') {
						i++;
					} else {
						inString= false;
					}
				}
			} else if (ch == ',' && !inString) {
				return i;
			}
		}
		if (inString)
			throw new CoreException(new ExpressionStatus(
				ExpressionStatus.STRING_NOT_TERMINATED,
				Messages.format(ExpressionMessages.Expression_string_not_terminated, str)));

		return -1;
	}

	public static Object convertArgument(String arg) throws CoreException {
		if (arg == null) {
			return null;
		} else if (arg.length() == 0) {
			return arg;
		} else if (arg.charAt(0) == '\'' && arg.charAt(arg.length() - 1) == '\'') {
			return unEscapeString(arg.substring(1, arg.length() - 1));
		} else if ("true".equals(arg)) { //$NON-NLS-1$
			return Boolean.TRUE;
		} else if ("false".equals(arg)) { //$NON-NLS-1$
			return Boolean.FALSE;
		} else if (arg.indexOf('.') != -1) {
			try {
				return Float.valueOf(arg);
			} catch (NumberFormatException e) {
				return arg;
			}
		} else {
			try {
				return Integer.valueOf(arg);
			} catch (NumberFormatException e) {
				return arg;
			}
		}
	}

	public static String unEscapeString(String str) throws CoreException {
		StringBuilder result= new StringBuilder();
		for (int i= 0; i < str.length(); i++) {
			char ch= str.charAt(i);
			if (ch == '\'') {
				if (i == str.length() - 1 || str.charAt(i + 1) != '\'')
					throw new CoreException(new ExpressionStatus(
						ExpressionStatus.STRING_NOT_CORRECT_ESCAPED,
						Messages.format(ExpressionMessages.Expression_string_not_correctly_escaped, str)));
				result.append('\'');
				i++;
			} else {
				result.append(ch);
			}
		}
		return result.toString();
	}
}
