'use strict';

var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };

var _lodash = require('lodash');

var _lodash2 = _interopRequireDefault(_lodash);

var _bluebird = require('bluebird');

var _bluebird2 = _interopRequireDefault(_bluebird);

var _vm = require('vm2');

var _mware = require('mware');

var _mware2 = _interopRequireDefault(_mware);

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

function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new _bluebird2.default(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return _bluebird2.default.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }

const callSubflowRegex = /(.+\.flow\.json)\s?@?\s?(.+)?/i; // e.g. './login.flow.json' or './login.flow.json @ username'
const MAX_STACK_SIZE = 100;

const TRUEISH_WORDS = {
  true: true,
  always: true,
  yes: true
};

const compileExp = _lodash2.default.memoize(expr => new _vm.VMScript(expr));

/** The Dialog Engine (or Dialog Manager) is the component that
 handles the flow logic. It it responsible for executing flows, including
 executing the actions and flowing to the nodes, redirections etc.
 @namespace DialogEngine
 @example
 bp.dialogEngine.processMessage(...)
 */
class DialogEngine {
  constructor({ flowProvider, stateManager, options, logger }) {
    this.onError = fn => this.errorHandlers.push(fn);

    this.logger = logger;
    this.flowProvider = flowProvider;
    this.stateManager = stateManager;

    this._flowsLoadingPromise = null;
    this.flows = [];
    this.defaultFlow = _lodash2.default.get(options, 'defaultFlow') || 'main.flow.json';
    this.outputProcessors = [];
    this.errorHandlers = [];
    this.actions = {};
    this.actionMetadataProviders = [];
    this.vm = new _vm.VM({
      timeout: 5000
    });

    /**
     * @typedef {Function} DialogEngine~DialogMiddleware
     * @param {object} ctx A mutable context object
     * @param {function} next Call this to continue processing
     */

    /**
     * Middleware triggered before a new session is started.
     * > **Note:** This middleware allows you to alter `ctx.flowName` to change
     * > which flow will be selected to start the conversation.
     * @function DialogEngine#onBeforeCreated
     * @param {DialogEngine~DialogMiddleware} middleware
     * @example
    bp.dialogEngine.onBeforeCreated((ctx, next) => {
    ctx.flowName = 'example.flow.json'
    next()
    })
     */
    this.onBeforeCreated = (0, _mware2.default)();

    /**
     * Middleware triggered **after** a new session is started.
     * `ctx` is not mutable.
     * @function DialogEngine#onAfterCreated
     * @param {DialogEngine~DialogMiddleware} middleware
     * @example
    bp.dialogEngine.onAfterCreated((ctx, next) => {
    // Do something here
    next()
    })
     */
    this.onAfterCreated = (0, _mware2.default)();

    /**
     * Middleware triggered **before** a conversation is ended for any reason.
     * `ctx` is not mutable.
     * @function DialogEngine#onBeforeEnd
     * @param {DialogEngine~DialogMiddleware} middleware
     * @example
    bp.dialogEngine.onBeforeEnd((ctx, next) => {
    // Do something here
    next()
    })
     */
    this.onBeforeEnd = (0, _mware2.default)();

    /**
     * Middleware triggered **before** a node is entered (before the `onEnter` execution).
     * > **⚠️ Warn:** It is **not** recommended to mutate `ctx.node` for now, it might break in a future version of Botpress.
     * @function DialogEngine#onBeforeNodeEnter
     * @param {DialogEngine~DialogMiddleware} middleware
     * @example
    bp.dialogEngine.onBeforeNodeEnter((ctx, next) => {
    // Do something here
    next()
    })
     */
    this.onBeforeNodeEnter = (0, _mware2.default)();

    /**
     * Middleware triggered **before** a conversation/session times out.
     * > **Note:** You can't prevent it from timing out at this point.
     * > You also can't change the timeout behavior/location at this time.
     * @function DialogEngine#onBeforeSessionTimeout
     * @param {DialogEngine~DialogMiddleware} middleware
     * @example
    bp.dialogEngine.onBeforeSessionTimeout((ctx, next) => {
    // Do something here
    next()
    })
     */
    this.onBeforeSessionTimeout = (0, _mware2.default)();

    flowProvider.on('flowsChanged', () => {
      this._flowsLoadingPromise = null;
      this.flows = [];
    });
  }

