'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _regenerator = require('babel-runtime/regenerator');

var _regenerator2 = _interopRequireDefault(_regenerator);

var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator');

var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2);

var _extends2 = require('babel-runtime/helpers/extends');

var _extends3 = _interopRequireDefault(_extends2);

exports.interpreterProvider = interpreterProvider;

var _lodash = require('lodash.clone');

var _lodash2 = _interopRequireDefault(_lodash);

var _lodash3 = require('lodash');

var _get_type = require('../lib/get_type');

var _ast = require('../lib/ast');

var _get_by_alias = require('../lib/get_by_alias');

var _cast = require('./cast');

var _create_error = require('./create_error');

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function interpreterProvider(config) {
  var interpret = function () {
    var _ref = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee(node) {
      var context = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
      return _regenerator2.default.wrap(function _callee$(_context) {
        while (1) {
          switch (_context.prev = _context.next) {
            case 0:
              _context.t0 = (0, _get_type.getType)(node);
              _context.next = _context.t0 === 'expression' ? 3 : _context.t0 === 'string' ? 4 : _context.t0 === 'number' ? 4 : _context.t0 === 'null' ? 4 : _context.t0 === 'boolean' ? 4 : 5;
              break;

            case 3:
              return _context.abrupt('return', invokeChain(node.chain, context));

            case 4:
              return _context.abrupt('return', node);

            case 5:
              throw new Error('Unknown AST object: ' + JSON.stringify(node));

            case 6:
            case 'end':
              return _context.stop();
          }
        }
      }, _callee, this);
    }));

    return function interpret(_x2) {
      return _ref.apply(this, arguments);
    };
  }();

  var invokeChain = function () {
    var _ref2 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee2(chainArr, context) {
      var chain, link, fnName, fnArgs, fnDef, _ref3, resolvedArgs, newContext;

      return _regenerator2.default.wrap(function _callee2$(_context2) {
        while (1) {
          switch (_context2.prev = _context2.next) {
            case 0:
              if (chainArr.length) {
                _context2.next = 2;
                break;
              }

              return _context2.abrupt('return', Promise.resolve(context));

            case 2:
              chain = (0, _lodash2.default)(chainArr);
              link = chain.shift(); // Every thing in the chain will always be a function right?

              fnName = link.function, fnArgs = link.arguments;
              fnDef = (0, _get_by_alias.getByAlias)(functions, fnName);

              if (fnDef) {
                _context2.next = 8;
                break;
              }

              return _context2.abrupt('return', (0, _create_error.createError)({ message: 'Function ' + fnName + ' could not be found.' }));

            case 8:
              _context2.prev = 8;
              _context2.next = 11;
              return resolveArgs(fnDef, context, fnArgs);

            case 11:
              _ref3 = _context2.sent;
              resolvedArgs = _ref3.resolvedArgs;
              _context2.next = 15;
              return invokeFunction(fnDef, context, resolvedArgs);

            case 15:
              newContext = _context2.sent;

              if (!((0, _get_type.getType)(newContext) === 'error')) {
                _context2.next = 18;
                break;
              }

              return _context2.abrupt('return', newContext);

            case 18:
              _context2.next = 20;
              return invokeChain(chain, newContext);

            case 20:
              return _context2.abrupt('return', _context2.sent);

            case 23:
              _context2.prev = 23;
              _context2.t0 = _context2['catch'](8);

              // Everything that throws from a function will hit this
              // The interpreter should *never* fail. It should always return a `{type: error}` on failure
              _context2.t0.message = '[' + fnName + '] > ' + _context2.t0.message;
              return _context2.abrupt('return', (0, _create_error.createError)(_context2.t0));

            case 27:
            case 'end':
              return _context2.stop();
          }
        }
      }, _callee2, this, [[8, 23]]);
    }));

    return function invokeChain(_x3, _x4) {
      return _ref2.apply(this, arguments);
    };
  }();

  var invokeFunction = function () {
    var _ref4 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee3(fnDef, context, args) {
      var acceptableContext, fnOutput, returnType, expectedType, type;
      return _regenerator2.default.wrap(function _callee3$(_context3) {
        while (1) {
          switch (_context3.prev = _context3.next) {
            case 0:
              // Check function input.
              acceptableContext = cast(context, fnDef.context.types);
              _context3.next = 3;
              return fnDef.fn(acceptableContext, args, handlers);

            case 3:
              fnOutput = _context3.sent;


              // Validate that the function returned the type it said it would.
              // This isn't really required, but it keeps function developers honest.
              returnType = (0, _get_type.getType)(fnOutput);
              expectedType = fnDef.type;

              if (!(expectedType && returnType !== expectedType)) {
                _context3.next = 8;
                break;
              }

              throw new Error('Function \'' + fnDef.name + '\' should return \'' + expectedType + '\',' + (' actually returned \'' + returnType + '\''));

            case 8:

              // Validate the function output against the type definition's validate function
              type = handlers.types[fnDef.type];

              if (!(type && type.validate)) {
                _context3.next = 17;
                break;
              }

              _context3.prev = 10;

              type.validate(fnOutput);
              _context3.next = 17;
              break;

            case 14:
              _context3.prev = 14;
              _context3.t0 = _context3['catch'](10);
              throw new Error('Output of \'' + fnDef.name + '\' is not a valid type \'' + fnDef.type + '\': ' + _context3.t0);

            case 17:
              return _context3.abrupt('return', fnOutput);

            case 18:
            case 'end':
              return _context3.stop();
          }
        }
      }, _callee3, this, [[10, 14]]);
    }));

    return function invokeFunction(_x5, _x6, _x7) {
      return _ref4.apply(this, arguments);
    };
  }();

  // Processes the multi-valued AST argument values into arguments that can be passed to the function


  var resolveArgs = function () {
    var _ref5 = (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee5(fnDef, context, argAsts) {
      var _this = this;

      var argDefs, dealiasedArgAsts, argAstsWithDefaults, resolveArgFns, argNames, resolvedArgValues, resolvedMultiArgs, resolvedArgs;
      return _regenerator2.default.wrap(function _callee5$(_context5) {
        while (1) {
          switch (_context5.prev = _context5.next) {
            case 0:
              argDefs = fnDef.args;

              // Use the non-alias name from the argument definition

              dealiasedArgAsts = (0, _lodash3.reduce)(argAsts, function (argAsts, argAst, argName) {
                var argDef = (0, _get_by_alias.getByAlias)(argDefs, argName);
                // TODO: Implement a system to allow for undeclared arguments
                if (!argDef) {
                  throw new Error('Unknown argument \'' + argName + '\' passed to function \'' + fnDef.name + '\'');
                }

                argAsts[argDef.name] = (argAsts[argDef.name] || []).concat(argAst);
                return argAsts;
              }, {});

              // Check for missing required arguments

              (0, _lodash3.each)(argDefs, function (argDef) {
                var aliases = argDef.aliases,
                    argDefault = argDef.default,
                    argName = argDef.name,
                    required = argDef.required;

                if (typeof argDefault === 'undefined' && required && typeof dealiasedArgAsts[argName] === 'undefined') {
                  if (aliases.length === 0) {
                    throw new Error(fnDef.name + ' requires an argument');
                  } else {
                    var errorArg = argName === '_' ? aliases[0] : argName; // use an alias if _ is the missing arg
                    throw new Error(fnDef.name + ' requires an "' + errorArg + '" argument');
                  }
                }
              });

              // Fill in default values from argument definition
              argAstsWithDefaults = (0, _lodash3.reduce)(argDefs, function (argAsts, argDef, argName) {
                if (typeof argAsts[argName] === 'undefined' && typeof argDef.default !== 'undefined') {
                  argAsts[argName] = [(0, _ast.fromExpression)(argDef.default, 'argument')];
                }

                return argAsts;
              }, dealiasedArgAsts);

              // Create the functions to resolve the argument ASTs into values
              // These are what are passed to the actual functions if you opt out of resolving

              resolveArgFns = (0, _lodash3.mapValues)(argAstsWithDefaults, function (argAsts, argName) {
                return argAsts.map(function (argAst) {
                  return (0, _asyncToGenerator3.default)( /*#__PURE__*/_regenerator2.default.mark(function _callee4() {
                    var ctx = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : context;
                    var newContext;
                    return _regenerator2.default.wrap(function _callee4$(_context4) {
                      while (1) {
                        switch (_context4.prev = _context4.next) {
                          case 0:
                            _context4.next = 2;
                            return interpret(argAst, ctx);

                          case 2:
                            newContext = _context4.sent;

                            if (!((0, _get_type.getType)(newContext) === 'error')) {
                              _context4.next = 5;
                              break;
                            }

                            throw newContext.error;

                          case 5:
                            return _context4.abrupt('return', cast(newContext, argDefs[argName].types));

                          case 6:
                          case 'end':
                            return _context4.stop();
                        }
                      }
                    }, _callee4, _this);
                  }));
                });
              });
              argNames = (0, _lodash3.keys)(resolveArgFns);

              // Actually resolve unless the argument definition says not to

              _context5.next = 8;
              return Promise.all(argNames.map(function (argName) {
                var interpretFns = resolveArgFns[argName];
                if (!argDefs[argName].resolve) return interpretFns;
                return Promise.all(interpretFns.map(function (fn) {
                  return fn();
                }));
              }));

            case 8:
              resolvedArgValues = _context5.sent;
              resolvedMultiArgs = (0, _lodash3.zipObject)(argNames, resolvedArgValues);

              // Just return the last unless the argument definition allows multiple

              resolvedArgs = (0, _lodash3.mapValues)(resolvedMultiArgs, function (argValues, argName) {
                if (argDefs[argName].multi) return argValues;
                return (0, _lodash3.last)(argValues);
              });

              // Return an object here because the arguments themselves might actually have a 'then'
              // function which would be treated as a promise

              return _context5.abrupt('return', { resolvedArgs: resolvedArgs });

            case 12:
            case 'end':
              return _context5.stop();
          }
        }
      }, _callee5, this);
    }));

    return function resolveArgs(_x8, _x9, _x10) {
      return _ref5.apply(this, arguments);
    };
  }();

  var functions = config.functions,
      types = config.types;

  var handlers = (0, _extends3.default)({}, config.handlers, { types: types });
  var cast = (0, _cast.castProvider)(types);

  return interpret;
} /*
   * Licensed to Elasticsearch B.V. under one or more contributor
   * license agreements. See the NOTICE file distributed with
   * this work for additional information regarding copyright
   * ownership. Elasticsearch B.V. licenses this file to you under
   * the Apache License, Version 2.0 (the "License"); you may
   * not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *    http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing,
   * software distributed under the License is distributed on an
   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   * KIND, either express or implied.  See the License for the
   * specific language governing permissions and limitations
   * under the License.
   */