import {ingest, Transform, tupleid} from 'vega-dataflow';
import {array, error, inherits} from 'vega-util';
import {nest} from 'd3-collection';
import {hierarchy} from 'd3-hierarchy';

 /**
  * Nest tuples into a tree structure, grouped by key values.
  * @constructor
  * @param {object} params - The parameters for this operator.
  * @param {Array<function(object): *>} params.keys - The key fields to nest by, in order.
  * @param {function(object): *} [params.key] - Unique key field for each tuple.
  *   If not provided, the tuple id field is used.
  * @param {boolean} [params.generate=false] - A boolean flag indicating if
  *   non-leaf nodes generated by this transform should be included in the
  *   output. The default (false) includes only the input data (leaf nodes)
  *   in the data stream.
  */
export default function Nest(params) {
  Transform.call(this, null, params);
}

Nest.Definition = {
  "type": "Nest",
  "metadata": {"treesource": true, "changes": true},
  "params": [
    { "name": "keys", "type": "field", "array": true },
    { "name": "key", "type": "field" },
    { "name": "generate", "type": "boolean" }
  ]
};

var prototype = inherits(Nest, Transform);

function children(n) {
  return n.values;
}

prototype.transform = function(_, pulse) {
  if (!pulse.source) {
    error('Nest transform requires an upstream data source.');
  }

  var key = _.key || tupleid,
      gen = _.generate,
      mod = _.modified(),
      out = pulse.clone(),
      root, tree, map;

  if (!this.value || mod || pulse.changed()) {
    // collect nodes to remove
    if (gen && this.value) {
      this.value.each(function(node) {
        if (node.children) out.rem.push(node);
      });
    }

    // generate new tree structure
    root = array(_.keys)
      .reduce(function(n, k) { n.key(k); return n; }, nest())
      .entries(out.source);
    this.value = tree = hierarchy({values: root}, children);

    // collect nodes to add
    if (gen) {
      tree.each(function(node) {
        if (node.children) {
          node = ingest(node.data);
          out.add.push(node);
          out.source.push(node);
        }
      });
    }

    // build lookup table
    map = tree.lookup = {};
    tree.each(function(node) {
      if (tupleid(node.data) != null) {
        map[key(node.data)] = node;
      }
    });
  }

  out.source.root = this.value;
  return out;
};