  _processMessage(stateId, event) {
    var _this = this;

    return _asyncToGenerator(function* () {
      yield _this.loadFlows();

      const context = yield _this._getOrCreateContext(stateId);
      let state = yield _this.stateManager.getState(stateId);

      if (event.type === 'bp_dialog_timeout') {
        state = yield _this._processTimeout(stateId, state, context, event);

        if (state != null) {
          yield _this.stateManager.setState(stateId, state);
        }

        return state;
      }

      _this._trace('<~', 'RECV', `"${(event.text || '').substr(0, 20)}"`, context, state);

      if (!context.currentFlow) {
        throw new Error('Expected currentFlow to be defined for stateId=' + stateId);
      }

      const catchAllOnReceive = _lodash2.default.get(context, 'currentFlow.catchAll.onReceive');

      if (catchAllOnReceive) {
        _this._trace('!!', 'KALL', '', context, state);
        state = yield _this._processInstructions(catchAllOnReceive, state, event, context);
      }

      // If there's a 'next' defined in catchAll, this will try to match any condition and if it is matched it
      // will run the node defined in the next instead of the current context node
      const catchAllNext = _lodash2.default.get(context, 'currentFlow.catchAll.next');
      if (catchAllNext) {
        _this._trace('..', 'KALL', '', context, state);
        for (const transition of catchAllNext) {
          if (_this._evaluateCondition(transition.condition, state, event)) {
            return _this._processNode(stateId, state, context, transition.node, event);
          }
        }

        _this._trace('?X', 'KALL', '', context, state);
      }

      state = yield _this._processNode(stateId, state, context, context.node, event);

      if (state != null) {
        yield _this.stateManager.setState(stateId, state);
      }

      return state;
    })();
  }

  /**
   * Process a new incoming message from the user.
   * This will execute and run the flow until the flow ends or gets paused by user input
   * @param  {string} stateId The Id of the state.
   *                          This is usually unique per user/group/channel, depending on the platform.
   * @param  {BPIncomingEvent} event   The incoming event (message)
   * @return {Promise<State>}         Returns a promise that resolves with the new state
   *                                  when the flow is done processing
   */
  processMessage(stateId, event) {
    var _this2 = this;

    return _asyncToGenerator(function* () {
      try {
        yield _this2._processMessage(stateId, event);
      } catch (e) {
        _this2.errorHandlers.forEach(function (errorHandler) {
          return errorHandler(e);
        });
      }
    })();
  }

  /**
   * Make the stateId jump to the specified flow and node
   * regardless of if there was already an active flow or not
   * in execution. If there was already an active flow executing,
   * this will override it. Note that by default, the current state
   * will be preserved; if you wish to reset the state as well,
   * set `resetState` to `true`.
   * Note that this will not continue processing, i.e. the user must send a message or
   * you should call {@link BotEngine#processMessage} manually to continue execution.
   * @example
   * // inside a bp.hear (...)
   * await bp.dialogEngine.jumpTo(stateId, 'main.flow.json')
   * await bp.dialogEngine.processMessage(stateId, event) // Continue processing
   * @param  {string} stateId  The stateId of the user/channel/group to make jump.
   * @param  {string} flowName The name of the flow, e.g. `main.flow.json`
   * @param  {string} [nodeName=null] The name of the node to jump to. Defaults to the flow's entry point.
   * @param  {boolean} [options.resetState=false] Whether or not the state should be reset
   */
  jumpTo(stateId, flowName, nodeName = null, options) {
    var _this3 = this;

    return _asyncToGenerator(function* () {
      options = _extends({
        resetState: false
      }, options);

      yield _this3.loadFlows();

      const flow = yield _this3._findFlow(flowName, true);

      if (nodeName) {
        // We're just calling for throwing if doesn't exist
        DialogEngine._findNode(flow, nodeName, true);
      }

      yield _this3._setContext(stateId, {
        currentFlow: flow,
        node: nodeName || flow.startNode,
        hasJumped: true,
        flowStack: [{ flow: flow.name, node: nodeName || flow.startNode }]
      });

      if (options.resetState) {
        yield _this3.stateManager.setState(stateId, {});
      }
    })();
  }

