// Copyright (c) 2013, 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.

library barback.graph.transform_node;

import 'dart:async';

import 'package:async/async.dart';

import '../asset/asset.dart';
import '../asset/asset_id.dart';
import '../asset/asset_node.dart';
import '../asset/asset_node_set.dart';
import '../errors.dart';
import '../log.dart';
import '../transformer/aggregate_transform.dart';
import '../transformer/aggregate_transformer.dart';
import '../transformer/declaring_aggregate_transform.dart';
import '../transformer/declaring_aggregate_transformer.dart';
import '../transformer/lazy_aggregate_transformer.dart';
import '../utils.dart';
import 'node_status.dart';
import 'node_streams.dart';
import 'phase.dart';
import 'transformer_classifier.dart';

/// Every `_applyLogDuration`, we will issue a fine log entry letting the user
/// know that the transform is still executing.
const _applyLogDuration = const Duration(seconds: 10);

/// Describes a transform on a set of assets and its relationship to the build
/// dependency graph.
///
/// Keeps track of whether it's dirty and needs to be run and which assets it
/// depends on.
class TransformNode {
  /// The aggregate key for this node.
  final String key;

  /// The [TransformerClassifier] that [this] belongs to.
  final TransformerClassifier classifier;

  /// The [Phase] that this transform runs in.
  Phase get phase => classifier.phase;

  /// The [AggregateTransformer] to apply to this node's inputs.
  final AggregateTransformer transformer;

  /// The primary asset nodes this transform runs on.
  final _primaries = new AssetNodeSet();

  /// A string describing the location of [this] in the transformer graph.
  final String _location;

  /// The subscription to the [_primaries]' [AssetNode.onStateChange] streams.
  final _primarySubscriptions = new Map<AssetId, StreamSubscription>();

  /// The subscription to [phase]'s [Phase.onAsset] stream.
  StreamSubscription<AssetNode> _phaseAssetSubscription;

  /// The subscription to [phase]'s [Phase.onStatusChange] stream.
  StreamSubscription<NodeStatus> _phaseStatusSubscription;

  /// How far along [this] is in processing its assets.
  NodeStatus get status {
    if (_state == _State.APPLIED || _state == _State.DECLARED) {
      return NodeStatus.IDLE;
    }

    if (_declaring &&
        _state != _State.DECLARING &&
        _state != _State.NEEDS_DECLARE) {
      return NodeStatus.MATERIALIZING;
    } else {
      return NodeStatus.RUNNING;
    }
  }

  /// The [TransformInfo] describing this node.
  ///
  /// [TransformInfo] is the publicly-visible representation of a transform
  /// node.
  TransformInfo get info =>
      new TransformInfo(transformer, new AssetId(phase.cascade.package, key));

  /// Whether this is a declaring transform.
  ///
  /// This is usually identical to `transformer is
  /// DeclaringAggregateTransformer`, but if a declaring and non-lazy
  /// transformer emits an error during `declareOutputs` it's treated as though
  /// it wasn't declaring.
  bool get _declaring =>
      transformer is DeclaringAggregateTransformer &&
      (_state == _State.DECLARING || _declaredOutputs != null);

  /// Whether this transform has been forced since it last finished applying.
  ///
  /// A transform being forced means it should run until it generates outputs
  /// and is no longer dirty. This is always true for non-declaring
  /// transformers, since they always need to eagerly generate outputs.
  bool _forced;

  /// The subscriptions to each secondary input's [AssetNode.onStateChange]
  /// stream.
  final _secondarySubscriptions = new Map<AssetId, StreamSubscription>();

  /// The subscriptions to the [AssetCascade.onAsset] stream for cascades that
  /// might generate assets in [_missingInputs].
  final _missingExternalInputSubscriptions =
      new Map<String, StreamSubscription>();

  /// The controllers for the asset nodes emitted by this node.
  final _outputControllers = new Map<AssetId, AssetNodeController>();

  /// The ids of inputs the transformer tried and failed to read last time it
  /// ran.
  final _missingInputs = new Set<AssetId>();

