/*
 * 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.deltaspike.core.util;

import jakarta.enterprise.inject.Vetoed;
import jakarta.enterprise.inject.spi.Annotated;
import jakarta.enterprise.inject.spi.AnnotatedCallable;
import jakarta.enterprise.inject.spi.AnnotatedConstructor;
import jakarta.enterprise.inject.spi.AnnotatedField;
import jakarta.enterprise.inject.spi.AnnotatedMethod;
import jakarta.enterprise.inject.spi.AnnotatedParameter;
import jakarta.enterprise.inject.spi.AnnotatedType;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * <p>
 * Utilities for working with {@link Annotated}s.
 * </p>
 * <p/>
 * <p>
 * Includes utilities to check the equality of and create unique id's for
 * <code>Annotated</code> instances.
 * </p>
 */
public final class Annotateds
{
    private static final char SEPARATOR = ';';

    private Annotateds()
    {
        // this is a utility class with statics only
    }

    /**
     * Does the first stage of comparing AnnoatedCallables, however it cannot
     * compare the method parameters
     */
    @Vetoed
    private static class AnnotatedCallableComparator<T>
        implements Comparator<AnnotatedCallable<? super T>>, Serializable
    {

        public int compare(AnnotatedCallable<? super T> arg0, AnnotatedCallable<? super T> arg1)
        {
            // compare the names first
            int result = (arg0.getJavaMember().getName().compareTo(arg1.getJavaMember().getName()));
            if (result != 0)
            {
                return result;
            }
            result = arg0.getJavaMember().getDeclaringClass().getName().compareTo(arg1.getJavaMember()
                    .getDeclaringClass().getName());
            if (result != 0)
            {
                return result;
            }
            result = arg0.getParameters().size() - arg1.getParameters().size();
            return result;
        }

    }

    @Vetoed
    private static class AnnotatedMethodComparator<T>
        implements Comparator<AnnotatedMethod<? super T>>, Serializable
    {

        private AnnotatedCallableComparator<T> callableComparator = new AnnotatedCallableComparator<T>();

        public static <T> Comparator<AnnotatedMethod<? super T>> instance()
        {
            return new AnnotatedMethodComparator<T>();
        }

        public int compare(AnnotatedMethod<? super T> arg0, AnnotatedMethod<? super T> arg1)
        {
            int result = callableComparator.compare(arg0, arg1);
            if (result != 0)
            {
                return result;
            }
            for (int i = 0; i < arg0.getJavaMember().getParameterTypes().length; ++i)
            {
                Class<?> p0 = arg0.getJavaMember().getParameterTypes()[i];
                Class<?> p1 = arg1.getJavaMember().getParameterTypes()[i];
                result = p0.getName().compareTo(p1.getName());
                if (result != 0)
                {
                    return result;
                }
            }
            return 0;
        }

    }

    @Vetoed
    private static class AnnotatedConstructorComparator<T>
        implements Comparator<AnnotatedConstructor<? super T>>, Serializable
    {

        private AnnotatedCallableComparator<T> callableComparator = new AnnotatedCallableComparator<T>();

        public static <T> Comparator<AnnotatedConstructor<? super T>> instance()
        {
            return new AnnotatedConstructorComparator<T>();
        }

        public int compare(AnnotatedConstructor<? super T> arg0, AnnotatedConstructor<? super T> arg1)
        {
            int result = callableComparator.compare(arg0, arg1);
            if (result != 0)
            {
                return result;
            }
            for (int i = 0; i < arg0.getJavaMember().getParameterTypes().length; ++i)
            {
                Class<?> p0 = arg0.getJavaMember().getParameterTypes()[i];
                Class<?> p1 = arg1.getJavaMember().getParameterTypes()[i];
                result = p0.getName().compareTo(p1.getName());
                if (result != 0)
                {
                    return result;
                }
            }
            return 0;
        }

    }

    @Vetoed
    private static class AnnotatedFieldComparator<T>
        implements Comparator<AnnotatedField<? super T>>, Serializable
    {

        public static <T> Comparator<AnnotatedField<? super T>> instance()
        {
            return new AnnotatedFieldComparator<T>();
        }

        public int compare(AnnotatedField<? super T> arg0, AnnotatedField<? super T> arg1)
        {
            if (arg0.getJavaMember().getName().equals(arg1.getJavaMember().getName()))
            {
                return arg0.getJavaMember().getDeclaringClass().getName().compareTo(arg1.getJavaMember()
                        .getDeclaringClass().getName());
            }
            return arg0.getJavaMember().getName().compareTo(arg1.getJavaMember().getName());
        }

    }

