// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
// * with the License.  You may obtain a copy of the License at                                                              *
// *                                                                                                                         *
// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
// *                                                                                                                         *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
// * specific language governing permissions and limitations under the License.                                              *
// ***************************************************************************************************************************
package org.apache.juneau.reflect;

import static org.apache.juneau.internal.StringUtils.*;

import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

import org.apache.juneau.*;
import org.apache.juneau.internal.*;

/**
 * Contains common methods between {@link ConstructorInfo} and {@link MethodInfo}.
 */
public abstract class ExecutableInfo {

	final ClassInfo declaringClass;
	final Executable e, re;
	final boolean isConstructor;

	private List<ParamInfo> params;
	private List<ClassInfo> paramTypes, exceptionInfos;
	private Class<?>[] rawParamTypes, rawExceptionTypes;
	private Type[] rawGenericParamTypes;
	private Parameter[] rawParameters;

	/**
	 * Constructor.
	 *
	 * @param declaringClass The class that declares this method or constructor.
	 * @param e The constructor or method that this info represents.
	 * @param re The "real" constructor if the constructor above is defined against a CGLIB proxy.
	 */
	protected ExecutableInfo(ClassInfo declaringClass, Executable e, Executable re) {
		this.declaringClass = declaringClass;
		this.e = e;
		this.re = re == null ? e : re;
		this.isConstructor = e instanceof Constructor;
	}

	/**
	 * Returns metadata about the class that declared this method or constructor.
	 *
	 * @return Metadata about the class that declared this method or constructor.
	 */
	public final ClassInfo getDeclaringClass() {
		return declaringClass;
	}