  /// The controllers that are used to pass each primary input through [this] if
  /// it's not consumed or overwritten.
  ///
  /// This needs an intervening controller to ensure that the output can be
  /// marked dirty when determining whether [this] will consume or overwrite it,
  /// and be marked removed if it does. No pass-through controller will exist
  /// for primary inputs that are not being passed through.
  final _passThroughControllers = new Map<AssetId, AssetNodeController>();

  /// The asset node for this transform.
  final _streams = new NodeStreams();
  Stream<NodeStatus> get onStatusChange => _streams.onStatusChange;
  Stream<AssetNode> get onAsset => _streams.onAsset;
  Stream<LogEntry> get onLog => _streams.onLog;

  /// The current state of [this].
  var _state = _State.DECLARED;

  /// Whether [this] has been marked as removed.
  bool get _isRemoved => _streams.onAssetController.isClosed;

  // If [transformer] is declaring but not lazy and [primary] is available, we
  // can run [apply] even if [force] hasn't been called, since [transformer]
  // should run eagerly if possible.
  bool get _canRunDeclaringEagerly =>
      _declaring &&
      transformer is! LazyAggregateTransformer &&
      _primaries.every((input) => input.state.isAvailable);

  /// Which primary inputs the most recent run of this transform has declared
  /// that it consumes.
  ///
  /// This starts out `null`, indicating that the transform hasn't declared
  /// anything yet. This is not meaningful unless [_state] is [_State.APPLIED]
  /// or [_State.DECLARED].
  Set<AssetId> _consumedPrimaries;

  /// The set of output ids that [transformer] declared it would emit.
  ///
  /// This is only non-null if [transformer] is a
  /// [DeclaringAggregateTransformer] and its [declareOutputs] has been run
  /// successfully.
  Set<AssetId> _declaredOutputs;

  /// The controller for the currently-running
  /// [DeclaringAggregateTransformer.declareOutputs] call's
  /// [DeclaringAggregateTransform].
  ///
  /// This will be non-`null` when
  /// [DeclaringAggregateTransformer.declareOutputs] is running. This means that
  /// it's always non-`null` when [_state] is [_State.DECLARING], sometimes
  /// non-`null` when it's [_State.NEEDS_DECLARE], and always `null` otherwise.
  DeclaringAggregateTransformController _declareController;

  /// The controller for the currently-running [AggregateTransformer.apply]
  /// call's [AggregateTransform].
  ///
  /// This will be non-`null` when [AggregateTransform.apply] is running, which
  /// means that it's always non-`null` when [_state] is [_State.APPLYING] or
  /// [_State.NEEDS_APPLY], sometimes non-`null` when it's
  /// [_State.NEEDS_DECLARE], and always `null` otherwise.
  AggregateTransformController _applyController;

  /// Map to track pending requests for secondary inputs.
  ///
  /// Keys are the secondary inputs that have been requested but not yet
  /// produced. Values are the number of requests for that input.
  final _pendingSecondaryInputs = <AssetId, int>{};

  /// A stopwatch that tracks the total time spent in a transformer's `apply`
  /// function.
  final _timeInTransformer = new Stopwatch();

  /// A stopwatch that tracks the time in a transformer's `apply` function spent
  /// waiting for [getInput] calls to complete.
  final _timeAwaitingInputs = new Stopwatch();

  TransformNode(this.classifier, this.transformer, this.key, this._location) {
    _forced = transformer is! DeclaringAggregateTransformer;

    _phaseAssetSubscription = phase.previous.onAsset.listen((node) {
      if (!_missingInputs.contains(node.id)) return;
      if (_forced) node.force();
      _dirty();
    });

    _phaseStatusSubscription = phase.previous.onStatusChange.listen((status) {
      if (status == NodeStatus.RUNNING) return;

      _maybeFinishDeclareController();
      _maybeFinishApplyController();
    });

    classifier.onDoneClassifying.listen((_) {
      _maybeFinishDeclareController();
      _maybeFinishApplyController();
    });

    _run();
  }

