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

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;

/** Imported from apache.commons.lang3 3.6 */
public class ConstructorUtils {

  /**
   * Validate that the specified argument is not {@code null}; otherwise throwing an exception with
   * the specified message.
   *
   * <pre>notNull(myObject, "The object must not be null");</pre>
   *
   * @param <T> the object type
   * @param object the object to check
   * @param message the {@link String#format(String, Object...)} exception message if invalid, not
   *     null
   * @param values the optional values for the formatted exception message
   * @return the validated object (never {@code null} for method chaining)
   * @throws NullPointerException if the object is {@code null}
   */
  private static <T> T notNull(final T object, final String message, final Object... values) {
    if (object == null) {
      throw new NullPointerException(String.format(message, values));
    }
    return object;
  }

  /**
   * Checks if the specified constructor is accessible.
   *
   * <p>This simply ensures that the constructor is accessible.
   *
   * @param <T> the constructor type
   * @param ctor the prototype constructor object, not {@code null}
   * @return the constructor, {@code null} if no matching accessible constructor found
   * @see java.lang.SecurityManager
   * @throws NullPointerException if {@code ctor} is {@code null}
   */
  public static <T> Constructor<T> getAccessibleConstructor(final Constructor<T> ctor) {
    notNull(ctor, "constructor cannot be null");
    return MemberUtils.isAccessible(ctor) && isAccessible(ctor.getDeclaringClass()) ? ctor : null;
  }

  /**
   * Finds an accessible constructor with compatible parameters.
   *
   * <p>This checks all the constructor and finds one with compatible parameters This requires that
   * every parameter is assignable from the given parameter types. This is a more flexible search
   * than the normal exact matching algorithm.
   *
   * <p>First it checks if there is a constructor matching the exact signature. If not then all the
   * constructors of the class are checked to see if their signatures are assignment-compatible with
   * the parameter types. The first assignment-compatible matching constructor is returned.
   *
   * @param <T> the constructor type
   * @param cls the class to find a constructor for, not {@code null}
   * @param parameterTypes find method with compatible parameters
   * @return the constructor, null if no matching accessible constructor found
   * @throws NullPointerException if {@code cls} is {@code null}
   */
  public static <T> Constructor<T> getMatchingAccessibleConstructor(
      final Class<T> cls, final Class<?>... parameterTypes) {
    notNull(cls, "class cannot be null");
    // see if we can find the constructor directly
    // most of the time this works and it's much faster
    try {
      final Constructor<T> ctor = cls.getConstructor(parameterTypes);
      MemberUtils.setAccessibleWorkaround(ctor);
      return ctor;
    } catch (final NoSuchMethodException e) { // NOPMD - Swallow
    }
    Constructor<T> result = null;
    /*
     * (1) Class.getConstructors() is documented to return Constructor<T> so as
     * long as the array is not subsequently modified, everything's fine.
     */
    final Constructor<?>[] ctors = cls.getConstructors();

    // return best match:
    for (Constructor<?> ctor : ctors) {
      // compare parameters
      if (MemberUtils.isMatchingConstructor(ctor, parameterTypes)) {
        // get accessible version of constructor
        ctor = getAccessibleConstructor(ctor);
        if (ctor != null) {
          MemberUtils.setAccessibleWorkaround(ctor);
          if (result == null
              || MemberUtils.compareConstructorFit(ctor, result, parameterTypes) < 0) {
            // temporary variable for annotation, see comment above (1)
            @SuppressWarnings("unchecked")
            final Constructor<T> constructor = (Constructor<T>) ctor;
            result = constructor;
          }
        }
      }
    }
    return result;
  }

  /**
   * Learn whether the specified class is generally accessible, i.e. is declared in an entirely
   * {@code public} manner.
   *
   * @param type to check
   * @return {@code true} if {@code type} and any enclosing classes are {@code public}.
   */
  private static boolean isAccessible(final Class<?> type) {
    Class<?> cls = type;
    while (cls != null) {
      if (!Modifier.isPublic(cls.getModifiers())) {
        return false;
      }
      cls = cls.getEnclosingClass();
    }
    return true;
  }
}
