// 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:process/process.dart';
import 'package:unified_analytics/unified_analytics.dart';

import '../artifacts.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/project_migrator.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../build_system/build_system.dart';
import '../cache.dart';
import '../flutter_plugins.dart';
import '../globals.dart' as globals;
import '../platform_plugins.dart';
import '../plugins.dart';
import '../project.dart';
import '../reporting/reporting.dart';
import '../version.dart';
import 'compiler_config.dart';
import 'file_generators/flutter_service_worker_js.dart';
import 'migrations/scrub_generated_plugin_registrant.dart';
import 'web_constants.dart';

export 'compiler_config.dart';

/// Whether the application has web plugins.
const String kHasWebPlugins = 'HasWebPlugins';

/// Base href to set in index.html in flutter build command
const String kBaseHref = 'baseHref';

/// The caching strategy to use for service worker generation.
const String kServiceWorkerStrategy = 'ServiceWorkerStrategy';

class WebBuilder {
  WebBuilder({
    required Logger logger,
    required ProcessManager processManager,
    required BuildSystem buildSystem,
    required Usage usage,
    required Analytics analytics,
    required FlutterVersion flutterVersion,
    required FileSystem fileSystem,
  })  : _logger = logger,
        _processManager = processManager,
        _buildSystem = buildSystem,
        _flutterUsage = usage,
        _analytics = analytics,
        _flutterVersion = flutterVersion,
        _fileSystem = fileSystem;

  final Logger _logger;
  final ProcessManager _processManager;
  final BuildSystem _buildSystem;
  final Usage _flutterUsage;
  final Analytics _analytics;
  final FlutterVersion _flutterVersion;
  final FileSystem _fileSystem;

  Future<void> buildWeb(
    FlutterProject flutterProject,
    String target,
    BuildInfo buildInfo,
    ServiceWorkerStrategy serviceWorkerStrategy, {
    required WebCompilerConfig compilerConfig,
    String? baseHref,
    String? outputDirectoryPath,
  }) async {
    if (compilerConfig.isWasm) {
      globals.logger.printBox(
        title: 'Experimental feature',
        '''
  WebAssembly compilation is experimental.
  $kWasmMoreInfo''',
      );
    }

    final bool hasWebPlugins =
        (await findPlugins(flutterProject)).any((Plugin p) => p.platforms.containsKey(WebPlugin.kConfigKey));
    final Directory outputDirectory = outputDirectoryPath == null
        ? _fileSystem.directory(getWebBuildDirectory(compilerConfig.isWasm))
        : _fileSystem.directory(outputDirectoryPath);
    outputDirectory.createSync(recursive: true);

    // The migrators to apply to a Web project.
    final List<ProjectMigrator> migrators = <ProjectMigrator>[
      ScrubGeneratedPluginRegistrant(flutterProject.web, _logger),
    ];

    final ProjectMigration migration = ProjectMigration(migrators);
    migration.run();

    final Status status = _logger.startProgress('Compiling $target for the Web...');
    final Stopwatch sw = Stopwatch()..start();
    try {
      final BuildResult result = await _buildSystem.build(
          globals.buildTargets.webServiceWorker(
            _fileSystem,
            webRenderer: buildInfo.webRenderer,
            isWasm: compilerConfig.isWasm,
          ),
          Environment(
            projectDir: _fileSystem.currentDirectory,
            outputDir: outputDirectory,
            buildDir: flutterProject.directory.childDirectory('.dart_tool').childDirectory('flutter_build'),
            defines: <String, String>{
              kTargetFile: target,
              kHasWebPlugins: hasWebPlugins.toString(),
              if (baseHref != null) kBaseHref: baseHref,
              kServiceWorkerStrategy: serviceWorkerStrategy.cliName,
              ...compilerConfig.toBuildSystemEnvironment(),
              ...buildInfo.toBuildSystemEnvironment(),
            },
            artifacts: globals.artifacts!,
            fileSystem: _fileSystem,
            logger: _logger,
            processManager: _processManager,
            platform: globals.platform,
            usage: _flutterUsage,
            analytics: _analytics,
            cacheDir: globals.cache.getRoot(),
            engineVersion: globals.artifacts!.isLocalEngine ? null : _flutterVersion.engineRevision,
            flutterRootDir: _fileSystem.directory(Cache.flutterRoot),
            // Web uses a different Dart plugin registry.
            // https://github.com/flutter/flutter/issues/80406
            generateDartPluginRegistry: false,
          ));
      if (!result.success) {
        for (final ExceptionMeasurement measurement in result.exceptions.values) {
          _logger.printError(
            'Target ${measurement.target} failed: ${measurement.exception}',
            stackTrace: measurement.fatal ? measurement.stackTrace : null,
          );
        }
        throwToolExit('Failed to compile application for the Web.');
      }
    } on Exception catch (err) {
      throwToolExit(err.toString());
    } finally {
      status.stop();
    }

    final String buildSettingsString = _buildEventAnalyticsSettings(
      config: compilerConfig,
      buildInfo: buildInfo,
    );

    BuildEvent(
      'web-compile',
      type: 'web',
      settings: buildSettingsString,
      flutterUsage: _flutterUsage,
    ).send();
    _analytics.send(Event.flutterBuildInfo(
      label: 'web-compile',
      buildType: 'web',
      settings: buildSettingsString,
    ));

    final Duration elapsedDuration = sw.elapsed;
    _flutterUsage.sendTiming(
      'build',
      compilerConfig.isWasm ? 'dart2wasm' : 'dart2js',
      elapsedDuration,
    );
    _analytics.send(Event.timing(
      workflow: 'build',
      variableName: compilerConfig.isWasm ? 'dart2wasm' : 'dart2js',
      elapsedMilliseconds: elapsedDuration.inMilliseconds,
    ));
  }
}

