// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:args/args.dart';
import 'package:flutter_tools_extension_linux_prototype/flutter_tools_extension_linux_prototype.dart';
import 'package:meta/meta.dart';

import 'runner.dart' as runner;
import 'src/android/android_workflow.dart' as android_workflow;
import 'src/android/gradle.dart';
import 'src/base/context.dart';
import 'src/base/io.dart';
import 'src/base/logger.dart';
import 'src/base/platform.dart';
import 'src/base/template.dart';
import 'src/base/terminal.dart';
import 'src/base/user_messages.dart';
import 'src/build_system/build_targets.dart';
import 'src/build_system/targets/hook_runner_native.dart' show FlutterHookRunnerNative;
import 'src/cache.dart';
import 'src/commands/analyze.dart';
import 'src/commands/assemble.dart';
import 'src/commands/attach.dart';
import 'src/commands/build.dart';
import 'src/commands/channel.dart';
import 'src/commands/clean.dart';
import 'src/commands/config.dart';
import 'src/commands/create.dart';
import 'src/commands/custom_devices.dart';
import 'src/commands/daemon.dart';
import 'src/commands/debug_adapter.dart';
import 'src/commands/devices.dart';
import 'src/commands/doctor.dart';
import 'src/commands/downgrade.dart';
import 'src/commands/drive.dart';
import 'src/commands/emulators.dart';
import 'src/commands/generate.dart';
import 'src/commands/generate_localizations.dart';
import 'src/commands/ide_config.dart';
import 'src/commands/install.dart';
import 'src/commands/logs.dart';
import 'src/commands/packages.dart';
import 'src/commands/precache.dart';
import 'src/commands/run.dart';
import 'src/commands/screenshot.dart';
import 'src/commands/shell_completion.dart';
import 'src/commands/symbolize.dart';
import 'src/commands/test.dart';
import 'src/commands/update_packages.dart';
import 'src/commands/upgrade.dart';
import 'src/commands/widget_preview.dart';
import 'src/context/tool_context.dart';
import 'src/context/tool_dependencies.dart';
import 'src/devtools_launcher.dart';
import 'src/experimental/extension_discovery.dart';
import 'src/experimental/extension_manager.dart';
import 'src/experimental/templates.dart';
import 'src/features.dart';
import 'src/globals.dart' as globals;
// Files in `isolated` are intentionally excluded from google3 tooling.
import 'src/hook_runner.dart' show FlutterHookRunner;
import 'src/isolated/build_targets.dart';
import 'src/isolated/mustache_template.dart';
import 'src/isolated/native_assets/test/native_assets.dart';
import 'src/isolated/resident_web_runner.dart';
import 'src/native_assets.dart';
import 'src/pre_run_validator.dart';
import 'src/project_validator.dart';
import 'src/resident_runner.dart';
import 'src/runner/flutter_command.dart';
import 'src/runner/flutter_command_runner.dart';
import 'src/web/web_runner.dart';