  /// Adds [input] as a primary input for this node.
  void addPrimary(AssetNode input) {
    _primaries.add(input);
    if (_forced) input.force();

    _primarySubscriptions[input.id] =
        input.onStateChange.listen((_) => _onPrimaryStateChange(input));

    if (_state == _State.DECLARING && !_declareController.isDone) {
      // If we're running `declareOutputs` and its id stream isn't closed yet,
      // pass this in as another id.
      _declareController.addId(input.id);
      _maybeFinishDeclareController();
    } else if (_state == _State.APPLYING) {
      // If we're running `apply`, we need to wait until [input] is available
      // before we pass it into the stream. If it's available now, great; if
      // not, [_onPrimaryStateChange] will handle it.
      if (!input.state.isAvailable) {
        // If we started running eagerly without being forced, abort that run if
        // a new unavailable asset comes in.
        if (input.isLazy && !_forced) _restartRun();
        return;
      }

      _onPrimaryStateChange(input);
      _maybeFinishApplyController();
    } else {
      // Otherwise, a new input means we'll need to re-run `declareOutputs`.
      _restartRun();
    }
  }

  /// Marks this transform as removed.
  ///
  /// This causes all of the transform's outputs to be marked as removed as
  /// well. Normally this will be automatically done internally based on events
  /// from the primary input, but it's possible for a transform to no longer be
  /// valid even if its primary input still exists.
  void remove() {
    _streams.close();
    _phaseAssetSubscription.cancel();
    _phaseStatusSubscription.cancel();
    if (_declareController != null) _declareController.cancel();
    if (_applyController != null) _applyController.cancel();
    _clearSecondarySubscriptions();
    _clearOutputs();

    for (var subscription in _primarySubscriptions.values) {
      subscription.cancel();
    }
    _primarySubscriptions.clear();

    for (var controller in _passThroughControllers.values) {
      controller.setRemoved();
    }
    _passThroughControllers.clear();
  }

  /// If [this] is deferred, ensures that its concrete outputs will be
  /// generated.
  void force() {
    if (_forced || _state == _State.APPLIED) return;
    for (var input in _primaries) {
      input.force();
    }

    _forced = true;
    if (_state == _State.DECLARED) _apply();
  }

  /// Marks this transform as dirty.
  ///
  /// Specifically, this should be called when one of the transform's inputs'
  /// contents change, or when a secondary input is removed. Primary inputs
  /// being added or removed are handled by [addInput] and
  /// [_onPrimaryStateChange].
  void _dirty() {
    if (_state == _State.DECLARING ||
        _state == _State.NEEDS_DECLARE ||
        _state == _State.NEEDS_APPLY) {
      // If we already know that [_apply] needs to be run, there's nothing to do
      // here.
      return;
    }

    if (!_forced && !_canRunDeclaringEagerly) {
      // [forced] should only ever be false for a declaring transformer.
      assert(_declaring);

      // If we've finished applying, transition to DECLARED, indicating that we
      // know what outputs [apply] will emit but we're waiting to emit them
      // concretely until [force] is called. If we're still applying, we'll
      // transition to DECLARED once we finish.
      if (_state == _State.APPLIED) _state = _State.DECLARED;
      for (var controller in _outputControllers.values) {
        controller.setLazy(force);
      }
      _emitDeclaredOutputs();
      return;
    }

    if (_state == _State.APPLIED) {
      if (_declaredOutputs != null) _emitDeclaredOutputs();
      _apply();
    } else if (_state == _State.DECLARED) {
      _apply();
    } else {
      _state = _State.NEEDS_APPLY;
    }
  }

