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

import '../artifacts.dart';
import '../base/common.dart';
import '../base/config.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../build_info.dart';
import '../bundle.dart';
import '../cache.dart';
import '../compile.dart';
import '../dart/language_version.dart';
import '../web/bootstrap.dart';
import '../web/compile.dart';
import '../web/memory_fs.dart';
import 'test_config.dart';

/// A web compiler for the test runner.
class WebTestCompiler {
  WebTestCompiler({
    required FileSystem fileSystem,
    required Logger logger,
    required Artifacts artifacts,
    required Platform platform,
    required ProcessManager processManager,
    required Config config,
  }) : _logger = logger,
       _fileSystem = fileSystem,
       _artifacts = artifacts,
       _platform = platform,
       _processManager = processManager,
       _config = config;

  final Logger _logger;
  final FileSystem _fileSystem;
  final Artifacts _artifacts;
  final Platform _platform;
  final ProcessManager _processManager;
  final Config _config;

  Future<File> _generateTestEntrypoint({
    required List<String> testFiles,
    required Directory projectDirectory,
    required Directory outputDirectory,
    required LanguageVersion languageVersion,
  }) async {
    final List<WebTestInfo> testInfos = testFiles.map((String testFilePath) {
      final List<String> relativeTestSegments = _fileSystem.path.split(
        _fileSystem.path.relative(
          testFilePath,
          from: projectDirectory.childDirectory('test').path
        )
      );

      final File? testConfigFile = findTestConfigFile(_fileSystem.file(testFilePath), _logger);
      String? testConfigPath;
      if (testConfigFile != null) {
        testConfigPath = _fileSystem.path.split(
          _fileSystem.path.relative(
            testConfigFile.path,
            from: projectDirectory.childDirectory('test').path
          )
        ).join('/');
      }
      return (
        entryPoint: relativeTestSegments.join('/'),
        configFile: testConfigPath,
        goldensUri: Uri.file(testFilePath),
      );
    }).toList();
    return _fileSystem.file(_fileSystem.path.join(outputDirectory.path, 'main.dart'))
      ..createSync(recursive: true)
      ..writeAsStringSync(generateTestEntrypoint(
        testInfos: testInfos,
        languageVersion: languageVersion
      )
    );
  }

  Future<WebMemoryFS> initialize({
    required Directory projectDirectory,
    required String testOutputDir,
    required List<String> testFiles,
    required BuildInfo buildInfo,
    required WebRendererMode webRenderer,
    required bool useWasm,
  }) async {
    return useWasm ? _compileWasm(
      projectDirectory: projectDirectory,
      testOutputDir: testOutputDir,
      testFiles: testFiles,
      buildInfo: buildInfo,
      webRenderer: webRenderer,
    ) : _compileJS(
      projectDirectory: projectDirectory,
      testOutputDir: testOutputDir,
      testFiles: testFiles,
      buildInfo: buildInfo,
      webRenderer: webRenderer,
    );
  }

