/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

#include "watchman/Options.h"
#include <getopt.h>
#include <string.h>
#include "watchman/CommandRegistry.h"
#include "watchman/LogConfig.h"
#include "watchman/Logging.h"
#include "watchman/WatchmanConfig.h"

#define IS_REQUIRED(x) (x) == REQ_STRING

namespace watchman {

Flags flags;

namespace {
const OptDesc opts[] = {
    {"help",
     'h',
     "Show this help",
     OPT_NONE,
     &flags.show_help,
     NULL,
     NOT_DAEMON},
#ifndef _WIN32
    {"inetd",
     0,
     "Spawning from an inetd style supervisor",
     OPT_NONE,
     &flags.inetd_style,
     NULL,
     IS_DAEMON},
#endif
    {"no-site-spawner",
     'S',
     "Don't use the site or system spawner",
     OPT_NONE,
     &flags.no_site_spawner,
     NULL,
     IS_DAEMON},
    {"version",
     'v',
     "Show version number",
     OPT_NONE,
     &flags.show_version,
     NULL,
     NOT_DAEMON},
/* -U / --sockname  have legacy meaning; unix domain on unix,
 * named pipe path on windows.  After we chose this assignment,
 * Windows evolved unix domain support which muddies this.
 * We need to preserve the sockname/U option here for backwards
 * compatibility */
#ifdef _WIN32
    {"sockname",
     'U',
     "DEPRECATED: Specify alternate named pipe path (specifying this will"
     " disable unix domain sockets unless `--unix-listener-path` is"
     " specified)",
     REQ_STRING,
     &flags.named_pipe_path,
     "PATH",
     IS_DAEMON},
#else
    {"sockname",
     'U',
     "DEPRECATED: Specify alternate sockname. Use `--unix-listener-path` instead.",
     REQ_STRING,
     &flags.unix_sock_name,
     "PATH",
     IS_DAEMON},
#endif
    {"named-pipe-path",
     0,
     "Specify alternate named pipe path",
     REQ_STRING,
     &flags.named_pipe_path,
     "PATH",
     IS_DAEMON},
    {"unix-listener-path",
     'u',
#ifdef _WIN32
     "Specify alternate unix domain socket path (specifying this will disable"
     " named pipes unless `--named-pipe-path` is specified)",
#else
     "Specify alternate unix domain socket path",
#endif
     REQ_STRING,
     &flags.unix_sock_name,
     "PATH",
     IS_DAEMON},
    {"tcp-listener-enable",
     't',
     "Enable listening on TCP; see also tcp-listener-address and tcp-listener-port",
     OPT_NONE,
     &flags.enable_tcp,
     nullptr,
     IS_DAEMON},
    {"tcp-listener-address",
     0,
     "Specify in <address>:<port> the address to bind to and listen on when tcp-listener-enable is true",
     REQ_STRING,
     &flags.tcp_host,
     "ADDRESS",
     IS_DAEMON},
    {"logfile",
     'o',
     "Specify path to logfile ('-' = stdout and stderr)",
     REQ_STRING,
     &watchman::logging::log_name,
     "PATH",
     IS_DAEMON},
    {"log-level",
     0,
     "set the log level (0 = off, default is 1, verbose = 2)",
     REQ_INT,
     &watchman::logging::log_level,
     NULL,
     IS_DAEMON},
    {"pidfile",
     0,
     "Specify path to pidfile",
     REQ_STRING,
     &flags.pid_file,
     "PATH",
     IS_DAEMON},
    {"persistent",
     'p',
     "Persist and wait for further responses",
     OPT_NONE,
     &flags.persistent,
     NULL,
     NOT_DAEMON},
    {"no-save-state",
     'n',
     "Don't save state between invocations",
     OPT_NONE,
     &flags.dont_save_state,
     NULL,
     IS_DAEMON},
    {"statefile",
     0,
     "Specify path to file to hold watch and trigger state",
     REQ_STRING,
     &flags.watchman_state_file,
     "PATH",
     IS_DAEMON},
    {"json-command",
     'j',
     "Instead of parsing CLI arguments, take a single "
     "json object from stdin",
     OPT_NONE,
     &flags.json_input_arg,
     NULL,
     NOT_DAEMON},
    {"output-encoding",
     0,
     "CLI output encoding. json (default) or bser",
     REQ_STRING,
     &flags.output_encoding,
     NULL,
     NOT_DAEMON},
    {"server-encoding",
     0,
     "CLI<->server encoding. bser (default) or json",
     REQ_STRING,
     &flags.server_encoding,
     NULL,
     NOT_DAEMON},
    {"foreground",
     'f',
     "Run the service in the foreground",
     OPT_NONE,
     &flags.foreground,
     NULL,
     NOT_DAEMON},
    {"no-pretty",
     0,
     "Don't pretty print JSON",
     OPT_NONE,
     &flags.no_pretty,
     NULL,
     NOT_DAEMON},
    {"no-spawn",
     0,
     "Don't try to start the service if it is not available",
     OPT_NONE,
     &flags.no_spawn,
     NULL,
     NOT_DAEMON},
    {"no-local",
     0,
     "When no-spawn is enabled, don't try to handle request"
     " in client mode if service is unavailable",
     OPT_NONE,
     &flags.no_local,
     NULL,
     NOT_DAEMON},
    // test-state-dir is for testing only and should not be used in production:
    // instead, use the compile-time WATCHMAN_STATE_DIR option
    {"test-state-dir",
     0,
     NULL,
     REQ_STRING,
     &flags.test_state_dir,
     "DIR",
     NOT_DAEMON},
    {0, 0, 0, OPT_NONE, 0, 0, 0},
};
} // namespace

/* One does not simply use getopt_long() */

[[noreturn]] void usage(const OptDesc* opts, FILE* where) {
  int i;
  size_t len;
  size_t longest = 0;
  const char* label;

  fprintf(where, "Usage: watchman [opts] command\n");

  /* measure up option names so we can format nicely */
  for (i = 0; opts[i].optname; i++) {
    label = opts[i].arglabel ? opts[i].arglabel : "ARG";

    len = strlen(opts[i].optname);
    switch (opts[i].argtype) {
      case REQ_STRING:
        len += strlen(label) + strlen("=");
        break;
      default:;
    }

    if (opts[i].shortopt) {
      len += strlen("-X, ");
    }

    if (len > longest) {
      longest = len;
    }
  }

  /* space between option definition and help text */
  longest += 3;

  for (i = 0; opts[i].optname; i++) {
    char buf[80];

    if (!opts[i].helptext) {
      // This is a signal that this option shouldn't be printed out.
      continue;
    }

    label = opts[i].arglabel ? opts[i].arglabel : "ARG";

    fprintf(where, "\n ");
    if (opts[i].shortopt) {
      fprintf(where, "-%c, ", opts[i].shortopt);
    } else {
      fprintf(where, "    ");
    }
    switch (opts[i].argtype) {
      case REQ_STRING:
        snprintf(buf, sizeof(buf), "--%s=%s", opts[i].optname, label);
        break;
      default:
        snprintf(buf, sizeof(buf), "--%s", opts[i].optname);
        break;
    }

    fprintf(where, "%-*s ", (unsigned int)longest, buf);

    fprintf(where, "%s", opts[i].helptext);
    fprintf(where, "\n");
  }

  print_command_list_for_help(where);

  fprintf(
      where,
      "\n"
      "See https://github.com/facebook/watchman#watchman for more help\n"
      "\n"
      "Watchman, by Wez Furlong.\n"
      "Copyright (c) Meta Platforms, Inc.\n");

  exit(1);
}

bool w_getopt(
    const OptDesc* opts,
    int* argcp,
    char*** argvp,
    char*** daemon_argvp) {
  int num_opts, i;
  char* nextshort;
  int argc = *argcp;
  char** argv = *argvp;
  int long_pos = -1;
  int res;
  int num_daemon = 0;

  /* first build up the getopt_long bits that we need */
  for (num_opts = 0; opts[num_opts].optname; num_opts++) {
    ;
  }

  /* to hold the args we pass to the daemon */
  auto daemon_argv = (char**)calloc(num_opts + 1, sizeof(char*));
  if (!daemon_argv) {
    log(FATAL, "calloc daemon opts\n");
  }
  *daemon_argvp = daemon_argv;

  /* something to hold the long options */
  auto long_opts = (option*)calloc(num_opts + 1, sizeof(struct option));
  if (!long_opts) {
    log(FATAL, "calloc struct option\n");
  }

  /* and the short options */
  auto shortopts = (char*)malloc((1 + num_opts) * 2);
  if (!shortopts) {
    log(FATAL, "malloc shortopts\n");
  }
  nextshort = shortopts;
  nextshort[0] = ':';
  nextshort++;

  /* now transfer information into the space we made */
  for (i = 0; i < num_opts; i++) {
    long_opts[i].name = (char*)opts[i].optname;
    long_opts[i].val = opts[i].shortopt;
    switch (opts[i].argtype) {
      case OPT_NONE:
        long_opts[i].has_arg = no_argument;
        break;
      case REQ_STRING:
      case REQ_INT:
        long_opts[i].has_arg = required_argument;
        break;
    }

    if (opts[i].shortopt) {
      nextshort[0] = (char)opts[i].shortopt;
      nextshort++;

      if (long_opts[i].has_arg != no_argument) {
        nextshort[0] = ':';
        nextshort++;
      }
    }
  }

  nextshort[0] = 0;

  while ((res = getopt_long(argc, argv, shortopts, long_opts, &long_pos)) !=
         -1) {
    const OptDesc* o;

    switch (res) {
      case ':':
        /* missing option argument.
         * Check to see if it was actually optional */
        for (long_pos = 0; long_pos < num_opts; long_pos++) {
          if (opts[long_pos].shortopt == optopt) {
            if (IS_REQUIRED(opts[long_pos].argtype)) {
              fprintf(
                  stderr,
                  "--%s (-%c) requires an argument",
                  opts[long_pos].optname,
                  opts[long_pos].shortopt);
              return false;
            }
          }
        }
        break;

      case '?':
        /* unknown option */
        fprintf(stderr, "Unknown or invalid option! %s\n", argv[optind - 1]);
        usage(opts, stderr);
        return false;

      default:
        if (res == 0) {
          /* we got a long option */
          o = &opts[long_pos];
        } else {
          /* map short option to the real thing */
          o = NULL;
          for (long_pos = 0; long_pos < num_opts; long_pos++) {
            if (opts[long_pos].shortopt == res) {
              o = &opts[long_pos];
              break;
            }
          }
        }

        if (o->is_daemon) {
          auto value = folly::to<std::string>(
              "--", o->optname, "=", optarg ? optarg : "");
          // we deliberately leak this value to the caller
          daemon_argv[num_daemon++] = strdup(value.c_str());
        }

        /* store the argument if we found one */
        if (o->argtype != OPT_NONE && o->val && optarg) {
          switch (o->argtype) {
            case REQ_INT: {
              auto ival = atoi(optarg);
              *(int*)o->val = ival;
              cfg_set_arg(o->optname, json_integer(ival));
              break;
            }
            case REQ_STRING: {
              auto sval = typed_string_to_json(optarg, W_STRING_UNICODE);
              *(std::string*)o->val = optarg;
              cfg_set_arg(o->optname, sval);
              break;
            }
            case OPT_NONE:;
          }
        }
        if (o->argtype == OPT_NONE && o->val) {
          auto bval = json_true();
          *(int*)o->val = 1;
          cfg_set_arg(o->optname, bval);
        }
    }

    long_pos = -1;
  }

  free(long_opts);
  free(shortopts);

  *argcp = argc - optind;
  *argvp = argv + optind;
  return true;
}

void parseOptions(int* argcp, char*** argvp, char*** daemon_argv) {
  w_getopt(opts, argcp, argvp, daemon_argv);
  if (flags.show_help) {
    usage(opts, stdout);
  }
  if (flags.show_version) {
    printf("%s\n", PACKAGE_VERSION);
    exit(0);
  }
}

void print_command_list_for_help(FILE* where) {
  auto defs = get_all_commands();
  std::sort(
      defs.begin(),
      defs.end(),
      [](command_handler_def* A, command_handler_def* B) {
        return strcmp(A->name, B->name) < 0;
      });

  fprintf(where, "\n\nAvailable commands:\n\n");
  for (auto& def : defs) {
    fprintf(where, "      %s\n", def->name);
  }
}

} // namespace watchman

/* vim:ts=2:sw=2:et:
 */
