// 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.phase_output;

import 'dart:async';
import 'dart:collection';

import '../asset/asset_forwarder.dart';
import '../asset/asset_node.dart';
import '../errors.dart';
import 'phase.dart';

/// A class that handles a single output of a phase.
///
/// Normally there's only a single [AssetNode] for a phase's output, but it's
/// possible that multiple transformers in the same phase emit assets with the
/// same id, causing collisions. This handles those collisions by forwarding the
/// chronologically first asset.
///
/// When the asset being forwarding changes, the old value of [output] will be
/// marked as removed and a new value will replace it. Users of this class can
/// be notified of this using [onAsset].
class PhaseOutput {
  /// The phase for which this is an output.
  final Phase _phase;

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

  /// The asset node for this output.
  AssetNode get output => _outputForwarder.node;
  AssetForwarder _outputForwarder;

  /// A stream that emits an [AssetNode] each time this output starts forwarding
  /// a new asset.
  Stream<AssetNode> get onAsset => _onAssetController.stream;
  final _onAssetController =
      new StreamController<AssetNode>.broadcast(sync: true);

  /// The assets for this output.
  ///
  /// If there's no collision, this will only have one element. Otherwise, it
  /// will be ordered by which asset was added first.
  final _assets = new Queue<AssetNode>();

  /// The [AssetCollisionException] for this output, or null if there is no
  /// collision currently.
  AssetCollisionException get collisionException {
    if (_assets.length == 1) return null;
    return new AssetCollisionException(
        _assets
            .where((asset) => asset.transform != null)
            .map((asset) => asset.transform.info),
        output.id);
  }

  PhaseOutput(this._phase, AssetNode output, this._location)
      : _outputForwarder = new AssetForwarder(output) {
    assert(!output.state.isRemoved);
    add(output);
  }

  /// Adds an asset node as an output with this id.
  void add(AssetNode node) {
    assert(node.id == output.id);
    assert(!output.state.isRemoved);
    _assets.add(node);
    _watchAsset(node);
  }

  /// Removes all existing listeners on [output] without actually closing
  /// [this].
  ///
  /// This marks [output] as removed, but immediately replaces it with a new
  /// [AssetNode] in the same state as the old output. This is used when adding
  /// a new [Phase] to cause consumers of the prior phase's outputs to be to
  /// start consuming the new phase's outputs instead.
  void removeListeners() {
    _outputForwarder.close();
    _outputForwarder = new AssetForwarder(_assets.first);
    _onAssetController.add(output);
  }

  /// Watches [node] to adjust [_assets] and [output] when it's removed.
  void _watchAsset(AssetNode node) {
    node.whenRemoved(() {
      if (_assets.length == 1) {
        assert(_assets.single == node);
        _outputForwarder.close();
        _onAssetController.close();
        return;
      }

      // If there was more than one asset, we're resolving a collision --
      // possibly partially.
      var wasFirst = _assets.first == node;
      _assets.remove(node);

      // If this was the first asset, we replace it with the next asset
      // (chronologically).
      if (wasFirst) removeListeners();

      // If there's still a collision, report it. This lets the user know if
      // they've successfully resolved the collision or not.
      if (_assets.length > 1) {
        // TODO(nweiz): report this through the output asset.
        _phase.cascade.reportError(collisionException);
      }
    });
  }

  String toString() => "phase output in $_location for $output";
}