  /**
   * Get the current flow and node for a specific stateId
   * @param  {string} stateId
   * @return {{ flow: string, node: string }} Returns the current flow and node
   */
  getCurrentPosition(stateId) {
    var _this4 = this;

    return _asyncToGenerator(function* () {
      const context = yield _this4._getContext(stateId);

      if (context) {
        return {
          flow: context.currentFlow && context.currentFlow.name,
          node: context.node
        };
      }

      return {
        flow: null,
        node: null
      };
    })();
  }

  /**
   * Ends the flow for a specific stateId if there's an active flow,
   * otherwise does nothing.
   * @param  {string} stateId [description]
   */
  endFlow(stateId) {
    var _this5 = this;

    return _asyncToGenerator(function* () {
      return _this5._endFlow(stateId);
    })();
  }

  loadFlows() {
    if (!this._flowsLoadingPromise) {
      this._trace('**', 'LOAD', '');
      this._flowsLoadingPromise = this.flowProvider.loadAll().then(flows => {
        this.flows = flows;
      });
    }

    return this._flowsLoadingPromise;
  }

  getFlows() {
    var _this6 = this;

    return _asyncToGenerator(function* () {
      yield _this6.loadFlows();
      return _this6.flows;
    })();
  }

  /**
   * Registers a new output processor (there can be many, which all get triggered on output).
   * @param {OutpoutProcessor} processor - Is an object with {id, send}
   * @param {string} processor.id - The unique id of the processor
   * @param {Function} processor.send - The `send` function of the processor
   * @returns {void}
   * @private
   */
  registerOutputProcessor(processor) {
    if (_lodash2.default.isNil(processor) || !_lodash2.default.isFunction(processor.send) || !_lodash2.default.isString(processor.id)) {
      throw new Error('Invalid processor. Processor must have a function `send` defined and a valid `id`');
    }

    // For now we only ever support a single output processor
    // We might want many output processors in the future, for example to hook up a debugger or a test suite
    this.outputProcessors = [processor];
  }

  /**
   * @typedef {object} DialogEngine~ActionMetadata
   * @var {string} title
   * @var {string} description
   * @var {boolean} required
   * @var {string} default
   * @var {string} type
   * @private
   */

  /**
   * A metadata provider returns metadata for an action or
   * returns null, in which case other providers will be called
   * @typedef {Function} DialogEngine~MetadataProvider
   * @param {string} action The name of the action
   * @returns {DialogEngine~ActionMetadata}
   * @private
   */

  /**
   * Adds a new provider of function metadata
   * @param {DialogEngine~MetadataProvider} provider
   * @returns {void}
   * @private
   */
  registerActionMetadataProvider(provider) {
    if (!_lodash2.default.isFunction(provider)) {
      throw new Error('Expected the function metadata provider to be a function');
    }

    if (!this.actionMetadataProviders.includes(provider)) {
      this.actionMetadataProviders.push(provider);
    }
  }

