#include "options.h"

#include <assert.h>
#include <getopt.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include "url.h"

namespace speedtest {
namespace {

const char* kDefaultHost = "any.speed.gfsvc.com";

}  

namespace {

bool ParseLong(const char *s, char **endptr, long *number) {
  assert(s);
  assert(endptr);
  assert(number != nullptr);
  *number = strtol(s, endptr, 10);
  return !**endptr;
}

bool ParseDouble(const char *s, char **endptr, double *number) {
  assert(s);
  assert(endptr);
  assert(number != nullptr);
  *number = strtod(s, endptr);
  return !**endptr;
}

bool ParseSize(const char *s, long *size) {
  long result;
  char *endptr;
  bool success = ParseLong(s, &endptr, &result);
  if (result <= 0) {
    return false;
  }
  if (success) {
    *size = result;
    return true;
  }
  if (strcasecmp(endptr, "m") == 0) {
    result <<= 20;
  } else if (strcasecmp(endptr, "k") == 0) {
    result <<= 10;
  } else {
    return false;
  }
  *size = result;
  return true;
}

const int kOptDisableDnsCache = 1000;
const int kOptMaxConnections = 1001;
const int kOptReportResults = 1002;
const int kOptSkipDownload = 1003;
const int kOptSkipUpload = 1004;
const int kOptSkipPing = 1005;
const int kOptNoReportResults = 1006;
const int kOptServerId = 1007;

const int kOptMinTransferTime = 1100;
const int kOptMaxTransferTime = 1101;
const int kOptMinTransferIntervals = 1102;
const int kOptMaxTransferIntervals = 1103;
const int kOptMaxTransferVariance = 1104;
const int kOptIntervalMillis = 1105;
const int kOptPingRuntime = 1106;
const int kOptPingTimeout = 1107;
const int kOptExponentialMovingAverage = 1108;

const char *kShortOpts = "hvg:a:d:s:t:u:p:";

struct option kLongOpts[] = {
    {"help", no_argument, nullptr, 'h'},
    {"verbose", no_argument, nullptr, 'v'},
    {"global_url", required_argument, nullptr, 'g'},
    {"user_agent", required_argument, nullptr, 'a'},
    {"disable_dns_cache", no_argument, nullptr, kOptDisableDnsCache},
    {"max_connections", required_argument, nullptr, kOptMaxConnections},
    {"progress_millis", required_argument, nullptr, 'p'},
    {"skip_download", no_argument, nullptr, kOptSkipDownload},
    {"skip_upload", no_argument, nullptr, kOptSkipUpload},
    {"skip_ping", no_argument, nullptr, kOptSkipPing},
    {"report_results", no_argument, nullptr, kOptReportResults},
    {"noreport_results", no_argument, nullptr, kOptNoReportResults},