  /// The callback called when [input]'s state changes.
  void _onPrimaryStateChange(AssetNode input) {
    if (input.state.isRemoved) {
      _primarySubscriptions.remove(input.id);

      if (_primaries.isEmpty) {
        // If there are no more primary inputs, there's no more use for this
        // node in the graph. It will be re-created by its
        // [TransformerClassifier] if a new input with [key] is added.
        remove();
        return;
      }

      // Any change to the number of primary inputs requires that we re-run the
      // transformation.
      _restartRun();
    } else if (input.state.isAvailable) {
      if (_state == _State.DECLARED) {
        // If we're passing through this input and its contents don't matter,
        // update the pass-through controller.
        var controller = _passThroughControllers[input.id];
        if (controller != null) controller.setAvailable(input.asset);
      }

      if (_state == _State.DECLARED && _canRunDeclaringEagerly) {
        // If [this] is fully declared but hasn't started applying, this input
        // becoming available may mean that all inputs are available, in which
        // case we can run apply eagerly.
        _apply();
        return;
      }

      // If we're not actively passing concrete assets to the transformer, the
      // distinction between a dirty asset and an available one isn't relevant.
      if (_state != _State.APPLYING) return;

      if (_applyController.isDone) {
        // If we get a new asset after we've closed the asset stream, we need to
        // re-run declare and then apply.
        _restartRun();
      } else {
        // If the new asset comes before the asset stream is done, we can just
        // pass it to the stream.
        _applyController.addInput(input.asset);
        _maybeFinishApplyController();
      }
    } else {
      if (_forced) input.force();

      var controller = _passThroughControllers[input.id];
      if (controller != null) controller.setDirty();

      if (_state == _State.APPLYING &&
          !_applyController.addedId(input.id) &&
          (_forced || !input.isLazy)) {
        // If the input hasn't yet been added to the transform's input stream,
        // there's no need to consider the transformation dirty. However, if the
        // input is lazy and we're running eagerly, we need to restart the
        // transformation.
        return;
      }
      _dirty();
    }
  }

  /// Run the entire transformation, including both `declareOutputs` (if
  /// applicable) and `apply`.
  void _run() {
    assert(_state != _State.DECLARING);
    assert(_state != _State.APPLYING);

    _markOutputsDirty();
    _declareOutputs(() {
      if (_forced || _canRunDeclaringEagerly) {
        _apply();
      } else {
        _state = _State.DECLARED;
        _streams.changeStatus(NodeStatus.IDLE);
      }
    });
  }

  /// Restart the entire transformation, including `declareOutputs` if
  /// applicable.
  void _restartRun() {
    if (_state == _State.DECLARED || _state == _State.APPLIED) {
      // If we're currently idle, we can restart the transformation immediately.
      _run();
      return;
    }

    // If we're actively running `declareOutputs` or `apply`, cancel the
    // transforms and transition to `NEEDS_DECLARE`. Once the transformer's
    // method returns, we'll transition to `DECLARING`.
    if (_declareController != null) _declareController.cancel();
    if (_applyController != null) _applyController.cancel();
    _state = _State.NEEDS_DECLARE;
  }

  /// Runs [transform.declareOutputs] and emits the resulting assets as dirty
  /// assets.
  ///
  /// Calls [callback] when it's finished. This doesn't return a future so that
  /// [callback] is called synchronously if there are no outputs to declare. If
  /// [this] is removed while inputs are being declared, [callback] will not be
  /// called.
  void _declareOutputs(void callback()) {
    if (transformer is! DeclaringAggregateTransformer) {
      callback();
      return;
    }

    _state = _State.DECLARING;
    var controller = new DeclaringAggregateTransformController(this);
    _declareController = controller;
    _streams.onLogPool.add(controller.onLog);
    for (var primary in _primaries) {
      controller.addId(primary.id);
    }
    _maybeFinishDeclareController();

    new Future.sync(() {
      return (transformer as DeclaringAggregateTransformer)
          .declareOutputs(controller.transform);
    }).whenComplete(() {
      // Cancel the controller here even if `declareOutputs` wasn't interrupted.
      // Since the declaration is finished, we want to close out the
      // controller's streams.
      controller.cancel();
      _declareController = null;
    }).then((_) {
      if (_isRemoved) return;
      if (_state == _State.NEEDS_DECLARE) {
        _declareOutputs(callback);
        return;
      }

      if (controller.loggedError) {
        // If `declareOutputs` fails, fall back to treating a declaring
        // transformer as though it were eager.
        if (transformer is! LazyAggregateTransformer) _forced = true;
        callback();
        return;
      }

      _consumedPrimaries = controller.consumedPrimaries;
      _declaredOutputs = controller.outputIds;
      var invalidIds = _declaredOutputs
          .where((id) => id.package != phase.cascade.package)
          .toSet();
      for (var id in invalidIds) {
        _declaredOutputs.remove(id);
        // TODO(nweiz): report this as a warning rather than a failing error.
        phase.cascade.reportError(new InvalidOutputException(info, id));
      }

      for (var primary in _primaries) {
        if (_declaredOutputs.contains(primary.id)) continue;
        _passThrough(primary.id);
      }
      _emitDeclaredOutputs();
      callback();
    }).catchError((error, stackTrace) {
      if (_isRemoved) return;
      if (transformer is! LazyAggregateTransformer) _forced = true;
      phase.cascade.reportError(_wrapException(error, stackTrace));
      callback();
    });
  }

