package org.checkerframework.framework.test;

import java.io.File;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
 * SimpleOptionMap is a very basic command-line option container. The keys of the option container
 * are the command-line flags and the values are the arguments to those options if they exist: e.g.,
 *
 * <pre>{@code
 * Map(
 *    "-AprintAllQualifiers" => null
 *    "-classpath" => "myDir1:myDir2"
 * )
 * }</pre>
 *
 * This class is mainly used by TestConfigurationBuilder to make working with existing options
 * simpler and less error prone. It is not intended for a general option container because users
 * creating tests via source code can more easily manipulate the map whereas a lot of sugar would be
 * needed to make this class usable from the command line.
 */
public class SimpleOptionMap {
  /** A Map from optionName to arg, where arg is null if the option doesn't require any args. */
  private final Map<String, @Nullable String> options = new LinkedHashMap<>();

  /**
   * Clears the current set of options and copies the input options to this map.
   *
   * @param options the new options to use for this object
   */
  public void setOptions(Map<String, @Nullable String> options) {
    this.options.clear();
    this.options.putAll(options);
  }

  /**
   * A method to easily add Strings to an option that takes a filepath as an argument.
   *
   * @param key an option with an argument of the form "arg1[path-separator]arg2..." e.g., "-cp
   *     myDir:myDir2:myDir3"
   * @param toAppend a string to append onto the path or, if the path is null/empty, the argument to
   *     the option indicated by key
   */
  public void addToPathOption(String key, String toAppend) {
    if (toAppend == null) {
      throw new IllegalArgumentException("Null string appended to sourcePath.");
    }

    String path = options.get(key);

    if (toAppend.startsWith(File.pathSeparator)) {
      if (path == null || path.isEmpty()) {
        path = toAppend.substring(1, toAppend.length());
      } else {
        path += toAppend;
      }
    } else {
      if (path == null || path.isEmpty()) {
        path = toAppend;
      } else {
        path += File.pathSeparator + toAppend;
      }
    }

    addOption(key, path);
  }

  /**
   * Adds an option that takes no argument.
   *
   * @param option the no-argument option to add to this object
   */
  public void addOption(String option) {
    this.options.put(option, null);
  }

  /**
   * Adds an option that takes an argument.
   *
   * @param option the option to add to this object
   * @param value the argument to the option
   */
  public void addOption(String option, String value) {
    this.options.put(option, value);
  }

  /**
   * Adds the option only if value is a non-null, non-empty String.
   *
   * @param option the option to add to this object
   * @param value the argument to the option (or null)
   */
  public void addOptionIfValueNonEmpty(String option, @Nullable String value) {
    if (value != null && !value.isEmpty()) {
      addOption(option, value);
    }
  }

  /**
   * Adds all of the options in the given map to this one.
   *
   * @param options the options to add to this object
   */
  public void addOptions(Map<String, @Nullable String> options) {
    this.options.putAll(options);
  }

  public void addOptions(Iterable<String> newOptions) {
    Iterator<String> optIter = newOptions.iterator();
    while (optIter.hasNext()) {
      String opt = optIter.next();
      if (this.options.get(opt) != null) {
        if (!optIter.hasNext()) {
          throw new RuntimeException(
              "Expected a value for option: "
                  + opt
                  + " in option list: "
                  + String.join(", ", newOptions));
        }
        this.options.put(opt, optIter.next());

      } else {
        this.options.put(opt, null);
      }
    }
  }

  /**
   * Returns the map that backs this SimpleOptionMap.
   *
   * @return the options in this object
   */
  public Map<String, @Nullable String> getOptions() {
    return options;
  }

  /**
   * Creates a "flat" list representation of these options.
   *
   * @return a list of the string representations of the options in this object
   */
  public List<String> getOptionsAsList() {
    return TestUtilities.optionMapToList(options);
  }
}