    {"num_downloads", required_argument, nullptr, 'd'},
    {"download_size", required_argument, nullptr, 's'},
    {"num_uploads", required_argument, nullptr, 'u'},
    {"upload_size", required_argument, nullptr, 't'},
    {"min_transfer_runtime", required_argument, nullptr, kOptMinTransferTime},
    {"max_transfer_runtime", required_argument, nullptr, kOptMaxTransferTime},
    {"min_transfer_intervals", required_argument, nullptr,
        kOptMinTransferIntervals},
    {"max_transfer_intervals", required_argument, nullptr,
        kOptMaxTransferIntervals},
    {"max_transfer_variance", required_argument, nullptr,
        kOptMaxTransferVariance},
    {"interval_millis", required_argument, nullptr, kOptIntervalMillis},
    {"ping_runtime", required_argument, nullptr, kOptPingRuntime},
    {"ping_timeout", required_argument, nullptr, kOptPingTimeout},
    {"exponential_moving_average", no_argument, nullptr,
        kOptExponentialMovingAverage},
    {"serverid", required_argument, nullptr, kOptServerId},  
    {nullptr, 0, nullptr, 0},
};
const int kMaxNumber = 1000;
const int kMaxProgress = 1000000;

const char *kSpeedtestHelp = R"USAGE(: run an HTTP speedtest.

If no hosts are specified, the global host is queried for a list
of servers to use, otherwise the list of supplied hosts will be
used. Each will be pinged several times and the one with the
lowest ping time will be used. If only one host is supplied, it
will be used without pinging.

Usage: speedtest [options] [host ...]
 -h, --help                    This help text
 -v, --verbose                 Verbose output
 -g, --global_url URL         Global host URL
 -a, --user_agent AGENT        User agent string for HTTP requests
 -p, --progress_millis NUM     Delay in milliseconds between updates
 --disable_dns_cache           Disable global DNS cache
 --max_connections NUM         Maximum number of parallel connections
 --skip_download               Skip the download test
 --skip_upload                 Skip the upload test
 --skip_ping                   Skip the ping test
 --[no]report_results          Whether to report Speedtest results to server

These options override the speedtest config parameters:
 -d, --num_downloads NUM       Number of simultaneous downloads
 -s, --download_size SIZE      Download size in bytes
 -t, --upload_size SIZE        Upload size in bytes
 -u, --num_uploads NUM         Number of simultaneous uploads
 --min_transfer_runtime TIME   Minimum transfer time in milliseconds
 --max_transfer_runtime TIME   Maximum transfer time in milliseconds
 --min_transfer_intervals NUM  Short moving average intervals
 --max_transfer_intervals NUM  Long moving average intervals
 --max_transfer_variance NUM   Max difference between moving averages
 --interval_millis TIME        Interval size in milliseconds
 --ping_runtime TIME           Ping runtime in milliseconds
 --ping_timeout TIME           Ping timeout in milliseconds
 --exponential_moving_average  Use exponential instead of simple moving average
)USAGE";

}  