/// Main entry point for commands.
///
/// This function is intended to be used from the `flutter` command line tool.
Future<void> main(List<String> args) async {
  final bool veryVerbose = args.contains('-vv');
  final bool verbose = args.contains('-v') || args.contains('--verbose') || veryVerbose;
  final bool prefixedErrors = args.contains('--prefixed-errors');
  // Support universal help idioms.
  final int powershellHelpIndex = args.indexOf('-?');
  if (powershellHelpIndex != -1) {
    args[powershellHelpIndex] = '-h';
  }
  final int slashQuestionHelpIndex = args.indexOf('/?');
  if (slashQuestionHelpIndex != -1) {
    args[slashQuestionHelpIndex] = '-h';
  }

  final String? commandName = findCommandName(args);
  final doctor = commandName == 'doctor';
  final bool help =
      args.contains('-h') ||
      args.contains('--help') ||
      commandName == 'help' ||
      (args.length == 1 && verbose);
  final bool muteCommandLogging = (help || doctor) && !veryVerbose;
  final bool verboseHelp = help && verbose;
  final daemon = commandName == 'daemon';
  final widgetPreviews = commandName == WidgetPreviewCommand.kWidgetPreview;
  final bool runMachine = args.contains('--machine');

  // Cache.flutterRoot must be set early because other features use it (e.g.
  // enginePath's initializer uses it). This can only work with the real
  // instances of the platform or filesystem, so just use those.
  Cache.flutterRoot = Cache.defaultFlutterRoot(
    platform: const LocalPlatform(),
    fileSystem: globals.localFileSystem,
    userMessages: UserMessages(),
  );

  await runner.run(
    args,
    (ToolDependencies toolDependencies) {
      final manager = ExtensionManager(
        hostPlatform: globals.os.hostPlatform,
        logger: globals.logger,
        entryPoints: <ExtensionEntryPoint>[linuxExtensionEntryPoint],
        featureFlags: featureFlags,
      );
      final templateManager = ExtensionTemplateManager(
        extensionManager: manager,
        fileSystem: toolDependencies.toolContext.fs,
        logger: toolDependencies.toolContext.logger,
        featureFlags: featureFlags,
      );
      return generateCommands(
        toolDependencies: toolDependencies,
        verboseHelp: verboseHelp,
        verbose: verbose,
        extensionManager: manager,
        extensionTemplateManager: templateManager,
      );
    },
    verbose: verbose,
    muteCommandLogging: muteCommandLogging,
    verboseHelp: verboseHelp,
    overrides: <Type, Generator>{
      FlutterHookRunner: () => FlutterHookRunnerNative(),
      // The web runner is not supported in google3 because it depends
      // on dwds.
      WebRunnerFactory: () => DwdsWebRunnerFactory(),
      // The mustache dependency is different in google3
      TemplateRenderer: () => const MustacheTemplateRenderer(),
      // The devtools launcher is not supported in google3 because it depends on
      // devtools source code.
      DevtoolsLauncher: () => DevtoolsServerLauncher(
        processManager: globals.processManager,
        artifacts: globals.artifacts!,
        logger: globals.logger,
        botDetector: globals.botDetector,
      ),
      BuildTargets: () => const BuildTargetsImpl(),
      Logger: () {
        final loggerFactory = LoggerFactory(
          outputPreferences: globals.outputPreferences,
          terminal: globals.terminal,
          stdio: globals.stdio,
        );
        return loggerFactory.createLogger(
          daemon: daemon,
          machine: runMachine,
          verbose: verbose && !muteCommandLogging,
          prefixedErrors: prefixedErrors,
          windows: globals.platform.isWindows,
          widgetPreviews: widgetPreviews,
        );
      },
      AnsiTerminal: () {
        return AnsiTerminal(
          stdio: globals.stdio,
          platform: globals.platform,
          now: DateTime.now(),
          // So that we don't animate anything before calling applyFeatureFlags, default
          // the animations to disabled in real apps.
          defaultCliAnimationEnabled: false,
          shutdownHooks: globals.shutdownHooks,
        );
        // runner.run calls "terminal.applyFeatureFlags()"
      },
      PreRunValidator: () => PreRunValidator(fileSystem: globals.fs),
      TestCompilerNativeAssetsBuilder: () => const TestCompilerNativeAssetsBuilderImpl(),
    },
    shutdownHooks: globals.shutdownHooks,
  );
}

/// The name of the command in `args`, or null if there isn't one.
///
/// Global options can come before the command, so it can't be found by
/// position. A throwaway parser walks past them instead: trailing options are
/// disabled, so parsing stops at the command and leaves it at the head of
/// [ArgResults.rest]. `help` is the exception, since the command runner
/// registers it on the parser itself and so reports it as a parsed command.
@visibleForTesting
String? findCommandName(List<String> args, {ToolContext? toolContext}) {
  final ArgResults results;
  try {
    results = FlutterCommandRunner(toolContext: toolContext ?? _FallbackToolContext()).argParser
        .parse(args);
  } on ArgParserException {
    // The real parser will complain about these later.
    return null;
  }
  return results.command?.name ?? results.rest.firstOrNull;
}

class _FallbackToolContext implements ToolContext {
  _FallbackToolContext() : _outputPreferences = null;

  final OutputPreferences? _outputPreferences;

  @override
  OutputPreferences get outputPreferences {
    if (_outputPreferences != null) {
      return _outputPreferences;
    }
    try {
      return globals.outputPreferences;
    } on Object catch (_) {
      return OutputPreferences.test();
    }
  }

