import {
  ASTNode,
  parse,
  codegen,
  functions as baseFunctions,
  constants
} from 'vega-expression';
import {error, stringValue} from 'vega-util';

var Literal = 'Literal';
var Identifier = 'Identifier';

export var signalPrefix = '$';
export var scalePrefix  = '%';
export var indexPrefix  = '@';
export var eventPrefix  = 'event.vega.';

function signalCode(id) {
  return '_[' + stringValue('$' + id) + ']';
}

export var functions = function(codegen) {
  var fn = baseFunctions(codegen);

  // view-specific event information
  fn.view   = eventPrefix + 'view';
  fn.item   = eventPrefix + 'item';
  fn.group  = eventPrefix + 'group';
  fn.xy     = eventPrefix + 'xy';
  fn.x      = eventPrefix + 'x';
  fn.y      = eventPrefix + 'y';
  fn.encode = 'this.encode';
  fn.modify = 'this.modify';

  // format functions
  fn.format = 'this.format';
  fn.timeFormat = 'this.timeFormat';
  fn.utcFormat = 'this.utcFormat';
  fn.pad = 'this.pad';
  fn.truncate = 'this.truncate';

  // color functions
  fn.rgb = 'this.rgb';
  fn.lab = 'this.lab';
  fn.hcl = 'this.hcl';
  fn.hsl = 'this.hsl';
  fn.gradient = 'this.gradient';

  // scales, projections, data
  fn.copy = 'this.scaleCopy';
  fn.bandwidth = 'this.bandwidth';
  fn.indata = 'this.indata';
  fn.inrange = 'this.inrange';
  fn.invert = 'this.scaleInvert';
  fn.range = 'this.range';
  fn.scale = 'this.scale';
  fn.span = 'this.span';

  // interaction support
  fn.clampRange    = 'this.clampRange';
  fn.pinchDistance = 'this.pinchDistance';
  fn.pinchAngle    = 'this.pinchAngle';

  // environment functions
  fn.open   = 'this.open';
  fn.screen = function() { return 'window.screen'; };
  fn.windowsize = function() {
    return '[window.innerWidth, window.innerHeight]';
  };

  return fn;
};

export var generator = codegen({
  blacklist:  ['_'],
  whitelist:  ['datum', 'event'],
  fieldvar:   'datum',
  globalvar:  signalCode,
  functions:  functions,
  constants:  constants
});

function signal(name, scope, params) {
  var signalName = signalPrefix + name;
  if (!params.hasOwnProperty(signalName)) {
    params[signalName] = scope.signalRef(name);
  }
}

function scale(name, scope, params) {
  var scaleName = scalePrefix + name;
  if (!params.hasOwnProperty(scaleName)) {
    try {
      params[scaleName] = scope.scaleRef(name);
    } catch (err) {
      // TODO: error handling? warning?
    }
  }
}

function index(data, field, scope, params) {
  var indexName = indexPrefix + field;
  if (!params.hasOwnProperty(indexName)) {
    params[indexName] = scope.getData(data).indataRef(scope, field);
  }
}

export default function(expr, scope, preamble) {
  var params = {}, ast, gen;

  try {
    ast = parse(expr);
  } catch (err) {
    error('Expression parse error: ' + expr);
  }

  // analyze ast for dependencies
  ast.visit(function visitor(node) {
    if (node.type !== 'CallExpression') return;

    var name = node.callee.name,
        args = node.arguments;

    switch (name) {
      case 'bandwidth':
      case 'copy':
      case 'range':
      case 'gradient':
      case 'invert':
      case 'scale':
        if (args[0].type === Literal) {           // scale dependency
          scale(args[0].value, scope, params);
        } else if (args[0].type === Identifier) { // forward reference to signal
          name = args[0].name;
          args[0] = new ASTNode(Literal);
          args[0].raw = '{signal:"' + name + '"}';
        }
        break;
      case 'indata':
        if (args[0].type !== Literal) error('First argument to indata must be a string literal.');
        if (args[1].type !== Literal) error('Second argument to indata must be a string literal.');
        index(args[0].value, args[1].value, scope, params);
        break;
    }
  });

  // perform code generation
  gen = generator(ast);
  gen.globals.forEach(function(name) { signal(name, scope, params); });

  // returned parsed expression
  return {
    $expr:   preamble ? preamble + 'return(' + gen.code + ');' : gen.code,
    $fields: gen.fields,
    $params: params
  };
}

@@ -298,16 +298,47 @@
  = '@';%0A
+export var tuplePrefix  = ':';%0A
 export v
@@ -370,16 +370,16 @@
 vega.';%0A
-
 %0Afunctio
@@ -1213,24 +1213,53 @@
 scaleCopy';%0A
+  fn.domain = 'this.domain';%0A
   fn.bandwid
@@ -1453,16 +1453,45 @@
 s.span';
+%0A  fn.tuples = 'this.tuples';
 %0A%0A  // i
@@ -2667,24 +2667,205 @@
 ld);%0A  %7D%0A%7D%0A%0A
+function tuples(data, scope, params) %7B%0A  var dataName = tuplePrefix + data;%0A  if (!params.hasOwnProperty(dataName)) %7B%0A    params%5BdataName%5D = scope.getData(data).tuplesRef();%0A  %7D%0A%7D%0A%0A
 export defau
@@ -3279,16 +3279,37 @@
 'copy':%0A
+      case 'domain':%0A
       ca
@@ -3943,32 +3943,32 @@
 ing literal.');%0A
-
         index(ar
@@ -4022,24 +4022,205 @@
      break;%0A
+      case 'tuples':%0A        if (args%5B0%5D.type !== Literal) error('First argument to tuples must be a string literal.');%0A        tuples(args%5B0%5D.value, scope, params);%0A        break;%0A
     %7D%0A  %7D);%0A

Add scale domain and tuple access expression functions.