bool ParseOptions(int argc, char *argv[], Options *options) {
  assert(options != nullptr);
  options->usage = false;
  options->verbose = false;
  options->global_url = http::Url(kDefaultHost);
  options->global = false;
  options->user_agent = "";
  options->progress_millis = 0;
  options->disable_dns_cache = false;
  options->max_connections = 0;
  options->exponential_moving_average = false;
  options->skip_download = false;
  options->skip_upload = false;
  options->skip_ping = false;
  options->report_results = true;

  options->num_downloads = 0;
  options->download_bytes = 0;
  options->num_uploads = 0;
  options->upload_bytes = 0;
  options->min_transfer_runtime = 0;
  options->max_transfer_runtime = 0;
  options->min_transfer_intervals = 0;
  options->max_transfer_intervals = 0;
  options->max_transfer_variance = 0.0;
  options->interval_millis = 0;
  options->ping_runtime_millis = 0;
  options->ping_timeout_millis = 0;

  options->regional_urls.clear();

  if (!options->global_url.ok()) {
    std::cerr << "Invalid global host " << kDefaultHost << "\n";
    return false;
  }

  optind = 0;
  int opt = 0, long_index = 0;
  while ((opt = getopt_long(argc, argv,
                            kShortOpts, kLongOpts, &long_index)) != -1) {
    switch (opt) {
      case 'a':
        options->user_agent = optarg;
        break;
      case 'd': {
        long number;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &number)) {
          std::cerr << "Could not parse number '" << optarg << "'\n";
          return false;
        }
        if (number < 1 || number > kMaxNumber) {
          std::cerr << "Number must be between 1 and " << kMaxNumber
                    << ", got '" << optarg << "'\n";
          return false;
        }
        options->num_downloads = static_cast<int>(number);
        break;
      }
      case 'g': {
        http::Url url(optarg);
        if (!url.ok()) {
          std::cerr << "Invalid global host " << optarg << "\n";
          return false;
        }
        options->global_url = url;
        break;
      }
      case 'h':
        options->usage = true;
        return true;
      case 'p': {
        long progress;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &progress)) {
          std::cerr << "Could not parse interval '" << optarg << "'\n";
          return false;
        }
        if (progress < 0 || progress > kMaxProgress) {
          std::cerr << "Number must be between 0 and " << kMaxProgress
                    << ", got '" << optarg << "'\n";
          return false;
        }
        options->progress_millis = progress;
        break;
      }
      case 's':
        if (!ParseSize(optarg, &options->download_bytes)) {
          std::cerr << "Invalid download size '" << optarg << "'\n";
          return false;
        }
        break;
      case 't':
        if (!ParseSize(optarg, &options->upload_bytes)) {
          std::cerr << "Invalid upload size '" << optarg << "'\n";
          return false;
        }
        break;
      case 'u': {
        long number;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &number)) {
          std::cerr << "Could not parse number '" << optarg << "'\n";
          return false;
        }
        if (number < 1 || number > kMaxNumber) {
          std::cerr << "Number must be between 1 and " << kMaxNumber
                    << ", got '" << optarg << "'\n";
          return false;
        }
        options->num_uploads = static_cast<int>(number);
        break;
      }
      case 'v':
        options->verbose = true;
        break;
      case kOptDisableDnsCache:
        options->disable_dns_cache = true;
        break;
      case kOptMaxConnections: {
        long max_connections;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &max_connections)) {
          std::cerr << "Could not parse max connections '" << optarg << "'\n";
          return false;
        }
        if (max_connections < 0) {
          std::cerr << "Max connections must be nonnegative, got "
          << optarg << "'\n";
          return false;
        }
        options->max_connections = static_cast<int>(max_connections);
        break;
      }
      case kOptReportResults:
        options->report_results = true;
        break;
      case kOptSkipDownload:
        options->skip_download = true;
        break;
      case kOptSkipUpload:
        options->skip_upload = true;
        break;
      case kOptSkipPing:
        options->skip_ping = true;
        break;
      case kOptNoReportResults:
        options->report_results = false;
        break;
      case kOptMinTransferTime: {
        long transfer_time;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &transfer_time)) {
          std::cerr << "Could not parse minimum transfer time '"
                    << optarg << "'\n";
          return false;
        }
        if (transfer_time < 0) {
          std::cerr << "Minimum transfer runtime must be nonnegative, got "
                    << optarg << "'\n";
          return false;
        }
        options->min_transfer_runtime = transfer_time;
        break;
      }
      case kOptMaxTransferTime: {
        long transfer_time;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &transfer_time)) {
          std::cerr << "Could not parse maximum transfer time '"
                    << optarg << "'\n";
          return false;
        }
        if (transfer_time < 0) {
          std::cerr << "Maximum transfer runtime must be nonnegative, got "
                    << optarg << "'\n";
          return false;
        }
        options->max_transfer_runtime = transfer_time;
        break;
      }
      case kOptMinTransferIntervals: {
        long intervals;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &intervals)) {
          std::cerr << "Could not parse minimum transfer intervals '"
                    << optarg << "'\n";
          return false;
        }
        if (intervals < 0) {
          std::cerr << "Minimum transfer intervals must be nonnegative, got "
                    << optarg << "'\n";
          return false;
        }
        options->min_transfer_intervals = static_cast<int>(intervals);
        break;
      }
      case kOptMaxTransferIntervals: {
        long intervals;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &intervals)) {
          std::cerr << "Could not parse maximum transfer intervals '"
                    << optarg << "'\n";
          return false;
        }
        if (intervals < 0) {
          std::cerr << "Maximum transfer intervals must be nonnegative, got "
                    << optarg << "'\n";
          return false;
        }
        options->max_transfer_intervals = static_cast<int>(intervals);
        break;
      }
      case kOptMaxTransferVariance: {
        double variance;
        char *endptr;
        if (!ParseDouble(optarg, &endptr, &variance)) {
          std::cerr << "Could not parse variance '" << optarg << "'\n";
          return false;
        }
        if (variance < 0) {
          std::cerr << "Variances must be nonnegative, got " << optarg << "'\n";
          return false;
        }
        options->max_transfer_variance = variance;
        break;
      }
      case kOptIntervalMillis: {
        long interval_millis;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &interval_millis)) {
          std::cerr << "Could not parse interval time '" << optarg << "'\n";
          return false;
        }
        if (interval_millis < 0) {
          std::cerr << "Interval time must be nonnegative, got "
                    << optarg << "'\n";
          return false;
        }
        options->interval_millis = interval_millis;
        break;
      }
      case kOptPingRuntime: {
        long ping_runtime;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &ping_runtime)) {
          std::cerr << "Could not parse ping time '" << optarg << "'\n";
          return false;
        }
        if (ping_runtime < 0) {
          std::cerr << "Ping time must be nonnegative, got "
                    << optarg << "'\n";
          return false;
        }
        options->ping_runtime_millis = ping_runtime;
        break;
      }
      case kOptPingTimeout: {
        long ping_timeout;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &ping_timeout)) {
          std::cerr << "Could not parse ping timeout '" << optarg << "'\n";
          return false;
        }
        if (ping_timeout < 0) {
          std::cerr << "Ping timeout must be nonnegative, got "
                    << optarg << "'\n";
          return false;
        }
        options->ping_timeout_millis = ping_timeout;
        break;
      }
      case kOptExponentialMovingAverage:
        options->exponential_moving_average = true;
        break;
      case kOptServerId:

        break;
      default:
        return false;
    }
  }
  if (optind < argc) {
    for (int i = optind; i < argc; i++) {
      http::Url url;
      if (!url.Parse(argv[i])) {
        std::cerr << "Could not parse URL '" << argv[i] << "'\n";
        return false;
      }
      if (!url.absolute()) {
        std::cerr << "URL '" << argv[i] << "' is not absolute\n";
        return false;
      }
      url.clear_path();
      url.clear_query_string();
      url.clear_fragment();
      options->regional_urls.emplace_back(url);
    }
  }
  if (options->regional_urls.empty()) {
    options->global = true;
  }
  return true;
}

