// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
// for details. 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 'dart:collection';
import 'dart:convert';

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:async/async.dart';
import 'package:crypto/crypto.dart';
import 'package:glob/glob.dart';
import 'package:package_config/package_config_types.dart';

import '../analyzer/resolver.dart';
import '../asset/exceptions.dart';
import '../asset/id.dart';
import '../asset/reader.dart';
import '../asset/writer.dart';
import '../resource/resource.dart';
import 'build_step.dart';
import 'exceptions.dart';

/// A single step in the build processes.
///
/// This represents a single input and its expected and real outputs. It also
/// handles tracking of dependencies.
class BuildStepImpl implements BuildStep {
  final Resolvers? _resolvers;
  final StageTracker _stageTracker;

  /// The primary input id for this build step.
  @override
  final AssetId inputId;

  @override
  Future<LibraryElement> get inputLibrary async {
    if (_isComplete) throw BuildStepCompletedException();
    return resolver.libraryFor(inputId);
  }

  /// The list of all outputs which are expected/allowed to be output from this
  /// step.
  @override
  final Set<AssetId> allowedOutputs;

  /// The result of any writes which are starting during this step.
  final _writeResults = <Future<Result<void>>>[];

  final _writtenAssets = <AssetId>{};

  /// Used internally for reading files.
  final AssetReader _reader;

  /// Used internally for writing files.
  final AssetWriter _writer;

  final ResourceManager _resourceManager;

  bool _isComplete = false;

  final void Function(Iterable<AssetId>)? _reportUnusedAssets;

  final Future<PackageConfig> Function() _resolvePackageConfig;
  Future<Result<PackageConfig>>? _resolvedPackageConfig;

  BuildStepImpl(
      this.inputId,
      Iterable<AssetId> expectedOutputs,
      this._reader,
      this._writer,
      this._resolvers,
      this._resourceManager,
      this._resolvePackageConfig,
      {StageTracker? stageTracker,
      void Function(Iterable<AssetId>)? reportUnusedAssets})
      : allowedOutputs = UnmodifiableSetView(expectedOutputs.toSet()),
        _stageTracker = stageTracker ?? NoOpStageTracker.instance,
        _reportUnusedAssets = reportUnusedAssets;

  @override
  Future<PackageConfig> get packageConfig async {
    final resolved =
        _resolvedPackageConfig ??= Result.capture(_resolvePackageConfig());

    return (await resolved).asFuture;
  }

  @override
  Resolver get resolver {
    if (_isComplete) throw BuildStepCompletedException();
    final resolvers = _resolvers;
    if (resolvers == null) {
      throw UnsupportedError('Resolvers are not available in this build.');
    }

    return _DelayedResolver(_resolver ??= resolvers.get(this));
  }

  Future<ReleasableResolver>? _resolver;

  @override
  Future<bool> canRead(AssetId id) {
    if (_isComplete) throw BuildStepCompletedException();
    return _reader.canRead(id);
  }

  @override
  Future<T> fetchResource<T>(Resource<T> resource) {
    if (_isComplete) throw BuildStepCompletedException();
    return _resourceManager.fetch(resource);
  }

  @override
  Future<List<int>> readAsBytes(AssetId id) {
    if (_isComplete) throw BuildStepCompletedException();
    return _reader.readAsBytes(id);
  }

  @override
  Future<String> readAsString(AssetId id, {Encoding encoding = utf8}) {
    if (_isComplete) throw BuildStepCompletedException();
    return _reader.readAsString(id, encoding: encoding);
  }

  @override
  Stream<AssetId> findAssets(Glob glob) {
    if (_isComplete) throw BuildStepCompletedException();
    if (_reader is MultiPackageAssetReader) {
      return _reader.findAssets(glob, package: inputId.package);
    } else {
      return _reader.findAssets(glob);
    }
  }

  @override
  Future<void> writeAsBytes(AssetId id, FutureOr<List<int>> bytes) {
    if (_isComplete) throw BuildStepCompletedException();
    _checkOutput(id);
    var done =
        _futureOrWrite(bytes, (List<int> b) => _writer.writeAsBytes(id, b));
    _writeResults.add(Result.capture(done));
    return done;
  }

  @override
  Future<void> writeAsString(AssetId id, FutureOr<String> content,
      {Encoding encoding = utf8}) {
    if (_isComplete) throw BuildStepCompletedException();
    _checkOutput(id);
    var done = _futureOrWrite(content,
        (String c) => _writer.writeAsString(id, c, encoding: encoding));
    _writeResults.add(Result.capture(done));
    return done;
  }

  @override
  Future<Digest> digest(AssetId id) {
    if (_isComplete) throw BuildStepCompletedException();
    return _reader.digest(id);
  }

  @override
  T trackStage<T>(String label, T Function() action,
          {bool isExternal = false}) =>
      _stageTracker.trackStage(label, action, isExternal: isExternal);

  Future<void> _futureOrWrite<T>(
          FutureOr<T> content, Future<void> Function(T content) write) =>
      (content is Future<T>) ? content.then(write) : write(content);

  /// Waits for work to finish and cleans up resources.
  ///
  /// This method should be called after a build has completed. After the
  /// returned [Future] completes then all outputs have been written and the
  /// [Resolver] for this build step - if any - has been released.
  Future<void> complete() async {
    _isComplete = true;
    await Future.wait(_writeResults.map(Result.release));
    try {
      (await _resolver)?.release();
    } catch (_) {}
  }

  /// Checks that [id] is an expected output, and throws an
  /// [InvalidOutputException] or [UnexpectedOutputException] if it's not.
  void _checkOutput(AssetId id) {
    if (!allowedOutputs.contains(id)) {
      throw UnexpectedOutputException(id, expected: allowedOutputs);
    }

    if (!_writtenAssets.add(id)) {
      throw InvalidOutputException(
        id,
        'This build step already wrote to `$id`. Duplicate writes to the same '
        'asset are forbidden.',
      );
    }
  }

  @override
  void reportUnusedAssets(Iterable<AssetId> assets) {
    _reportUnusedAssets?.call(assets);
  }
}

class _DelayedResolver implements Resolver {
  final Future<Resolver> _delegate;

  _DelayedResolver(this._delegate);

  @override
  Future<bool> isLibrary(AssetId assetId) async =>
      (await _delegate).isLibrary(assetId);

  @override
  Stream<LibraryElement> get libraries {
    var completer = StreamCompleter<LibraryElement>();
    _delegate.then((r) => completer.setSourceStream(r.libraries));
    return completer.stream;
  }

  @override
  Future<AstNode?> astNodeFor(Element element, {bool resolve = false}) async =>
      (await _delegate).astNodeFor(element, resolve: resolve);

  @override
  Future<CompilationUnit> compilationUnitFor(AssetId assetId,
          {bool allowSyntaxErrors = false}) async =>
      (await _delegate)
          .compilationUnitFor(assetId, allowSyntaxErrors: allowSyntaxErrors);

  @override
  Future<LibraryElement> libraryFor(AssetId assetId,
          {bool allowSyntaxErrors = false}) async =>
      (await _delegate)
          .libraryFor(assetId, allowSyntaxErrors: allowSyntaxErrors);

  @override
  Future<LibraryElement?> findLibraryByName(String libraryName) async =>
      (await _delegate).findLibraryByName(libraryName);

  @override
  Future<AssetId> assetIdForElement(Element element) async =>
      (await _delegate).assetIdForElement(element);
}
