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

import org.apache.commons.lang3.Validate;
import org.apache.wayang.core.api.exception.WayangException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Utilities for reflection code.
 */
public class ReflectionUtils {

    private static final Logger logger = LogManager.getLogger(ReflectionUtils.class);

    private static final Pattern defaultConstructorPattern = Pattern.compile(
            "new ([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)\\(\\)"
    );

    private static final Pattern arglessMethodPattern = Pattern.compile(
            "([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)\\.([a-zA-Z_][a-zA-Z0-9_]*)\\(\\)"
    );

    private static final Pattern constantPattern = Pattern.compile(
            "([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)\\.([a-zA-Z_][a-zA-Z0-9_]*)"
    );

    private static final List<Tuple<Class<?>, Supplier<?>>> defaultParameterSuppliers = Arrays.asList(
            new Tuple<>(byte.class, () -> (byte) 0),
            new Tuple<>(Integer.class, () -> (byte) 0),
            new Tuple<>(short.class, () -> (short) 0),
            new Tuple<>(Short.class, () -> (short) 0),
            new Tuple<>(int.class, () -> 0),
            new Tuple<>(Integer.class, () -> 0),
            new Tuple<>(long.class, () -> 0L),
            new Tuple<>(Long.class, () -> 0L),
            new Tuple<>(boolean.class, () -> false),
            new Tuple<>(Boolean.class, () -> false),
            new Tuple<>(float.class, () -> 0f),
            new Tuple<>(Float.class, () -> 0f),
            new Tuple<>(double.class, () -> 0d),
            new Tuple<>(Double.class, () -> 0d),
            new Tuple<>(char.class, () -> '\0'),
            new Tuple<>(Character.class, () -> '\0'),
            new Tuple<>(String.class, () -> "")
    );

    /**
     * Identifies and returns the JAR file declaring the {@link Class} of the given {@code object} or {@code null} if
     * no such file could be determined.
     */
    public static String getDeclaringJar(Object object) {
        Objects.requireNonNull(object);

        return getDeclaringJar(object.getClass());
    }

    /**
     * Identifies and returns the JAR file declaring the given {@link Class} if no such file could be determined.
     */
    public static String getDeclaringJar(Class<?> cls) {
        try {
            final URL location = cls.getProtectionDomain().getCodeSource().getLocation();
            final URI uri = location.toURI();
            final String path = uri.getPath();
            if (path.endsWith(".jar")) {
                return path;
            } else {
                logger.warn("Class {} is not loaded from a JAR file, but from {}. Thus, cannot provide the JAR file.", cls, path);
            }
        } catch (Exception e) {
            logger.error(String.format("Could not determine JAR file declaring %s.", cls), e);
        }
        return null;
    }

    /**
     * Provides a resource as an {@link InputStream}.
     *
     * @param resourceName the name or path of the resource
     * @return an {@link InputStream} with the contents of the resource or {@code null} if the resource could not be found
     */
    public static InputStream loadResource(String resourceName) {
        return Thread.currentThread().getContextClassLoader().getResourceAsStream(resourceName);
    }

    /**
     * Provides a resource as {@link URL}.
     *
     * @param resourceName the name or path of the resource
     * @return a {@link URL} describing the path to the resource or {@code null} if the resource could not be found
     */
    public static URL getResourceURL(String resourceName) {
        return Thread.currentThread().getContextClassLoader().getResource(resourceName);
    }

    /**
     * Casts the given {@link Class} to a more specific one.
     *
     * @param baseClass that should be casted
     * @param <T>       the specific type parameter for the {@code baseClass}
     * @return the {@code baseClass}, casted
     */
    @SuppressWarnings("unchecked")
    public static <T> Class<T> specify(Class<? super T> baseClass) {
        return (Class<T>) baseClass;
    }