    @Vetoed
    private static class AnnotationComparator implements Comparator<Annotation>, Serializable
    {

        public static final Comparator<Annotation> INSTANCE = new AnnotationComparator();

        public int compare(Annotation arg0, Annotation arg1)
        {
            return arg0.annotationType().getName().compareTo(arg1.annotationType().getName());
        }
    }

    @Vetoed
    private static class MethodComparator implements Comparator<Method>
    {

        public static final Comparator<Method> INSTANCE = new MethodComparator();

        public int compare(Method arg0, Method arg1)
        {
            return arg0.getName().compareTo(arg1.getName());
        }
    }

    /**
     * Generates a deterministic signature for an {@link AnnotatedType}. Two
     * <code>AnnotatedType</code>s that have the same annotations and underlying
     * type will generate the same signature.
     * <p/>
     * This can be used to create a unique bean id for a passivation capable bean
     * that is added directly through the SPI.
     *
     * @param annotatedType The type to generate a signature for
     * @return A string representation of the annotated type
     */
    public static <X> String createTypeId(AnnotatedType<X> annotatedType)
    {
        return createTypeId(annotatedType.getJavaClass(), annotatedType.getAnnotations(), annotatedType.getMethods(),
                annotatedType.getFields(), annotatedType.getConstructors());
    }

    /**
     * Generates a unique signature for a concrete class. Annotations are not
     * read directly from the class, but are read from the
     * <code>annotations</code>, <code>methods</code>, <code>fields</code> and
     * <code>constructors</code> arguments
     *
     * @param clazz        The java class type
     * @param annotations  Annotations present on the java class
     * @param methods      The AnnotatedMethods to include in the signature
     * @param fields       The AnnotatedFields to include in the signature
     * @param constructors The AnnotatedConstructors to include in the signature
     * @return A string representation of the type
     */
    public static <X> String createTypeId(Class<X> clazz, Collection<Annotation> annotations,
                                          Collection<AnnotatedMethod<? super X>> methods,
                                          Collection<AnnotatedField<? super X>> fields,
                                          Collection<AnnotatedConstructor<X>> constructors)
    {
        StringBuilder builder = new StringBuilder();

        builder.append(clazz.getName());
        builder.append(createAnnotationCollectionId(annotations));
        builder.append("{");

        // now deal with the fields
        List<AnnotatedField<? super X>> sortedFields = new ArrayList<AnnotatedField<? super X>>();
        sortedFields.addAll(fields);
        Collections.sort(sortedFields, AnnotatedFieldComparator.<X>instance());
        for (AnnotatedField<? super X> field : sortedFields)
        {
            if (!field.getAnnotations().isEmpty())
            {
                builder.append(createFieldId(field));
                builder.append(SEPARATOR);
            }
        }

        // methods
        List<AnnotatedMethod<? super X>> sortedMethods = new ArrayList<AnnotatedMethod<? super X>>();
        sortedMethods.addAll(methods);
        Collections.sort(sortedMethods, AnnotatedMethodComparator.<X>instance());
        for (AnnotatedMethod<? super X> method : sortedMethods)
        {
            if (!method.getAnnotations().isEmpty() || hasMethodParameters(method))
            {
                builder.append(createCallableId(method));
                builder.append(SEPARATOR);
            }
        }

        // constructors
        List<AnnotatedConstructor<? super X>> sortedConstructors = new ArrayList<AnnotatedConstructor<? super X>>();
        sortedConstructors.addAll(constructors);
        Collections.sort(sortedConstructors, AnnotatedConstructorComparator.<X>instance());
        for (AnnotatedConstructor<? super X> constructor : sortedConstructors)
        {
            if (!constructor.getAnnotations().isEmpty() || hasMethodParameters(constructor))
            {
                builder.append(createCallableId(constructor));
                builder.append(SEPARATOR);
            }
        }
        builder.append("}");

        return builder.toString();
    }