  /**
   * Introduce new actions to the Flows that they can call.
   * @param {Object} fnMap
   * @param {bool} [overwrite=false] - Whether or not it should overwrite existing actions with the same name.
   * Note that if overwrite is false, an error will be thrown on conflict.
   * @returns {Promise.<void>}
   */
  registerActions(fnMap, overwrite = false) {
    var _this7 = this;

    return _asyncToGenerator(function* () {
      _lodash2.default.keys(fnMap).forEach(function (name) {
        if (_this7.actions[name] && !overwrite) {
          throw new Error(`There is already a function named "${name}" registered`);
        }

        let handler = fnMap[name];
        let metadata = null;

        if (!_lodash2.default.isFunction(handler)) {
          if (!_lodash2.default.isObject(handler) || !_lodash2.default.isFunction(handler.handler)) {
            throw new Error(`Expected function "${name}" to be a function or an object with a 'hander' function`);
          }

          handler = handler.handler;
          metadata = _extends({}, fnMap[name], { name, handler: null });
        }

        for (const provider of _this7.actionMetadataProviders) {
          const extra = provider(name);

          if (extra) {
            metadata = _extends({}, extra, metadata);
            break;
          }
        }

        _this7.actions[name] = {
          name,
          metadata,
          fn: handler

          // Make the method available in the conditions evaluation context
        };_this7.vm.freeze(handler, name);
      });
    })();
  }

  /**
   * @deprecated Use registerActions() instead
   */
  registerFunctions(fnMap, overwrite = false) {
    return this.registerActions(fnMap, overwrite);
  }

  /**
   * Returns all the available actions along with their metadata
   * @private
   */
  getAvailableActions() {
    return _lodash2.default.values(this.actions).filter(x => !String(x.name).startsWith('__')).map(x => _extends({}, x, { fn: null }));
  }

  _processTimeout(stateId, userState, context, event) {
    var _this8 = this;

    return _asyncToGenerator(function* () {
      const beforeCtx = { stateId };
      yield _bluebird2.default.fromCallback(function (callback) {
        return _this8.onBeforeSessionTimeout.run(beforeCtx, callback);
      });

      const currentNodeTimeout = _lodash2.default.get(DialogEngine._findNode(context.currentFlow, context.node), 'timeoutNode');
      const currentFlowTimeout = _lodash2.default.get(context, 'currentFlow.timeoutNode');
      const fallbackTimeoutNode = DialogEngine._findNode(context.currentFlow, 'timeout');
      const fallbackTimeoutFlow = yield _this8._findFlow('timeout.flow.json');

      if (currentNodeTimeout) {
        _this8._trace('<>', 'SNDE', '', context);
        userState = yield _this8._processNode(stateId, userState, context, currentNodeTimeout, event);
      } else if (currentFlowTimeout) {
        _this8._trace('<>', 'SFLW', '', context);
        userState = yield _this8._processNode(stateId, userState, context, currentFlowTimeout, event);
      } else if (fallbackTimeoutNode) {
        _this8._trace('<>', 'DNDE', '', context);
        userState = yield _this8._processNode(stateId, userState, context, fallbackTimeoutNode.name, event);
      } else if (fallbackTimeoutFlow) {
        _this8._trace('<>', 'DFLW', '', context);
        userState = yield _this8._processNode(stateId, userState, context, fallbackTimeoutFlow.name, event);
      } else {
        _this8._trace('<>', 'NTHG', '', context);
        userState = yield _this8._endFlow(stateId);
      }

      return userState;
    })();
  }

