// Copyright (c) 2014, 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.transformer_classifier;

import 'dart:async';

import '../asset/asset_forwarder.dart';
import '../asset/asset_node.dart';
import '../errors.dart';
import '../log.dart';
import '../transformer/aggregate_transformer.dart';
import '../transformer/wrapping_aggregate_transformer.dart';
import 'node_status.dart';
import 'node_streams.dart';
import 'phase.dart';
import 'transform_node.dart';

/// A class for classifying the primary inputs for a transformer according to
/// its [AggregateTransformer.classifyPrimary] method.
///
/// This is also used for non-aggregate transformers; they're modeled as
/// aggregate transformers that return the primary path if `isPrimary` is true
/// and `null` if `isPrimary` is `null`.
class TransformerClassifier {
  /// The containing [Phase].
  final Phase phase;

  /// The [AggregateTransformer] used to classify the inputs.
  final AggregateTransformer transformer;

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

  /// The individual transforms for each classiciation key.
  final _transforms = new Map<Object, TransformNode>();

  /// Forwarders used to pass through assets that aren't used by [transformer].
  final _passThroughForwarders = new Set<AssetForwarder>();

  /// The streams exposed by this classifier.
  final _streams = new NodeStreams();
  Stream get onStatusChange => _streams.onStatusChange;
  Stream<AssetNode> get onAsset => _streams.onAsset;
  Stream<LogEntry> get onLog => _streams.onLog;

  /// A broadcast stream that emits an event whenever [this] has finished
  /// classifying all available inputs.
  Stream get onDoneClassifying => _onDoneClassifyingController.stream;
  final _onDoneClassifyingController =
      new StreamController<Null>.broadcast(sync: true);

  /// The number of currently-active calls to [transformer.classifyPrimary].
  ///
  /// This is used to determine whether [this] is dirty.
  var _activeClassifications = 0;

  /// Whether this is currently classifying any inputs.
  bool get isClassifying => _activeClassifications > 0;

  /// How far along [this] is in processing its assets.
  NodeStatus get status {
    if (isClassifying) return NodeStatus.RUNNING;
    return NodeStatus
        .dirtiest(_transforms.values.map((transform) => transform.status));
  }

  TransformerClassifier(this.phase, transformer, this._location)
      : transformer = transformer is AggregateTransformer
            ? transformer
            : new WrappingAggregateTransformer(transformer);

  /// Adds a new asset as an input for this transformer.
  void addInput(AssetNode input) {
    _activeClassifications++;
    new Future.sync(() => transformer.classifyPrimary(input.id))
        .catchError((error, stackTrace) {
      if (input.state.isRemoved) return null;

      // Catch all transformer errors and pipe them to the results stream. This
      // is so a broken transformer doesn't take down the whole graph.
      var info = new TransformInfo(transformer, input.id);
      if (error is! AssetNotFoundException) {
        error = new TransformerException(info, error, stackTrace);
      } else {
        error = new MissingInputException(info, error.id);
      }
      phase.cascade.reportError(error);

      return null;
    }).then((key) {
      if (input.state.isRemoved) return;
      if (key == null) {
        var forwarder = new AssetForwarder(input);
        _passThroughForwarders.add(forwarder);
        forwarder.node
            .whenRemoved(() => _passThroughForwarders.remove(forwarder));
        _streams.onAssetController.add(forwarder.node);
      } else if (_transforms.containsKey(key)) {
        _transforms[key].addPrimary(input);
      } else {
        var transform = new TransformNode(this, transformer, key, _location);
        _transforms[key] = transform;

        transform.onStatusChange.listen((_) => _streams.changeStatus(status),
            onDone: () {
          _transforms.remove(transform.key);
          if (!_streams.isClosed) _streams.changeStatus(status);
        });

        _streams.onAssetPool.add(transform.onAsset);
        _streams.onLogPool.add(transform.onLog);
        transform.addPrimary(input);
      }
    }).whenComplete(() {
      _activeClassifications--;
      if (_streams.isClosed) return;
      if (!isClassifying) _onDoneClassifyingController.add(null);
      _streams.changeStatus(status);
    });
  }

  /// Removes this transformer.
  ///
  /// This marks all outputs of the transformer as removed.
  void remove() {
    _streams.close();
    _onDoneClassifyingController.close();
    for (var transform in _transforms.values.toList()) {
      transform.remove();
    }
    for (var forwarder in _passThroughForwarders.toList()) {
      forwarder.close();
    }
  }

  /// Force all deferred transforms to begin producing concrete assets.
  void forceAllTransforms() {
    for (var transform in _transforms.values) {
      transform.force();
    }
  }

  String toString() => "classifier in $_location for $transformer";
}