void PrintOptions(const Options &options) {
  PrintOptions(std::cout, options);
}

void PrintOptions(std::ostream &out, const Options &options) {
  out << "Usage: " << (options.usage ? "true" : "false") << "\n"
      << "Verbose: " << (options.verbose ? "true" : "false") << "\n"
      << "Global host: " << options.global_url.url() << "\n"
      << "Global: " << (options.global ? "true" : "false") << "\n"
      << "User agent: " << options.user_agent << "\n"
      << "Progress interval: " << options.progress_millis << " ms\n"
      << "Disable DNS cache: "
      << (options.disable_dns_cache ? "true" : "false") << "\n"
      << "Max connections: " << options.max_connections << "\n"
      << "Skip download: "
      << (options.skip_download ? "true" : "false") << "\n"
      << "Skip upload: "
      << (options.skip_upload ? "true" : "false") << "\n"
      << "Skip ping: "
      << (options.skip_ping ? "true" : "false") << "\n"
      << "Report results: "
      << (options.report_results ? "true" : "false") << "\n"
      << "Number of downloads: " << options.num_downloads << "\n"
      << "Download size: " << options.download_bytes << " bytes\n"
      << "Number of uploads: " << options.num_uploads << "\n"
      << "Upload size: " << options.upload_bytes << " bytes\n"
      << "Min transfer runtime: " << options.min_transfer_runtime << " ms\n"
      << "Max transfer runtime: " << options.max_transfer_runtime << " ms\n"
      << "Min transfer intervals: " << options.min_transfer_intervals << "\n"
      << "Max transfer intervals: " << options.max_transfer_intervals << "\n"
      << "Max transfer variance: " << options.max_transfer_variance << "\n"
      << "Interval size: " << options.interval_millis << " ms\n"
      << "Ping runtime: " << options.ping_runtime_millis << " ms\n"
      << "Ping timeout: " << options.ping_timeout_millis << " ms\n"
      << "Exponential moving average: "
      << (options.exponential_moving_average ? "true" : "false") << "\n"
      << "Hosts:\n";
  for (const http::Url &host : options.regional_urls) {
    out << "  " << host.url() << "\n";
  }
}

void PrintUsage(const char *app_name) {
  PrintUsage(std::cout, app_name);
}

void PrintUsage(std::ostream &out, const char *app_path) {
  assert(app_path != nullptr);
  const char *last_slash = strrchr(app_path, '/');
  const char *app_name = last_slash == nullptr ? app_path : last_slash + 1;
  out << basename(app_name) << kSpeedtestHelp;
}

}
/*
 * Copyright 2016 Google Inc. All rights reserved.
 *
 * 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.
 */

#include "options.h"

#include <assert.h>
#include <getopt.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include "url.h"

