/*
 * 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.jclouds.reflect;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Throwables.propagate;
import static com.google.common.collect.Iterables.tryFind;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;

import com.google.common.annotations.Beta;
import com.google.common.base.Function;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.reflect.Invokable;
import com.google.common.reflect.Parameter;
import com.google.common.reflect.TypeToken;
import com.google.common.util.concurrent.UncheckedExecutionException;

/**
 * Utilities that allow access to {@link Invokable}s with {@link Invokable#getOwnerType() owner types}.
 *
 * @since 1.6
 */
@Beta
public class Reflection2 {

   /**
    * gets a {@link TypeToken} for the given type.
    */
   @SuppressWarnings("unchecked")
   public static <T> TypeToken<T> typeToken(Type in) {
      return (TypeToken<T>) get(typeTokenForType, checkNotNull(in, "class"));
   }

   /**
    * gets a {@link TypeToken} for the given class.
    */
   @SuppressWarnings("unchecked")
   public static <T> TypeToken<T> typeToken(Class<T> in) {
      return (TypeToken<T>) get(typeTokenForClass, checkNotNull(in, "class"));
   }

   /**
    * returns an {@link Invokable} object that reflects a constructor present in the {@link TypeToken} type.
    *
    * @param ownerType
    *           corresponds to {@link Invokable#getOwnerType()}
    * @param parameterTypes
    *           corresponds to {@link Constructor#getParameterTypes()}
    *
    * @throws IllegalArgumentException
    *            if the constructor doesn't exist or a security exception occurred
    */
   @SuppressWarnings("unchecked")
   public static <T> Invokable<T, T> constructor(Class<T> ownerType, Class<?>... parameterTypes) {
      return (Invokable<T, T>) get(constructorForParams, new TypeTokenAndParameterTypes(typeToken(ownerType),
            parameterTypes));
   }

   /**
    * return all constructors or static factory methods present in the class as {@link Invokable}s.
    *
    * @param ownerType
    *           corresponds to {@link Invokable#getOwnerType()}
    */
   @SuppressWarnings("unchecked")
   public static <T> Collection<Invokable<T, T>> constructors(TypeToken<T> ownerType) {
      return Collection.class.cast(get(constructorsForTypeToken, ownerType));
   }

   /**
    * returns an {@link Invokable} object that links the {@code method} to its owner.
    *
    * @param ownerType
    *           corresponds to {@link Invokable#getOwnerType()}
    * @param method
    *           present in {@code ownerType}
    */
   @SuppressWarnings("unchecked")
   public static <T, R> Invokable<T, R> method(TypeToken<T> ownerType, Method method) {
      return (Invokable<T, R>) method(ownerType.getRawType(), method.getName(), method.getParameterTypes());
   }

   /**
    * returns an {@link Invokable} object that reflects a method present in the {@link TypeToken} type.
    * If there are multiple methods of the same name and parameter list, returns the method in the nearest
    * ancestor with the most specific return type (see {@link Class#getDeclaredMethod}).
    *
    * @param ownerType
    *           corresponds to {@link Invokable#getOwnerType()}
    * @param name
    *           name of the method to be returned
    * @param parameterTypes
    *           corresponds to {@link Method#getParameterTypes()}
    *
    * @throws IllegalArgumentException
    *            if the method doesn't exist or a security exception occurred
    */
   @SuppressWarnings("unchecked")
   public static <T, R> Invokable<T, R> method(Class<T> ownerType, String name, Class<?>... parameterTypes) {
      return (Invokable<T, R>) get(methodForParams, new TypeTokenNameAndParameterTypes(typeToken(ownerType), name,
            parameterTypes));
   }

   /**
    * return all methods present in the class as {@link Invokable}s.
    *
    * @param ownerType
    *           corresponds to {@link Invokable#getOwnerType()}
    */
   @SuppressWarnings("unchecked")
   public static <T> Collection<Invokable<T, Object>> methods(Class<T> ownerType) {
      return Collection.class.cast(get(methodsForTypeToken, typeToken(ownerType)));
   }

