// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

#include "bin/dartdev_options.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "bin/common_options.h"
#include "bin/error_exit.h"
#include "bin/file_system_watcher.h"
#include "bin/platform.h"
#include "bin/socket.h"
#include "bin/utils.h"
#include "include/dart_api.h"
#include "platform/assert.h"
#include "platform/globals.h"
#include "platform/hashmap.h"
#include "platform/syslog.h"

namespace dart {
namespace bin {

#if defined(DART_PRECOMPILED_RUNTIME)

static bool PotentialDartdevCommand(const char* script_uri) {
  // If script_uri is a known DartDev command, we should not try to run it.
  //
  // Otherwise if script_uri is not a file path or of a known URI scheme, we
  // assume this is a mistyped DartDev command.
  //
  // This should be kept in sync with the commands in
  // `pkg/dartdev/lib/dartdev.dart`.
  return (
      (strcmp(script_uri, "analyze") == 0) ||
      (strcmp(script_uri, "compilation-server") == 0) ||
      (strcmp(script_uri, "build") == 0) ||
      (strcmp(script_uri, "compile") == 0) ||
      (strcmp(script_uri, "create") == 0) ||
      (strcmp(script_uri, "development-service") == 0) ||
      (strcmp(script_uri, "devtools") == 0) ||
      (strcmp(script_uri, "doc") == 0) || (strcmp(script_uri, "fix") == 0) ||
      (strcmp(script_uri, "format") == 0) ||
      (strcmp(script_uri, "info") == 0) ||
      (strcmp(script_uri, "mcp-server") == 0) ||
      (strcmp(script_uri, "pub") == 0) || (strcmp(script_uri, "run") == 0) ||
      (strcmp(script_uri, "test") == 0) || (strcmp(script_uri, "info") == 0) ||
      (strcmp(script_uri, "language-server") == 0) ||
      (strcmp(script_uri, "tooling-daemon") == 0) ||
      (!File::ExistsUri(nullptr, script_uri) &&
       (strncmp(script_uri, "http://", 7) != 0) &&
       (strncmp(script_uri, "https://", 8) != 0) &&
       (strncmp(script_uri, "file://", 7) != 0) &&
       (strncmp(script_uri, "package:", 8) != 0) &&
       (strncmp(script_uri, "google3://", 10) != 0)));
}

#define OPTION_FIELD(variable) Options::variable##_

#define STRING_OPTION_DEFINITION(name, variable)                               \
  const char* OPTION_FIELD(variable) = nullptr;                                \
  DEFINE_STRING_OPTION(name, OPTION_FIELD(variable))
STRING_OPTIONS_LIST(STRING_OPTION_DEFINITION)
#undef STRING_OPTION_DEFINITION

#define BOOL_OPTION_DEFINITION(name, variable)                                 \
  bool OPTION_FIELD(variable) = false;                                         \
  DEFINE_BOOL_OPTION(name, OPTION_FIELD(variable))
BOOL_OPTIONS_LIST(BOOL_OPTION_DEFINITION)
#undef BOOL_OPTION_DEFINITION

#define SHORT_BOOL_OPTION_DEFINITION(short_name, long_name, variable)          \
  bool OPTION_FIELD(variable) = false;                                         \
  DEFINE_BOOL_OPTION_SHORT(short_name, long_name, OPTION_FIELD(variable))
SHORT_BOOL_OPTIONS_LIST(SHORT_BOOL_OPTION_DEFINITION)
#undef SHORT_BOOL_OPTION_DEFINITION

#define CB_OPTION_DEFINITION(callback)                                         \
  static bool callback##Helper(const char* arg, CommandLineOptions* o) {       \
    return Options::callback(arg, o);                                          \
  }                                                                            \
  DEFINE_CB_OPTION(callback##Helper)
CB_OPTIONS_LIST(CB_OPTION_DEFINITION)
#undef CB_OPTION_DEFINITION

// Explicitly handle VM flags that can be parsed by DartDev's run command.
bool Options::ProcessVMOptions(const char* arg,
                               CommandLineOptions* vm_options) {
#define IS_VM_OPTION(name, arg)                                                \
  if (OptionProcessor::ProcessOption(arg, name) != nullptr) {                  \
    vm_options->AddArgument(arg);                                              \
    return true;                                                               \
  }

// This is an exhaustive set of VM flags that are accepted by 'dart run' and
// 'dart test' commands, these options need to be collected and passed to
// the dart VM process that is going to run the command.
//
// NOTE: When updating this list of VM flags, be sure to make the corresponding
// changes in pkg/dartdev/lib/src/commands/run.dart.
#define HANDLE_DARTDEV_VM_OPTIONS(V, arg)                                      \
  V("--enable-asserts", arg)                                                   \
  V("--pause-isolates-on-exit", arg)                                           \
  V("--no-pause-isolates-on-exit", arg)                                        \
  V("--pause-isolates-on-start", arg)                                          \
  V("--no-pause-isolates-on-start", arg)                                       \
  V("--pause-isolates-on-unhandled-exception", arg)                            \
  V("--no-pause-isolates-on-unhandled-exception", arg)                         \
  V("--warn-on-pause-with-no-debugger", arg)                                   \
  V("--no-warn-on-pause-with-no-debugger", arg)                                \
  V("--timeline-streams", arg)                                                 \
  V("--timeline-recorder", arg)                                                \
  V("--dds", arg)                                                              \
  V("--no-dds", arg)                                                           \
  V("--profiler", arg)                                                         \
  V("--disable-service-auth-codes", arg)                                       \
  V("--write-service-info", arg)                                               \
  V("--enable-service-port-fallback", arg)                                     \
  V("--define", arg)                                                           \
  V("-D", arg)                                                                 \
  V("--disable-service-auth-codes", arg)                                       \
  V("--serve-observatory", arg)                                                \
  V("--print-dtd", arg)                                                        \
  V("--packages", arg)                                                         \
  V("--resident", arg)                                                         \
  V("--resident-server-info-file", arg)                                        \
  V("--resident-compiler-info-file", arg)                                      \
  V("--observe", arg)                                                          \
  V("--enable-vm-service", arg)                                                \
  V("--serve-devtools", arg)                                                   \
  V("--no-serve-devtools", arg)                                                \
  V("--serve-observatory", arg)                                                \
  V("--no-serve-observatory", arg)                                             \
  V("--profile-microtasks", arg)                                               \
  V("--profile-startup", arg)                                                  \
  V("--enable-experiment", arg)
  HANDLE_DARTDEV_VM_OPTIONS(IS_VM_OPTION, arg);

#undef IS_VM_OPTION
#undef HANDLE_DARTDEV_VM_OPTIONS

  return false;
}

bool Options::ParseDartDevArguments(int argc,
                                    char** argv,
                                    CommandLineOptions* vm_options,
                                    CommandLineOptions* dart_vm_options,
                                    CommandLineOptions* dart_options,
                                    bool* skip_dartdev) {
  // Store the executable name.
  Platform::SetExecutableName(argv[0]);

  // First figure out if a dartdev command has been explicitly specified.
  *skip_dartdev = false;
  int tmp_i = 1;
  while (tmp_i < argc) {
    // Check if this flag is a potentially valid VM flag, we skip over all
    // VM flags until we see a command or a script file.
    if (!OptionProcessor::IsValidFlag(argv[tmp_i]) &&
        !OptionProcessor::IsValidShortFlag(argv[tmp_i])) {
      break;
    }
    tmp_i++;
  }
  if (tmp_i < argc) {
    // Check if we have a dartdev command.
    if (!PotentialDartdevCommand(argv[tmp_i])) {
      // We don't have a dartdev command so skip dartdev and execute the
      // script directly.
      *skip_dartdev = true;
      return true;
    }
  }

  bool enable_dartdev_analytics = false;
  bool disable_dartdev_analytics = false;
  char* packages_argument = nullptr;

  // First parse out the vm options into dart_vm_options so that it can be
  // passed down to the 'run' and 'test' commands.
  // Start processing arguments after argv[0] which would be the executable.
  int i = 1;
  while (i < argc) {
    bool skipVmOption = false;
    if (!OptionProcessor::TryProcess(argv[i], dart_vm_options)) {
      // Check if this flag is a potentially valid VM flag.
      if (!OptionProcessor::IsValidFlag(argv[i])) {
        break;
      }
      // The following flags are processed as DartDev flags and are not to
      // be treated as if they are VM flags.
      if (IsOption(argv[i], "enable-analytics")) {
        enable_dartdev_analytics = true;
        skipVmOption = true;
      } else if (IsOption(argv[i], "disable-analytics")) {
        disable_dartdev_analytics = true;
        skipVmOption = true;
      } else if (IsOption(argv[i], "disable-telemetry")) {
        disable_dartdev_analytics = true;
        skipVmOption = true;
      } else if (IsOption(argv[i], "suppress-analytics")) {
        dart_options->AddArgument("--suppress-analytics");
        skipVmOption = true;
      } else if (IsOption(argv[i], "no-analytics")) {
        // Just add this option even if we don't go to dartdev.
        // It is irrelevant for the vm.
        dart_options->AddArgument("--no-analytics");
        skipVmOption = true;
      } else if (IsOption(argv[i], "serve-observatory")) {
        // This flag is currently set by default in vmservice_io.dart, so we
        // ignore it. --no-serve-observatory is a VM flag so we don't need to
        // handle that case here.
        skipVmOption = true;
      } else if (IsOption(argv[i], "print-dtd-uri")) {
        skipVmOption = true;
      } else if (IsOption(argv[i], "executable-name")) {
        skipVmOption = true;
      } else if (IsOption(argv[i], "enable-experiment")) {
        dart_options->AddArgument(argv[i]);
      } else if (IsOption(argv[i], "resident")) {
        resident_ = true;
      } else if (IsOption(argv[i], "resident-compiler-info-file")) {
        resident_compiler_info_file_path_ = OptionProcessor::ProcessOption(
            argv[i], "--resident-compiler-info-file");
      }
    }
    if (!skipVmOption) {
      dart_vm_options->AddArgument(argv[i]);
    }
    if (IsOption(argv[i], "packages")) {
      packages_argument = argv[i];
    }
    i++;
  }

  // The arguments to the VM are at positions 1 through i-1 in argv.
  Platform::SetExecutableArguments(i, argv);

  // If we have exhausted all the arguments and haven't see a dartdev
  // command then we set up some scenarios where it still makes sense
  // to start up dartdev and have it process the options.
  if (i >= argc) {
    // Handles following invocation arguments:
    //   - dart help
    //   - dart --help
    //   - dart
    if (((Options::help_option() && !Options::verbose_option()) ||
         (argc == 1))) {
      // Let DartDev handle the default help message.
      dart_options->AddArgument("help");
      return true;
    }
    // Handles cases where only analytics flags are provided. We need to launch
    // DartDev for this.
    else if (enable_dartdev_analytics || disable_dartdev_analytics) {  // NOLINT
      // The analytics flags are a special case as we don't have a target script
      // or DartDev command but we still want to launch DartDev.
      dart_options->AddArgument(enable_dartdev_analytics
                                    ? "--enable-analytics"
                                    : "--disable-analytics");
      return true;
    }
    // If it is not '--version' and '--help' we will launch DartDev
    // to print its help message and set an error exit code.
    else if (!Options::help_option() && !Options::version_option()) {  // NOLINT
      // Pass in an invalid option so that dartdev prints the help message
      // and exits with an error exit code.
      dart_options->Reset();
      dart_options->AddArgument(argv[argc - 1]);
      dart_options->AddArgument("help");
      return true;
    }
    return false;
  }

  USE(enable_dartdev_analytics);
  USE(disable_dartdev_analytics);
  USE(packages_argument);

  // Record the dartdev command.
  dart_options->AddArgument(argv[i++]);

  // Bring any --packages option into the dartdev command
  if (packages_argument != nullptr) {
    dart_options->AddArgument(packages_argument);
    dart_vm_options->AddArgument(packages_argument);
  }

  // Scan remaining arguments and separate them into
  // dart_vm_options (vm options to be passed to the dart process executing
  // the dartdev command) or dart_options (options to be passed to the
  // executing dart script).
  bool script_seen = false;
  while (i < argc) {
    if (!IsOption(argv[i], "disable-dart-dev")) {
      if (!script_seen) {
        // We scan for VM options that are passed to the 'run' and 'test'
        // command. These options are accepted by both the VM and dartdev
        // commands and need to be carried over to the VM running the app for
        // these commands.
        if (Options::ProcessVMOptions(argv[i], dart_vm_options)) {
          // dartdev isn't able to parse these options properly. Since it
          // doesn't need to use the values from these options, just strip them
          // from the argument list passed to dartdev.
          if (!IsOption(argv[i], "observe") &&
              !IsOption(argv[i], "enable-vm-service")) {
            dart_options->AddArgument(argv[i]);
          }
        } else {
          if (!OptionProcessor::IsValidFlag(argv[i]) &&
              !OptionProcessor::IsValidShortFlag(argv[i])) {
            script_seen = true;
          }
          dart_options->AddArgument(argv[i]);
        }
      } else {
        dart_options->AddArgument(argv[i]);
      }
    } else {
      Syslog::PrintErr(
          "Attempted to use --disable-dart-dev with a Dart CLI command.\n");
      return false;
    }
    i++;
  }

  // Verify consistency of arguments.
  if ((packages_file_ != nullptr) && (strlen(packages_file_) == 0)) {
    Syslog::PrintErr("Empty package file name specified.\n");
    return false;
  }

  return true;
}

void Options::PrintVersion() {
  _PrintVersion();
}

// clang-format off
void Options::PrintUsage() {
  _PrintUsage();
  if (!Options::verbose_option()) {
    _PrintNonVerboseUsage();
  } else {
    _PrintVerboseUsage();
  }
}
// clang-format on

bool Options::resident_ = false;
const char* Options::resident_compiler_info_file_path_ = nullptr;

dart::SimpleHashMap* Options::environment_ = nullptr;
bool Options::ProcessEnvironmentOption(const char* arg,
                                       CommandLineOptions* vm_options) {
  return OptionProcessor::ProcessEnvironmentOption(arg, vm_options,
                                                   &Options::environment_);
}

void Options::Cleanup() {
  DestroyEnvironment();
}

void Options::DestroyEnvironment() {
  if (environment_ != nullptr) {
    for (SimpleHashMap::Entry* p = environment_->Start(); p != nullptr;
         p = environment_->Next(p)) {
      free(p->key);
      free(p->value);
    }
    delete environment_;
    environment_ = nullptr;
  }
}

char** Options::GetEnvArguments(const char* executable_name, int* argc) {
  return nullptr;
}

void Options::DestroyEnvArgv() {}

#endif  // defined(DART_PRECOMPILED_RUNTIME)

}  // namespace bin
}  // namespace dart
