/*******************************************************************************
 * Copyright (c) 2011, 2012 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
 *******************************************************************************/
package org.eclipse.jdt.debug.tests.ui;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.eclipse.core.runtime.Assert;

/**
 * Helper class for accessing classes and members which cannot be accessed using standard Java
 * access control like private or package visible elements.
 *
 * @since 3.7
 */
public class Accessor {

	/** The class to access. */
	private Class<?> fClass;
	/** The instance to access. */
	private Object fInstance;

	/**
	 * Creates an accessor for the given <code>instance</code> and <code>class</code>. Only
	 * non-inherited members that particular <code>class</code> can be accessed.
	 *
	 * @param instance
	 *            the instance
	 * @param clazz
	 *            the class
	 */
	public Accessor(Object instance, Class<?> clazz) {
		org.eclipse.core.runtime.Assert.isNotNull(instance);
		Assert.isNotNull(clazz);
		fInstance = instance;
		fClass = clazz;
	}

	/**
	 * Creates an accessor for the given <code>instance</code> and <code>class</code>. Only
	 * non-inherited members that particular <code>class</code> can be accessed.
	 *
	 * @param instance
	 *            the instance
	 * @param className
	 *            the name of the class
	 * @param classLoader
	 *            the class loader to use i.e. <code>getClass().getClassLoader()</code>
	 */
	public Accessor(Object instance, String className, ClassLoader classLoader) {
		Assert.isNotNull(instance);
		Assert.isNotNull(className);
		Assert.isNotNull(classLoader);
		fInstance = instance;
		try {
			fClass = Class.forName(className, true, classLoader);
		} catch (ClassNotFoundException e) {
			fail();
		} catch (ExceptionInInitializerError e) {
			fail();
		}
	}

	/**
	 * Creates an accessor for the given class.
	 * <p>
	 * In order to get the type information from the given arguments they must all be instanceof
	 * Object. Use {@link #Accessor(String, ClassLoader, Class[], Object[])} if this is not the
	 * case.
	 * </p>
	 *
	 * @param className
	 *            the name of the class
	 * @param classLoader
	 *            the class loader to use i.e. <code>getClass().getClassLoader()</code>
	 * @param constructorArgs
	 *            the constructor arguments which must all be instance of Object
	 */
	public Accessor(String className, ClassLoader classLoader, Object[] constructorArgs) {
		this(className, classLoader, getTypes(constructorArgs), constructorArgs);
	}

	/**
	 * Creates an accessor for the given class.
	 *
	 * @param className
	 *            the name of the class
	 * @param classLoader
	 *            the class loader to use i.e. <code>getClass().getClassLoader()</code>
	 * @param constructorTypes
	 *            the types of the constructor arguments
	 * @param constructorArgs
	 *            the constructor arguments
	 */
	public Accessor(String className, ClassLoader classLoader, Class<?>[] constructorTypes, Object[] constructorArgs) {
		try {
			fClass = Class.forName(className, true, classLoader);
		} catch (ClassNotFoundException e) {
			fail();
		} catch (ExceptionInInitializerError e) {
			fail();
		}
		Constructor<?> constructor = null;
		try {
			constructor = fClass.getDeclaredConstructor(constructorTypes);
		} catch (SecurityException e2) {
			fail();
		} catch (NoSuchMethodException e2) {
			fail();
		}
		Assert.isNotNull(constructor);
		constructor.setAccessible(true);
		try {
			fInstance = constructor.newInstance(constructorArgs);
		} catch (IllegalArgumentException e) {
			fail();
		} catch (InvocationTargetException e) {
			fail();
		} catch (InstantiationException e) {
			fail();
		} catch (IllegalAccessException e) {
			fail();
		}
	}

	/**
	 * Creates an accessor for the given class.
	 * <p>
	 * This constructor is used to access static stuff.
	 * </p>
	 *
	 * @param className
	 *            the name of the class
	 * @param classLoader
	 *            the class loader to use i.e. <code>getClass().getClassLoader()</code>
	 */
	public Accessor(String className, ClassLoader classLoader) {
		try {
			fClass = Class.forName(className, true, classLoader);
		} catch (ClassNotFoundException e) {
			fail();
		} catch (ExceptionInInitializerError e) {
			fail();
		}
	}

	/**
	 * Creates an accessor for the given class.
	 * <p>
	 * This constructor is used to access static stuff.
	 * </p>
	 *
	 * @param clazz
	 *            the class
	 */
	public Accessor(Class<?> clazz) {
		Assert.isNotNull(clazz);
		fClass = clazz;
	}