    /**
     * Generates a deterministic signature for an {@link AnnotatedField}. Two
     * <code>AnnotatedField</code>s that have the same annotations and
     * underlying field will generate the same signature.
     */
    public static <X> String createFieldId(AnnotatedField<X> field)
    {
        return createFieldId(field.getJavaMember(), field.getAnnotations());
    }

    /**
     * Creates a deterministic signature for a {@link Field}.
     *
     * @param field       The field to generate the signature for
     * @param annotations The annotations to include in the signature
     */
    public static <X> String createFieldId(Field field, Collection<Annotation> annotations)
    {
        StringBuilder builder = new StringBuilder();
        builder.append(field.getDeclaringClass().getName());
        builder.append('.');
        builder.append(field.getName());
        builder.append(createAnnotationCollectionId(annotations));
        return builder.toString();
    }

    /**
     * Generates a deterministic signature for an {@link AnnotatedCallable}. Two
     * <code>AnnotatedCallable</code>s that have the same annotations and
     * underlying callable will generate the same signature.
     */
    public static <X> String createCallableId(AnnotatedCallable<X> method)
    {
        StringBuilder builder = new StringBuilder();
        builder.append(method.getJavaMember().getDeclaringClass().getName());
        builder.append('.');
        builder.append(method.getJavaMember().getName());
        builder.append(createAnnotationCollectionId(method.getAnnotations()));
        builder.append(createParameterListId(method.getParameters()));
        return builder.toString();
    }

    /**
     * Creates a deterministic signature for a {@link Method}.
     *
     * @param method      The method to generate the signature for
     * @param annotations The annotations to include in the signature
     * @param parameters  The {@link AnnotatedParameter}s to include in the
     *                    signature
     */
    public static <X> String createMethodId(Method method, Set<Annotation> annotations,
                                            List<AnnotatedParameter<X>> parameters)
    {
        StringBuilder builder = new StringBuilder();
        builder.append(method.getDeclaringClass().getName());
        builder.append('.');
        builder.append(method.getName());
        builder.append(createAnnotationCollectionId(annotations));
        builder.append(createParameterListId(parameters));
        return builder.toString();
    }

    /**
     * Creates a deterministic signature for a {@link Constructor}.
     *
     * @param constructor The constructor to generate the signature for
     * @param annotations The annotations to include in the signature
     * @param parameters  The {@link AnnotatedParameter}s to include in the
     *                    signature
     */
    public static <X> String createConstructorId(Constructor<X> constructor, Set<Annotation> annotations,
                                                 List<AnnotatedParameter<X>> parameters)
    {
        StringBuilder builder = new StringBuilder();
        builder.append(constructor.getDeclaringClass().getName());
        builder.append('.');
        builder.append(constructor.getName());
        builder.append(createAnnotationCollectionId(annotations));
        builder.append(createParameterListId(parameters));
        return builder.toString();
    }

    /**
     * Generates a unique string representation of a list of
     * {@link AnnotatedParameter}s.
     */
    public static <X> String createParameterListId(List<AnnotatedParameter<X>> parameters)
    {
        StringBuilder builder = new StringBuilder();
        builder.append("(");
        for (int i = 0; i < parameters.size(); ++i)
        {
            AnnotatedParameter<X> ap = parameters.get(i);
            builder.append(createParameterId(ap));
            if (i + 1 != parameters.size())
            {
                builder.append(',');
            }
        }
        builder.append(")");
        return builder.toString();
    }

    /**
     * Creates a string representation of an {@link AnnotatedParameter}.
     */
    public static <X> String createParameterId(AnnotatedParameter<X> annotatedParameter)
    {
        return createParameterId(annotatedParameter.getBaseType(), annotatedParameter.getAnnotations());
    }

    /**
     * Creates a string representation of a given type and set of annotations.
     */
    public static <X> String createParameterId(Type type, Set<Annotation> annotations)
    {
        StringBuilder builder = new StringBuilder();
        if (type instanceof Class<?>)
        {
            Class<?> c = (Class<?>) type;
            builder.append(c.getName());
        }
        else
        {
            builder.append(type.toString());
        }
        builder.append(createAnnotationCollectionId(annotations));
        return builder.toString();
    }