  /// Emits a dirty asset node for all outputs that were declared by the
  /// transformer.
  ///
  /// This won't emit any outputs for which there already exist output
  /// controllers. It should only be called for transforms that have declared
  /// their outputs.
  void _emitDeclaredOutputs() {
    assert(_declaredOutputs != null);
    for (var id in _declaredOutputs) {
      if (_outputControllers.containsKey(id)) continue;
      var controller = _forced
          ? new AssetNodeController(id, this)
          : new AssetNodeController.lazy(id, force, this);
      _outputControllers[id] = controller;
      _streams.onAssetController.add(controller.node);
    }
  }

  //// Mark all emitted and passed-through outputs of this transform as dirty.
  void _markOutputsDirty() {
    for (var controller in _passThroughControllers.values) {
      controller.setDirty();
    }
    for (var controller in _outputControllers.values) {
      if (_forced) {
        controller.setDirty();
      } else {
        controller.setLazy(force);
      }
    }
  }

  /// Applies this transform.
  void _apply() {
    assert(!_isRemoved);

    _markOutputsDirty();
    _clearSecondarySubscriptions();
    _state = _State.APPLYING;
    _streams.changeStatus(status);
    _runApply().then((hadError) {
      if (_isRemoved) return;

      if (_state == _State.DECLARED) return;

      if (_state == _State.NEEDS_DECLARE) {
        _run();
        return;
      }

      // If an input's contents changed while running `apply`, retry unless the
      // transformer is deferred and hasn't been forced.
      if (_state == _State.NEEDS_APPLY) {
        if (_forced || _canRunDeclaringEagerly) {
          _apply();
        } else {
          _state = _State.DECLARED;
        }
        return;
      }

      if (_declaring) _forced = false;

      assert(_state == _State.APPLYING);
      if (hadError) {
        _clearOutputs();
        // If the transformer threw an error, we don't want to emit the
        // pass-through assets in case they'll be overwritten by the
        // transformer. However, if the transformer declared that it wouldn't
        // overwrite or consume a pass-through asset, we can safely emit it.
        if (_declaredOutputs != null) {
          for (var input in _primaries) {
            if (_consumedPrimaries.contains(input.id) ||
                _declaredOutputs.contains(input.id)) {
              _consumePrimary(input.id);
            } else {
              _passThrough(input.id);
            }
          }
        }
      }

      _state = _State.APPLIED;
      _streams.changeStatus(NodeStatus.IDLE);
    });
  }

