/*
 * =================================================================================================
 *                             Copyright (C) 2014 Martin Albedinsky
 * =================================================================================================
 *         Licensed under the Apache License, Version 2.0 or later (further "License" only).
 * -------------------------------------------------------------------------------------------------
 * You may use this file only in compliance with the License. More details and copy of this License
 * you may obtain at
 *
 * 		http://www.apache.org/licenses/LICENSE-2.0
 *
 * You can redistribute, modify or publish any part of the code written within this file but as it
 * is described in the License, the software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES or CONDITIONS OF ANY KIND.
 *
 * See the License for the specific language governing permissions and limitations under the License.
 * =================================================================================================
 */
package com.albedinsky.android.fragment.util;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import com.albedinsky.android.fragment.FragmentsConfig;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.HashMap;

/**
 * Annotation utils for the Fragments library.
 *
 * @author Martin Albedinsky
 */
public final class FragmentAnnotations {

	/**
	 * Interface ===================================================================================
	 */

	/**
	 * Simple callback which allows processing of all declared fields of a desired class using one of
	 * {@link #iterateFields(FragmentAnnotations.FieldProcessor, Class)},
	 * {@link #iterateFields(FragmentAnnotations.FieldProcessor, Class, Class)} methods.
	 *
	 * @author Martin Albedinsky
	 */
	public interface FieldProcessor {

		/**
		 * Invoked for each of iterated fields.
		 *
		 * @param field The currently iterated field.
		 * @param name  Name of the currently iterated field.
		 */
		void onProcessField(@NonNull Field field, @NonNull String name);
	}

	/**
	 * Constants ===================================================================================
	 */

	/**
	 * Log TAG.
	 */
	// private static final String TAG = "FragmentAnnotations";

	/**
	 * Static members ==============================================================================
	 */

	/**
	 * Lock used for synchronized operations.
	 */
	private static final Object LOCK = new Object();

	/**
	 * Map with annotation handlers where each handler is mapped to a particular class for which
	 * has been that handler created.
	 */
	private static HashMap<Class<?>, Object> ANNOTATION_HANDLERS;

	/**
	 * Members =====================================================================================
	 */

	/**
	 * Constructors ================================================================================
	 */

	/**
	 * Creating of instances of this class is not allowed.
	 */
	private FragmentAnnotations() {
	}

	/**
	 * Methods =====================================================================================
	 */

	/**
	 * Performs check for enabled state of the annotations processing for the Fragments library.
	 * <p>
	 * This check is requested mostly from parts of the Fragments library that require to be annotations
	 * processing enabled.
	 *
	 * @throws IllegalStateException If annotations processing is disabled.
	 */
	public static void checkIfEnabledOrThrow() {
		if (!FragmentsConfig.ANNOTATIONS_PROCESSING_ENABLED) {
			throw new IllegalStateException(
					"Trying to access logic that requires annotations processing to be enabled, " +
					"but it appears that the annotations processing is disabled for the Fragments library."
			);
		}
	}

	/**
	 */
	@Nullable
	public static FragmentFactoryAnnotationHandler obtainFactoryHandler(@NonNull Class<?> classOfFactory) {
		return obtainHandler(FragmentFactoryAnnotationHandlerImpl.class, classOfFactory);
	}

	/**
	 */
	@Nullable
	public static FragmentAnnotationHandler obtainFragmentHandler(@NonNull Class<?> classOfFragment) {
		return obtainHandler(FragmentAnnotationHandlers.FragmentHandler.class, classOfFragment);
	}

	/**
	 */
	@Nullable
	public static ActionBarFragmentAnnotationHandler obtainActionBarFragmentHandler(@NonNull Class<?> classOfFragment) {
		return obtainHandler(FragmentAnnotationHandlers.ActionBarFragmentHandler.class, classOfFragment);
	}

	/**
	 */
	@Nullable
	public static WebFragmentAnnotationHandler obtainWebFragmentHandler(@NonNull Class<?> classOfFragment) {
		return obtainHandler(FragmentAnnotationHandlers.WebFragmentHandler.class, classOfFragment);
	}

	/**
	 * Obtains an annotations handler of the specified <var>classOfHandler</var> for the requested
	 * <var>annotatedClass</var>. If there is no such handler already cached, its instance will be
	 * created and cached. Each handler is mapped to its annotated class, so there can be only one
	 * handler of the same type created for the same class.
	 *
	 * @param classOfHandler Class of the handler used if there need to be instantiated a new instance
	 *                       of that handler.
	 * @param annotatedClass Class for which to obtain the requested handler.
	 * @param <T>            Type of the handler to be obtained.
	 * @return Always valid instance of the requested handler or {@code null} if annotations processing
	 * is disabled for the Fragments library.
	 * @throws ClassCastException If there was requested new handler while there is already presented
	 *                            handler but of different type for the same class.
	 * @see FragmentsConfig#ANNOTATIONS_PROCESSING_ENABLED
	 */
	@SuppressWarnings({"unchecked", "ConstantConditions"})
	private static <T> T obtainHandler(Class<T> classOfHandler, Class<?> annotatedClass) {
		if (!FragmentsConfig.ANNOTATIONS_PROCESSING_ENABLED) return null;
		Object handler;
		synchronized (LOCK) {
			ensureHandlers();
			handler = ANNOTATION_HANDLERS.get(annotatedClass);
			if (handler == null) {
				handler = instantiateHandler(classOfHandler, annotatedClass);
				ANNOTATION_HANDLERS.put(annotatedClass, handler);
			} else if (!handler.getClass().equals(classOfHandler)) {
				final String newHandlerName = classOfHandler.getSimpleName();
				final String currentHandlerName = handler.getClass().getSimpleName();
				final String className = annotatedClass.getSimpleName();
				throw new ClassCastException(
						"Trying to obtain handler(" + newHandlerName + ") for class(" + className + ") while there " +
								"is already handler(" + currentHandlerName + ") of different type for that class!"
				);
			}
		}
		return (T) handler;
	}