    /**
     * <p>
     * Compares {@link AnnotatedField}s for equality.
     * </p>
     * <p>
     * Two {@link AnnotatedField}s are considered equal if they have the same
     * underlying field and annotations.
     * </p>
     */
    public static boolean compareAnnotatedField(AnnotatedField<?> f1, AnnotatedField<?> f2)
    {
        if (!f1.getJavaMember().equals(f2.getJavaMember()))
        {
            return false;
        }
        return compareAnnotated(f1, f2);
    }

    /**
     * <p>
     * Compare {@link AnnotatedCallable}s for equality.
     * </p>
     * <p/>
     * <p>
     * Two {@link AnnotatedCallable}s are considered equal if they have the same
     * underlying callable and annotations.
     * </p>
     */
    public static boolean compareAnnotatedCallable(AnnotatedCallable<?> m1, AnnotatedCallable<?> m2)
    {
        if (!m1.getJavaMember().equals(m2.getJavaMember()))
        {
            return false;
        }
        if (!compareAnnotated(m1, m2))
        {
            return false;
        }
        return compareAnnotatedParameters(m1.getParameters(), m2.getParameters());
    }

    /**
     * <p>
     * Compares two {@link AnnotatedType}s for equality.
     * </p>
     * <p/>
     * <p>
     * Two {@link AnnotatedType}s are considered equal if they have the same
     * underlying type and annotations, and all members have the same
     * annotations.
     * </p>
     */
    public static boolean compareAnnotatedTypes(AnnotatedType<?> t1, AnnotatedType<?> t2)
    {
        if (!t1.getJavaClass().equals(t2.getJavaClass()))
        {
            return false;
        }
        if (!compareAnnotated(t1, t2))
        {
            return false;
        }

        if (t1.getFields().size() != t2.getFields().size())
        {
            return false;
        }
        Map<Field, AnnotatedField<?>> fields = new HashMap<Field, AnnotatedField<?>>();
        for (AnnotatedField<?> f : t2.getFields())
        {
            fields.put(f.getJavaMember(), f);
        }
        for (AnnotatedField<?> f : t1.getFields())
        {
            if (fields.containsKey(f.getJavaMember()))
            {
                if (!compareAnnotatedField(f, fields.get(f.getJavaMember())))
                {
                    return false;
                }
            }
            else
            {
                return false;
            }
        }

        if (t1.getMethods().size() != t2.getMethods().size())
        {
            return false;
        }
        Map<Method, AnnotatedMethod<?>> methods = new HashMap<Method, AnnotatedMethod<?>>();
        for (AnnotatedMethod<?> f : t2.getMethods())
        {
            methods.put(f.getJavaMember(), f);
        }
        for (AnnotatedMethod<?> f : t1.getMethods())
        {
            if (methods.containsKey(f.getJavaMember()))
            {
                if (!compareAnnotatedCallable(f, methods.get(f.getJavaMember())))
                {
                    return false;
                }
            }
            else
            {
                return false;
            }
        }
        if (t1.getConstructors().size() != t2.getConstructors().size())
        {
            return false;
        }
        Map<Constructor<?>, AnnotatedConstructor<?>> constructors =
                new HashMap<Constructor<?>, AnnotatedConstructor<?>>();
        for (AnnotatedConstructor<?> f : t2.getConstructors())
        {
            constructors.put(f.getJavaMember(), f);
        }
        for (AnnotatedConstructor<?> f : t1.getConstructors())
        {
            if (constructors.containsKey(f.getJavaMember()))
            {
                if (!compareAnnotatedCallable(f, constructors.get(f.getJavaMember())))
                {
                    return false;
                }
            }
            else
            {
                return false;
            }
        }
        return true;

    }

    private static <X> boolean hasMethodParameters(AnnotatedCallable<X> callable)
    {
        for (AnnotatedParameter<X> parameter : callable.getParameters())
        {
            if (!parameter.getAnnotations().isEmpty())
            {
                return true;
            }
        }
        return false;
    }