	/**
	 * Invokes the method with the given method name and arguments.
	 * <p>
	 * In order to get the type information from the given arguments all those arguments must be
	 * instance of Object. Use {@link #invoke(String, Class[], Object[])} if this is not the case.
	 * </p>
	 *
	 * @param methodName
	 *            the method name
	 * @param arguments
	 *            the method arguments which must all be instance of Object
	 * @return the method return value
	 */
	public Object invoke(String methodName, Object[] arguments) {
		return invoke(methodName, getTypes(arguments), arguments);
	}

	/**
	 * Invokes the method with the given method name and arguments.
	 *
	 * @param methodName
	 *            the method name
	 * @param types
	 *            the argument types
	 * @param arguments
	 *            the method arguments
	 * @return the method return value
	 */
	public Object invoke(String methodName, Class<?>[] types, Object[] arguments) {
		Method method = null;
		try {
			method = fClass.getDeclaredMethod(methodName, types);
		} catch (SecurityException e) {
			fail();
		} catch (NoSuchMethodException ex) {
			fail();
		}
		Assert.isNotNull(method);
		method.setAccessible(true);
		try {
			return method.invoke(fInstance, arguments);
		} catch (IllegalArgumentException e) {
			fail();
		} catch (InvocationTargetException e) {
			fail();
		} catch (IllegalAccessException e) {
			fail();
		}
		return null;
	}

	/**
	 * Assigns the given value to the field with the given name.
	 *
	 * @param fieldName
	 *            the field name
	 * @param value
	 *            the value to assign to the field
	 */
	public void set(String fieldName, Object value) {
		Field field = getField(fieldName);
		try {
			field.set(fInstance, value);
		} catch (IllegalArgumentException e) {
			fail();
		} catch (IllegalAccessException e) {
			fail();
		}
	}

	/**
	 * Assigns the given value to the field with the given name.
	 *
	 * @param fieldName
	 *            the field name
	 * @param value
	 *            the value to assign to the field
	 */
	public void set(String fieldName, boolean value) {
		Field field = getField(fieldName);
		try {
			field.setBoolean(fInstance, value);
		} catch (IllegalArgumentException e) {
			fail();
		} catch (IllegalAccessException e) {
			fail();
		}
	}

	/**
	 * Assigns the given value to the field with the given name.
	 *
	 * @param fieldName
	 *            the field name
	 * @param value
	 *            the value to assign to the field
	 */
	public void set(String fieldName, int value) {
		Field field = getField(fieldName);
		try {
			field.setInt(fInstance, value);
		} catch (IllegalArgumentException e) {
			fail();
		} catch (IllegalAccessException e) {
			fail();
		}
	}

	/**
	 * Returns the value of the field with the given name.
	 *
	 * @param fieldName
	 *            the field name
	 * @return the value of the field
	 */
	public Object get(String fieldName) {
		Field field = getField(fieldName);
		try {
			return field.get(fInstance);
		} catch (IllegalArgumentException e) {
			fail();
		} catch (IllegalAccessException e) {
			fail();
		}
		// Unreachable code
		return null;
	}

	/**
	 * Returns the value of the field with the given name.
	 *
	 * @param fieldName
	 *            the field name
	 * @return the value of the field
	 */
	public boolean getBoolean(String fieldName) {
		Field field = getField(fieldName);
		try {
			return field.getBoolean(fInstance);
		} catch (IllegalArgumentException e) {
			fail();
		} catch (IllegalAccessException e) {
			fail();
		}
		// Unreachable code
		return false;
	}

	/**
	 * Returns the value of the field with the given name.
	 *
	 * @param fieldName
	 *            the field name
	 * @return the value of the field
	 */
	public int getInt(String fieldName) {
		Field field = getField(fieldName);
		try {
			return field.getInt(fInstance);
		} catch (IllegalArgumentException e) {
			fail();
		} catch (IllegalAccessException e) {
			fail();
		}
		// Unreachable code
		return 0;
	}

	public Field getField(String fieldName) {
		Field field = null;
		try {
			field = fClass.getDeclaredField(fieldName);
		} catch (SecurityException e) {
			fail();
		} catch (NoSuchFieldException e) {
			fail();
		}
		field.setAccessible(true);
		return field;
	}

	private static Class<?>[] getTypes(Object[] objects) {
		if (objects == null)
			return null;

		int length = objects.length;
		Class<?>[] classes = new Class[length];
		for (int i = 0; i < length; i++) {
			Assert.isNotNull(objects[i]);
			classes[i] = objects[i].getClass();
		}
		return classes;
	}

	private void fail() {
		Assert.isTrue(false);
	}
}