   /**
    * This gets all declared constructors or factory methods on abstract types, not just public ones, and makes them
    * accessible.
    */
   private static LoadingCache<TypeToken<?>, Set<Invokable<?, ?>>> constructorsForTypeToken = CacheBuilder
         .newBuilder().build(new CacheLoader<TypeToken<?>, Set<Invokable<?, ?>>>() {
            public Set<Invokable<?, ?>> load(TypeToken<?> key) {
               ImmutableSet.Builder<Invokable<?, ?>> builder = ImmutableSet.<Invokable<?, ?>> builder();
               Class<?> raw = key.getRawType();
               for (Constructor<?> ctor : raw.getDeclaredConstructors()) {
                  // TODO replace isAccessible() with canAccess() when using Java >= 9
                  if (!ctor.isAccessible() && !coreJavaClass(raw)) {
                     // In JDK 11 up to 14, the only uses for `java.beans.ConstructorProperties` annotation
                     // are in the `java.awt`, `java.beans` and `javax.swing` packages.
                     // Since these constructors are public, there is no need to call `setAccessible()`
                     ctor.setAccessible(true);
                  }
                  builder.add(key.constructor(ctor));
               }
               // Look for factory methods, if this is an abstract type.
               if (Modifier.isAbstract(raw.getModifiers())) {
                  for (Invokable<?, Object> method : methods(raw)){
                     if (method.isStatic() && method.getReturnType().equals(key)) {
                        // TODO replace isAccessible() with canAccess() when using Java >= 9
                        if (!method.isAccessible() && !coreJavaClass(raw)) {
                           method.setAccessible(true);
                        }
                        builder.add(method);
                     }
                  }
               }
               return builder.build();
            }
         });

   protected static List<Class<?>> toClasses(ImmutableList<Parameter> params) {
      return Lists.transform(params, new Function<Parameter, Class<?>>() {
         public Class<?> apply(Parameter input) {
            return input.getType().getRawType();
         }
      });
   }

   private static LoadingCache<Type, TypeToken<?>> typeTokenForType = CacheBuilder.newBuilder().build(
         new CacheLoader<Type, TypeToken<?>>() {
            public TypeToken<?> load(Type key) {
               return TypeToken.of(key);
            }
         });

   private static LoadingCache<Class<?>, TypeToken<?>> typeTokenForClass = CacheBuilder.newBuilder().build(
         new CacheLoader<Class<?>, TypeToken<?>>() {
            public TypeToken<?> load(Class<?> key) {
               return TypeToken.of(key);
            }
         });

   private static LoadingCache<TypeTokenAndParameterTypes, Invokable<?, ?>> constructorForParams = CacheBuilder
         .newBuilder().build(new CacheLoader<TypeTokenAndParameterTypes, Invokable<?, ?>>() {
            public Invokable<?, ?> load(final TypeTokenAndParameterTypes key) {
               Set<Invokable<?, ?>> constructors = get(constructorsForTypeToken, key.type);
               Optional<Invokable<?, ?>> constructor = tryFind(constructors, new Predicate<Invokable<?, ?>>() {
                  public boolean apply(Invokable<?, ?> input) {
                     return Objects.equal(toClasses(input.getParameters()), key.parameterTypes);
                  }
               });
               if (constructor.isPresent())
                  return constructor.get();
               throw new IllegalArgumentException("no such constructor " + key.toString() + "in: " + constructors);
            }
         });

   private static final LoadingCache<Invokable<?, ?>, ImmutableList<Parameter>> invokableParamsCache =
      CacheBuilder.newBuilder().maximumSize(100).build(new CacheLoader<Invokable<?, ?>, ImmutableList<Parameter>>() {
            @Override
            public ImmutableList<Parameter> load(Invokable<?, ?> invokable) {
               return invokable.getParameters();
            }
         });

   /**
    * Returns the {@link Parameter}s associated with the given {@link Invokable}. This function is backed by a cache.
    *
    * @param invokable
    *           The {@link Invokable} we want to get Parameters from
    */
   public static List<Parameter> getInvokableParameters(final Invokable<?, ?> invokable) {
      return invokableParamsCache.getUnchecked(invokable);
   }

   private static class TypeTokenAndParameterTypes {

      protected TypeToken<?> type;
      protected List<Class<?>> parameterTypes;

      public TypeTokenAndParameterTypes(TypeToken<?> type, Class<?>... parameterTypes) {
         this.type = checkNotNull(type, "type");
         this.parameterTypes = Arrays.asList(checkNotNull(parameterTypes, "parameterTypes"));
      }

      public int hashCode() {
         return Objects.hashCode(type, parameterTypes);
      }