    /**
     * Casts the given {@link Class} to a more general one.
     *
     * @param baseClass that should be casted
     * @param <T>       the specific type parameter for the {@code baseClass}
     * @return the {@code baseClass}, casted
     */
    @SuppressWarnings("unchecked")
    public static <T> Class<T> generalize(Class<? extends T> baseClass) {
        return (Class<T>) baseClass;
    }

    /**
     * Tries to evaluate the given statement, thereby supporting the following statement types:
     * <ul>
     * <li>{@code new <class name>()}</li>
     * <li>{@code <class name>.<constant>}</li>
     * <li>{@code <class name>.<static method>}()</li>
     * </ul>
     *
     * @param statement the statement
     * @param <T>       the return type
     * @return the result of the evaluated statement
     */
    public static <T> T evaluate(String statement) throws IllegalArgumentException {
        statement = statement.trim();
        Matcher matcher = defaultConstructorPattern.matcher(statement);
        if (matcher.matches()) {
            try {
                return instantiateDefault(matcher.group(1));
            } catch (Exception e) {
                throw new IllegalArgumentException(String.format("Could not instantiate '%s'.", statement), e);
            }
        }

        matcher = arglessMethodPattern.matcher(statement);
        if (matcher.matches()) {
            try {
                return executeStaticArglessMethod(matcher.group(1), matcher.group(2));
            } catch (Exception e) {
                throw new IllegalArgumentException(String.format("Could not execute '%s'.", statement), e);
            }
        }

        matcher = constantPattern.matcher(statement);
        if (matcher.matches()) {
            try {
                return retrieveStaticVariable(matcher.group(1), matcher.group(2));
            } catch (Exception e) {
                throw new IllegalArgumentException(String.format("Could not execute '%s'.", statement), e);
            }
        }

        throw new IllegalArgumentException(String.format("Unknown expression type: '%s'.", statement));
    }