	/**
	 * Returns <jk>true</jk> if this executable represents a {@link Constructor}.
	 *
	 * @return
	 * 	<jk>true</jk> if this executable represents a {@link Constructor} and can be cast to {@link ConstructorInfo}.
	 * 	<jk>false</jk> if this executable represents a {@link Method} and can be cast to {@link MethodInfo}.
	 */
	public final boolean isConstructor() {
		return isConstructor;
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Parameters
	//-----------------------------------------------------------------------------------------------------------------

	/**
	 * Returns the number of parameters in this executable.
	 *
	 * <p>
	 * Same as calling {@link Executable#getParameterCount()}.
	 *
	 * @return The number of parameters in this executable.
	 */
	public final int getParamCount() {
		return e.getParameterCount();
	}

	/**
	 * Returns <jk>true</jk> if this executable has at least one parameter.
	 *
	 * <p>
	 * Same as calling {@link Executable#getParameterCount()} and comparing with zero.
	 *
	 * @return <jk>true</jk> if this executable has at least one parameter.
	 */
	public final boolean hasParams() {
		return getParamCount() != 0;
	}

	/**
	 * Returns <jk>true</jk> if this executable has no parameters.
	 *
	 * <p>
	 * Same as calling {@link Executable#getParameterCount()} and comparing with zero.
	 *
	 * @return <jk>true</jk> if this executable has no parameters.
	 */
	public final boolean hasNoParams() {
		return getParamCount() == 0;
	}

	/**
	 * Returns <jk>true</jk> if this executable has this number of arguments.
	 *
	 * <p>
	 * Same as calling {@link Executable#getParameterCount()} and comparing the count.
	 *
	 * @param number The number of expected arguments.
	 * @return <jk>true</jk> if this executable has this number of arguments.
	 */
	public final boolean hasNumParams(int number) {
		return getParamCount() == number;
	}

	/**
	 * Returns the parameters defined on this executable.
	 *
	 * <p>
	 * Same as calling {@link Executable#getParameters()} but wraps the results
	 *
	 * @return An array of parameter information, never <jk>null</jk>.
	 */
	public final List<ParamInfo> getParams() {
		if (params == null) {
			Parameter[] rp = rawParameters();
			List<ParamInfo> l = new ArrayList<>(rp.length);
			for (int i = 0; i < rp.length; i++)
				l.add(new ParamInfo(this, rp[i], i));
			params = Collections.unmodifiableList(l);
		}
		return params;
	}

	/**
	 * Returns parameter information at the specified index.
	 *
	 * @param index The parameter index.
	 * @return The parameter information, never <jk>null</jk>.
	 */
	public final ParamInfo getParam(int index) {
		checkIndex(index);
		if (params != null)
			return params.get(index);
		return new ParamInfo(this, rawParameters()[index], index);
	}

	/**
	 * Returns the parameter types on this executable.
	 *
	 * @return The parameter types on this executable.
	 */
	public final List<ClassInfo> getParamTypes() {
		if (paramTypes == null) {
			Class<?>[] ptc = rawParamTypes();
			// Note that due to a bug involving Enum constructors, getGenericParameterTypes() may
			// always return an empty array.  This appears to be fixed in Java 8 b75.
			Type[] ptt = rawGenericParamTypes();
			if (ptt.length != ptc.length)
				ptt = ptc;
			List<ClassInfo> l = new ArrayList<>(ptc.length);
			for (int i = 0; i < ptc.length; i++)
				l.add(ClassInfo.of(ptc[i], ptt[i]));
			paramTypes = Collections.unmodifiableList(l);
		}
		return paramTypes;
	}

	/**
	 * Returns the parameter type of the parameter at the specified index.
	 *
	 * @param index The parameter index.
	 * @return The parameter type of the parameter at the specified index.
	 */
	public final ClassInfo getParamType(int index) {
		checkIndex(index);
		if (paramTypes != null)
			return getParamTypes().get(index);
		return ClassInfo.of(getRawParamType(index), getRawGenericParamType(index));
	}

	/**
	 * Returns the raw parameter types on this executable.
	 *
	 * @return The raw parameter types on this executable.
	 */
	public final Class<?>[] getRawParamTypes() {
		return rawParamTypes().clone();
	}

	/**
	 * Returns the raw parameter type of the parameter at the specified index.
	 *
	 * @param index The parameter index.
	 * @return The raw parameter type of the parameter at the specified index.
	 */
	public final Class<?> getRawParamType(int index) {
		checkIndex(index);
		return rawParamTypes()[index];
	}

	/**
	 * Returns the raw generic parameter types on this executable.
	 *
	 * @return The raw generic parameter types on this executable.
	 */
	public final Type[] getRawGenericParamTypes() {
		return rawGenericParamTypes().clone();
	}

	/**
	 * Returns the raw generic parameter type of the parameter at the specified index.
	 *
	 * @param index The parameter index.
	 * @return The raw generic parameter type of the parameter at the specified index.
	 */
	public final Type getRawGenericParamType(int index) {
		checkIndex(index);
		return rawGenericParamTypes()[index];
	}

	/**
	 * Returns an array of raw {@link Parameter} objects that represent all the parameters to the underlying executable represented by this object.
	 *
	 * @return An array of raw {@link Parameter} objects, or an empty array if executable has no parameters.
	 * @see Executable#getParameters()
	 */
	public final Parameter[] getRawParameters() {
		return rawParameters().clone();
	}

	/**
	 * Returns the raw {@link Parameter} object that represents the parameter at the specified index.
	 *
	 * @param index The parameter index.
	 * @return The raw {@link Parameter} object that represents the parameter at the specified index.
	 * @see Executable#getParameters()
	 */
	public final Parameter getRawParameter(int index) {
		checkIndex(index);
		return rawParameters()[index];
	}

	Class<?>[] rawParamTypes() {
		if (rawParamTypes == null)
			rawParamTypes = e.getParameterTypes();
		return rawParamTypes;
	}

	Type[] rawGenericParamTypes() {
		if (rawGenericParamTypes == null)
			rawGenericParamTypes = re.getGenericParameterTypes();
		return rawGenericParamTypes;
	}

	Parameter[] rawParameters() {
		if (rawParameters == null)
			rawParameters = re.getParameters();
		return rawParameters;
	}

	private void checkIndex(int index) {
		int pc = getParamCount();
		if (pc == 0)
			throw new IndexOutOfBoundsException(format("Invalid index ''{0}''.  No parameters.", index));
		if (index < 0 || index >= pc)
			throw new IndexOutOfBoundsException(format("Invalid index ''{0}''.  Parameter count: {1}", index, pc));
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Annotations
	//-----------------------------------------------------------------------------------------------------------------

	/**
	 * Returns the parameter annotations on this executable.
	 *
	 * @return The parameter annotations on this executable.
	 */
	public final Annotation[][] getParameterAnnotations() {
		return e.getParameterAnnotations();
	}

	/**
	 * Returns the parameter annotations on the parameter at the specified index.
	 *
	 * @param index The parameter index.
	 * @return The parameter annotations on the parameter at the specified index.
	 */
	public final Annotation[] getParameterAnnotations(int index) {
		checkIndex(index);
		return e.getParameterAnnotations()[index];
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Exceptions
	//-----------------------------------------------------------------------------------------------------------------

	/**
	 * Returns the exception types on this executable.
	 *
	 * @return The exception types on this executable.
	 */
	public final List<ClassInfo> getExceptionTypes() {
		if (exceptionInfos == null) {
			Class<?>[] exceptionTypes = rawExceptionTypes();
			List<ClassInfo> l = new ArrayList<>(exceptionTypes.length);
			for (Class<?> et : exceptionTypes)
				l.add(ClassInfo.of(et));
			exceptionInfos = Collections.unmodifiableList(l);
		}
		return exceptionInfos;
	}

	/**
	 * Returns the raw exception types on this executable.
	 *
	 * @return The raw exception types on this executable.
	 */
	public final Class<?>[] getRawExceptionTypes() {
		return rawExceptionTypes().clone();
	}

	private Class<?>[] rawExceptionTypes() {
		if (rawExceptionTypes == null)
			rawExceptionTypes = e.getExceptionTypes();
		return rawExceptionTypes;
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Characteristics
	//-----------------------------------------------------------------------------------------------------------------

	/**
	 * Returns <jk>true</jk> if all specified flags are applicable to this method.
	 *
	 * @param flags The flags to test for.
	 * @return <jk>true</jk> if all specified flags are applicable to this method.
	 */
	public final boolean isAll(ReflectFlags...flags) {
		for (ReflectFlags f : flags) {
			switch (f) {
				case DEPRECATED:
					if (isNotDeprecated())
						return false;
					break;
				case NOT_DEPRECATED:
					if (isDeprecated())
						return false;
					break;
				case HAS_PARAMS:
					if (hasNoParams())
						return false;
					break;
				case HAS_NO_PARAMS:
					if (hasParams())
						return false;
					break;
				case PUBLIC:
					if (isNotPublic())
						return false;
					break;
				case NOT_PUBLIC:
					if (isPublic())
						return false;
					break;
				case STATIC:
					if (isNotStatic())
						return false;
					break;
				case NOT_STATIC:
					if (isStatic())
						return false;
					break;
				case ABSTRACT:
					if (isNotAbstract())
						return false;
					break;
				case NOT_ABSTRACT:
					if (isAbstract())
						return false;
					break;
				default:
					throw new RuntimeException("Invalid flag for executable: " + f);
			}
		}
		return true;
	}

	/**
	 * Returns <jk>true</jk> if all specified flags are applicable to this method.
	 *
	 * @param flags The flags to test for.
	 * @return <jk>true</jk> if all specified flags are applicable to this method.
	 */
	public final boolean isAny(ReflectFlags...flags) {
		for (ReflectFlags f : flags) {
			switch (f) {
				case DEPRECATED:
					if (isDeprecated())
						return true;
					break;
				case NOT_DEPRECATED:
					if (isNotDeprecated())
						return true;
					break;
				case HAS_PARAMS:
					if (hasParams())
						return true;
					break;
				case HAS_NO_PARAMS:
					if (hasNoParams())
						return true;
					break;
				case PUBLIC:
					if (isPublic())
						return true;
					break;
				case NOT_PUBLIC:
					if (isNotPublic())
						return true;
					break;
				case STATIC:
					if (isStatic())
						return true;
					break;
				case NOT_STATIC:
					if (isNotStatic())
						return true;
					break;
				case ABSTRACT:
					if (isAbstract())
						return true;
					break;
				case NOT_ABSTRACT:
					if (isNotAbstract())
						return true;
					break;
				default:
					throw new RuntimeException("Invalid flag for executable: " + f);
			}
		}
		return false;
	}

	/**
	 * Returns <jk>true</jk> if this method has the specified arguments.
	 *
	 * @param args The arguments to test for.
	 * @return <jk>true</jk> if this method has this arguments in the exact order.
	 */
	public final boolean hasParamTypes(Class<?>...args) {
		Class<?>[] pt = rawParamTypes();
		if (pt.length == args.length) {
			for (int i = 0; i < pt.length; i++)
				if (! pt[i].equals(args[i]))
					return false;
			return true;
		}
		return false;
	}

	/**
	 * Returns <jk>true</jk> if this method has the specified arguments.
	 *
	 * @param args The arguments to test for.
	 * @return <jk>true</jk> if this method has this arguments in the exact order.
	 */
	public final boolean hasParamTypes(ClassInfo...args) {
		Class<?>[] pt = rawParamTypes();
		if (pt.length == args.length) {
			for (int i = 0; i < pt.length; i++)
				if (! pt[i].equals(args[i].inner()))
					return false;
			return true;
		}
		return false;
	}

	/**
	 * Returns <jk>true</jk> if this method has the specified argument parent classes.
	 *
	 * @param args The arguments to test for.
	 * @return <jk>true</jk> if this method has this arguments in the exact order.
	 */
	public final boolean hasParamTypeParents(Class<?>...args) {
		Class<?>[] pt = rawParamTypes();
		if (pt.length == args.length) {
			for (int i = 0; i < pt.length; i++)
				if (! args[i].isAssignableFrom(pt[i]))
					return false;
			return true;
		}
		return false;
	}

	/**
	 * Returns <jk>true</jk> if this method has the specified argument parent classes.
	 *
	 * @param args The arguments to test for.
	 * @return <jk>true</jk> if this method has this arguments in the exact order.
	 */
	public final boolean hasParamTypeParents(ClassInfo...args) {
		Class<?>[] pt = rawParamTypes();
		if (pt.length == args.length) {
			for (int i = 0; i < pt.length; i++)
				if (! args[i].inner().isAssignableFrom(pt[i]))
					return false;
			return true;
		}
		return false;
	}

	/**
	 * Returns <jk>true</jk> if this method has at most only this arguments in any order.
	 *
	 * @param args The arguments to test for.
	 * @return <jk>true</jk> if this method has at most only this arguments in any order.
	 */
	public final boolean hasFuzzyParamTypes(Class<?>...args) {
		return ClassUtils.fuzzyArgsMatch(rawParamTypes(), args) != -1;
	}

	/**
	 * Returns <jk>true</jk> if this method has at most only this arguments in any order.
	 *
	 * @param args The arguments to test for.
	 * @return <jk>true</jk> if this method has at most only this arguments in any order.
	 */
	public boolean hasFuzzyParamTypes(ClassInfo...args) {
		return ClassUtils.fuzzyArgsMatch(rawParamTypes(), args) != -1;
	}

	/**
	 * Returns <jk>true</jk> if this method has the {@link Deprecated @Deprecated} annotation on it.
	 *
	 * @return <jk>true</jk> if this method has the {@link Deprecated @Deprecated} annotation on it.
	 */
	public final boolean isDeprecated() {
		return e.isAnnotationPresent(Deprecated.class);

	}

	/**
	 * Returns <jk>true</jk> if this method doesn't have the {@link Deprecated @Deprecated} annotation on it.
	 *
	 * @return <jk>true</jk> if this method doesn't have the {@link Deprecated @Deprecated} annotation on it.
	 */
	public final boolean isNotDeprecated() {
		return ! e.isAnnotationPresent(Deprecated.class);

	}

	/**
	 * Returns <jk>true</jk> if this method is abstract.
	 *
	 * @return <jk>true</jk> if this method is abstract.
	 */
	public final boolean isAbstract() {
		return Modifier.isAbstract(e.getModifiers());
	}

	/**
	 * Returns <jk>true</jk> if this method is not abstract.
	 *
	 * @return <jk>true</jk> if this method is not abstract.
	 */
	public final boolean isNotAbstract() {
		return ! Modifier.isAbstract(e.getModifiers());
	}

	/**
	 * Returns <jk>true</jk> if this method is public.
	 *
	 * @return <jk>true</jk> if this method is public.
	 */
	public final boolean isPublic() {
		return Modifier.isPublic(e.getModifiers());
	}

	/**
	 * Returns <jk>true</jk> if this method is not public.
	 *
	 * @return <jk>true</jk> if this method is not public.
	 */
	public final boolean isNotPublic() {
		return ! Modifier.isPublic(e.getModifiers());
	}

	/**
	 * Returns <jk>true</jk> if this method is static.
	 *
	 * @return <jk>true</jk> if this method is static.
	 */
	public final boolean isStatic() {
		return Modifier.isStatic(e.getModifiers());
	}

	/**
	 * Returns <jk>true</jk> if this method is not static.
	 *
	 * @return <jk>true</jk> if this method is not static.
	 */
	public final boolean isNotStatic() {
		return ! Modifier.isStatic(e.getModifiers());
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Visibility
	//-----------------------------------------------------------------------------------------------------------------

	/**
	 * Attempts to call <code>x.setAccessible(<jk>true</jk>)</code> and quietly ignores security exceptions.
	 *
	 * @return <jk>true</jk> if call was successful.
	 */
	public final boolean setAccessible() {
		try {
			if (! (e.isAccessible()))
				e.setAccessible(true);
			return true;
		} catch (SecurityException e) {
			return false;
		}
	}

	/**
	 * Identifies if the specified visibility matches this method.
	 *
	 * @param v The visibility to validate against.
	 * @return <jk>true</jk> if this visibility matches the modifier attribute of this method.
	 */
	public final boolean isVisible(Visibility v) {
		return v.isVisible(e);
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Labels
	//-----------------------------------------------------------------------------------------------------------------

	/**
	 * Returns <jk>true</jk> if this method has this name.
	 *
	 * @param name The name to test for.
	 * @return <jk>true</jk> if this method has this name.
	 */
	public final boolean hasName(String name) {
		return getSimpleName().equals(name);
	}

	/**
	 * Returns <jk>true</jk> if this method has a name in the specified list.
	 *
	 * @param names The names to test for.
	 * @return <jk>true</jk> if this method has one of the names.
	 */
	public final boolean hasName(String...names) {
		for (String n : names)
			if (getSimpleName().equals(n))
				return true;
		return false;
	}

	/**
	 * Returns <jk>true</jk> if this method has a name in the specified set.
	 *
	 * @param names The names to test for.
	 * @return <jk>true</jk> if this method has one of the names.
	 */
	public final boolean hasName(Set<String> names) {
		return names.contains(getSimpleName());
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Labels
	//-----------------------------------------------------------------------------------------------------------------

	/**
	 * Returns the full name of this executable.
	 *
	 * <h5 class='section'>Examples:</h5>
	 * <ul>
	 * 	<li><js>"com.foo.MyClass.get(java.util.String)"<js> - Method.
	 * 	<li><js>"com.foo.MyClass(java.util.String)"<js> - Constructor.
	 * </ul>
	 *
	 * @return The underlying executable name.
	 */
	public final String getFullName() {
		StringBuilder sb = new StringBuilder(128);
		ClassInfo dc = declaringClass;
		Package p = dc.getPackage();
		if (p != null)
			sb.append(p.getName()).append('.');
		dc.appendShortName(sb);
		if (! isConstructor)
			sb.append('.').append(getSimpleName());
		sb.append('(');
		List<ClassInfo> pt = getParamTypes();
		for (int i = 0; i < pt.size(); i++) {
			if (i > 0)
				sb.append(',');
			pt.get(i).appendFullName(sb);
		}
		sb.append(')');
		return sb.toString();
	}

	/**
	 * Returns the short name of this executable.
	 *
	 * <h5 class='section'>Examples:</h5>
	 * <ul>
	 * 	<li><js>"MyClass.get(String)"<js> - Method.
	 * 	<li><js>"MyClass(String)"<js> - Constructor.
	 * </ul>
	 *
	 * @return The underlying executable name.
	 */
	public final String getShortName() {
		StringBuilder sb = new StringBuilder(64);
		sb.append(getSimpleName()).append('(');
		Class<?>[] pt = rawParamTypes();
		for (int i = 0; i < pt.length; i++) {
			if (i > 0)
				sb.append(',');
			sb.append(pt[i].getSimpleName());
		}
		sb.append(')');
		return sb.toString();
	}

	/**
	 * Returns the simple name of the underlying method.
	 *
	 * @return The simple name of the underlying method;
	 */
	public final String getSimpleName() {
		return isConstructor ? e.getDeclaringClass().getSimpleName() : e.getName();
	}

	@Override
	public String toString() {
		return getShortName();
	}
}