namespace speedtest {
namespace {

const char* kDefaultHost = "any.speed.gfsvc.com";

}  // namespace

namespace {

bool ParseLong(const char *s, char **endptr, long *number) {
  assert(s);
  assert(endptr);
  assert(number != nullptr);
  *number = strtol(s, endptr, 10);
  return !**endptr;
}

bool ParseDouble(const char *s, char **endptr, double *number) {
  assert(s);
  assert(endptr);
  assert(number != nullptr);
  *number = strtod(s, endptr);
  return !**endptr;
}

bool ParseSize(const char *s, long *size) {
  long result;
  char *endptr;
  bool success = ParseLong(s, &endptr, &result);
  if (result <= 0) {
    return false;
  }
  if (success) {
    *size = result;
    return true;
  }
  if (strcasecmp(endptr, "m") == 0) {
    result <<= 20;
  } else if (strcasecmp(endptr, "k") == 0) {
    result <<= 10;
  } else {
    return false;
  }
  *size = result;
  return true;
}

const int kOptDisableDnsCache = 1000;
const int kOptMaxConnections = 1001;
const int kOptReportResults = 1002;
const int kOptSkipDownload = 1003;
const int kOptSkipUpload = 1004;
const int kOptSkipPing = 1005;
const int kOptNoReportResults = 1006;
const int kOptServerId = 1007;

const int kOptMinTransferTime = 1100;
const int kOptMaxTransferTime = 1101;
const int kOptMinTransferIntervals = 1102;
const int kOptMaxTransferIntervals = 1103;
const int kOptMaxTransferVariance = 1104;
const int kOptIntervalMillis = 1105;
const int kOptPingRuntime = 1106;
const int kOptPingTimeout = 1107;
const int kOptExponentialMovingAverage = 1108;

const char *kShortOpts = "hvg:a:d:s:t:u:p:";

struct option kLongOpts[] = {
    {"help", no_argument, nullptr, 'h'},
    {"verbose", no_argument, nullptr, 'v'},
    {"global_url", required_argument, nullptr, 'g'},
    {"user_agent", required_argument, nullptr, 'a'},
    {"disable_dns_cache", no_argument, nullptr, kOptDisableDnsCache},
    {"max_connections", required_argument, nullptr, kOptMaxConnections},
    {"progress_millis", required_argument, nullptr, 'p'},
    {"skip_download", no_argument, nullptr, kOptSkipDownload},
    {"skip_upload", no_argument, nullptr, kOptSkipUpload},
    {"skip_ping", no_argument, nullptr, kOptSkipPing},
    {"report_results", no_argument, nullptr, kOptReportResults},
    {"noreport_results", no_argument, nullptr, kOptNoReportResults},

