import {AxisRole, Group, LegendRole, TitleRole} from './constants';
import {Transform} from 'vega-dataflow';
import {Marks, boundClip} from 'vega-scenegraph';
import {inherits} from 'vega-util';

/**
 * Calculate bounding boxes for scenegraph items.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {object} params.mark - The scenegraph mark instance to bound.
 */
export default function Bound(params) {
  Transform.call(this, null, params);
}

inherits(Bound, Transform, {
  transform(_, pulse) {
    const view = pulse.dataflow,
          mark = _.mark,
          type = mark.marktype,
          entry = Marks[type],
          bound = entry.bound;

    let markBounds = mark.bounds, rebound;

    if (entry.nested) {
      // multi-item marks have a single bounds instance
      if (mark.items.length) view.dirty(mark.items[0]);
      markBounds = boundItem(mark, bound);
      mark.items.forEach(item => {
        item.bounds.clear().union(markBounds);
      });
    }

    else if (type === Group || _.modified()) {
      // operator parameters modified -> re-bound all items
      // updates group bounds in response to modified group content
      pulse.visit(pulse.MOD, item => view.dirty(item));
      markBounds.clear();
      mark.items.forEach(item => markBounds.union(boundItem(item, bound)));

      // force reflow for axes/legends/titles to propagate any layout changes
      switch (mark.role) {
        case AxisRole:
        case LegendRole:
        case TitleRole:
          pulse.reflow();
      }
    }

    else {
      // incrementally update bounds, re-bound mark as needed
      rebound = pulse.changed(pulse.REM);

      pulse.visit(pulse.ADD, item => {
        markBounds.union(boundItem(item, bound));
      });

      pulse.visit(pulse.MOD, item => {
        rebound = rebound || markBounds.alignsWith(item.bounds);
        view.dirty(item);
        markBounds.union(boundItem(item, bound));
      });

      if (rebound) {
        markBounds.clear();
        mark.items.forEach(item => markBounds.union(item.bounds));
      }
    }

    // ensure mark bounds do not exceed any clipping region
    boundClip(mark);

    return pulse.modifies('bounds');
  }
});

function boundItem(item, bound, opt) {
  return bound(item.bounds.clear(), item, opt);
}
