/*
 * Copyright (c) 2011-2019 Contributors to the Eclipse Foundation
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
 * which is available at https://www.apache.org/licenses/LICENSE-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
 */

package io.vertx.core.cli;

import io.vertx.codegen.annotations.DataObject;
import io.vertx.core.json.JsonObject;

import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;

/**
 * Models command line options. Options are values passed to a command line interface using -x or --x. Supported
 * syntaxes depend on the parser.
 * <p/>
 * Short name is generally used with a single dash, while long name requires a double-dash.
 *
 * @author Clement Escoffier <clement@apache.org>
 */
@DataObject(generateConverter = true, publicConverter = false)
public class Option {

  /**
   * Default name in the usage message.
   */
  public static final String DEFAULT_ARG_NAME = "value";

  /**
   * The default long name / short name of the option. Notice that options requires at least a regular long name or
   * short name.
   */
  public static final String NO_NAME = "\0";

  /**
   * the option long name.
   */
  protected String longName = NO_NAME;

  /**
   * the option short name.
   */
  protected String shortName = NO_NAME;

  /**
   * the option name used in usage message.
   */
  protected String argName = DEFAULT_ARG_NAME;

  /**
   * The option description.
   */
  protected String description;

  /**
   * whether or not the option is required. A mandatory not set throws a {@link MissingOptionException}.
   */
  protected boolean required;

  /**
   * whether or not the option is hidden. Hidden options are not displayed in usage.
   */
  protected boolean hidden;

  /**
   * whether or not the option receives a single value. {@code true} by default.
   */
  protected boolean singleValued = true;

  /**
   * whether or not the option can recevie multiple values.
   */
  protected boolean multiValued;

  /**
   * the option default value.
   */
  protected String defaultValue;

  /**
   * whether or not the option is a flag. Flag option does not require a value. If an option is a flag, it is
   * evaluated to {@link true} if the option is used in the command line.
   */
  protected boolean flag;

  /**
   * whether or not the option is a "help" option. Is the user execute the command line enabling a help option, the
   * command line validation won't fail, and give the command the opportunity to display the usage message, instead
   * of throwing an exception during the parsing.
   */
  protected boolean help;

  /**
   * if the option value has to be in a definited set, this field represents the set of values. Value are sorted
   * alphabetically.
   */
  protected Set<String> choices = new TreeSet<>();

  /**
   * Creates a new empty instance of {@link Option}.
   */
  public Option() {
  }

  /**
   * Creates a new instance of {@link Option} by copying the state of another {@link Option}.
   *
   * @param other the other option
   */
  public Option(Option other) {
    this();
    this.longName = other.longName;
    this.shortName = other.shortName;
    this.argName = other.argName;
    this.description = other.description;
    this.required = other.required;
    this.hidden = other.hidden;
    this.singleValued = other.singleValued;
    this.multiValued = other.multiValued;
    this.defaultValue = other.defaultValue;
    this.flag = other.flag;
    this.help = other.help;
    this.choices = other.choices;
  }

  /**
   * Creates a new instance of {@link Option} from the given {@link JsonObject}
   *
   * @param json the json object representing the option
   * @see #toJson()
   */
  public Option(JsonObject json) {
    this();
    OptionConverter.fromJson(json, this);
  }

  /**
   * Gets the json representation of this {@link Option}.
   *
   * @return the json representation
   */
  public JsonObject toJson() {
    JsonObject json = new JsonObject();
    OptionConverter.toJson(this, json);
    return json;
  }

  /**
   * Checks whether or not the option is valid. This implementation check that it has a short name or a long name.
   * This method is intended to be extended by sub-class. Parser should check that the set of
   * option of a {@link CLI} is valid before starting the parsing.
   * <p/>
   * If the configuration is not valid, this method throws a {@link IllegalArgumentException}.
   */
  public void ensureValidity() {
    if ((shortName == null || shortName.equals(NO_NAME)) && (longName == null || longName.equals(NO_NAME))) {
      throw new IllegalArgumentException("An option needs at least a long name or a short name");
    }
  }

  /**
   * @return whether or not the option can receive a value.
   */
  public boolean acceptValue() {
    return singleValued || multiValued;
  }

  /**
   * @return the option name. It returns the long name if set, the short name otherwise. It cannot return {@code
   * null} for valid option
   * @see #ensureValidity()
   */
  public String getName() {
    if (longName != null && !longName.equals(NO_NAME)) {
      return longName;
    }
    // So by validity, it necessarily has a short name.
    return shortName;
  }

  /**
   * @return whether or not this option can receive several values.
   */
  public boolean isMultiValued() {
    return multiValued;
  }

  /**
   * Sets whether or not this option can receive several values.
   *
   * @param multiValued whether or not this option is multi-valued.
   * @return the current {@link Option} instance
   */
  public Option setMultiValued(boolean multiValued) {
    this.multiValued = multiValued;
    if (this.multiValued) {
      // If we accept more than one, we also accept 1.
      this.singleValued = true;
    }
    // Else we cannot say what is the desired value of singleValued, it needs to be explicitly set.
    return this;
  }