    {"num_downloads", required_argument, nullptr, 'd'},
    {"download_size", required_argument, nullptr, 's'},
    {"num_uploads", required_argument, nullptr, 'u'},
    {"upload_size", required_argument, nullptr, 't'},
    {"min_transfer_runtime", required_argument, nullptr, kOptMinTransferTime},
    {"max_transfer_runtime", required_argument, nullptr, kOptMaxTransferTime},
    {"min_transfer_intervals", required_argument, nullptr,
        kOptMinTransferIntervals},
    {"max_transfer_intervals", required_argument, nullptr,
        kOptMaxTransferIntervals},
    {"max_transfer_variance", required_argument, nullptr,
        kOptMaxTransferVariance},
    {"interval_millis", required_argument, nullptr, kOptIntervalMillis},
    {"ping_runtime", required_argument, nullptr, kOptPingRuntime},
    {"ping_timeout", required_argument, nullptr, kOptPingTimeout},
    {"exponential_moving_average", no_argument, nullptr,
        kOptExponentialMovingAverage},
    {"serverid", required_argument, nullptr, kOptServerId},  // ignored
    {nullptr, 0, nullptr, 0},
};
const int kMaxNumber = 1000;
const int kMaxProgress = 1000000;

const char *kSpeedtestHelp = R"USAGE(: run an HTTP speedtest.

If no hosts are specified, the global host is queried for a list
of servers to use, otherwise the list of supplied hosts will be
used. Each will be pinged several times and the one with the
lowest ping time will be used. If only one host is supplied, it
will be used without pinging.

Usage: speedtest [options] [host ...]
 -h, --help                    This help text
 -v, --verbose                 Verbose output
 -g, --global_url URL         Global host URL
 -a, --user_agent AGENT        User agent string for HTTP requests
 -p, --progress_millis NUM     Delay in milliseconds between updates
 --disable_dns_cache           Disable global DNS cache
 --max_connections NUM         Maximum number of parallel connections
 --skip_download               Skip the download test
 --skip_upload                 Skip the upload test
 --skip_ping                   Skip the ping test
 --[no]report_results          Whether to report Speedtest results to server

These options override the speedtest config parameters:
 -d, --num_downloads NUM       Number of simultaneous downloads
 -s, --download_size SIZE      Download size in bytes
 -t, --upload_size SIZE        Upload size in bytes
 -u, --num_uploads NUM         Number of simultaneous uploads
 --min_transfer_runtime TIME   Minimum transfer time in milliseconds
 --max_transfer_runtime TIME   Maximum transfer time in milliseconds
 --min_transfer_intervals NUM  Short moving average intervals
 --max_transfer_intervals NUM  Long moving average intervals
 --max_transfer_variance NUM   Max difference between moving averages
 --interval_millis TIME        Interval size in milliseconds
 --ping_runtime TIME           Ping runtime in milliseconds
 --ping_timeout TIME           Ping timeout in milliseconds
 --exponential_moving_average  Use exponential instead of simple moving average
)USAGE";

}  // namespace