      public boolean equals(Object obj) {
         if (this == obj)
            return true;
         if (obj == null || getClass() != obj.getClass())
            return false;
         TypeTokenAndParameterTypes that = TypeTokenAndParameterTypes.class.cast(obj);
         return Objects.equal(this.type, that.type) && Objects.equal(this.parameterTypes, that.parameterTypes);
      }

      public String toString() {
         return MoreObjects.toStringHelper("").add("type", type).add("parameterTypes", parameterTypes).toString();
      }
   }

   private static LoadingCache<TypeTokenNameAndParameterTypes, Invokable<?, ?>> methodForParams = CacheBuilder
         .newBuilder().build(new CacheLoader<TypeTokenNameAndParameterTypes, Invokable<?, ?>>() {
            public Invokable<?, ?> load(final TypeTokenNameAndParameterTypes key) {
               Set<Invokable<?, ?>> methods = get(methodsForTypeToken, key.type);
               /*
                * There may be multiple instances, even on the most immediate ancestor,
                * of a method with the required name and parameter set. This will occur
                * if the method overrides one declared in a parent class with a less specific
                * return type. These bridge methods inserted by the compiler will be marked
                * as "synthetic".
                */
               Optional<Invokable<?, ?>> method = tryFind(methods, new Predicate<Invokable<?, ?>>() {
                  public boolean apply(Invokable<?, ?> input) {
                     // Invokable doesn't expose Method#isBridge
                     return !input.isSynthetic() && Objects.equal(input.getName(), key.name)
                           && Objects.equal(toClasses(input.getParameters()), key.parameterTypes);
                  }
               });
               checkArgument(method.isPresent(), "no such method %s in: %s", key.toString(), methods);
               return method.get();
            }
         });

   private static class TypeTokenNameAndParameterTypes extends TypeTokenAndParameterTypes {

      private String name;

      public TypeTokenNameAndParameterTypes(TypeToken<?> type, String name, Class<?>... parameterTypes) {
         super(type, parameterTypes);
         this.name = checkNotNull(name, "name");
      }

      public int hashCode() {
         return Objects.hashCode(super.hashCode(), name);
      }

      public boolean equals(Object obj) {
         if (super.equals(obj)) {
            TypeTokenNameAndParameterTypes that = TypeTokenNameAndParameterTypes.class.cast(obj);
            return name.equals(that.name);
         }
         return false;
      }

      public String toString() {
         return MoreObjects.toStringHelper("").add("type", type).add("name", name).add("parameterTypes", parameterTypes)
               .toString();
      }
   }

   /**
    * this gets all declared methods, not just public ones. makes them accessible. Does not include Object methods.
    * Invokables for a type are ordered so all invokables on a subtype are always listed before invokables on a
    * supertype (see {@link TypeToken#getTypes()}).
    */
   private static LoadingCache<TypeToken<?>, Set<Invokable<?, ?>>> methodsForTypeToken = CacheBuilder
         .newBuilder().build(new CacheLoader<TypeToken<?>, Set<Invokable<?, ?>>>() {
            public Set<Invokable<?, ?>> load(TypeToken<?> key) {
               ImmutableSet.Builder<Invokable<?, ?>> builder = ImmutableSet.<Invokable<?, ?>> builder();
               for (TypeToken<?> token : key.getTypes()) {
                  Class<?> raw = token.getRawType();
                  if (raw == Object.class)
                     continue;
                  for (Method method : raw.getDeclaredMethods()) {
                     // TODO replace isAccessible() with canAccess() when using Java >= 9
                     if (!method.isAccessible() && !coreJavaClass(raw)) {
                        method.setAccessible(true);
                     }
                     builder.add(key.method(method));
                  }
               }
               return builder.build();
            }
         });

   private static boolean coreJavaClass(Class<?> clazz) {
      // treat null packages (e.g. for proxy objects) as "non-core"
      Package clazzPackage = clazz.getPackage();
      if (clazzPackage == null) {
         return false;
      }
      String packageName = clazzPackage.getName();
      return packageName.startsWith("com.sun.") || packageName.startsWith("java.")
              || packageName.startsWith("javax.") || packageName.startsWith("sun.");
   }

   /**
    * ensures that exceptions are not doubly-wrapped
    */
   private static <K, V> V get(LoadingCache<K, V> cache, K key) {
      try {
         return cache.get(key);
      } catch (UncheckedExecutionException e) {
         throw propagate(e.getCause());
      } catch (ExecutionException e) {
         throw propagate(e.getCause());
      }
   }
}
