// ***************************************************************************************************************************
// * 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 java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

import org.apache.juneau.*;
import org.apache.juneau.annotation.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.marshall.*;
import org.apache.juneau.svl.*;

/**
 * Represents an annotation instance on a class and the class it was found on.
 *
 * @param <T> The annotation type.
 */
public class AnnotationInfo<T extends Annotation> {

	private final ClassInfo c;
	private final MethodInfo m;
	private final Package p;
	private final T a;
	final int rank;

	/**
	 * Constructor.
	 *
	 * @param c The class where the annotation was found.
	 * @param m The method where the annotation was found.
	 * @param p The package where the annotation was found.
	 * @param a The annotation found.
	 */
	protected AnnotationInfo(ClassInfo c, MethodInfo m, Package p, T a) {
		this.c = c;
		this.m = m;
		this.p = p;
		this.a = a;
		this.rank = getRank(a);
	}

	private static int getRank(Object a) {
		ClassInfo ci = ClassInfo.ofc(a);
		MethodInfo mi = ci.getPublicMethod("rank");
		if (mi != null && mi.hasReturnType(int.class)) {
			try {
				return (int)mi.invoke(a);
			} catch (ExecutableException e) {
				e.printStackTrace();
			}
		}
		return 0;
	}

	/**
	 * Convenience constructor when annotation is found on a class.
	 *
	 * @param c The class where the annotation was found.
	 * @param a The annotation found.
	 * @return A new {@link AnnotationInfo} object.
	 */
	public static <T extends Annotation> AnnotationInfo<T> of(ClassInfo c, T a) {
		return new AnnotationInfo<>(c, null, null, a);
	}

	/**
	 * Convenience constructor when annotation is found on a method.
	 *
	 * @param m The method where the annotation was found.
	 * @param a The annotation found.
	 * @return A new {@link AnnotationInfo} object.
	 */
	public static <T extends Annotation> AnnotationInfo<T> of(MethodInfo m, T a) {
		return new AnnotationInfo<>(null, m, null, a);
	}

	/**
	 * Convenience constructor when annotation is found on a package.
	 *
	 * @param p The package where the annotation was found.
	 * @param a The annotation found.
	 * @return A new {@link AnnotationInfo} object.
	 */
	public static <T extends Annotation> AnnotationInfo<T> of(Package p, T a) {
		return new AnnotationInfo<>(null, null, p, a);
	}

	/**
	 * Returns the class where the annotation was found.
	 *
	 * @return the class where the annotation was found, or <jk>null</jk> if it wasn't found on a method.
	 */
	public ClassInfo getClassOn() {
		return c;
	}

	/**
	 * Returns the method where the annotation was found.
	 *
	 * @return the method where the annotation was found, or <jk>null</jk> if it wasn't found on a method.
	 */
	public MethodInfo getMethodOn() {
		return m;
	}

	/**
	 * Returns the package where the annotation was found.
	 *
	 * @return the package where the annotation was found, or <jk>null</jk> if it wasn't found on a package.
	 */
	public Package getPackageOn() {
		return p;
	}

	/**
	 * Returns the annotation found.
	 *
	 * @return The annotation found.
	 */
	public T getAnnotation() {
		return a;
	}

	/**
	 * Converts this object to a readable JSON object for debugging purposes.
	 *
	 * @return A new map showing the attributes of this object as a JSON object.
	 */
	public ObjectMap toObjectMap() {
		ObjectMap om = new ObjectMap();
		if (c != null)
			om.put("class", c.getSimpleName());
		if (m != null)
			om.put("method", m.getShortName());
		if (p != null)
			om.put("package", p.getName());
		ObjectMap oa = new ObjectMap();
		Class<?> ca = a.annotationType();
		for (Method m : ca.getDeclaredMethods()) {
			try {
				Object v = m.invoke(a);
				Object d = m.getDefaultValue();
				if (! Objects.equals(v, d)) {
					if (! (ArrayUtils.isArray(v) && Array.getLength(v) == 0 && Array.getLength(d) == 0))
						oa.put(m.getName(), v);
				}
			} catch (Exception e) {
				oa.put(m.getName(), e.getLocalizedMessage());
			}
		}
		om.put("@" + ca.getSimpleName(), oa);
		return om;
	}

	private Constructor<? extends ConfigApply<?>> configApplyConstructor;

	/**
	 * If this annotation has a {@link PropertyStoreApply} annotation, returns an instance of the specified {@link ConfigApply} class.
	 *
	 * @param vrs Variable resolver passed to the {@link ConfigApply} object.
	 * @return A new {@link ConfigApply} object.  Never <jk>null</jk>.
	 * @throws ExecutableException Exception occurred on invoked constructor/method/field.
	 */
	@SuppressWarnings("unchecked")
	public ConfigApply<Annotation> getConfigApply(VarResolverSession vrs) throws ExecutableException {
		try {
			if (configApplyConstructor == null) {
				PropertyStoreApply psa = a.annotationType().getAnnotation(PropertyStoreApply.class);
				if (psa != null)
					configApplyConstructor = (Constructor<? extends ConfigApply<?>>)psa.value().getConstructor(Class.class, VarResolverSession.class);
				if (configApplyConstructor == null)
					throw new NoSuchFieldError("Could not find ConfigApply constructor for annotation:\n" + toString());
			}
			ClassInfo ci = getClassInfo();
			return (ConfigApply<Annotation>) configApplyConstructor.newInstance(ci == null ? null : ci.inner(), vrs);
		} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
			throw new ExecutableException(e);
		}
	}

	/**
	 * Returns the class that this annotation was found on.
	 *
	 * @return The class that this annotation was found on, or <jk>null</jk> if it was found on a package.
	 */
	public ClassInfo getClassInfo() {
		if (this.c != null)
			return this.c;
		if (this.m != null)
			return this.m.getDeclaringClass();
		return null;
	}

	/**
	 * Returns <jk>true</jk> if this annotation is the specified type.
	 *
	 * @param a The type to test against.
	 * @return <jk>true</jk> if this annotation is the specified type.
	 */
	public boolean isType(Class<? extends Annotation> a) {
		Class<? extends Annotation> at = this.a.annotationType();
		return at == a;
	}

	/**
	 * Returns <jk>true</jk> if this annotation has the specified annotation defined on it.
	 *
	 * @param a The annotation to test for.
	 * @return <jk>true</jk> if this annotation has the specified annotation defined on it.
	 */
	public boolean hasAnnotation(Class<? extends Annotation> a) {
		return this.a.annotationType().getAnnotation(a) != null;
	}

	@Override
	public String toString() {
		return SimpleJson.DEFAULT_READABLE.toString(toObjectMap());
	}
}