bool ParseOptions(int argc, char *argv[], Options *options) {
  assert(options != nullptr);
  options->usage = false;
  options->verbose = false;
  options->global_url = http::Url(kDefaultHost);
  options->global = false;
  options->user_agent = "";
  options->progress_millis = 0;
  options->disable_dns_cache = false;
  options->max_connections = 0;
  options->exponential_moving_average = false;
  options->skip_download = false;
  options->skip_upload = false;
  options->skip_ping = false;
  options->report_results = true;

  options->num_downloads = 0;
  options->download_bytes = 0;
  options->num_uploads = 0;
  options->upload_bytes = 0;
  options->min_transfer_runtime = 0;
  options->max_transfer_runtime = 0;
  options->min_transfer_intervals = 0;
  options->max_transfer_intervals = 0;
  options->max_transfer_variance = 0.0;
  options->interval_millis = 0;
  options->ping_runtime_millis = 0;
  options->ping_timeout_millis = 0;

  options->regional_urls.clear();

  if (!options->global_url.ok()) {
    std::cerr << "Invalid global host " << kDefaultHost << "\n";
    return false;
  }

  // Manually set this to 0 to allow repeated calls
  optind = 0;
  int opt = 0, long_index = 0;
  while ((opt = getopt_long(argc, argv,
                            kShortOpts, kLongOpts, &long_index)) != -1) {
    switch (opt) {
      case 'a':
        options->user_agent = optarg;
        break;
      case 'd': {
        long number;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &number)) {
          std::cerr << "Could not parse number '" << optarg << "'\n";
          return false;
        }
        if (number < 1 || number > kMaxNumber) {
          std::cerr << "Number must be between 1 and " << kMaxNumber
                    << ", got '" << optarg << "'\n";
          return false;
        }
        options->num_downloads = static_cast<int>(number);
        break;
      }
      case 'g': {
        http::Url url(optarg);
        if (!url.ok()) {
          std::cerr << "Invalid global host " << optarg << "\n";
          return false;
        }
        options->global_url = url;
        break;
      }
      case 'h':
        options->usage = true;
        return true;
      case 'p': {
        long progress;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &progress)) {
          std::cerr << "Could not parse interval '" << optarg << "'\n";
          return false;
        }
        if (progress < 0 || progress > kMaxProgress) {
          std::cerr << "Number must be between 0 and " << kMaxProgress
                    << ", got '" << optarg << "'\n";
          return false;
        }
        options->progress_millis = progress;
        break;
      }
      case 's':
        if (!ParseSize(optarg, &options->download_bytes)) {
          std::cerr << "Invalid download size '" << optarg << "'\n";
          return false;
        }
        break;
      case 't':
        if (!ParseSize(optarg, &options->upload_bytes)) {
          std::cerr << "Invalid upload size '" << optarg << "'\n";
          return false;
        }
        break;
      case 'u': {
        long number;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &number)) {
          std::cerr << "Could not parse number '" << optarg << "'\n";
          return false;
        }
        if (number < 1 || number > kMaxNumber) {
          std::cerr << "Number must be between 1 and " << kMaxNumber
                    << ", got '" << optarg << "'\n";
          return false;
        }
        options->num_uploads = static_cast<int>(number);
        break;
      }
      case 'v':
        options->verbose = true;
        break;
      case kOptDisableDnsCache:
        options->disable_dns_cache = true;
        break;
      case kOptMaxConnections: {
        long max_connections;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &max_connections)) {
          std::cerr << "Could not parse max connections '" << optarg << "'\n";
          return false;
        }
        if (max_connections < 0) {
          std::cerr << "Max connections must be nonnegative, got "
          << optarg << "'\n";
          return false;
        }
        options->max_connections = static_cast<int>(max_connections);
        break;
      }
      case kOptReportResults:
        options->report_results = true;
        break;
      case kOptSkipDownload:
        options->skip_download = true;
        break;
      case kOptSkipUpload:
        options->skip_upload = true;
        break;
      case kOptSkipPing:
        options->skip_ping = true;
        break;
      case kOptNoReportResults:
        options->report_results = false;
        break;
      case kOptMinTransferTime: {
        long transfer_time;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &transfer_time)) {
          std::cerr << "Could not parse minimum transfer time '"
                    << optarg << "'\n";
          return false;
        }
        if (transfer_time < 0) {
          std::cerr << "Minimum transfer runtime must be nonnegative, got "
                    << optarg << "'\n";
          return false;
        }
        options->min_transfer_runtime = transfer_time;
        break;
      }
      case kOptMaxTransferTime: {
        long transfer_time;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &transfer_time)) {
          std::cerr << "Could not parse maximum transfer time '"
                    << optarg << "'\n";
          return false;
        }
        if (transfer_time < 0) {
          std::cerr << "Maximum transfer runtime must be nonnegative, got "
                    << optarg << "'\n";
          return false;
        }
        options->max_transfer_runtime = transfer_time;
        break;
      }
      case kOptMinTransferIntervals: {
        long intervals;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &intervals)) {
          std::cerr << "Could not parse minimum transfer intervals '"
                    << optarg << "'\n";
          return false;
        }
        if (intervals < 0) {
          std::cerr << "Minimum transfer intervals must be nonnegative, got "
                    << optarg << "'\n";
          return false;
        }
        options->min_transfer_intervals = static_cast<int>(intervals);
        break;
      }
      case kOptMaxTransferIntervals: {
        long intervals;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &intervals)) {
          std::cerr << "Could not parse maximum transfer intervals '"
                    << optarg << "'\n";
          return false;
        }
        if (intervals < 0) {
          std::cerr << "Maximum transfer intervals must be nonnegative, got "
                    << optarg << "'\n";
          return false;
        }
        options->max_transfer_intervals = static_cast<int>(intervals);
        break;
      }
      case kOptMaxTransferVariance: {
        double variance;
        char *endptr;
        if (!ParseDouble(optarg, &endptr, &variance)) {
          std::cerr << "Could not parse variance '" << optarg << "'\n";
          return false;
        }
        if (variance < 0) {
          std::cerr << "Variances must be nonnegative, got " << optarg << "'\n";
          return false;
        }
        options->max_transfer_variance = variance;
        break;
      }
      case kOptIntervalMillis: {
        long interval_millis;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &interval_millis)) {
          std::cerr << "Could not parse interval time '" << optarg << "'\n";
          return false;
        }
        if (interval_millis < 0) {
          std::cerr << "Interval time must be nonnegative, got "
                    << optarg << "'\n";
          return false;
        }
        options->interval_millis = interval_millis;
        break;
      }
      case kOptPingRuntime: {
        long ping_runtime;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &ping_runtime)) {
          std::cerr << "Could not parse ping time '" << optarg << "'\n";
          return false;
        }
        if (ping_runtime < 0) {
          std::cerr << "Ping time must be nonnegative, got "
                    << optarg << "'\n";
          return false;
        }
        options->ping_runtime_millis = ping_runtime;
        break;
      }
      case kOptPingTimeout: {
        long ping_timeout;
        char *endptr;
        if (!ParseLong(optarg, &endptr, &ping_timeout)) {
          std::cerr << "Could not parse ping timeout '" << optarg << "'\n";
          return false;
        }
        if (ping_timeout < 0) {
          std::cerr << "Ping timeout must be nonnegative, got "
                    << optarg << "'\n";
          return false;
        }
        options->ping_timeout_millis = ping_timeout;
        break;
      }
      case kOptExponentialMovingAverage:
        options->exponential_moving_average = true;
        break;
      case kOptServerId:
        // --serverid is accepted but ignored, for backwards compatibility.
        break;
      default:
        return false;
    }
  }
  if (optind < argc) {
    for (int i = optind; i < argc; i++) {
      http::Url url;
      if (!url.Parse(argv[i])) {
        std::cerr << "Could not parse URL '" << argv[i] << "'\n";
        return false;
      }
      if (!url.absolute()) {
        std::cerr << "URL '" << argv[i] << "' is not absolute\n";
        return false;
      }
      url.clear_path();
      url.clear_query_string();
      url.clear_fragment();
      options->regional_urls.emplace_back(url);
    }
  }
  if (options->regional_urls.empty()) {
    options->global = true;
  }
  return true;
}