  _processNode(stateId, userState, context, nodeName, event) {
    var _this9 = this;

    return _asyncToGenerator(function* () {
      let switchedFlow = false;
      let switchedNode = context.hasJumped;
      context = _extends({}, context, { hasJumped: false });

      const originalFlow = context.currentFlow.name;

      if (callSubflowRegex.test(nodeName)) {
        _this9._trace('>>', 'FLOW', `"${nodeName}"`, context, null);
        context = yield _this9._gotoSubflow(nodeName, context);
        switchedFlow = true;
      } else if (nodeName && nodeName[0] === '#') {
        // e.g. '#success'
        _this9._trace('<<', 'FLOW', `"${nodeName}"`, context, null);
        context = yield _this9._gotoPreviousFlow(nodeName, context);
        switchedFlow = true;
      } else if (context.node !== nodeName) {
        _this9._trace('>>', 'FLOW', `"${nodeName}"`);
        switchedNode = true;
        context = _extends({}, context, { node: nodeName });
      } else if (context.node == null) {
        // We just created the context
        switchedNode = true;
        context = _extends({}, context, { node: context.currentFlow.startNode });
      }

      const node = DialogEngine._findNode(context.currentFlow, context.node);

      if (!node || !node.name) {
        userState = yield _this9._endFlow(stateId);
        return userState;
        // TODO Trace error
        // throw new Error(`Could not find node "${context.node}" in flow "${context.currentFlow.name}"`)
      }

      if (switchedFlow || switchedNode) {
        const flowStack = context.flowStack.concat({
          flow: context.currentFlow.name,
          node: context.node
        });

        // Flattens the stack to only include flow jumps, not node jumps
        context = _extends({}, context, {
          // Flattens the stack to only include flow jumps, not node jumps
          flowStack: flowStack.filter(function (el, i) {
            return i === flowStack.length - 1 || flowStack[i + 1].flow !== el.flow;
          })
        });

        if (context.flowStack.length >= MAX_STACK_SIZE) {
          throw new Error(`Exceeded maximum flow stack size (${MAX_STACK_SIZE}).
         This might be due to an unexpected infinite loop in your flows.
         Current flow: ${context.currentFlow.name}
         Current node: ${context.node}`);
        }

        yield _this9._setContext(stateId, context);

        const beforeCtx = { stateId, node };
        yield _bluebird2.default.fromCallback(function (callback) {
          return _this9.onBeforeNodeEnter.run(beforeCtx, callback);
        });

        if (node.onEnter) {
          _this9._trace('!!', 'ENTR', '', context, userState);
          userState = yield _this9._processInstructions(node.onEnter, userState, event, context);
        }

        if (!node.onReceive) {
          _this9._trace('..', 'NOWT', '', context, userState);

          if (node.type === 'skill-call' && originalFlow !== node.flow) {
            userState = yield _this9._processNode(stateId, userState, context, node.flow, event);
          } else {
            userState = yield _this9._transitionToNextNodes(node, context, userState, stateId, event);
          }
        }
      } else {
        // i.e. we were already on that node before we received the message
        if (node.onReceive) {
          _this9._trace('!!', 'RECV', '', context, userState);
          userState = yield _this9._processInstructions(node.onReceive, userState, event, context);
        }

        _this9._trace('..', 'RECV', '', context, userState);

        if (node.type === 'skill-call' && originalFlow !== node.flow) {
          userState = yield _this9._processNode(stateId, userState, context, node.flow, event);
        } else {
          userState = yield _this9._transitionToNextNodes(node, context, userState, stateId, event);
        }
      }

      return userState;
    })();
  }

  _transitionToNextNodes(node, context, userState, stateId, event) {
    var _this10 = this;

    return _asyncToGenerator(function* () {
      const nextNodes = node.next || [];
      for (const nextNode of nextNodes) {
        if (_this10._evaluateCondition(nextNode.condition, userState, event)) {
          _this10._trace('??', 'MTCH', `cond = "${nextNode.condition}"`, context);
          if (/^end$/i.test(nextNode.node)) {
            // Node "END" or "end" ends the flow (reserved keyword)
            return _this10._endFlow(stateId);
          } else {
            return _this10._processNode(stateId, userState, context, nextNode.node, event);
          }
        }
      }

      if (!nextNodes.length) {
        // You reach this if there were no next nodes, in which case we end the flow
        return _this10._endFlow(stateId);
      }

      return userState;
    })();
  }

  _endFlow(stateId) {
    var _this11 = this;

    return _asyncToGenerator(function* () {
      const beforeCtx = { stateId };
      yield _bluebird2.default.fromCallback(function (callback) {
        return _this11.onBeforeEnd.run(beforeCtx, callback);
      });

      _this11._trace('--', 'ENDF', '', null, null);
      yield _this11.stateManager.deleteState(stateId, ['context']);

      return null;
    })();
  }