    private static String createAnnotationCollectionId(Collection<Annotation> annotations)
    {
        if (annotations.isEmpty())
        {
            return "";
        }

        StringBuilder builder = new StringBuilder();
        builder.append('[');

        List<Annotation> annotationList = new ArrayList<Annotation>(annotations.size());
        annotationList.addAll(annotations);
        Collections.sort(annotationList, AnnotationComparator.INSTANCE);

        for (Annotation a : annotationList)
        {
            builder.append('@');
            builder.append(a.annotationType().getName());
            builder.append('(');
            Method[] declaredMethods = a.annotationType().getDeclaredMethods();
            List<Method> methods = new ArrayList<Method>(declaredMethods.length);
            methods.addAll(Arrays.asList(declaredMethods));
            Collections.sort(methods, MethodComparator.INSTANCE);

            for (int i = 0; i < methods.size(); ++i)
            {
                Method method = methods.get(i);
                try
                {
                    Object value = method.invoke(a);
                    builder.append(method.getName());
                    builder.append('=');
                    if (value.getClass().isArray())
                    {
                        Class<?> componentType = value.getClass().getComponentType();
                        if (componentType.isAnnotation())
                        {
                            builder.append(Arrays.asList((Object[]) value));
                        }
                        else if (componentType instanceof Class<?>)
                        {
                            builder.append(createArrayId(value));
                        }
                        else
                        {
                            builder.append(value.toString());
                        }
                    }
                    else
                    {
                        builder.append(value.toString());
                    }
                }
                catch (NullPointerException e)
                {
                    throw new RuntimeException("NullPointerException accessing annotation member, annotation:"
                            + a.annotationType().getName() + " member: " + method.getName(), e);
                }
                catch (IllegalArgumentException e)
                {
                    throw new RuntimeException("IllegalArgumentException accessing annotation member, annotation:"
                            + a.annotationType().getName() + " member: " + method.getName(), e);
                }
                catch (IllegalAccessException e)
                {
                    throw new RuntimeException("IllegalAccessException accessing annotation member, annotation:"
                            + a.annotationType().getName() + " member: " + method.getName(), e);
                }
                catch (InvocationTargetException e)
                {
                    throw new RuntimeException("InvocationTargetException accessing annotation member, annotation:"
                            + a.annotationType().getName() + " member: " + method.getName(), e);
                }
                if (i + 1 != methods.size())
                {
                    builder.append(',');
                }
            }
            builder.append(')');
        }
        builder.append(']');
        return builder.toString();
    }

    /**
     * Appends comma separated list of class names from classArray to builder
     */
    private static String createArrayId(Object value)
    {
        if (value instanceof int[])
        {
            return Arrays.toString((int[]) value);
        }
        if (value instanceof short[])
        {
            return Arrays.toString((short[]) value);
        }
        if (value instanceof byte[])
        {
            return Arrays.toString((byte[]) value);
        }
        if (value instanceof char[])
        {
            return Arrays.toString((char[]) value);
        }
        if (value instanceof long[])
        {
            return Arrays.toString((long[]) value);
        }
        if (value instanceof float[])
        {
            return Arrays.toString((float[]) value);
        }
        if (value instanceof double[])
        {
            return Arrays.toString((double[]) value);
        }
        if (value instanceof String[])
        {
            return Arrays.toString((String[]) value);
        }

        return createClassArrayId((Class<?>[]) value);
    }

    /**
     * Appends comma separated list of class names from classArray to builder
     */
    private static String createClassArrayId(Class<?>[] classArray)
    {
        StringBuilder builder = new StringBuilder();
        builder.append('[');
        for (int i = 0; i < classArray.length; i++)
        {
            builder.append(classArray[i].getName());
            if (i + 1 != classArray.length)
            {
                builder.append(',');
            }
        }
        builder.append(']');
        return builder.toString();
    }

    /**
     * Compares two annotated elements to see if they have the same annotations
     */
    private static boolean compareAnnotated(Annotated a1, Annotated a2)
    {
        return a1.getAnnotations().equals(a2.getAnnotations());
    }

    /**
     * Compares two annotated elements to see if they have the same annotations
     */
    private static boolean compareAnnotatedParameters(List<? extends AnnotatedParameter<?>> p1,
                                                      List<? extends AnnotatedParameter<?>> p2)
    {
        if (p1.size() != p2.size())
        {
            return false;
        }
        for (int i = 0; i < p1.size(); ++i)
        {
            if (!compareAnnotated(p1.get(i), p2.get(i)))
            {
                return false;
            }
        }
        return true;
    }

}
