/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * vim: set ts=8 sts=2 et sw=2 tw=80:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifndef shell_jsoptparse_h
#define shell_jsoptparse_h

#include <stdio.h>

#include "js/AllocPolicy.h"
#include "js/Utility.h"
#include "js/Vector.h"

namespace js {
namespace cli {

namespace detail {

// We want to use the shell's option parser before initializing the JS engine.
// The JS malloc arena isn't available yet at this point, so we use a custom
// allocation policy that uses the system malloc instead.
class OptionAllocPolicy {
 public:
  template <typename T>
  T* pod_malloc(size_t numElems) {
    size_t bytes;
    if (MOZ_UNLIKELY(!js::CalculateAllocSize<T>(numElems, &bytes))) {
      return nullptr;
    }
    return static_cast<T*>(malloc(bytes));
  }

  template <typename T>
  T* pod_realloc(T* p, size_t oldSize, size_t newSize) {
    size_t bytes;
    if (MOZ_UNLIKELY(!js::CalculateAllocSize<T>(newSize, &bytes))) {
      return nullptr;
    }
    return static_cast<T*>(realloc(p, bytes));
  }

  void reportAllocOverflow() const {}
  bool checkSimulatedOOM() const { return !js::oom::ShouldFailWithOOM(); }

  template <typename T>
  void free_(T* p, size_t numElems = 0) {
    free(p);
  }
};

struct BoolOption;
struct MultiStringOption;
struct ValuedOption;
struct StringOption;
struct IntOption;

enum OptionKind {
  OptionKindBool,
  OptionKindString,
  OptionKindInt,
  OptionKindMultiString,
  OptionKindInvalid
};

struct Option {
  const char* longflag;
  const char* help;
  OptionKind kind;
  char shortflag;
  bool terminatesOptions = false;
  bool ignoresUnknownOptions = false;

  Option(OptionKind kind, char shortflag, const char* longflag,
         const char* help)
      : longflag(longflag), help(help), kind(kind), shortflag(shortflag) {}

  virtual ~Option() = 0;

  void setTerminatesOptions(bool enabled) { terminatesOptions = enabled; }
  bool getTerminatesOptions() const { return terminatesOptions; }

  void setIgnoresUnknownOptions(bool enabled) {
    ignoresUnknownOptions = enabled;
  }
  bool getIgnoresUnknownOptions() const { return ignoresUnknownOptions; }

  virtual bool isValued() const { return false; }

  /* Only some valued options are variadic (like MultiStringOptions). */
  virtual bool isVariadic() const { return false; }

  /*
   * For arguments, the shortflag field is used to indicate whether the
   * argument is optional.
   */
  bool isOptional() { return shortflag; }

  void setFlagInfo(char shortflag, const char* longflag, const char* help) {
    this->shortflag = shortflag;
    this->longflag = longflag;
    this->help = help;
  }

  ValuedOption* asValued();
  const ValuedOption* asValued() const;

#define OPTION_CONVERT_DECL(__cls)    \
  bool is##__cls##Option() const;     \
  __cls##Option* as##__cls##Option(); \
  const __cls##Option* as##__cls##Option() const;

  OPTION_CONVERT_DECL(Bool)
  OPTION_CONVERT_DECL(String)
  OPTION_CONVERT_DECL(Int)
  OPTION_CONVERT_DECL(MultiString)
};

inline Option::~Option() {}

struct BoolOption : public Option {
  size_t argno;
  bool value;

  BoolOption(char shortflag, const char* longflag, const char* help)
      : Option(OptionKindBool, shortflag, longflag, help), value(false) {}

  virtual ~BoolOption() {}
};

struct ValuedOption : public Option {
  const char* metavar;

  ValuedOption(OptionKind kind, char shortflag, const char* longflag,
               const char* help, const char* metavar)
      : Option(kind, shortflag, longflag, help), metavar(metavar) {}

  virtual ~ValuedOption() = 0;
  virtual bool isValued() const override { return true; }
};

inline ValuedOption::~ValuedOption() {}

struct StringOption : public ValuedOption {
  const char* value;

  StringOption(char shortflag, const char* longflag, const char* help,
               const char* metavar)
      : ValuedOption(OptionKindString, shortflag, longflag, help, metavar),
        value(nullptr) {}

  virtual ~StringOption() {}
};

struct IntOption : public ValuedOption {
  int value;

  IntOption(char shortflag, const char* longflag, const char* help,
            const char* metavar, int defaultValue)
      : ValuedOption(OptionKindInt, shortflag, longflag, help, metavar),
        value(defaultValue) {}

  virtual ~IntOption() {}
};

struct StringArg {
  char* value;
  size_t argno;

  StringArg(char* value, size_t argno) : value(value), argno(argno) {}
};

struct MultiStringOption : public ValuedOption {
  Vector<StringArg, 0, detail::OptionAllocPolicy> strings;

  MultiStringOption(char shortflag, const char* longflag, const char* help,
                    const char* metavar)
      : ValuedOption(OptionKindMultiString, shortflag, longflag, help,
                     metavar) {}