  /// Gets the asset for an input [id].
  ///
  /// If an input with [id] cannot be found, throws an [AssetNotFoundException].
  Future<Asset> getInput(AssetId id) {
    _timeAwaitingInputs.start();
    _pendingSecondaryInputs[id] = _pendingSecondaryInputs.containsKey(id)
        ? _pendingSecondaryInputs[id] + 1
        : 1;
    return phase.previous.getOutput(id).then((node) {
      // Throw if the input isn't found. This ensures the transformer's apply
      // is exited. We'll then catch this and report it through the proper
      // results stream.
      if (node == null) {
        _missingInputs.add(id);

        // If this id is for an asset in another package, subscribe to that
        // package's asset cascade so when it starts emitting the id we know to
        // re-run the transformer.
        if (id.package != phase.cascade.package) {
          var stream = phase.cascade.graph.onAssetFor(id.package);
          if (stream != null) {
            _missingExternalInputSubscriptions.putIfAbsent(id.package, () {
              return stream.listen((node) {
                if (!_missingInputs.contains(node.id)) return;
                if (_forced) node.force();
                _dirty();
              });
            });
          }
        }

        throw new AssetNotFoundException(id);
      }

      _secondarySubscriptions.putIfAbsent(node.id, () {
        return node.onStateChange.listen((_) => _dirty());
      });

      return node.asset;
    }).whenComplete(() {
      assert(_pendingSecondaryInputs.containsKey(id));
      if (_pendingSecondaryInputs[id] == 1) {
        _pendingSecondaryInputs.remove(id);
      } else {
        _pendingSecondaryInputs[id]--;
      }
      if (_pendingSecondaryInputs.isEmpty) _timeAwaitingInputs.stop();
    });
  }

  /// Run [AggregateTransformer.apply].
  ///
  /// Returns whether or not an error occurred while running the transformer.
  Future<bool> _runApply() {
    var controller = new AggregateTransformController(this);
    _applyController = controller;
    _streams.onLogPool.add(controller.onLog);
    for (var primary in _primaries) {
      if (!primary.state.isAvailable) continue;
      controller.addInput(primary.asset);
    }
    _maybeFinishApplyController();

    var transformCounterTimer;

    return DelegatingFuture.typed(new Future.sync(() {
      _timeInTransformer.reset();
      _timeAwaitingInputs.reset();
      _timeInTransformer.start();

      transformCounterTimer = new Timer.periodic(_applyLogDuration, (_) {
        if (_streams.onLogController.isClosed ||
            !_timeInTransformer.isRunning) {
          return;
        }

        var message = new StringBuffer("Not yet complete after "
            "${niceDuration(_timeInTransformer.elapsed)}");
        if (_pendingSecondaryInputs.isNotEmpty) {
          message.write(", waiting on input(s) "
              "${_pendingSecondaryInputs.keys.join(", ")}");
        }
        _streams.onLogController.add(new LogEntry(
            info, info.primaryId, LogLevel.FINE, message.toString(), null));
      });

      return transformer.apply(controller.transform);
    }).whenComplete(() {
      transformCounterTimer.cancel();
      _timeInTransformer.stop();
      _timeAwaitingInputs.stop();

      // Cancel the controller here even if `apply` wasn't interrupted. Since
      // the apply is finished, we want to close out the controller's streams.
      controller.cancel();
      _applyController = null;
    }).then((_) {
      assert(_state != _State.DECLARED);
      assert(_state != _State.DECLARING);
      assert(_state != _State.APPLIED);

      if (!_forced && _primaries.any((node) => !node.state.isAvailable)) {
        _state = _State.DECLARED;
        _streams.changeStatus(NodeStatus.IDLE);
        return false;
      }

      if (_isRemoved) return false;
      if (_state == _State.NEEDS_APPLY) return false;
      if (_state == _State.NEEDS_DECLARE) return false;
      if (controller.loggedError) return true;

      // If the transformer took long enough, log its duration in fine output.
      // That way it's not always visible, but users running with "pub serve
      // --verbose" can see it.
      var ranLong = _timeInTransformer.elapsed > new Duration(seconds: 1);
      var ranLongLocally =
          _timeInTransformer.elapsed - _timeAwaitingInputs.elapsed >
              new Duration(milliseconds: 200);

      // Report the transformer's timing information if it spent more than 0.2s
      // doing things other than waiting for its secondary inputs or if it spent
      // more than 1s in total.
      if (ranLongLocally || ranLong) {
        _streams.onLogController.add(new LogEntry(
            info,
            info.primaryId,
            LogLevel.FINE,
            "Took ${niceDuration(_timeInTransformer.elapsed)} "
            "(${niceDuration(_timeAwaitingInputs.elapsed)} awaiting "
            "secondary inputs).",
            null));
      }

      _handleApplyResults(controller);
      return false;
    }).catchError((error, stackTrace) {
      // If the transform became dirty while processing, ignore any errors from
      // it.
      if (_state == _State.NEEDS_APPLY || _isRemoved) return false;

      // Catch all transformer errors and pipe them to the results stream. This
      // is so a broken transformer doesn't take down the whole graph.
      phase.cascade.reportError(_wrapException(error, stackTrace));
      return true;
    }));
  }

