/*
 * 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.ignite.internal.processors.datastructures;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.internal.util.GridBoundedLinkedHashMap;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.lang.IgniteBiTuple;

/**
 * Utility class for getting annotated values from classes.
 * Contains local cache of annotated methods and fields by classes for best performance.
 */
public class GridCacheAnnotationHelper<A extends Annotation> {
    /** Number of entries to keep in annotation cache. */
    private static final int DFLT_CLASS_CACHE_SIZE = 1000;

    /** Field cache. */
    private final GridBoundedLinkedHashMap<Class<?>, List<Field>> fieldCache;

    /** Method cache. */
    private final GridBoundedLinkedHashMap<Class<?>, List<Method>> mtdCache;

    /** Annotation class. */
    private final Class<A> annCls;

    /** Mutex. */
    private final Object mux = new Object();

    /**
     * Constructor.
     *
     * @param annCls Annotation class.
     */
    public GridCacheAnnotationHelper(Class<A> annCls) {
        this(annCls, DFLT_CLASS_CACHE_SIZE);
    }

    /**
     * Constructor.
     *
     * @param annCls Annotation class.
     * @param capacity Capacity of local caches.
     */
    public GridCacheAnnotationHelper(Class<A> annCls, int capacity) {
        assert annCls != null : "Annotated class mustn't be null.";
        assert capacity > 0 : "Capacity must be more then zero.";

        this.annCls = annCls;

        fieldCache = new GridBoundedLinkedHashMap<>(capacity);

        mtdCache = new GridBoundedLinkedHashMap<>(capacity);
    }

    /**
     * Returns annotated value.
     *
     * @param target Object to find a value in.
     * @return Value of annotated field or method.
     * @throws IgniteCheckedException If failed to find.
     */
    public Object annotatedValue(Object target) throws IgniteCheckedException {
        IgniteBiTuple<Object, Boolean> res = annotatedValue(target, new HashSet<>(), false);

        assert res != null;

        return res.get1();
    }

    /**
     * Returns annotated value.
     *
     * @param target Object to find a value in.
     * @param visited Set of visited objects to avoid cycling.
     * @param annFound Flag indicating if value has already been found.
     * @return Value of annotated field or method.
     * @throws IgniteCheckedException If failed to find.
     */
    private IgniteBiTuple<Object, Boolean> annotatedValue(Object target, Set<Object> visited, boolean annFound)
        throws IgniteCheckedException {
        assert target != null;

        // To avoid infinite recursion.
        if (visited.contains(target))
            return F.t(null, annFound);

        visited.add(target);

        Object val = null;

        for (Class<?> cls = target.getClass(); !cls.equals(Object.class); cls = cls.getSuperclass()) {
            // Fields.
            for (Field f : fieldsWithAnnotation(cls)) {
                f.setAccessible(true);

                Object fieldVal;

                try {
                    fieldVal = f.get(target);
                }
                catch (IllegalAccessException e) {
                    throw new IgniteCheckedException("Failed to get annotated field value [cls=" + cls.getName() +
                        ", ann=" + annCls.getSimpleName() + ']', e);
                }

                if (needsRecursion(f)) {
                    if (fieldVal != null) {
                        // Recursion.
                        IgniteBiTuple<Object, Boolean> tup = annotatedValue(fieldVal, visited, annFound);

                        if (!annFound && tup.get2())
                            // Update value only if annotation was found in recursive call.
                            val = tup.get1();

                        annFound = tup.get2();
                    }
                }
                else {
                    if (annFound)
                        throw new IgniteCheckedException("Multiple annotations has been found [cls=" + cls.getName() +
                            ", ann=" + annCls.getSimpleName() + ']');

                    val = fieldVal;

                    annFound = true;
                }
            }

            // Methods.
            for (Method m : methodsWithAnnotation(cls)) {
                if (annFound)
                    throw new IgniteCheckedException("Multiple annotations has been found [cls=" + cls.getName() +
                        ", ann=" + annCls.getSimpleName() + ']');

                m.setAccessible(true);

                try {
                    val = m.invoke(target);
                }
                catch (Exception e) {
                    throw new IgniteCheckedException("Failed to get annotated method value [cls=" + cls.getName() +
                        ", ann=" + annCls.getSimpleName() + ']', e);
                }

                annFound = true;
            }
        }

        return F.t(val, annFound);
    }

    /**
     * @param f Field.
     * @return {@code true} if recursive inspection is required.
     */
    private boolean needsRecursion(Field f) {
        assert f != null;

        // Need to inspect anonymous classes, callable and runnable instances.
        return f.getName().startsWith("this$") || f.getName().startsWith("val$") ||
            Callable.class.isAssignableFrom(f.getType()) || Runnable.class.isAssignableFrom(f.getType());
    }

    /**
     * Gets all entries from the specified class or its super-classes that have
     * been annotated with annotation provided.
     *
     * @param cls Class in which search for fields.
     * @return Set of entries with given annotations.
     */
    private Iterable<Field> fieldsWithAnnotation(Class<?> cls) {
        synchronized (mux) {
            List<Field> fields = fieldCache.get(cls);
            if (fields == null) {
                fields = new ArrayList<>();

                for (Field field : cls.getDeclaredFields()) {
                    Annotation ann = field.getAnnotation(annCls);

                    if (ann != null || needsRecursion(field))
                        fields.add(field);
                }

                if (!fields.isEmpty())
                    fieldCache.put(cls, fields);
            }

            return fields;
        }
    }

    /**
     * Gets set of methods with given annotation.
     *
     * @param cls Class in which search for methods.
     * @return Set of methods with given annotations.
     */
    private Iterable<Method> methodsWithAnnotation(Class<?> cls) {
        synchronized (mux) {
            List<Method> mtds = mtdCache.get(cls);

            if (mtds == null) {
                mtds = new ArrayList<>();

                for (Method mtd : cls.getDeclaredMethods()) {
                    Annotation ann = mtd.getAnnotation(annCls);

                    if (ann != null)
                        mtds.add(mtd);
                }

                if (!mtds.isEmpty())
                    mtdCache.put(cls, mtds);
            }

            return mtds;
        }
    }
}
