/*
 * 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.gobblin.util.reflection;

import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.List;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.reflect.ConstructorUtils;


/**
 * Helper methods to instantiate classes
 */
@Slf4j
public class GobblinConstructorUtils {

  /**
   * Convenience method on top of {@link ConstructorUtils#invokeConstructor(Class, Object[])} that returns a new
   * instance of the <code>cls</code> based on a constructor priority order. Each {@link List} in the
   * <code>constructorArgs</code> array contains the arguments for a constructor of <code>cls</code>. The first
   * constructor whose signature matches the argument types will be invoked.
   *
   * @param cls the class to be instantiated
   * @param constructorArgs An array of constructor argument list. Order defines the priority of a constructor.
   * @return
   *
   * @throws NoSuchMethodException if no constructor matched was found
   */
  @SafeVarargs
  public static <T> T invokeFirstConstructor(Class<T> cls, List<Object>... constructorArgs)
      throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

    for (List<Object> args : constructorArgs) {

      Class<?>[] parameterTypes = new Class[args.size()];
      for (int i = 0; i < args.size(); i++) {
        parameterTypes[i] = args.get(i).getClass();
      }

      if (ConstructorUtils.getMatchingAccessibleConstructor(cls, parameterTypes) != null) {
        return ConstructorUtils.invokeConstructor(cls, args.toArray(new Object[args.size()]));
      }
    }
    throw new NoSuchMethodException("No accessible constructor found");
  }

  /**
   * Returns a new instance of the <code>cls</code> based on a set of arguments. The method will search for a
   * constructor accepting the first k arguments in <code>args</code> for every k from args.length to 0, and will
   * invoke the first constructor found.
   *
   * For example, {@link #invokeLongestConstructor}(cls, myString, myInt) will first attempt to create an object with
   * of class <code>cls</code> with constructor <init>(String, int), if it fails it will attempt <init>(String), and
   * finally <init>().
   *
   * @param cls the class to instantiate.
   * @param args the arguments to use for instantiation.
   * @throws ReflectiveOperationException
   */
  public static <T> T invokeLongestConstructor(Class<T> cls, Object... args) throws ReflectiveOperationException {

    Class<?>[] parameterTypes = new Class[args.length];
    for (int i = 0; i < args.length; i++) {
      parameterTypes[i] = args[i].getClass();
    }

    for (int i = args.length; i >= 0; i--) {
      if (ConstructorUtils.getMatchingAccessibleConstructor(cls, Arrays.copyOfRange(parameterTypes, 0, i)) != null) {
        log.debug(
            String.format("Found accessible constructor for class %s with parameter types %s.", cls,
                Arrays.toString(Arrays.copyOfRange(parameterTypes, 0, i))));
        return ConstructorUtils.invokeConstructor(cls, Arrays.copyOfRange(args, 0, i));
      }
    }
    throw new NoSuchMethodException(String.format("No accessible constructor for class %s with parameters a subset of %s.",
        cls, Arrays.toString(parameterTypes)));
  }

  /**
   * Utility method to create an instance of <code>clsName</code> using the constructor matching the arguments, <code>args</code>
   *
   * @param superType of <code>clsName</code>. The new instance is cast to superType
   * @param clsName complete cannonical name of the class to be instantiated
   * @param args constructor args to be used
   *
   * @throws IllegalArgumentException if there was an issue creating the instance due to
   * {@link NoSuchMethodException}, {@link InvocationTargetException},{@link InstantiationException},
   *  {@link ClassNotFoundException}
   *
   * @return A new instance of <code>clsName</code>
   */
  @SuppressWarnings("unchecked")
  public static <T> T invokeConstructor(final Class<T> superType, final String clsName, Object... args) {

    try {
      return (T) ConstructorUtils.invokeConstructor(Class.forName(clsName), args);
    } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException
        | ClassNotFoundException e) {
      throw new IllegalArgumentException(e);
    }
  }
}