  @override
  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}

List<FlutterCommand> generateCommands({
  required ToolDependencies toolDependencies,
  required bool verbose,
  required bool verboseHelp,
  ExtensionManager? extensionManager,
  ExtensionTemplateManager? extensionTemplateManager,
}) => <FlutterCommand>[
  AnalyzeCommand(
    allProjectValidators: <ProjectValidator>[
      GeneralInfoProjectValidator(),
      VariableDumpMachineProjectValidator(
        logger: toolDependencies.toolContext.logger,
        fileSystem: toolDependencies.toolContext.fs,
        platform: toolDependencies.toolContext.platform,
        git: toolDependencies.toolContext.git,
      ),
    ],
    suppressAnalytics: !toolDependencies.analytics.okToSend,
    toolContext: toolDependencies.toolContext,
    verboseHelp: verboseHelp,
  ),
  AssembleCommand(
    buildSystem: toolDependencies.buildSystem,
    featureFlags: toolDependencies.featureFlags,
    toolContext: toolDependencies.toolContext,
    verboseHelp: verboseHelp,
  ),
  AttachCommand(
    verboseHelp: verboseHelp,
    stdio: toolDependencies.toolContext.stdio,
    logger: toolDependencies.toolContext.logger,
    terminal: toolDependencies.toolContext.terminal,
    signals: toolDependencies.toolContext.signals,
    platform: toolDependencies.toolContext.platform,
    processInfo: ProcessInfo(toolDependencies.toolContext.fs),
    fileSystem: toolDependencies.toolContext.fs,
  ),
  BuildCommand(
    androidBuilder: AndroidGradleBuilder.fromContexts(
      analytics: toolDependencies.analytics,
      androidContext: toolDependencies.androidContext,
      toolContext: toolDependencies.toolContext,
    ),
    androidContext: toolDependencies.androidContext,
    appleContext: toolDependencies.appleContext,
    buildSystem: toolDependencies.buildSystem,
    featureFlags: toolDependencies.featureFlags,
    templateRenderer: const MustacheTemplateRenderer(),
    toolContext: toolDependencies.toolContext,
    verboseHelp: verboseHelp,
  ),
  ChannelCommand(verboseHelp: verboseHelp, toolContext: toolDependencies.toolContext),
  CleanCommand(
    verbose: verbose,
    toolContext: toolDependencies.toolContext,
    xcode: toolDependencies.appleContext.xcode,
    xcodeProjectInterpreter: toolDependencies.appleContext.xcodeProjectInterpreter,
  ),
  ConfigCommand(
    verboseHelp: verboseHelp,
    androidContext: toolDependencies.androidContext,
    toolContext: toolDependencies.toolContext,
    featureFlags: featureFlags,
    extensionManager: extensionManager,
  ),
  CustomDevicesCommand(featureFlags: featureFlags, toolContext: toolDependencies.toolContext),
  CreateCommand(
    androidContext: toolDependencies.androidContext,
    appleContext: toolDependencies.appleContext,
    templateRenderer: const MustacheTemplateRenderer(),
    toolContext: toolDependencies.toolContext,
    extensionTemplateManager: extensionTemplateManager,
    verboseHelp: verboseHelp,
  ),
  DaemonCommand(
    androidContext: toolDependencies.androidContext,
    androidWorkflow: android_workflow.androidWorkflow,
    deviceManager: globals.deviceManager,
    hidden: !verboseHelp,
    toolContext: toolDependencies.toolContext,
  ),
  DebugAdapterCommand(toolContext: toolDependencies.toolContext, verboseHelp: verboseHelp),
  DevicesCommand(
    deviceManager: globals.deviceManager!,
    doctor: globals.doctor!,
    toolContext: toolDependencies.toolContext,
    verboseHelp: verboseHelp,
  ),
  DoctorCommand(
    verbose: verbose,
    toolContext: toolDependencies.toolContext,
    // Provide the shared singleton from globals until dependent commands
    // (e.g. DevicesCommand, EmulatorsCommand) are migrated to DI.
    doctor: globals.doctor,
    extensionManager: extensionManager,
  ),
  DowngradeCommand(toolContext: toolDependencies.toolContext, verboseHelp: verboseHelp),
  DriveCommand(
    verboseHelp: verboseHelp,
    fileSystem: toolDependencies.toolContext.fs,
    logger: toolDependencies.toolContext.logger,
    platform: toolDependencies.toolContext.platform,
    terminal: toolDependencies.toolContext.terminal,
    outputPreferences: toolDependencies.toolContext.outputPreferences,
    signals: toolDependencies.toolContext.signals,
  ),
  EmulatorsCommand(
    doctor: toolDependencies.doctor,
    emulatorManager: toolDependencies.emulatorManager,
    toolContext: toolDependencies.toolContext,
    verboseHelp: verboseHelp,
  ),
  GenerateCommand(toolContext: toolDependencies.toolContext),
  GenerateLocalizationsCommand(toolContext: toolDependencies.toolContext),
  InstallCommand(toolContext: toolDependencies.toolContext, verboseHelp: verboseHelp),
  LogsCommand(toolContext: toolDependencies.toolContext),
  PackagesCommand(
    buildSystem: toolDependencies.buildSystem,
    toolContext: toolDependencies.toolContext,
    verboseHelp: verboseHelp,
  ),
  PrecacheCommand(
    verboseHelp: verboseHelp,
    cache: toolDependencies.toolContext.cache,
    logger: toolDependencies.toolContext.logger,
    platform: toolDependencies.toolContext.platform,
    featureFlags: featureFlags,
  ),
  RunCommand(verboseHelp: verboseHelp),
  ScreenshotCommand(toolContext: toolDependencies.toolContext),
  ShellCompletionCommand(toolContext: toolDependencies.toolContext),
  TestCommand(
    verboseHelp: verboseHelp,
    verbose: verbose,
    nativeAssetsBuilder: toolDependencies.toolContext.nativeAssetsBuilder,
  ),
  WidgetPreviewCommand(toolContext: toolDependencies.toolContext, verboseHelp: verboseHelp),
  UpgradeCommand(toolContext: toolDependencies.toolContext, verboseHelp: verboseHelp),
  SymbolizeCommand(toolContext: toolDependencies.toolContext),
  // Development-only commands. These are always hidden,
  IdeConfigCommand(toolContext: toolDependencies.toolContext),
  UpdatePackagesCommand(toolContext: toolDependencies.toolContext, verboseHelp: verboseHelp),
];

