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

// ignore_for_file: implementation_imports
import 'dart:async';
import 'dart:io' as io; // ignore: dart_io_import

import 'package:build/build.dart';
import 'package:build_runner_core/build_runner_core.dart' as core;
import 'package:build_runner_core/src/asset_graph/graph.dart';
import 'package:build_runner_core/src/asset_graph/node.dart';
import 'package:build_runner_core/src/generate/build_impl.dart';
import 'package:build_runner_core/src/generate/options.dart';
import 'package:glob/glob.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:watcher/watcher.dart';

import '../artifacts.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../compile.dart';
import '../convert.dart';
import '../dart/package_map.dart';
import '../globals.dart';
import '../web/compile.dart';
import 'build_script.dart';

/// A build_runner specific implementation of the [WebCompilationProxy].
class BuildRunnerWebCompilationProxy extends WebCompilationProxy {
  BuildRunnerWebCompilationProxy();

  core.PackageGraph _packageGraph;
  BuildImpl _builder;
  PackageUriMapper _packageUriMapper;

  @override
  Future<bool> initialize({
    Directory projectDirectory,
    String testOutputDir,
    bool release = false,
  }) async {
    // Create the .dart_tool directory if it doesn't exist.
    projectDirectory.childDirectory('.dart_tool').createSync();
    final Directory generatedDirectory = projectDirectory
      .childDirectory('.dart_tool')
      .childDirectory('build')
      .childDirectory('generated');

    // Override the generated output directory so this does not conflict with
    // other build_runner output.
    core.overrideGeneratedOutputDirectory('flutter_web');
    _packageUriMapper = PackageUriMapper(
        path.absolute('lib/main.dart'), PackageMap.globalPackagesPath, null, null);
    _packageGraph = core.PackageGraph.forPath(projectDirectory.path);
    final core.BuildEnvironment buildEnvironment = core.OverrideableEnvironment(
        core.IOEnvironment(_packageGraph), onLog: (LogRecord record) {
      if (record.level == Level.SEVERE || record.level == Level.SHOUT) {
        printError(record.message);
      } else {
        printTrace(record.message);
      }
    }, reader: MultirootFileBasedAssetReader(_packageGraph, generatedDirectory));
    final LogSubscription logSubscription = LogSubscription(
      buildEnvironment,
      verbose: false,
      logLevel: Level.FINE,
    );
    final BuildOptions buildOptions = await BuildOptions.create(
      logSubscription,
      packageGraph: _packageGraph,
      skipBuildScriptCheck: true,
      trackPerformance: false,
      deleteFilesByDefault: true,
      enableLowResourcesMode: platform.environment['FLUTTER_LOW_RESOURCE_MODE']?.toLowerCase() == 'true',
    );
    final Set<core.BuildDirectory> buildDirs = <core.BuildDirectory>{
      if (testOutputDir != null)
        core.BuildDirectory(
          'test',
          outputLocation: core.OutputLocation(
            testOutputDir,
            useSymlinks: !platform.isWindows,
          ),
      ),
    };
    core.BuildResult result;
    try {
      result = await _runBuilder(
        buildEnvironment,
        buildOptions,
        release,
        buildDirs,
      );
      return result.status == core.BuildStatus.success;
    } on core.BuildConfigChangedException {
      await _cleanAssets(projectDirectory);
      result = await _runBuilder(
        buildEnvironment,
        buildOptions,
        release,
        buildDirs,
      );
      return result.status == core.BuildStatus.success;
    } on core.BuildScriptChangedException {
      await _cleanAssets(projectDirectory);
      result = await _runBuilder(
        buildEnvironment,
        buildOptions,
        release,
        buildDirs,
      );
      return result.status == core.BuildStatus.success;
    }
  }

  @override
  Future<bool> invalidate({@required List<Uri> inputs}) async {
    final Status status =
        logger.startProgress('Recompiling sources...', timeout: null);
    final Map<AssetId, ChangeType> updates = <AssetId, ChangeType>{};
    for (Uri input in inputs) {
      final AssetId assetId = AssetId.resolve(_packageUriMapper.map(input.toFilePath()).toString());
      updates[assetId] = ChangeType.MODIFY;
    }
    core.BuildResult result;
    try {
      result = await _builder.run(updates);
    } finally {
      status.cancel();
    }
    return result.status == core.BuildStatus.success;
  }

  Future<core.BuildResult> _runBuilder(core.BuildEnvironment buildEnvironment, BuildOptions buildOptions, bool release, Set<core.BuildDirectory> buildDirs) async {
    _builder = await BuildImpl.create(
      buildOptions,
      buildEnvironment,
      builders,
      <String, Map<String, dynamic>>{
        'flutter_tools:ddc': <String, dynamic>{
          'flutterWebSdk': artifacts.getArtifactPath(Artifact.flutterWebSdk),
        },
        'flutter_tools:entrypoint': <String, dynamic>{
          'release': release,
          'flutterWebSdk': artifacts.getArtifactPath(Artifact.flutterWebSdk),
        },
        'flutter_tools:test_entrypoint': <String, dynamic>{
          'release': release,
        },
      },
      isReleaseBuild: false,
    );
    return _builder.run(
      const <AssetId, ChangeType>{},
      buildDirs: buildDirs,
    );
  }