  _getOrCreateContext(stateId) {
    var _this12 = this;

    return _asyncToGenerator(function* () {
      let state = yield _this12._getContext(stateId);

      if (state && state.currentFlow) {
        return state;
      }

      const beforeCtx = { stateId, flowName: _this12.defaultFlow };
      yield _bluebird2.default.fromCallback(function (callback) {
        return _this12.onBeforeCreated.run(beforeCtx, callback);
      });

      const flow = yield _this12._findFlow(beforeCtx.flowName, true);

      if (!flow) {
        throw new Error(`Could not find the default flow "${_this12.defaultFlow}"`);
      }

      state = {
        currentFlow: flow,
        flowStack: [{ flow: flow.name, node: flow.startNode }]
      };

      yield _this12._setContext(stateId, state);

      yield _bluebird2.default.fromCallback(function (callback) {
        return _this12.onAfterCreated.run(_extends({}, beforeCtx), callback);
      });

      return state;
    })();
  }

  _getContext(stateId) {
    return this.stateManager.getState(stateId + '___context');
  }

  _setContext(stateId, state) {
    return this.stateManager.setState(stateId + '___context', state);
  }

  _gotoSubflow(nodeName, context) {
    var _this13 = this;

    return _asyncToGenerator(function* () {
      const [, subflow, subflowNode] = nodeName.match(callSubflowRegex);

      const flow = yield _this13._findFlow(subflow, true);

      return _extends({}, context, {
        currentFlow: flow,
        node: subflowNode || flow.startNode
      });
    })();
  }

  _gotoPreviousFlow(nodeName, context) {
    var _this14 = this;

    return _asyncToGenerator(function* () {
      if (!context.flowStack) {
        context = _extends({}, context, { flowStack: [] });
      } else {
        const flowStack = [...context.flowStack];
        const currentFlow = context.currentFlow.name;
        while (flowStack[flowStack.length - 1].flow === currentFlow) {
          flowStack.pop();
        }
        context = _extends({}, context, { flowStack });
      }

      if (context.flowStack.length < 1) {
        _this14._trace('Flow tried to go back to previous flow but there was none. Exiting flow.', context, null);
        // TODO END FLOW
        return context;
      }

      let { flow, node } = _lodash2.default.last(context.flowStack);

      if (nodeName !== '#') {
        node = nodeName.substr(1);
      }

      return _extends({}, context, {
        currentFlow: yield _this14._findFlow(flow, true),
        node
      });
    })();
  }