/// An abstraction for instantiation of the correct logger type.
///
/// Our logger class hierarchy and runtime requirements are overly complicated.
class LoggerFactory {
  LoggerFactory({
    required this._terminal,
    required this._stdio,
    required this._outputPreferences,
    this._stopwatchFactory = const StopwatchFactory(),
  });

  final Terminal _terminal;
  final Stdio _stdio;
  final StopwatchFactory _stopwatchFactory;
  final OutputPreferences _outputPreferences;

  /// Create the appropriate logger for the current platform and configuration.
  Logger createLogger({
    required bool verbose,
    required bool prefixedErrors,
    required bool machine,
    required bool daemon,
    required bool windows,
    required bool widgetPreviews,
  }) {
    Logger logger;
    if (windows) {
      logger = WindowsStdoutLogger(
        terminal: _terminal,
        stdio: _stdio,
        outputPreferences: _outputPreferences,
        stopwatchFactory: _stopwatchFactory,
      );
    } else {
      logger = StdoutLogger(
        terminal: _terminal,
        stdio: _stdio,
        outputPreferences: _outputPreferences,
        stopwatchFactory: _stopwatchFactory,
      );
    }
    if (verbose) {
      logger = VerboseLogger(logger, stopwatchFactory: _stopwatchFactory);
    }
    if (prefixedErrors) {
      logger = PrefixedErrorLogger(logger);
    }
    if (widgetPreviews) {
      return WidgetPreviewMachineAwareLogger(
        logger,
        machine: machine,
        verbose: verbose,
        stdio: _stdio,
      );
    }
    if (daemon) {
      return NotifyingLogger(verbose: verbose, parent: logger);
    }
    if (machine) {
      return MachineOutputLogger(parent: logger);
    }
    return logger;
  }
}