  /**
   * @return whether or not this option is single valued.
   */
  public boolean isSingleValued() {
    return singleValued;
  }

  /**
   * Sets whether or not this option can receive a value.
   *
   * @param singleValued whether or not this option is single-valued.
   * @return the current {@link Option} instance
   */
  public Option setSingleValued(boolean singleValued) {
    this.singleValued = singleValued;
    return this;
  }

  /**
   * @return the option arg name used in usage messages, {@code null} if not set.
   */
  public String getArgName() {
    return argName;
  }

  /**
   * Sets te arg name for this option.
   *
   * @param argName the arg name, must not be {@code null}
   * @return the current {@link Option} instance
   */
  public Option setArgName(String argName) {
    Objects.requireNonNull(argName);
    this.argName = argName;
    return this;
  }

  /**
   * @return the description of this option, {@code null} if not set.
   */
  public String getDescription() {
    return description;
  }

  /**
   * Sets te description of this option.
   *
   * @param description the description
   * @return the current {@link Option} instance
   */
  public Option setDescription(String description) {
    this.description = description;
    return this;
  }

  /**
   * @return whtehr or not this option is hidden.
   */
  public boolean isHidden() {
    return hidden;
  }

  /**
   * Sets whether or not this option should be hidden
   *
   * @param hidden {@code true} to make this option hidden, {@link false} otherwise
   * @return the current {@link Option} instance
   */
  public Option setHidden(boolean hidden) {
    this.hidden = hidden;
    return this;
  }

  /**
   * @return the option long name, {@code null} if not set.
   */
  public String getLongName() {
    return longName;
  }

  /**
   * Sets the long name of this option.
   *
   * @param longName the long name
   * @return the current {@link Option} instance
   */
  public Option setLongName(String longName) {
    this.longName = longName;
    return this;
  }

  /**
   * @return whether or not this option is mandatory.
   */
  public boolean isRequired() {
    return required;
  }

  /**
   * Sets whether or not this option is mandatory.
   *
   * @param required {@code true} to make this option mandatory, {@link false} otherwise
   * @return the current {@link Option} instance
   */
  public Option setRequired(boolean required) {
    this.required = required;
    return this;
  }

  /**
   * @return the short name of this option, {@code null} if not set.
   */
  public String getShortName() {
    return shortName;
  }

  /**
   * Sets the short name of this option.
   *
   * @param shortName the short name
   * @return the current {@link Option} instance
   */
  public Option setShortName(String shortName) {
    this.shortName = shortName;
    return this;
  }

  /**
   * @return the default value of this option, {@code null} if not set.
   */
  public String getDefaultValue() {
    return defaultValue;
  }

  /**
   * Sets the default value of this option
   *
   * @param defaultValue the default value
   * @return the current {@link Option} instance
   */
  public Option setDefaultValue(String defaultValue) {
    this.defaultValue = defaultValue;
    if (this.defaultValue != null) {
      setRequired(false);
    }
    return this;
  }

  /**
   * @return whether or not this option is a flag.
   */
  public boolean isFlag() {
    return flag;
  }

  /**
   * Configures the current {@link Option} to be a flag. It will be evaluated to {@code true} if it's found in
   * the command line. If you need a flag that may receive a value, use, in this order:
   * <code><pre>
   *   option.setFlag(true).setSingleValued(true)
   * </pre></code>
   *
   * @param flag whether or not the option is a flag.
   * @return the current {@link Option}
   */
  public Option setFlag(boolean flag) {
    this.flag = flag;
    if (flag) {
      setSingleValued(false);
    }
    return this;
  }

  /**
   * Checks whether or not this option is a "Help" option.
   *
   * @return {@code true} if this option is a "help" option.
   */
  public boolean isHelp() {
    return help;
  }

  /**
   * Sets whether or not this option is a "help" option
   *
   * @param help {@code true} to set this option as a "Help" option
   * @return the current {@link Option}
   */
  public Option setHelp(boolean help) {
    this.help = help;
    return this;
  }

  /**
   * @return get the list of choices for the given option. Empty if this option does not define choices.
   */
  public Set<String> getChoices() {
    return choices;
  }

  /**
   * Sets the list of values accepted by this option. If the value set by the user does not match once of these
   * values, a {@link InvalidValueException} exception is thrown.
   *
   * @param choices the choices
   * @return the current {@link Option}
   */
  public Option setChoices(Set<String> choices) {
    this.choices = choices;
    return this;
  }

  /**
   * Adds a choice to the list of values accepted by this option. If the value set by the user does not match once of these
   * values, a {@link InvalidValueException} exception is thrown.
   *
   * @param choice the choice
   * @return the current {@link Option}
   */
  public Option addChoice(String choice) {
    this.choices.add(choice);
    return this;
  }
}