  _processInstructions(instructions, userState, event, context) {
    var _this15 = this;

    return _asyncToGenerator(function* () {
      if (!_lodash2.default.isArray(instructions)) {
        instructions = [instructions];
      }

      yield _bluebird2.default.mapSeries(instructions, (() => {
        var _ref = _asyncToGenerator(function* (instruction) {
          if (!_lodash2.default.isString(instruction) || !instruction.startsWith('say ')) {
            userState = yield _this15._invokeAction(instruction, userState, event, context);
            return;
          }

          const chunks = instruction.split(' ');
          if (chunks.length < 2) {
            _this15.trace('ERROR Invalid text instruction. Expected an instruction along "say #text Something"');
            return;
          }

          yield _this15._dispatchOutput({
            type: chunks[1], // e.g. "#text" or "#!trivia-12342"
            value: chunks.slice(2).join(' ') // e.g. Any additional parameter provided to the template
          }, userState, event, context);
        });

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

      return userState;
    })();
  }

  _dispatchOutput(output, userState, event, context) {
    var _this16 = this;

    return _asyncToGenerator(function* () {
      const msg = String(output.type + (output.value || '')).substr(0, 20);
      _this16._trace('~>', 'SEND', `"${msg}"`);

      return _bluebird2.default.map(_this16.outputProcessors, function (processor) {
        return processor.send({ message: output, state: userState, originalEvent: event, flowContext: context });
      });
    })();
  }

  _invokeAction(instruction, userState, event, context) {
    var _this17 = this;

    return _asyncToGenerator(function* () {
      let name = null;
      let args = {};

      if (_lodash2.default.isString(instruction)) {
        if (instruction.includes(' ')) {
          const chunks = instruction.split(' ');
          const argsStr = _lodash2.default.tail(chunks).join(' ');
          name = _lodash2.default.first(chunks);
          try {
            args = JSON.parse(argsStr);
            const actionCtx = { state: userState, s: userState, event: event, e: event };
            args = _lodash2.default.mapValues(args, function (value) {
              if (_lodash2.default.isString(value) && value.startsWith('{{') && value.endsWith('}}')) {
                const key = value.substr(2, value.length - 4);
                return _lodash2.default.get(actionCtx, key);
              }
              return value;
            });
          } catch (err) {
            throw new Error('ERROR function has invalid arguments (not a valid JSON string): ' + argsStr);
          }
        } else {
          name = instruction;
        }
      } else {
        _this17._trace(`ERROR function is not a valid string`);
      }

      if (!_this17.actions[name]) {
        _this17._trace(`ERROR function "${name}" not found`, context, userState);
      } else {
        try {
          _this17._trace('!!', 'EXEC', `func "${name}"`, context, userState);
          const ret = yield _this17.actions[name].fn(Object.freeze(userState), event, args || {});

          if (ret && _lodash2.default.isObject(ret)) {
            if (Object.isFrozen(ret)) {
              _this17._trace(`ERROR function "${name}" returned the original (frozen) state. You should clone the state (see 'Object.assign()') instead of returning the original state.`, context, userState);
            } else {
              _this17._trace('!!', 'SSET', '', context);
              return ret;
            }
          }
        } catch (err) {
          throw new Error(`ERROR function "${name}" thrown an error: ${err && err.message}`);
        }
      }

      return userState;
    })();
  }

  _evaluateCondition(condition, userState, event) {
    if (TRUEISH_WORDS[condition] || condition === '') {
      return true;
    }

    const vm = this.vm;
    vm.freeze(userState, 's');
    vm.freeze(userState, 'state');
    vm.freeze(event, 'event');
    vm.freeze(event, 'e');

    try {
      return !!vm.run(compileExp(condition));
    } catch (err) {
      throw new Error(`ERROR evaluating condition "${condition}": ${err.message}`);
    }
  }

  static _findNode(flow, nodeName, throwIfNotFound = false) {
    if (throwIfNotFound && !flow) {
      throw new Error(`Could not find node ${nodeName} because the flow was not defined (null)`);
    }

    const node = _lodash2.default.find(flow.nodes, { name: nodeName });

    if (throwIfNotFound && !node) {
      throw new Error(`Could not find node "${nodeName}" in flow "${flow.name}"`);
    }

    return node;
  }

  _findFlow(flowName, throwIfNotFound = false) {
    var _this18 = this;

    return _asyncToGenerator(function* () {
      const flows = yield _this18.getFlows();
      const flow = _lodash2.default.find(flows, { name: flowName });

      if (throwIfNotFound && !flow) {
        throw new Error(`Could not find flow "${flowName}"`);
      }

      return flow;
    })();
  }

  log(message, context) {
    const flow = _lodash2.default.get(context, 'currentFlow.name', 'NONE').replace(/\.flow\.json$/i, '');
    const node = context && context.node || 'NONE';
    const msg = `Dialog: [${flow} – ${node}]\t${message}`;
    this.logger.debug(msg);
  }

  /**
   * Dialog: [flow] (node) \t OP  RESN  "Description"
   * @param message
   * @param context
   * @param state
   * @private
   */
  _trace(operation, reason, message, context) {
    if (this.logger.level !== 'debug') {
      // don't do string formatting if we're not going to log it anyway
      return;
    }

    let flow = _lodash2.default.get(context, 'currentFlow.name', ' N/A ').replace(/\.flow\.json/i, '');
    let node = context && context.node || ' N/A ';

    flow = flow.length > 13 ? flow.substr(0, 13) + '&' : flow;
    node = node.length > 13 ? node.substr(0, 13) + '&' : node;

    const spc = _lodash2.default.repeat(' ', 30 - flow.length - node.length);

    const msg = `Dialog: [${flow}] (${node}) ${spc} ${operation}  ${reason} \t ${message}`;

    this.logger.debug(msg);
  }
}

module.exports = DialogEngine;
//# sourceMappingURL=engine.js.map