/**
 * Licensed 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.aurora.scheduler.config;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Map;

import javax.security.auth.kerberos.KerberosPrincipal;

import com.beust.jcommander.IStringConverter;
import com.beust.jcommander.IStringConverterFactory;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;

import org.apache.aurora.gen.DockerParameter;
import org.apache.aurora.gen.Volume;
import org.apache.aurora.scheduler.app.SchedulerMain;
import org.apache.aurora.scheduler.app.VolumeConverter;
import org.apache.aurora.scheduler.config.converters.ClassConverter;
import org.apache.aurora.scheduler.config.converters.DataAmountConverter;
import org.apache.aurora.scheduler.config.converters.DockerParameterConverter;
import org.apache.aurora.scheduler.config.converters.InetSocketAddressConverter;
import org.apache.aurora.scheduler.config.converters.TimeAmountConverter;
import org.apache.aurora.scheduler.config.types.DataAmount;
import org.apache.aurora.scheduler.config.types.TimeAmount;
import org.apache.aurora.scheduler.http.api.security.KerberosPrincipalConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Parses command line options and populates {@link CliOptions}.
 */
public final class CommandLine {

  private static final Logger LOG = LoggerFactory.getLogger(CommandLine.class);

  // TODO(wfarner): This can go away if/when options are no longer accessed statically.
  private static CliOptions instance = null;

  private static List<Object> customOptions = Lists.newArrayList();

  private CommandLine() {
    // Utility class.
  }

  /**
   * Similar to {@link #initializeForTest()}, but resets the class to an un-parsed state.
   */
  @VisibleForTesting
  static void clearForTest() {
    instance = null;
    customOptions = Lists.newArrayList();
  }

  /**
   * Initializes static command line state - the static parsed instance, and custom options objects.
   */
  @VisibleForTesting
  public static void initializeForTest() {
    instance = new CliOptions();
    customOptions = Lists.newArrayList();
  }

  private static JCommander prepareParser(CliOptions options) {
    JCommander.Builder builder = JCommander.newBuilder()
        .programName(SchedulerMain.class.getName());

    builder.addConverterFactory(new IStringConverterFactory() {
      private Map<Class<?>, Class<? extends IStringConverter<?>>> classConverters =
          ImmutableMap.<Class<?>, Class<? extends IStringConverter<?>>>builder()
              .put(Class.class, ClassConverter.class)
              .put(DataAmount.class, DataAmountConverter.class)
              .put(DockerParameter.class, DockerParameterConverter.class)
              .put(InetSocketAddress.class, InetSocketAddressConverter.class)
              .put(KerberosPrincipal.class, KerberosPrincipalConverter.class)
              .put(TimeAmount.class, TimeAmountConverter.class)
              .put(Volume.class, VolumeConverter.class)
              .build();

      @SuppressWarnings("unchecked")
      @Override
      public <T> Class<? extends IStringConverter<T>> getConverter(Class<T> forType) {
        return (Class<IStringConverter<T>>) classConverters.get(forType);
      }
    });

    builder.addObject(getOptionsObjects(options));
    return builder.build();
  }

  /**
   * Applies arg values to the options object.
   *
   * @param args Command line arguments.
   */
  @VisibleForTesting
  public static CliOptions parseOptions(String... args) {
    JCommander parser = null;
    try {
      parser = prepareParser(new CliOptions(ImmutableList.copyOf(customOptions)));

      // We first perform a 'dummy' parsing round.  This induces classloading on any third-party
      // code, where they can statically invoke registerCustomOptions().
      parser.setAcceptUnknownOptions(true);
      parser.parseWithoutValidation(args);

      CliOptions options = new CliOptions(ImmutableList.copyOf(customOptions));
      parser = prepareParser(options);
      parser.parse(args);

      LOG.info("-----------------------------------------------------------------------");
      LOG.info("Parameters:");
      parser.getParameters().stream()
          .map(param ->
              param.getLongestName() + ": " + param.getParameterized().get(param.getObject()))
          .sorted()
          .forEach(LOG::info);
      LOG.info("-----------------------------------------------------------------------");

      instance = options;
      return options;
    } catch (ParameterException e) {
      if (parser != null) {
        parser.usage();
      }
      LOG.error(e.getMessage());
      System.exit(1);
      throw new RuntimeException(e);
    }
  }

  /**
   * Gets the static and globally-accessible CLI options.  This exists only to support legacy use
   * cases that cannot yet support injected arguments.  New callers should not be added.
   *
   * @return global options
   */
  public static CliOptions legacyGetStaticOptions() {
    if (instance == null) {
      throw new IllegalStateException("Attempted to fetch command line arguments before parsing.");
    }
    return instance;
  }

  /**
   * Registers a custom options container for inclusion during command line option parsing.  This
   * is useful to allow third-party modules to include custom command line options.
   *
   * @param options Custom options object.
   *                See {@link com.beust.jcommander.JCommander.Builder#addObject(Object)} for
   *                details.
   */
  public static void registerCustomOptions(Object options) {
    Preconditions.checkState(
        instance == null,
        "Attempted to register custom options after command line parsing.");

    customOptions.add(options);
  }

  @VisibleForTesting
  static List<Object> getOptionsObjects(CliOptions options) {
    ImmutableList.Builder<Object> objects = ImmutableList.builder();

    // Reflect on fields defined in CliOptions to DRY and avoid mistakes of forgetting to add an
    // option field here.
    for (Field field : CliOptions.class.getDeclaredFields()) {
      if (Modifier.isStatic(field.getModifiers())) {
        continue;
      }

      try {
        if (Iterable.class.isAssignableFrom(field.getType())) {
          Iterable<?> iterableValue = (Iterable<?>) field.get(options);
          objects.addAll(iterableValue);
        } else {
          objects.add(field.get(options));
        }
      } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
      }
    }

    return objects.build();
  }
}