import parseStream from './stream.js';
import {Scope, View} from '../util.js';
import {parseSelector} from 'vega-event-selector';
import {parseExpression} from 'vega-functions';
import {array, error, extend, isString, stringValue} from 'vega-util';

// bypass expression parser for internal operator references
const OP_VALUE_EXPR = {
  code: '_.$value',
  ast: {type: 'Identifier', value: 'value'}
};

export default function(spec, scope, target) {
  const encode = spec.encode,
        entry = {target: target};

  let events = spec.events,
      update = spec.update,
      sources = [];

  if (!events) {
    error('Signal update missing events specification.');
  }

  // interpret as an event selector string
  if (isString(events)) {
    events = parseSelector(events, scope.isSubscope() ? Scope : View);
  }

  // separate event streams from signal updates
  events = array(events)
    .filter(s => s.signal || s.scale ? (sources.push(s), 0) : 1);

  // merge internal operator listeners
  if (sources.length > 1) {
    sources = [mergeSources(sources)];
  }

  // merge event streams, include as source
  if (events.length) {
    sources.push(events.length > 1 ? {merge: events} : events[0]);
  }

  if (encode != null) {
    if (update) error('Signal encode and update are mutually exclusive.');
    update = 'encode(item(),' + stringValue(encode) + ')';
  }

  // resolve update value
  entry.update = isString(update) ? parseExpression(update, scope)
    : update.expr != null ? parseExpression(update.expr, scope)
    : update.value != null ? update.value
    : update.signal != null ? {
        $expr:   OP_VALUE_EXPR,
        $params: {$value: scope.signalRef(update.signal)}
      }
    : error('Invalid signal update specification.');

  if (spec.force) {
    entry.options = {force: true};
  }

  sources.forEach(source =>
    scope.addUpdate(extend(streamSource(source, scope), entry))
  );
}

function streamSource(stream, scope) {
  return {
    source: stream.signal ? scope.signalRef(stream.signal)
          : stream.scale ? scope.scaleRef(stream.scale)
          : parseStream(stream, scope)
  };
}

function mergeSources(sources) {
  return {
    signal: '['
      + sources.map(s => s.scale ? 'scale("' + s.scale + '")' : s.signal)
      + ']'
  };
}