  virtual ~MultiStringOption() {}

  virtual bool isVariadic() const override { return true; }
};

} /* namespace detail */

class MultiStringRange {
  using StringArg = detail::StringArg;
  const StringArg* cur;
  const StringArg* end;

 public:
  explicit MultiStringRange(const StringArg* cur, const StringArg* end)
      : cur(cur), end(end) {
    MOZ_ASSERT(end - cur >= 0);
  }

  bool empty() const { return cur == end; }
  void popFront() {
    MOZ_ASSERT(!empty());
    ++cur;
  }
  char* front() const {
    MOZ_ASSERT(!empty());
    return cur->value;
  }
  size_t argno() const {
    MOZ_ASSERT(!empty());
    return cur->argno;
  }
};

/*
 * Builder for describing a command line interface and parsing the resulting
 * specification.
 *
 * - A multi-option is an option that can appear multiple times and still
 *   parse as valid command line arguments.
 * - An "optional argument" is supported for backwards compatibility with prior
 *   command line interface usage. Once one optional argument has been added,
 *   *only* optional arguments may be added.
 */
class OptionParser {
 public:
  enum Result {
    Okay = 0,
    Fail,       /* As in, allocation fail. */
    ParseError, /* Successfully parsed but with an error. */
    EarlyExit   /* Successfully parsed but exits the program,
                 * for example with --help and --version. */
  };

 private:
  using Options = Vector<detail::Option*, 0, detail::OptionAllocPolicy>;
  using Option = detail::Option;
  using BoolOption = detail::BoolOption;

  Options options;
  Options arguments;
  BoolOption helpOption;
  BoolOption versionOption;
  const char* usage;
  const char* version;
  const char* descr;
  size_t descrWidth;
  size_t helpWidth;
  size_t nextArgument;

  // If '--' is passed, all remaining arguments should be interpreted as the
  // argument at index 'restArgument'. Defaults to the next unassigned
  // argument.
  int restArgument;

  Option* findOption(char shortflag);
  const Option* findOption(char shortflag) const;
  const Option* tryFindOption(char shortflag) const;
  Option* findOption(const char* longflag);
  const Option* findOption(const char* longflag) const;
  const Option* tryFindOption(const char* longflag) const;
  int findArgumentIndex(const char* name) const;
  Option* findArgument(const char* name);
  const Option* findArgument(const char* name) const;

  Result error(const char* fmt, ...) MOZ_FORMAT_PRINTF(2, 3);
  Result extractValue(size_t argc, char** argv, size_t* i, char** value);
  Result handleArg(size_t argc, char** argv, size_t* i, bool* optsAllowed);
  Result handleOption(Option* opt, size_t argc, char** argv, size_t* i,
                      bool* optsAllowed);

 public:
  explicit OptionParser(const char* usage)
      : helpOption('h', "help", "Display help information"),
        versionOption('v', "version", "Display version information and exit"),
        usage(usage),
        version(nullptr),
        descr(nullptr),
        descrWidth(80),
        helpWidth(80),
        nextArgument(0),
        restArgument(-1) {}

  ~OptionParser();

  Result parseArgs(int argc, char** argv);
  Result printHelp(const char* progname);
  Result printVersion();

  /* Metadata */

  void setVersion(const char* v) { version = v; }
  void setHelpWidth(size_t width) { helpWidth = width; }
  void setDescriptionWidth(size_t width) { descrWidth = width; }
  void setDescription(const char* description) { descr = description; }
  void setHelpOption(char shortflag, const char* longflag, const char* help);
  void setArgTerminatesOptions(const char* name, bool enabled);
  void setIgnoresUnknownOptions(const char* name, bool enabled);
  void setArgCapturesRest(const char* name);

  /* Arguments: no further arguments may be added after a variadic argument. */

  bool addOptionalStringArg(const char* name, const char* help);
  bool addOptionalMultiStringArg(const char* name, const char* help);

  const char* getStringArg(const char* name) const;
  MultiStringRange getMultiStringArg(const char* name) const;

  /* Options */

  bool addBoolOption(char shortflag, const char* longflag, const char* help);
  bool addStringOption(char shortflag, const char* longflag, const char* help,
                       const char* metavar);
  bool addIntOption(char shortflag, const char* longflag, const char* help,
                    const char* metavar, int defaultValue);
  bool addMultiStringOption(char shortflag, const char* longflag,
                            const char* help, const char* metavar);
  bool addOptionalVariadicArg(const char* name);

  int getIntOption(char shortflag) const;
  int getIntOption(const char* longflag) const;
  const char* getStringOption(char shortflag) const;
  const char* getStringOption(const char* longflag) const;
  bool getBoolOption(char shortflag) const;
  bool getBoolOption(const char* longflag) const;
  MultiStringRange getMultiStringOption(char shortflag) const;
  MultiStringRange getMultiStringOption(const char* longflag) const;

  /*
   * Return whether the help option was present (and thus help was already
   * displayed during parse_args).
   */
  bool getHelpOption() const;
};

} /* namespace cli */
} /* namespace js */

#endif /* shell_jsoptparse_h */