  Future<WebMemoryFS> _compileJS({
    required Directory projectDirectory,
    required String testOutputDir,
    required List<String> testFiles,
    required BuildInfo buildInfo,
    required WebRendererMode webRenderer,
  }) async {
    LanguageVersion languageVersion = LanguageVersion(2, 8);
    late final String platformDillName;

    // TODO(zanderso): to support autodetect this would need to partition the source code into
    // a sound and unsound set and perform separate compilations
    final List<String> extraFrontEndOptions = List<String>.of(buildInfo.extraFrontEndOptions);
    switch (buildInfo.nullSafetyMode) {
      case NullSafetyMode.unsound || NullSafetyMode.autodetect:
        platformDillName = 'ddc_outline.dill';
        if (!extraFrontEndOptions.contains('--no-sound-null-safety')) {
          extraFrontEndOptions.add('--no-sound-null-safety');
        }
      case NullSafetyMode.sound:
        languageVersion = currentLanguageVersion(_fileSystem, Cache.flutterRoot!);
        platformDillName = 'ddc_outline_sound.dill';
        if (!extraFrontEndOptions.contains('--sound-null-safety')) {
          extraFrontEndOptions.add('--sound-null-safety');
        }
    }

    final String platformDillPath = _fileSystem.path.join(
      _artifacts.getHostArtifact(HostArtifact.webPlatformKernelFolder).path,
      platformDillName
    );

    final Directory outputDirectory = _fileSystem.directory(testOutputDir)
      ..createSync(recursive: true);
    final File testFile = await _generateTestEntrypoint(
      testFiles: testFiles,
      projectDirectory: projectDirectory,
      outputDirectory: outputDirectory,
      languageVersion: languageVersion
    );

    final String cachedKernelPath = getDefaultCachedKernelPath(
      trackWidgetCreation: buildInfo.trackWidgetCreation,
      dartDefines: buildInfo.dartDefines,
      extraFrontEndOptions: extraFrontEndOptions,
      fileSystem: _fileSystem,
      config: _config,
    );
    final List<String> dartDefines = webRenderer.updateDartDefines(buildInfo.dartDefines);
    final ResidentCompiler residentCompiler = ResidentCompiler(
      _artifacts.getHostArtifact(HostArtifact.flutterWebSdk).path,
      buildMode: buildInfo.mode,
      trackWidgetCreation: buildInfo.trackWidgetCreation,
      fileSystemRoots: <String>[
        projectDirectory.childDirectory('test').path,
        testOutputDir,
      ],
      // Override the filesystem scheme so that the frontend_server can find
      // the generated entrypoint code.
      fileSystemScheme: 'org-dartlang-app',
      initializeFromDill: cachedKernelPath,
      targetModel: TargetModel.dartdevc,
      extraFrontEndOptions: extraFrontEndOptions,
      platformDill: _fileSystem.file(platformDillPath).absolute.uri.toString(),
      dartDefines: dartDefines,
      librariesSpec: _artifacts.getHostArtifact(HostArtifact.flutterWebLibrariesJson).uri.toString(),
      packagesPath: buildInfo.packagesPath,
      artifacts: _artifacts,
      processManager: _processManager,
      logger: _logger,
      platform: _platform,
      fileSystem: _fileSystem,
    );

    final CompilerOutput? output = await residentCompiler.recompile(
      Uri.parse('org-dartlang-app:///${testFile.basename}'),
      <Uri>[],
      outputPath: outputDirectory.childFile('out').path,
      packageConfig: buildInfo.packageConfig,
      fs: _fileSystem,
      projectRootPath: projectDirectory.absolute.path,
    );
    if (output == null || output.errorCount > 0) {
      throwToolExit('Failed to compile');
    }
    // Cache the output kernel file to speed up subsequent compiles.
    _fileSystem.file(cachedKernelPath).parent.createSync(recursive: true);
    _fileSystem.file(output.outputFilename).copySync(cachedKernelPath);

    final File codeFile = outputDirectory.childFile('${output.outputFilename}.sources');
    final File manifestFile = outputDirectory.childFile('${output.outputFilename}.json');
    final File sourcemapFile = outputDirectory.childFile('${output.outputFilename}.map');
    final File metadataFile = outputDirectory.childFile('${output.outputFilename}.metadata');

    return WebMemoryFS()
      ..write(codeFile, manifestFile, sourcemapFile, metadataFile);
  }

  Future<WebMemoryFS> _compileWasm({
    required Directory projectDirectory,
    required String testOutputDir,
    required List<String> testFiles,
    required BuildInfo buildInfo,
    required WebRendererMode webRenderer,
  }) async {
    final Directory outputDirectory = _fileSystem.directory(testOutputDir)
      ..createSync(recursive: true);
    final File testFile = await _generateTestEntrypoint(
      testFiles: testFiles,
      projectDirectory: projectDirectory,
      outputDirectory: outputDirectory,
      languageVersion: currentLanguageVersion(_fileSystem, Cache.flutterRoot!),
    );

    final String dartSdkPath = _artifacts.getArtifactPath(Artifact.engineDartSdkPath, platform: TargetPlatform.web_javascript);
    final String platformBinariesPath = _artifacts.getHostArtifact(HostArtifact.webPlatformKernelFolder).path;
    final String platformFilePath = _fileSystem.path.join(platformBinariesPath, 'dart2wasm_platform.dill');
    final List<String> dartDefines = webRenderer.updateDartDefines(buildInfo.dartDefines);
    final File outputWasmFile = outputDirectory.childFile('main.dart.wasm');

    final List<String> compilationArgs = <String>[
      _artifacts.getArtifactPath(Artifact.engineDartBinary, platform: TargetPlatform.web_javascript),
      'compile',
      'wasm',
      '--packages=.dart_tool/package_config.json',
      '--extra-compiler-option=--dart-sdk=$dartSdkPath',
      '--extra-compiler-option=--platform=$platformFilePath',
      '--extra-compiler-option=--multi-root-scheme=org-dartlang-app',
      '--extra-compiler-option=--multi-root=${projectDirectory.childDirectory('test').path}',
      '--extra-compiler-option=--multi-root=${outputDirectory.path}',
      if (webRenderer == WebRendererMode.skwasm) ...<String>[
        '--extra-compiler-option=--import-shared-memory',
        '--extra-compiler-option=--shared-memory-max-pages=32768',
        ],
      ...buildInfo.extraFrontEndOptions,
      for (final String dartDefine in dartDefines)
        '-D$dartDefine',

      '-O1',
      '-o',
      outputWasmFile.path,
      testFile.path, // dartfile
    ];

    final ProcessUtils processUtils = ProcessUtils(
      logger: _logger,
      processManager: _processManager,
    );

    await processUtils.run(
      throwOnError: true,
      compilationArgs,
    );

    return WebMemoryFS();
  }
}