/// Web rendering backend mode.
enum WebRendererMode implements CliEnum {
  /// Auto detects which rendering backend to use.
  auto,

  /// Always uses canvaskit.
  canvaskit,

  /// Always uses html.
  html,

  /// Always use skwasm.
  skwasm;

  @override
  String get cliName => snakeCase(name, '-');

  @override
  String get helpText => switch (this) {
        auto =>
          'Use the HTML renderer on mobile devices, and CanvasKit on desktop devices.',
        canvaskit =>
          'Always use the CanvasKit renderer. This renderer uses WebGL and WebAssembly to render graphics.',
        html =>
          'Always use the HTML renderer. This renderer uses a combination of HTML, CSS, SVG, 2D Canvas, and WebGL.',
        skwasm => 'Always use the experimental skwasm renderer.'
      };

  Iterable<String> get dartDefines => switch (this) {
        WebRendererMode.auto => <String>[
            'FLUTTER_WEB_AUTO_DETECT=true',
          ],
        WebRendererMode.canvaskit => <String>[
            'FLUTTER_WEB_AUTO_DETECT=false',
            'FLUTTER_WEB_USE_SKIA=true',
          ],
        WebRendererMode.html => <String>[
            'FLUTTER_WEB_AUTO_DETECT=false',
            'FLUTTER_WEB_USE_SKIA=false',
          ],
        WebRendererMode.skwasm => <String>[
            'FLUTTER_WEB_AUTO_DETECT=false',
            'FLUTTER_WEB_USE_SKIA=false',
            'FLUTTER_WEB_USE_SKWASM=true',
          ]
      };
}

/// The correct precompiled artifact to use for each build and render mode.
const Map<WebRendererMode, Map<NullSafetyMode, HostArtifact>> kDartSdkJsArtifactMap = <WebRendererMode, Map<NullSafetyMode, HostArtifact>>{
  WebRendererMode.auto: <NullSafetyMode, HostArtifact> {
    NullSafetyMode.sound: HostArtifact.webPrecompiledCanvaskitAndHtmlSoundSdk,
    NullSafetyMode.unsound: HostArtifact.webPrecompiledCanvaskitAndHtmlSdk,
  },
  WebRendererMode.canvaskit: <NullSafetyMode, HostArtifact> {
    NullSafetyMode.sound: HostArtifact.webPrecompiledCanvaskitSoundSdk,
    NullSafetyMode.unsound: HostArtifact.webPrecompiledCanvaskitSdk,
  },
  WebRendererMode.html: <NullSafetyMode, HostArtifact> {
    NullSafetyMode.sound: HostArtifact.webPrecompiledSoundSdk,
    NullSafetyMode.unsound: HostArtifact.webPrecompiledSdk,
  },
};

/// The correct source map artifact to use for each build and render mode.
const Map<WebRendererMode, Map<NullSafetyMode, HostArtifact>> kDartSdkJsMapArtifactMap = <WebRendererMode, Map<NullSafetyMode, HostArtifact>>{
  WebRendererMode.auto: <NullSafetyMode, HostArtifact> {
    NullSafetyMode.sound: HostArtifact.webPrecompiledCanvaskitAndHtmlSoundSdkSourcemaps,
    NullSafetyMode.unsound: HostArtifact.webPrecompiledCanvaskitAndHtmlSdkSourcemaps,
  },
  WebRendererMode.canvaskit: <NullSafetyMode, HostArtifact> {
    NullSafetyMode.sound: HostArtifact.webPrecompiledCanvaskitSoundSdkSourcemaps,
    NullSafetyMode.unsound: HostArtifact.webPrecompiledCanvaskitSdkSourcemaps,
  },
  WebRendererMode.html: <NullSafetyMode, HostArtifact> {
    NullSafetyMode.sound: HostArtifact.webPrecompiledSoundSdkSourcemaps,
    NullSafetyMode.unsound: HostArtifact.webPrecompiledSdkSourcemaps,
  },
};

String _buildEventAnalyticsSettings({
  required WebCompilerConfig config,
  required BuildInfo buildInfo,
}) {
  final Map<String, Object> values = <String, Object>{
    ...config.buildEventAnalyticsValues,
    'web-renderer': buildInfo.webRenderer.cliName,
  };

  final List<String> sortedList = values.entries
      .map((MapEntry<String, Object> e) => '${e.key}: ${e.value};')
      .toList()
    ..sort();

  return sortedList.join(' ');
}
