// Copyright 2013 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 'dart:async';

import 'package:args/command_runner.dart';
import 'package:path/path.dart' as path;
import 'package:watcher/src/watch_event.dart';

import 'environment.dart';
import 'exceptions.dart';
import 'pipeline.dart';
import 'utils.dart';

const Map<String, String> targetAliases = <String, String>{
  'sdk': 'flutter/web_sdk',
  'web_sdk': 'flutter/web_sdk',
  'canvaskit': 'flutter/third_party/canvaskit:canvaskit_group',
  'canvaskit_chromium': 'flutter/third_party/canvaskit:canvaskit_chromium_group',
  'skwasm': 'flutter/third_party/canvaskit:skwasm_group',
  'archive': 'flutter/web_sdk:flutter_web_sdk_archive',
};

class BuildCommand extends Command<bool> with ArgUtils<bool> {
  BuildCommand() {
    argParser.addFlag(
      'watch',
      abbr: 'w',
      help: 'Run the build in watch mode so it rebuilds whenever a change is '
          'made. Disabled by default.',
    );
    argParser.addFlag(
      'host',
      help: 'Build the host build instead of the wasm build, which is '
          'currently needed for `flutter run --local-engine` to work.'
    );
    argParser.addFlag(
      'profile',
      help: 'Build in profile mode instead of release mode. In this mode, the '
          'output will be located at "out/wasm_profile".\nThis only applies to '
          'the wasm build. The host build is always built in release mode.',
    );
    argParser.addFlag(
      'debug',
      help: 'Build in debug mode instead of release mode. In this mode, the '
          'output will be located at "out/wasm_debug".\nThis only applies to '
          'the wasm build. The host build is always built in release mode.',
    );
    argParser.addFlag(
      'dwarf',
      help: 'Embed DWARF debugging info into the output wasm modules. This is '
          'only valid in debug mode.',
    );
  }

  @override
  String get name => 'build';

  @override
  String get description => 'Build the Flutter web engine.';

  bool get isWatchMode => boolArg('watch');

  bool get host => boolArg('host');

  List<String> get targets => argResults?.rest ?? <String>[];
  bool get embedDwarf => boolArg('dwarf');

  @override
  FutureOr<bool> run() async {
    if (embedDwarf && runtimeMode != RuntimeMode.debug) {
      throw ToolExit('Embedding DWARF data requires debug runtime mode.');
    }
    final FilePath libPath = FilePath.fromWebUi('lib');
    final List<PipelineStep> steps = <PipelineStep>[
      GnPipelineStep(
        host: host,
        runtimeMode: runtimeMode,
        embedDwarf: embedDwarf,
      ),
      NinjaPipelineStep(
        host: host,
        runtimeMode: runtimeMode,
        targets: targets.map((String target) => targetAliases[target] ?? target),
      ),
    ];
    final Pipeline buildPipeline = Pipeline(steps: steps);
    await buildPipeline.run();

    if (isWatchMode) {
      print('Initial build done!');
      print('Watching directory: ${libPath.relativeToCwd}/');
      await PipelineWatcher(
        dir: libPath.absolute,
        pipeline: buildPipeline,
        // Ignore font files that are copied whenever tests run.
        ignore: (WatchEvent event) => event.path.endsWith('.ttf'),
      ).start();
    }
    return true;
  }
}

/// Runs `gn`.
///
/// Not safe to interrupt as it may leave the `out/` directory in a corrupted
/// state. GN is pretty quick though, so it's OK to not support interruption.
class GnPipelineStep extends ProcessStep {
  GnPipelineStep({
    required this.host,
    required this.runtimeMode,
    required this.embedDwarf,
  });

  final bool host;
  final RuntimeMode runtimeMode;
  final bool embedDwarf;

  @override
  String get description => 'gn';

  @override
  bool get isSafeToInterrupt => false;

  List<String> get _gnArgs {
    if (host) {
      return <String>[
        '--unoptimized',
        '--full-dart-sdk',
      ];
    } else {
      return <String>[
        '--web',
        '--runtime-mode=${runtimeMode.name}',
        if (runtimeMode == RuntimeMode.debug)
          '--unoptimized',
        if (embedDwarf)
          '--wasm-use-dwarf',
      ];
    }
  }

  @override
  Future<ProcessManager> createProcess() {
    print('Running gn...');
    return startProcess(
      path.join(environment.flutterDirectory.path, 'tools', 'gn'),
      _gnArgs,
    );
  }
}

/// Runs `autoninja`.
///
/// Can be safely interrupted.
class NinjaPipelineStep extends ProcessStep {
  NinjaPipelineStep({
    required this.host,
    required this.runtimeMode,
    required this.targets,
  });

  @override
  String get description => 'ninja';

  @override
  bool get isSafeToInterrupt => true;

  final bool host;
  final Iterable<String> targets;
  final RuntimeMode runtimeMode;

  String get buildDirectory {
    if (host) {
      return environment.hostDebugUnoptDir.path;
    }
    return getBuildDirectoryForRuntimeMode(runtimeMode).path;
  }

  @override
  Future<ProcessManager> createProcess() {
    print('Running autoninja...');
    return startProcess(
      'autoninja',
      <String>[
        '-C',
        buildDirectory,
        ...targets,
      ],
    );
  }
}