	/**
	 * Performs instantiation process to instantiate an instance of the specified <var>classOfHandler</var>
	 * for the specified <var>annotatedClass</var>.
	 *
	 * @param classOfHandler Class of the handler to instantiate.
	 * @param annotatedClass Class for which to instantiate the requested handler.
	 * @param <T>            Type of the handler that will be instantiated.
	 * @return New instance of the requested handler with the annotated class attached.
	 * @throws IllegalStateException If the requested handler failed to be instantiated.
	 */
	private static <T> T instantiateHandler(Class<T> classOfHandler, Class<?> annotatedClass) {
		try {
			return classOfHandler.getConstructor(Class.class).newInstance(annotatedClass);
		} catch (Exception e) {
			// Happens when the handler implementation is not properly implemented:
			// - handler class is not visible
			// - handler class does not have public constructor taking annotated class as single argument
			final String handlerName = classOfHandler.getSimpleName();
			final String className = annotatedClass.getSimpleName();
			throw new IllegalStateException("Failed to instantiate annotation handler(" + handlerName + ") for(" + className + ").", e);
		}
	}

	/**
	 * Ensures that map for the annotation handlers is initialized.
	 */
	private static void ensureHandlers() {
		if (ANNOTATION_HANDLERS == null) ANNOTATION_HANDLERS = new HashMap<>(10);
	}

	/**
	 * Same as {@link #obtainAnnotationFrom(Class, Class, Class)} with no <var>maxSuperClass</var>
	 * specified so the requested annotation will be obtained only from the given class (if presented).
	 */
	@Nullable
	public static <A extends Annotation> A obtainAnnotationFrom(@NonNull Class<A> classOfAnnotation, @NonNull Class<?> fromClass) {
		return obtainAnnotationFrom(classOfAnnotation, fromClass, null);
	}

	/**
	 * Obtains the requested type of annotation from the given <var>fromClass</var> if it is presented.
	 *
	 * @param classOfAnnotation Class of the requested annotation.
	 * @param fromClass         Class from which should be the requested annotation obtained.
	 * @param maxSuperClass     If {@code not null}, this method will be called (recursively) for
	 *                          all super classes of the given annotated class (max to the specified
	 *                          <var>maxSuperClass</var> excluding) until the requested annotation
	 *                          is presented and obtained, otherwise annotation will be obtained only
	 *                          from the given annotated class.
	 * @param <A>               Type of the requested annotation.
	 * @return Obtained annotation or {@code null} if the requested annotation is not presented
	 * for the given class or its supers if requested.
	 * @see #obtainAnnotationFrom(Class, Class)
	 */
	@Nullable
	public static <A extends Annotation> A obtainAnnotationFrom(@NonNull Class<A> classOfAnnotation, @NonNull Class<?> fromClass, @Nullable Class<?> maxSuperClass) {
		final A annotation = fromClass.getAnnotation(classOfAnnotation);
		if (annotation != null) {
			return annotation;
		} else if (maxSuperClass != null) {
			final Class<?> parent = fromClass.getSuperclass();
			if (parent != null && !parent.equals(maxSuperClass)) {
				return obtainAnnotationFrom(classOfAnnotation, parent, maxSuperClass);
			}
		}
		return null;
	}

	/**
	 * Same as {@link #iterateFields(FieldProcessor, Class, Class)} with no <var>maxSuperClass</var>
	 * specified.
	 */
	public static void iterateFields(@NonNull FieldProcessor processor, @NonNull Class<?> ofClass) {
		iterateFields(processor, ofClass, null);
	}

	/**
	 * Iterates all declared fields of the given <var>ofClass</var>.
	 *
	 * @param processor     Field processor callback to be invoked for each of iterated fields.
	 * @param ofClass       Class of which fields to iterate.
	 * @param maxSuperClass If {@code not null}, this method will be called (recursively) for all
	 *                      super classes of the given class (max to the specified <var>maxSuperClass</var>
	 *                      excluding), otherwise only fields of the given class will be iterated.
	 * @see #iterateFields(FieldProcessor, Class)
	 */
	public static void iterateFields(@NonNull FieldProcessor processor, @NonNull Class<?> ofClass, @Nullable Class<?> maxSuperClass) {
		final Field[] fields = ofClass.getDeclaredFields();
		if (fields.length > 0) {
			for (Field field : fields) {
				processor.onProcessField(field, field.getName());
			}
		}
		if (maxSuperClass != null) {
			final Class<?> parent = ofClass.getSuperclass();
			if (parent != null && !parent.equals(maxSuperClass)) {
				iterateFields(processor, parent, maxSuperClass);
			}
		}
	}

	/**
	 * Inner classes ===============================================================================
	 */
}