  /// Handle the results of running [Transformer.apply].
  ///
  /// [controller] should be the controller for the [AggegateTransform] passed
  /// to [AggregateTransformer.apply].
  void _handleApplyResults(AggregateTransformController controller) {
    _consumedPrimaries = controller.consumedPrimaries;

    var newOutputs = controller.outputs;
    // Any ids that are for a different package are invalid.
    var invalidIds = newOutputs
        .map((asset) => asset.id)
        .where((id) => id.package != phase.cascade.package)
        .toSet();
    for (var id in invalidIds) {
      newOutputs.removeId(id);
      // TODO(nweiz): report this as a warning rather than a failing error.
      phase.cascade.reportError(new InvalidOutputException(info, id));
    }

    // Remove outputs that used to exist but don't anymore.
    for (var id in _outputControllers.keys.toList()) {
      if (newOutputs.containsId(id)) continue;
      _outputControllers.remove(id).setRemoved();
    }

    // Emit or stop emitting pass-through assets between removing and adding
    // outputs to ensure there are no collisions.
    for (var id in _primaries.map((node) => node.id)) {
      if (_consumedPrimaries.contains(id) || newOutputs.containsId(id)) {
        _consumePrimary(id);
      } else {
        _passThrough(id);
      }
    }

    // Store any new outputs or new contents for existing outputs.
    for (var asset in newOutputs) {
      var controller = _outputControllers[asset.id];
      if (controller != null) {
        controller.setAvailable(asset);
      } else {
        var controller = new AssetNodeController.available(asset, this);
        _outputControllers[asset.id] = controller;
        _streams.onAssetController.add(controller.node);
      }
    }
  }

  /// Cancels all subscriptions to secondary input nodes and other cascades.
  void _clearSecondarySubscriptions() {
    _missingInputs.clear();
    for (var subscription in _secondarySubscriptions.values) {
      subscription.cancel();
    }
    for (var subscription in _missingExternalInputSubscriptions.values) {
      subscription.cancel();
    }
    _secondarySubscriptions.clear();
    _missingExternalInputSubscriptions.clear();
  }

  /// Removes all output assets.
  void _clearOutputs() {
    // Remove all the previously-emitted assets.
    for (var controller in _outputControllers.values) {
      controller.setRemoved();
    }
    _outputControllers.clear();
  }

  /// Emit the pass-through node for the primary input [id] if it's not being
  /// emitted already.
  void _passThrough(AssetId id) {
    assert(!_outputControllers.containsKey(id));

    if (_consumedPrimaries.contains(id)) return;
    var controller = _passThroughControllers[id];
    var primary = _primaries[id];
    if (controller == null) {
      controller = new AssetNodeController.from(primary);
      _passThroughControllers[id] = controller;
      _streams.onAssetController.add(controller.node);
    } else if (primary.state.isDirty) {
      controller.setDirty();
    } else if (!controller.node.state.isAvailable) {
      controller.setAvailable(primary.asset);
    }
  }

  /// Stops emitting the pass-through node for the primary input [id] if it's
  /// being emitted.
  void _consumePrimary(AssetId id) {
    var controller = _passThroughControllers.remove(id);
    if (controller == null) return;
    controller.setRemoved();
  }