  Future<void> _cleanAssets(Directory projectDirectory) async {
    final File assetGraphFile = fs.file(core.assetGraphPath);
    AssetGraph assetGraph;
    try {
      assetGraph = AssetGraph.deserialize(await assetGraphFile.readAsBytes());
    } catch (_) {
      printTrace('Failed to clean up asset graph.');
    }
    final core.PackageGraph packageGraph = core.PackageGraph.forThisPackage();
    await _cleanUpSourceOutputs(assetGraph, packageGraph);
    final Directory cacheDirectory = fs.directory(fs.path.join(
      projectDirectory.path,
      '.dart_tool',
      'build',
      'flutter_web',
    ));
    if (assetGraphFile.existsSync()) {
      assetGraphFile.deleteSync();
    }
    if (cacheDirectory.existsSync()) {
      cacheDirectory.deleteSync(recursive: true);
    }
  }

  Future<void> _cleanUpSourceOutputs(AssetGraph assetGraph, core.PackageGraph packageGraph) async {
    final core.FileBasedAssetWriter writer = core.FileBasedAssetWriter(packageGraph);
    if (assetGraph?.outputs == null) {
      return;
    }
    for (AssetId id in assetGraph.outputs) {
      if (id.package != packageGraph.root.name) {
        continue;
      }
      final GeneratedAssetNode node = assetGraph.get(id);
      if (node.wasOutput) {
        // Note that this does a file.exists check in the root package and
        // only tries to delete the file if it exists. This way we only
        // actually delete to_source outputs, without reading in the build
        // actions.
        await writer.delete(id);
      }
    }
  }
}

/// Handles mapping a single root file scheme to a multiroot scheme.
///
/// This allows one build_runner build to read the output from a previous
/// isolated build.
class MultirootFileBasedAssetReader extends core.FileBasedAssetReader {
  MultirootFileBasedAssetReader(
    core.PackageGraph packageGraph,
    this.generatedDirectory,
  ) : super(packageGraph);

  final Directory generatedDirectory;

  @override
  Future<bool> canRead(AssetId id) {
    if (packageGraph[id.package] == packageGraph.root && _missingSource(id)) {
      return _generatedFile(id).exists();
    }
    return super.canRead(id);
  }

  @override
  Future<List<int>> readAsBytes(AssetId id) {
    if (packageGraph[id.package] == packageGraph.root && _missingSource(id)) {
      return _generatedFile(id).readAsBytes();
    }
    return super.readAsBytes(id);
  }

  @override
  Future<String> readAsString(AssetId id, {Encoding encoding}) {
    if (packageGraph[id.package] == packageGraph.root && _missingSource(id)) {
      return _generatedFile(id).readAsString();
    }
    return super.readAsString(id, encoding: encoding);
  }

  @override
  Stream<AssetId> findAssets(Glob glob, {String package}) async* {
    if (package == null || packageGraph.root.name == package) {
      await for (io.FileSystemEntity entity in glob.list(followLinks: true, root: packageGraph.root.path)) {
        if (entity is io.File && _isNotHidden(entity)) {
          yield _fileToAssetId(entity, packageGraph.root);
        }
      }
      final String generatedRoot = fs.path.join(
        generatedDirectory.path, packageGraph.root.name
      );
      if (!fs.isDirectorySync(generatedRoot)) {
        return;
      }
      await for (io.FileSystemEntity entity in glob.list(followLinks: true, root: generatedRoot)) {
        if (entity is io.File && _isNotHidden(entity)) {
          yield _fileToAssetId(entity, packageGraph.root, generatedRoot);
        }
      }
      return;
    }
    yield* super.findAssets(glob, package: package);
  }

  bool _isNotHidden(io.FileSystemEntity entity) {
    return !path.basename(entity.path).startsWith('._');
  }

  bool _missingSource(AssetId id) {
    return !fs.file(path.joinAll(<String>[packageGraph.root.path, ...id.pathSegments])).existsSync();
  }

  File _generatedFile(AssetId id) {
    return fs.file(
      path.joinAll(<String>[generatedDirectory.path, packageGraph.root.name, ...id.pathSegments])
    );
  }

  /// Creates an [AssetId] for [file], which is a part of [packageNode].
  AssetId _fileToAssetId(io.File file, core.PackageNode packageNode, [String root]) {
    final String filePath = path.normalize(file.absolute.path);
    final String relativePath = path.relative(filePath, from: root ?? packageNode.path);
    return AssetId(packageNode.name, relativePath);
  }
}