void PrintOptions(const Options &options) {
  PrintOptions(std::cout, options);
}

void PrintOptions(std::ostream &out, const Options &options) {
  out << "Usage: " << (options.usage ? "true" : "false") << "\n"
      << "Verbose: " << (options.verbose ? "true" : "false") << "\n"
      << "Global host: " << options.global_url.url() << "\n"
      << "Global: " << (options.global ? "true" : "false") << "\n"
      << "User agent: " << options.user_agent << "\n"
      << "Progress interval: " << options.progress_millis << " ms\n"
      << "Disable DNS cache: "
      << (options.disable_dns_cache ? "true" : "false") << "\n"
      << "Max connections: " << options.max_connections << "\n"
      << "Skip download: "
      << (options.skip_download ? "true" : "false") << "\n"
      << "Skip upload: "
      << (options.skip_upload ? "true" : "false") << "\n"
      << "Skip ping: "
      << (options.skip_ping ? "true" : "false") << "\n"
      << "Report results: "
      << (options.report_results ? "true" : "false") << "\n"
      << "Number of downloads: " << options.num_downloads << "\n"
      << "Download size: " << options.download_bytes << " bytes\n"
      << "Number of uploads: " << options.num_uploads << "\n"
      << "Upload size: " << options.upload_bytes << " bytes\n"
      << "Min transfer runtime: " << options.min_transfer_runtime << " ms\n"
      << "Max transfer runtime: " << options.max_transfer_runtime << " ms\n"
      << "Min transfer intervals: " << options.min_transfer_intervals << "\n"
      << "Max transfer intervals: " << options.max_transfer_intervals << "\n"
      << "Max transfer variance: " << options.max_transfer_variance << "\n"
      << "Interval size: " << options.interval_millis << " ms\n"
      << "Ping runtime: " << options.ping_runtime_millis << " ms\n"
      << "Ping timeout: " << options.ping_timeout_millis << " ms\n"
      << "Exponential moving average: "
      << (options.exponential_moving_average ? "true" : "false") << "\n"
      << "Hosts:\n";
  for (const http::Url &host : options.regional_urls) {
    out << "  " << host.url() << "\n";
  }
}

void PrintUsage(const char *app_name) {
  PrintUsage(std::cout, app_name);
}

void PrintUsage(std::ostream &out, const char *app_path) {
  assert(app_path != nullptr);
  const char *last_slash = strrchr(app_path, '/');
  const char *app_name = last_slash == nullptr ? app_path : last_slash + 1;
  out << basename(app_name) << kSpeedtestHelp;
}

}  // namespace speedtest
