/*
 * 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.johnzon.mapper.access;

import static java.util.Arrays.asList;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;

public final class Meta {
    private Meta() {
        // no-op
    }

    public static <T extends Annotation> T getAnnotation(final AnnotatedElement holder, final Class<T> api) {
        return getDirectAnnotation(holder, api);
    }

    public static <T extends Annotation> T getClassOrPackageAnnotation(final Method holder, final Class<T> api) {
        return getIndirectAnnotation(api, holder::getDeclaringClass, () -> holder.getDeclaringClass().getPackage());
    }

    public static <T extends Annotation> T getClassOrPackageAnnotation(final Field holder, final Class<T> api) {
        return getIndirectAnnotation(api, holder::getDeclaringClass, () -> holder.getDeclaringClass().getPackage());
    }

    private static <T extends Annotation> T getDirectAnnotation(final AnnotatedElement holder, final Class<T> api) {
        final T annotation = holder.getAnnotation(api);
        if (annotation != null) {
            return annotation;
        }
        final T meta = findMeta(holder.getAnnotations(), api);
        if (meta != null) {
            return meta;
        }
        return null;
    }

    private static <T extends Annotation> T getIndirectAnnotation(final Class<T> api,
                                                                  final Supplier<Class<?>> ownerSupplier,
                                                                  final Supplier<Package> packageSupplier) {
        final T ownerAnnotation = ownerSupplier.get().getAnnotation(api);
        if (ownerAnnotation != null) {
            return ownerAnnotation;
        } // todo: meta?
        final Package pck = packageSupplier.get();
        if (pck != null) {
            final T pckAnnotation = pck.getAnnotation(api);
            if (pckAnnotation != null) {
                return pckAnnotation;
            }
        } // todo: meta?
        return null;
    }

    public static <T extends Annotation> T getAnnotation(final Class<?> clazz, final Class<T> api) {
        Class<?> current = clazz;
        final Set<Class<?>> visited = new HashSet<>();
        while (current != null && current != Object.class) {
            if (!visited.add(current)) {
                return null;
            }
            final T annotation = current.getAnnotation(api);
            if (annotation != null) {
                return annotation;
            }
            final T meta = findMeta(clazz.getAnnotations(), api);
            if (meta != null) {
                return meta;
            }
            current = current.getSuperclass();
        }
        return null;
    }

    public static <T extends Annotation> T getAnnotation(final Package pck, final Class<T> api) {
        final T annotation = pck.getAnnotation(api);
        if (annotation != null) {
            return annotation;
        }
        return findMeta(pck.getAnnotations(), api);
    }

    public static <T extends Annotation> T findMeta(final Annotation[] annotations, final Class<T> api) {
        for (final Annotation a : annotations) {
            final Class<? extends Annotation> userType = a.annotationType();
            final T aa = userType.getAnnotation(api);
            if (aa != null) {
                boolean overriden = false;
                final Map<String, Method> mapping = new HashMap<String, Method>();
                for (final Class<?> cm : asList(api, userType)) {
                    for (final Method m : cm.getMethods()) {
                        overriden = mapping.put(m.getName(), m) != null || overriden;
                    }
                }
                if (!overriden) {
                    return aa;
                }
                return api.cast(newAnnotation(mapping, a, aa));
            }
        }
        return null;
    }

    private static <T extends Annotation> T newAnnotation(final Map<String, Method> methodMapping, final Annotation user, final T johnzon) {
        return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{johnzon.annotationType()},
                (proxy, method, args) -> {
                    final Method m = methodMapping.get(method.getName());
                    try {
                        if (m.getDeclaringClass() == user.annotationType()) {
                            return m.invoke(user, args);
                        }
                        return m.invoke(johnzon, args);
                    } catch (final InvocationTargetException ite) {
                        throw ite.getTargetException();
                    }
                });
    }
}