    /**
     * Executes a parameterless static method.
     *
     * @param className  the name of the {@link Class} that comprises the method
     * @param methodName the name of the method
     * @param <T>
     * @return the return value of the executed method
     */
    @SuppressWarnings("unchecked")
    public static <T> T executeStaticArglessMethod(String className, String methodName) {
        try {
            final Class<?> cls = Class.forName(className);
            final Method method = cls.getMethod(methodName);
            Validate.isTrue(method.getParameters().length == 0, "Method has parameters.");
            return (T) method.invoke(null);
        } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            throw new IllegalArgumentException(String.format("Could not execute %s.%s().", className, methodName), e);
        }
    }

    /**
     * Retrieves a static variable.
     *
     * @param className    the name of the {@link Class} that comprises the method
     * @param variableName the name of the method
     * @param <T>
     * @return the return value of the executed method
     */
    @SuppressWarnings("unchecked")
    public static <T> T retrieveStaticVariable(String className, String variableName) {
        try {
            final Class<?> cls = Class.forName(className);
            final Field field = cls.getField(variableName);
            return (T) field.get(null);
        } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
            throw new IllegalArgumentException(String.format("Could not retrieve %s.%s.", className, variableName), e);
        }
    }

    /**
     * Creates a new instance of a {@link Class} via the default constructor.
     *
     * @param className name of the {@link Class} to be instantiated
     * @return the instance
     */
    public static <T> T instantiateDefault(String className) {
        try {
            @SuppressWarnings("unchecked") // Will fail anyway, if incorrect.
                    Class<T> cls = (Class<T>) Class.forName(className);
            return cls.newInstance();
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            throw new WayangException("Could not instantiate class.", e);
        }
    }

    /**
     * Creates a new instance of the given {@link Class} via the default constructor.
     *
     * @param cls the {@link Class} to be instantiated
     * @return the instance
     */
    public static <T> T instantiateDefault(Class<? extends T> cls) {
        try {
            return cls.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new WayangException("Could not instantiate class.", e);
        }
    }

    /**
     * Tries to instantiate an arbitrary instance of the given {@link Class}.
     *
     * @param cls               that should be instantiated
     * @param defaultParameters designate specific default parameter values for parameter {@link Class}es
     * @return the instance
     */
    @SuppressWarnings("unchecked")
    public static <T> T instantiateSomehow(Class<T> cls, Tuple<Class<?>, Supplier<?>>... defaultParameters) {
        return instantiateSomehow(cls, Arrays.asList(defaultParameters));
    }

    /**
     * Tries to instantiate an arbitrary instance of the given {@link Class}.
     *
     * @param cls               that should be instantiated
     * @param defaultParameters designate specific default parameter values for parameter {@link Class}es
     * @return the instance
     */
    @SuppressWarnings("unchecked")
    public static <T> T instantiateSomehow(Class<T> cls, List<Tuple<Class<?>, Supplier<?>>> defaultParameters) {
        try {
            for (Constructor<?> constructor : cls.getConstructors()) {
                try {
                    final Class<?>[] parameterTypes = constructor.getParameterTypes();
                    Object[] parameters = new Object[parameterTypes.length];
                    for (int i = 0; i < parameterTypes.length; i++) {
                        Class<?> parameterType = parameterTypes[i];
                        Object parameter = getDefaultParameter(parameterType, defaultParameters);
                        if (parameter == null) {
                            parameter = getDefaultParameter(parameterType, defaultParameterSuppliers);
                        }
                        parameters[i] = parameter;
                    }
                    return (T) constructor.newInstance(parameters);
                } catch (Throwable t) {
                    logger.debug("Could not instantiate {}.", cls.getSimpleName(), t);
                }
            }
        } catch (Throwable t) {
            throw new WayangException(String.format("Could not get constructors for %s.", cls.getSimpleName()));
        }

        throw new WayangException(String.format("Could not instantiate %s.", cls.getSimpleName()));
    }

    /**
     * Searches for a compatible {@link Class} in the given {@link List} (subclass or equal) for the given parameter
     * {@link Class}. If a match is found, the corresponding {@link Supplier} is used to create a default parameter.
     *
     * @param parameterClass            {@link Class} of a parameter
     * @param defaultParameterSuppliers supply default values for various parameter {@link Class}es
     * @return the first match's {@link Supplier} value or {@code null} if no match was found
     */
    private static Object getDefaultParameter(Class<?> parameterClass, List<Tuple<Class<?>, Supplier<?>>> defaultParameterSuppliers) {
        for (Tuple<Class<?>, Supplier<?>> defaultParameterSupplier : defaultParameterSuppliers) {
            if (parameterClass.isAssignableFrom(defaultParameterSupplier.getField0())) {
                return defaultParameterSupplier.getField1().get();
            }
        }
        return null;
    }

    /**
     * Retrieve the {@link Type}s of type parameters from an extended/implemented superclass/interface.
     *
     * @param subclass   the {@link Class} implementing the superclass/interface with type parameters
     * @param superclass the superclass/interface defining the type parameters
     * @return a {@link Map} mapping the type parameter names to their implemented {@link Type}
     */
    public static Map<String, Type> getTypeParameters(Class<?> subclass, Class<?> superclass) {
        // Gather implemented interfaces and superclass.
        List<Type> genericSupertypes = Stream.concat(
                Stream.of(subclass.getGenericSuperclass()), Stream.of(subclass.getGenericInterfaces())
        ).collect(Collectors.toList());

        for (Type supertype : genericSupertypes) {

            if (supertype instanceof Class<?>) {
                // If the supertype is a Class, there are no type parameters to worry about.
                Class<?> cls = (Class<?>) supertype;
                if (!superclass.isAssignableFrom(cls)) continue;
                return getTypeParameters(cls, superclass);

            } else if (supertype instanceof ParameterizedType) {
                // Handle type parameters.
                ParameterizedType parameterizedType = (ParameterizedType) supertype;
                final Type rawType = parameterizedType.getRawType();
                if (!(rawType instanceof Class<?>)) continue;
                Class<?> cls = (Class<?>) rawType;
                if (!superclass.isAssignableFrom(cls)) continue;
                final Map<String, Type> localTypeArguments = getTypeArguments(parameterizedType);

                if (cls.equals(superclass)) {
                    // If we reached the superclass, we are good.
                    return localTypeArguments;

                } else {
                    // If we are not there yet, we need to consider to "redirect" type parameters.
                    final Map<String, Type> preliminaryResult = getTypeParameters(cls, superclass);
                    final Map<String, Type> result = new HashMap<>(preliminaryResult.size());
                    for (Map.Entry<String, Type> entry : preliminaryResult.entrySet()) {
                        if (entry.getValue() instanceof TypeVariable<?>) {
                            final Type translatedTypeArgument = localTypeArguments.getOrDefault(
                                    ((TypeVariable) entry.getValue()).getName(),
                                    entry.getValue()
                            );
                            result.put(entry.getKey(), translatedTypeArgument);
                        } else {
                            result.put(entry.getKey(), entry.getValue());
                        }
                    }
                    return result;

                }
            }
        }

        return Collections.emptyMap();
    }

    /**
     * Put the {@link TypeVariable}s of a {@link ParameterizedType} into a {@link Map}.
     *
     * @param type the {@link ParameterizedType}
     * @return the {@link Map} with the type parameter names as keys
     */
    private static Map<String, Type> getTypeArguments(ParameterizedType type) {
        final Type[] typeArguments = type.getActualTypeArguments();
        final Class<?> rawType = (Class<?>) type.getRawType();
        final TypeVariable<? extends Class<?>>[] typeParameters = rawType.getTypeParameters();
        Map<String, Type> result = new HashMap<>(typeArguments.length);
        for (int i = 0; i < typeArguments.length; i++) {
            final TypeVariable<?> typeParameter = typeParameters[i];
            final Type typeArgument = typeArguments[i];
            result.put(typeParameter.getName(), typeArgument);
        }
        return result;
    }

    /**
     * Retrieve a property from an object.
     *
     * @param obj      from which the property should be retrieved
     * @param property the property
     * @param <T>      the return type
     * @return the property
     */
    @SuppressWarnings("unchecked")
    public static <T> T getProperty(Object obj, String property) {
        Class<?> artifactClass = obj.getClass();
        String accessorName = "get" + Character.toUpperCase(property.charAt(0)) + property.substring(1);
        try {
            do {
                final Optional<Method> optMethod = Arrays.stream(artifactClass.getMethods())
                        .filter(method -> method.getName().equals(accessorName))
                        .findAny();
                if (optMethod.isPresent()) {
                    return (T) artifactClass.getMethod(accessorName).invoke(obj);
                }
                artifactClass = artifactClass.getSuperclass();
            } while (artifactClass != null);
        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            throw new RuntimeException(String.format("Could not execute %s() on %s.", accessorName, obj), e);
        }
        throw new IllegalArgumentException(String.format("Did not find method %s() for %s.", accessorName, obj));
    }

    /**
     * Convert the given {@link Object} to a {@code double}.
     *
     * @param o the {@link Object}
     * @return the {@code double}
     */
    public static double toDouble(Object o) {
        if (o instanceof Double) return (Double) o;
        else if (o instanceof Integer) return (Integer) o;
        else if (o instanceof Float) return (Float) o;
        else if (o instanceof Long) return (Long) o;
        else if (o instanceof Short) return (Short) o;
        else if (o instanceof Byte) return (Byte) o;
        else if (o instanceof BigDecimal) return ((BigDecimal) o).doubleValue();
        else if (o instanceof BigInteger) return ((BigInteger) o).doubleValue();
        throw new IllegalStateException(String.format("%s (%s) cannot be retrieved as double.", o,
                o == null ? "unknown class" : o.getClass().getCanonicalName()));

    }


    public static Type getWrapperClass(Type type, int index) {
        if (type != null && (type instanceof ParameterizedType)) {
            ParameterizedType parameterizedType = (ParameterizedType) type;
            return parameterizedType.getActualTypeArguments()[index];
        }
        return null;
    }
}