  /// If `declareOutputs` is running and all previous phases have declared their
  /// outputs, mark [_declareController] as done.
  void _maybeFinishDeclareController() {
    if (_declareController == null) return;
    if (classifier.isClassifying) return;
    if (phase.previous.status == NodeStatus.RUNNING) return;
    _declareController.done();
  }

  /// If `apply` is running, all previous phases have declared their outputs,
  /// and all primary inputs are available and thus have been passed to the
  /// transformer, mark [_applyController] as done.
  void _maybeFinishApplyController() {
    if (_applyController == null) return;
    if (classifier.isClassifying) return;
    if (_primaries.any((input) => !input.state.isAvailable)) return;
    if (phase.previous.status == NodeStatus.RUNNING) return;
    _applyController.done();
  }

  BarbackException _wrapException(error, StackTrace stackTrace) {
    if (error is! AssetNotFoundException) {
      return new TransformerException(info, error, stackTrace);
    } else {
      return new MissingInputException(info, error.id);
    }
  }

  String toString() =>
      "transform node in $_location for $transformer on ${info.primaryId} "
      "($_state, $status, ${_forced ? '' : 'un'}forced)";
}

/// The enum of states that [TransformNode] can be in.
class _State {
  /// The transform is running [DeclaringAggregateTransformer.declareOutputs].
  ///
  /// If the set of primary inputs changes while in this state, it will
  /// transition to [NEEDS_DECLARE]. If the [TransformNode] is still in this
  /// state when `declareOutputs` finishes running, it will transition to
  /// [APPLYING] if the transform is non-lazy and all of its primary inputs are
  /// available, and [DECLARED] otherwise.
  ///
  /// Non-declaring transformers will transition out of this state and into
  /// [APPLYING] immediately.
  static const DECLARING = const _State._("declaring outputs");

  /// The transform is running [AggregateTransformer.declareOutputs] or
  /// [AggregateTransform.apply], but a primary input was added or removed after
  /// it started, so it will need to re-run `declareOutputs`.
  ///
  /// The [TransformNode] will transition to [DECLARING] once `declareOutputs`
  /// or `apply` finishes running.
  static const NEEDS_DECLARE = const _State._("needs declare");

  /// The transform is deferred and has run
  /// [DeclaringAggregateTransformer.declareOutputs] but hasn't yet been forced.
  ///
  /// The [TransformNode] will transition to [APPLYING] when one of the outputs
  /// has been forced or if the transformer is non-lazy and all of its primary
  /// inputs become available.
  static const DECLARED = const _State._("declared");

  /// The transform is running [AggregateTransformer.apply].
  ///
  /// If an input's contents change or a secondary input is added or removed
  /// while in this state, the [TransformNode] will transition to [NEEDS_APPLY].
  /// If a primary input is added or removed, it will transition to
  /// [NEEDS_DECLARE]. If it's still in this state when `apply` finishes
  /// running, it will transition to [APPLIED].
  static const APPLYING = const _State._("applying");

  /// The transform is running [AggregateTransformer.apply], but an input's
  /// contents changed or a secondary input was added or removed after it
  /// started, so it will need to re-run `apply`.
  ///
  /// If a primary input is added or removed while in this state, the
  /// [TranformNode] will transition to [NEEDS_DECLARE]. If it's still in this
  /// state when `apply` finishes running, it will transition to [APPLYING].
  static const NEEDS_APPLY = const _State._("needs apply");

  /// The transform has finished running [AggregateTransformer.apply], whether
  /// or not it emitted an error.
  ///
  /// If an input's contents change or a secondary input is added or removed,
  /// the [TransformNode] will transition to [DECLARED] if the transform is
  /// declaring and [APPLYING] otherwise. If a primary input is added or
  /// removed, this will transition to [DECLARING].
  static const APPLIED = const _State._("applied");

  final String name;

  const _State._(this.name);

  String toString() => name;
}
