// 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:io';

import 'package:async/async.dart';
import 'package:build_runner_core/build_runner_core.dart';
import 'package:logging/logging.dart';

import 'asset_change.dart';
import 'node_watcher.dart';

PackageNodeWatcher _default(PackageNode node) => PackageNodeWatcher(node);

/// Allows watching an entire graph of packages to schedule rebuilds.
class PackageGraphWatcher {
  // TODO: Consider pulling logging out and providing hooks instead.
  final Logger _logger;
  final PackageNodeWatcher Function(PackageNode) _strategy;
  final PackageGraph _graph;

  final _readyCompleter = Completer<void>();
  Future<void> get ready => _readyCompleter.future;

  bool _isWatching = false;

  /// Creates a new watcher for a [PackageGraph].
  ///
  /// May optionally specify a [watch] strategy, otherwise will attempt a
  /// reasonable default based on the current platform.
  PackageGraphWatcher(
    this._graph, {
    Logger? logger,
    PackageNodeWatcher Function(PackageNode node)? watch,
  })  : _logger = logger ?? Logger('build_runner'),
        _strategy = watch ?? _default;

  /// Returns a stream of records for assets that changed in the package graph.
  Stream<AssetChange> watch() {
    assert(!_isWatching);
    _isWatching = true;
    return LazyStream(
        () => logTimedSync(_logger, 'Setting up file watchers', _watch));
  }

  Stream<AssetChange> _watch() {
    final allWatchers = _graph.allPackages.values
        .where((node) => node.dependencyType == DependencyType.path)
        .map(_strategy)
        .toList();
    final filteredEvents = allWatchers
        .map((w) => w
                .watch()
                .where(_nestedPathFilter(w.node))
                .handleError((Object e, StackTrace s) {
              _logger.severe(
                  'Error from directory watcher for package:${w.node.name}\n\n'
                  'If you see this consistently then it is recommended that '
                  'you enable the polling file watcher with '
                  '--use-polling-watcher.\n\n',
                  e,
                  s);
            }))
        .toList();
    // Asynchronously complete the `_readyCompleter` once all the watchers
    // are done.
    () async {
      await Future.wait(
          allWatchers.map((nodeWatcher) => nodeWatcher.watcher.ready));
      _readyCompleter.complete();
    }();
    return StreamGroup.merge(filteredEvents);
  }

  bool Function(AssetChange) _nestedPathFilter(PackageNode rootNode) {
    final ignorePaths = _nestedPaths(rootNode);
    return (change) => !ignorePaths.any(change.id.path.startsWith);
  }

  // Returns a set of all package paths that are "nested" within a node.
  //
  // This allows the watcher to optimize and avoid duplicate events.
  List<String> _nestedPaths(PackageNode rootNode) {
    return _graph.allPackages.values
        .where((node) {
          return node.path.length > rootNode.path.length &&
              node.path.startsWith(rootNode.path);
        })
        .map((node) =>
            node.path.substring(rootNode.path.length + 1) +
            Platform.pathSeparator)
        .toList();
  }
}
