var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
  return typeof obj;
} : function (obj) {
  return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};

var toConsumableArray = function (arr) {
  if (Array.isArray(arr)) {
    for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];

    return arr2;
  } else {
    return Array.from(arr);
  }
};

var is = {
  undef: function undef(v) {
    return v === null || v === undefined;
  },
  notUndef: function notUndef(v) {
    return v !== null && v !== undefined;
  },
  func: function func(f) {
    return typeof f === 'function';
  },
  number: function number(n) {
    return typeof n === 'number';
  },
  string: function string(s) {
    return typeof s === 'string';
  },
  bool: function bool(s) {
    return s === true || s === false;
  },
  array: Array.isArray,
  object: function object(obj) {
    return obj && !is.array(obj) && (typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) === 'object';
  },
  promise: function promise(p) {
    return p && is.func(p.then);
  },
  generator: function generator(obj) {
    return is.func(obj.next) && is.func(obj.throw);
  },
  generatorFunc: function generatorFunc(obj) {
    var constructor = obj.constructor;
    if (!constructor) return false;
    if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
    return is.generator(constructor.prototype);
  }
};

var noop = function noop() {
  return null;
};

var sym = function sym(id) {
  return '@@mangojuice/' + id;
};

/**
 * A symbol for setting custom promise cancel function.
 * You can use it to specify some specific logic that
 * should be executed when some task canceled. Like for
 * `delay` function you can clear a timer
 * (see `delay` sources below).
 * @private
 * @type {string}
 */
var CANCEL = sym('CANCEL_PROMISE');

function autoInc() {
  var seed = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;

  return function () {
    return ++seed;
  };
}

var nextId = autoInc();

var identify = function identify(val) {
  if (!val.__id) {
    Object.defineProperty(val, '__id', {
      value: nextId(),
      enumerable: false,
      configurable: false
    });
  }
  return val.__id;
};

var fastFind = function fastFind(subject, comparator) {
  var length = subject.length;
  for (var i = 0; i < length; i++) {
    if (comparator(subject[i], i)) {
      return subject[i];
    }
  }
};

var fastForEach = function fastForEach(subject, iterator) {
  var length = subject.length;
  for (var i = 0; i < length; i++) {
    iterator(subject[i], i, subject);
  }
};

var fastMap = function fastMap(subject, iterator) {
  var length = subject.length;
  var result = new Array(length);
  for (var i = 0; i < length; i++) {
    result[i] = iterator(subject[i], i, subject);
  }
  return result;
};

var ensureError = function ensureError(err) {
  return !(err instanceof Error) ? new Error(err) : err;
};

var fastTry = function fastTry(fn) {
  try {
    return { result: fn(), error: null };
  } catch (err) {
    return { result: null, error: ensureError(err) };
  }
};

var maybeMap = function maybeMap(subject, mapper) {
  if (is.array(subject)) {
    return fastMap(subject, function (x) {
      return mapper(x);
    });
  } else if (is.notUndef(subject)) {
    return [mapper(subject, 0)];
  }
  return [];
};

var maybeForEach = function maybeForEach(subject, iterator) {
  if (is.array(subject)) {
    fastForEach(subject, function (x) {
      return iterator(x);
    });
  } else if (is.notUndef(subject)) {
    iterator(subject, 0);
  }
};

var maybeFind = function maybeFind(subject, comparator) {
  if (is.array(subject)) {
    return fastFind(subject, comparator);
  } else {
    return comparator(subject, 0);
  }
};

var deepForEach = function deepForEach(vals, fn) {
  maybeForEach(vals, function deepMapIterator(v) {
    if (is.array(v)) {
      deepForEach(v, fn);
    } else {
      fn(v);
    }
  });
};

function extend(obj, props) {
  if (props) {
    for (var i in props) {
      obj[i] = props[i];
    }
  }
  return obj;
}

var memoize = function memoize(func) {
  var data = void 0;
  var computed = false;
  var momoizer = function momoizer() {
    if (!computed) {
      computed = true;
      data = func.apply(undefined, arguments);
    }
    return data;
  };
  momoizer.computed = function () {
    return computed;
  };
  momoizer.reset = function () {
    data = undefined;
    computed = false;
  };
  return momoizer;
};

function safeExecFunction(logger, func, proc, cmd) {
  var _fastTry = fastTry(func),
      result = _fastTry.result,
      error = _fastTry.error;

  if (error && logger) {
    logger.onCatchError(error, proc, cmd);
  }
  return result;
}

/**
 * A helper function for delaying execution. Returns a Promise
 * which will be resolved in given amount of milliseconds. You can
 * use it in {@link task} to implement some delay in execution, for
 * debouncing for example.
 *
 * @param  {number}  ms  An amount of milliseconds to wait
 * @return {Promise} A promise that resolved after given amount of milliseconds
 */
function delay(ms) {
  var timeoutId = void 0;
  var res = new Promise(function (resolve) {
    timeoutId = setTimeout(function () {
      return resolve();
    }, ms);
  });
  res[CANCEL] = function () {
    return clearTimeout(timeoutId);
  };
  return res;
}


var utils = Object.freeze({
	is: is,
	noop: noop,
	sym: sym,
	CANCEL: CANCEL,
	autoInc: autoInc,
	nextId: nextId,
	identify: identify,
	fastFind: fastFind,
	fastForEach: fastForEach,
	fastMap: fastMap,
	ensureError: ensureError,
	fastTry: fastTry,
	maybeMap: maybeMap,
	maybeForEach: maybeForEach,
	maybeFind: maybeFind,
	deepForEach: deepForEach,
	extend: extend,
	memoize: memoize,
	safeExecFunction: safeExecFunction,
	delay: delay
});

/**
 * Class for declaring an async task. An instance of this object returned
 * from {@link taks} function.
 *
 * A task function is a function that could return a Promise. The resolved value
 * will be passed to a success command handler, the rejected command will be passed
 * to a fail command handler.
 *
 * If task function do not return a Promise, then the returned value passed to a
 * success command, and if the function throw an error then the error passed to
 * a fail command.
 *
 * The task function receive at least one argument – an object with `model`, `shared`
 * and `meta`. All the next arguments will be given from a command that returned a task,
 * if it is not overrided by {@link TaskCmd#args}
 *
 * By default a task is single-threaded. It means that every call to a task will cancel
 * previously running task if it is running. You can make the task to be multi-threaded
 * by {@link TaskCmd#multithread}, so every call to a task won't cancel the running one.
 *
 * In a context of the task function defined special `call` function. This function should
 * be used to invoke sub-tasks. It is important to use `call` to run sub-tasks because
 * `call` create a cancellation point of the task – if the task cancelled, then nothing
 * will be executed after currently running `call` sub-task.
 *
 * `call` returns an object with `result` and `error` fields. If `error` field is not
 * empty, then something weird happened in the sub-task. `call` internally have a try/catch,
 * so it is not necessary to wrap it with try/catch, just check the `error` field in the
 * returned object and if it is not empty – handle it somehow (throw it, or do any custom
 * error handling)
 *
 * Also in a context of the task defined a `notify` function. It is a function that execute
 * notify command if it is defined. The command executed with the same arguments as passed
 * to `notify` function. You can use it to incrementally update a model while the long
 * task is executing, to show some progress for the user.
 *
 * @example
 * async function SubTask(a, b, c) {
 *   this.notify(a + b + c);
 *   await this.call(delay, 100);
 *   return (a / b * c);
 * }
 * async function RootTask({ model }) {
 *   const { result, error } = await this.call(SubTask, 1, 0, 3);
 *   if (error) {
 *     throw new Error('Something weird happened in subtask')
 *   }
 *   return result + 10;
 * }
 * @example
 * class MyLogic {
 *   \@cmd AsyncCommand() {
 *     return task(Tasks.RootTask)
 *       .success(this.SuccessCommand)
 *       .fail(this.FailCommand)
 *       .notify(this.NotifyCommand)
 *   }
 * }
 * @class TaskCmd
 * @param {function} taskFn
 * @param {function} executor
 */
function TaskCmd(taskFn, executor) {
  this.task = taskFn;
  this.executor = executor;
  Object.defineProperty(this, 'id', { value: identify(taskFn) });
}

extend(TaskCmd.prototype, /** @lends TaskCmd.prototype */{
  /**
   * Set a success handler command. Will be executed with a value returned
   * from the task, or if the task returned a Promise – with resovled value.
   * @param  {Command} cmd
   * @return {TaskCmd}
   */
  success: function success(cmd) {
    this.successCmd = cmd;
    return this;
  },


  /**
   * Set a fail handler command. Will be executed with error throwed in the task,
   * or if the task returned a Promise – with rejected value.
   * @param  {Command} cmd
   * @return {TaskCmd}
   */
  fail: function fail(cmd) {
    this.failCmd = cmd;
    return this;
  },


  /**
   * Define the task to be "multi-thread", so every call
   * will run in parallel with other calls of the same task
   * in scope of one process (model).
   * @param  {boolean} val
   * @return {TaskCmd}
   */
  multithread: function multithread(val) {
    this.execEvery = is.undef(val) ? true : val;
    return this;
  },


  /**
   * Set task executor function. The executor function should return an object
   * that should have at least two fields: `exec` and `cancel` functions.
   * `exec` should return a promise which should be resolved or rejected
   * with an object `{ result, error }`. A `cancel` function should stop
   * the task execution.
   *
   * @param  {Function} engine  A function that returns
   *                            `{ exec: Function, cancel: Function }` object
   * @return {TaskCmd}
   */
  engine: function engine(_engine) {
    this.executor = _engine;
    return this;
  },


  /**
   * Override arguments which will be passed to the task
   * starting from second argument. By default a task will receive
   * the same set of arguments as a task command. If this function invoked,
   * then the task will receive given arguments instead of command arguments.
   * @param  {...any} args
   * @return {TaskCmd}
   */
  args: function args() {
    for (var _len = arguments.length, _args = Array(_len), _key = 0; _key < _len; _key++) {
      _args[_key] = arguments[_key];
    }

    this.customArgs = _args;
    return this;
  },
  cancel: function cancel() {
    this.cancelTask = true;
    return this;
  },
  execId: function execId(id) {
    this.customExecId = id;
    return this;
  },
  meta: function meta(metaObj) {
    this.metaObj = metaObj;
    return this;
  }
});

function Message(msgCreator, args) {
  extend(this, msgCreator.apply(undefined, toConsumableArray(args)));
  Object.defineProperty(this, '__name', { value: msgCreator.name });
  Object.defineProperty(this, '__creator', { value: msgCreator });
}

extend(Message.prototype, /** @lends LogicBase.prototype */{
  is: function is$$1(msgCreator) {
    var actualCreator = msgCreator && msgCreator.__creator || msgCreator;
    return this.__creator === actualCreator;
  },
  when: function when(msgCreator, func) {
    if (this.is(msgCreator)) {
      return func(this);
    }
  }
});

function message(msgCreator) {
  for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
    args[_key - 1] = arguments[_key];
  }

  return new Message(msgCreator, args);
}

/**
 * Describes child logic which will be attached to some model field.
 * Created by {@link child} function. Should be used in {@link LogicBase#children} to
 * define a logic that should be instantiated with some arguments passed
 * to {@link LogicBase#config} and {@link LogicBase#children}.
 *
 * @example
 * class MetaChild {
 *   config(amount) {
 *     return { meta: { amount } };
 *   }
 * }
 * class RootLogic {
 *   children() {
 *     return {
 *       modelField: child(MetaChild, 10)
 *       // `config` of `MetaChild` will be invoked with
 *       // `10` as a first argument
 *     }
 *   }
 * }
 * @class MetaChild
 * @property {LogicBase} logicClass  A Logic class you want to attach to the model
 * @property {Array<any>} args  A list of arguments that should be passed to `config` and
 *                              `children` methods of the logic.
 * @param {Array<Object>} logicClass
 * @param {Array<Object>} args
 */
function ChildCmd(logicClass) {
  this.logicClass = logicClass;
}

extend(ChildCmd.prototype, /** @lends ChildCmd.prototype */{
  update: function update(msgCreator) {
    for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
      args[_key - 1] = arguments[_key];
    }

    this.updateMsg = msgCreator instanceof Message ? msgCreator : message.apply(undefined, [msgCreator].concat(args));
    return this;
  },
  create: function create() {
    for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
      args[_key2] = arguments[_key2];
    }

    this.createArgs = args;
    return this;
  }
});

function ContextCmd(contextFn) {
  this.contextFn = contextFn;
  Object.defineProperty(this, 'id', { value: identify(contextFn) });
}

extend(ContextCmd.prototype, ChildCmd.prototype);
extend(ContextCmd.prototype, /** @lends ContextCmd.prototype */{
  subscribe: function subscribe() {
    this.subscribeVal = true;
    return this;
  },
  get: function get(requestFn) {
    this.requestFn = requestFn;
    return this;
  }
});

var ContextLogic = function ContextLogic() {};

extend(ContextLogic.prototype, {
  create: function create(creator, args) {
    return creator.apply(null, args);
  }
});

var ZERO_DELAY = false;

/**
 * Set all delays in the mangojuice to zero. It means, that:
 * - any `delay(...)` will be resolved right at the end of call stack
 * - eny cmd debounce/throttle will be executed right at the end of a call stack
 *
 * @private
 * @param  {bool} val
 */
var setZeroDelay = function setZeroDelay(val) {
  return ZERO_DELAY = !!val;
};

var config = Object.freeze({
	get ZERO_DELAY () { return ZERO_DELAY; },
	setZeroDelay: setZeroDelay
});

/**
 * An async task executor which aimed only to help with
 * delayed (throttle/debounce) command execution.
 * Supports to run in "debounce" and "throttle" mode,
 * with instant init call or without.
 *
 * @private
 */
function DebounceTask(proc, _ref) {
  var wait = _ref.wait,
      options = _ref.options;

  this.lastTask = undefined;
  this.maxWait = undefined;
  this.timerId = undefined;
  this.lastCallTime = undefined;
  this.lastInvokeTime = 0;
  this.leading = false;
  this.maxing = false;
  this.trailing = true;
  this.wait = +wait || 0;

  if (is.object(options)) {
    this.leading = !!options.leading;
    this.maxing = 'maxWait' in options;
    this.maxWait = this.maxing ? Math.max(+options.maxWait || 0, this.wait) : this.maxWait;
    this.trailing = 'trailing' in options ? !!options.trailing : this.trailing;
  }

  this.proc = proc;
  this.timerExpired = this.timerExpired.bind(this);
  this.finish = noop;
}

extend(DebounceTask.prototype, {
  exec: function exec(taskObj) {
    var time = Date.now();
    var isInvoking = this.shouldInvoke(time);
    this.ensureExecution();

    this.lastTask = taskObj;
    this.lastCallTime = time;

    if (isInvoking) {
      if (this.timerId === undefined) {
        this.leadingEdge(this.lastCallTime);
      } else if (this.maxing) {
        // Handle invocations in a tight loop.
        this.timerId = this.startTimer(this.timerExpired, this.wait);
        this.invokeFunc(this.lastCallTime);
      }
    } else if (this.timerId === undefined) {
      this.timerId = this.startTimer(this.timerExpired, this.wait);
    }

    return this.execution;
  },
  invokeFunc: function invokeFunc(time) {
    var _this = this;

    var _lastTask = this.lastTask,
        task = _lastTask.task,
        customArgs = _lastTask.customArgs;

    this.lastTask = undefined;
    this.lastInvokeTime = time;
    this.proc.exec(function () {
      return task.apply(_this.proc.logic, customArgs);
    });
  },
  startTimer: function startTimer(pendingFunc, wait) {
    return setTimeout(pendingFunc, wait);
  },
  cancelTimer: function cancelTimer(id) {
    clearTimeout(id);
  },
  leadingEdge: function leadingEdge(time) {
    // Reset any `maxWait` timer.
    this.lastInvokeTime = time;
    // Start the timer for the trailing edge.
    this.timerId = this.startTimer(this.timerExpired, this.wait);
    // Invoke the leading edge.
    if (this.leading) {
      this.invokeFunc(time);
    }
  },
  remainingWait: function remainingWait(time) {
    var timeSinceLastCall = time - this.lastCallTime;
    var timeSinceLastInvoke = time - this.lastInvokeTime;
    var timeWaiting = this.wait - timeSinceLastCall;

    return this.maxing ? Math.min(timeWaiting, this.maxWait - timeSinceLastInvoke) : timeWaiting;
  },
  shouldInvoke: function shouldInvoke(time) {
    var timeSinceLastCall = time - this.lastCallTime;
    var timeSinceLastInvoke = time - this.lastInvokeTime;

    // Either this is the first call, activity has stopped and we're at the
    // trailing edge, the system time has gone backwards and we're treating
    // it as the trailing edge, or we've hit the `maxWait` limit.
    return this.lastCallTime === undefined || timeSinceLastCall >= this.wait || timeSinceLastCall < 0 || this.maxing && timeSinceLastInvoke >= this.maxWait;
  },
  timerExpired: function timerExpired() {
    var time = Date.now();
    if (this.shouldInvoke(time)) {
      return this.trailingEdge(time);
    }
    // Restart the timer.
    this.timerId = this.startTimer(this.timerExpired, this.remainingWait(time));
  },
  trailingEdge: function trailingEdge(time) {
    this.timerId = undefined;

    // Only invoke if we have `lastArgs` which means `func` has been
    // debounced at least once.
    if (this.trailing && this.lastTask) {
      this.invokeFunc(time);
    } else {
      this.lastTask = undefined;
    }

    this.finalize();
  },
  cancel: function cancel() {
    if (this.timerId !== undefined) {
      this.cancelTimer(this.timerId);
    }
    this.lastInvokeTime = 0;
    this.lastTask = this.lastCallTime = this.timerId = undefined;
    this.finalize(null, { cancelled: true });
  },
  ensureExecution: function ensureExecution() {
    var _this2 = this;

    if (!this.execution) {
      this.execution = new Promise(function (r) {
        return _this2.finish = r;
      });
    }
  },
  finalize: function finalize(result, error) {
    this.finish({ result: result, error: error });
    this.execution = null;
    this.finish = noop;
  }
});

/**
 * A class which defins a logger for tracking command execution process.
 * The default implmenetation just defines all possible logger
 * methods (log points) and print errors to the console.
 * Extend this class to define your own logging functionality
 * (like logging errors with Setnry or Loggly)
 *
 * @class  DefaultLogger
 * @example
 * // ./index.js
 * import { DefaultLogger, run } from 'mangojuice-core';
 * import MainBlock from './blocks/MainBlock';
 *
 * // Implement custom logger
 * class SetnryLogger extends DefaultLogger {
 *   onCatchError(error, cmd) {
 *     Raven.catchException(error);
 *   }
 * }
 *
 * // Pass instance of the Logger to `run` options object.
 * run(MainBlock, { logger: new SetnryLogger() });
 */
function DefaultLogger() {}

extend(DefaultLogger.prototype, /** @lends DefaultLogger.prototype */{
  /**
   * This method invoked right before anything else - as the first thing
   * in {@link Process#exec}. Even before any {@link LogicBase#hubBefore}.
   * @param  {Command} cmd  Command that just started execution process
   */
  onStartExec: function onStartExec(proc, cmd) {},


  /**
   * Invoked right after the command go throught "after" hubs. It is the final
   * stage of command execution.
   * @param  {Command} cmd  Command that was fully executed and handled by parent blocks
   */
  onEndExec: function onEndExec(proc, cmd) {},


  /**
   * Invoked if any uncaught error happened while execution of the command
   * or anywhere else in the logic, like in {@link LogicBase#port} or in {@link LogicBase#computed}.
   * By default print the error using `console.error`.
   * @param  {Error} error  The error instance
   * @param  {?Command} cmd   Command that was executing while the error happened
   */
  onCatchError: function onCatchError(error, proc, cmd) {
    console.error(error);
  }
});

/**
 * Get a {@link Process} instance attached to the given model object.
 * Internally it get a `__proc` field from the given model and returns it.
 *
 * If given model do not have attached Process, then an Error will be throwed,
 * but only if the second argument is not true, which ignores the error.
 *
 * @param  {Object} model  A model with attached {@link Process}
 * @return {Process}  An instance of {@link Process} attached to the model
 */
function procOf(model) {
  return model && model.__proc;
}

function context(contextFn) {
  return new ContextCmd(contextFn);
}

/**
 * Queue run of model observer. The observer
 * will be executed ASAP afther the end of the stack.
 * @private
 * @param  {Process} proc
 * @return {Promise}
 */
function addEmptyStackHandler(proc, handler) {
  proc.internalContext.emptyStackHandlers[handler.id] = handler;
}

function observeModel(model, handler, options) {
  var modelProc = procOf(model);
  var type = options.type || 'observers';
  if (!modelProc[type]) return;

  // Wrap original handler to execute with error logger
  var finalHandler = function finalHandler(arg) {
    return safeExecFunction(modelProc.logger, function () {
      return handler(arg);
    });
  };

  // Wrap again to execute at the end of the commands stack
  if (options.batched) {
    var originalHandler = finalHandler;
    identify(originalHandler);
    finalHandler = function batchedObserver() {
      addEmptyStackHandler(modelProc, originalHandler);
    };
  }

  modelProc[type].push(finalHandler);

  var stopper = function removeObserver() {
    if (modelProc[type]) {
      modelProc[type] = modelProc[type].filter(function (x) {
        return x !== finalHandler;
      });
    }
  };

  return { stopper: stopper, model: model };
}

function observeContext(ctxFunc, handler, options) {
  var rootModel = options.model,
      batched = options.batched;

  var modelProc = procOf(rootModel);
  var model = null;

  modelProc.exec(context(ctxFunc).get(function (m) {
    return model = m;
  }));
  var stopper = observeModel(model, handler, { batched: batched, type: 'childrenObservers' });

  return { stopper: stopper, model: model };
}

/**
 * A function that adds a handler to the {@link Process} instance attached
 * to a given model, that will be invoked on every command that changed
 * the model. Useful for tracking changes to re-render a view of the model.
 *
 * @example
 * import { run, cmd, observe, logicOf } from 'mangojuice-core';
 *
 * class MyLogic {
 *   \@cmd MultipleUpdates() {
 *     return [
 *       this.UpdateOne,
 *       this.UpdateTwo
 *     ];
 *   }
 *   \@cmd UpdateOne() {
 *     return { one: this.model.one + 1 };
 *   }
 *   \@cmd UpdateTwo() {
 *     return { two: this.model.two + 1 };
 *   }
 * }
 *
 * const res = run({
 *   Logic: MyLogic,
 *   createModel: () => ({ one: 1, two: 1 })
 * });
 *
 * observe(res.model, () => console.log('not-batched'));
 * observe(res.model, () => console.log('batched'), { batched: true });
 *
 * res.proc.exec(logicOf(res.model).MultipleUpdates);
 *
 * // `not-batched` will be printed two times
 * // `batched` will be printed only once
 * @param  {Object} model      A model with attached {@link Process} instance
 * @param  {Function} handler  A function that will be invoked after every command
 *                             that changed the model.
 * @param  {?Object} options   An object with options
 * @param  {bool} options.batched  If true then the handler will be invoked only when
 *                                 the commands stack is empty and model was changed during
 *                                 the stack execution. Useful to reduce amount of not necessary
 *                                 view re-renderings.
 * @return {Function}
 */
function observe(model, handler) {
  var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};

  return is.func(model) ? observeContext(model, handler, options) : observeModel(model, handler, options);
}

function handle(model, handler) {
  return observe(model, handler, { type: 'handlers' });
}

/**
 * Creates an object which describes child logic which will be attached
 * to some model field. Should be used in {@link LogicBase#children} to
 * define a logic that should be instantiated with some arguments passed
 * to {@link LogicBase#config}.
 *
 * @example
 * class ChildLogic {
 *   config(amount) {
 *     return { meta: { amount } };
 *   }
 * }
 * class RootLogic {
 *   children() {
 *     return {
 *       modelField: child(ChildLogic, 10)
 *       // `config` of `ChildLogic` will be invoked with
 *       // `10` as a first argument
 *     }
 *   }
 * }
 * @param  {LogicBase} logicClass  Class that imlements {@link LogicBase} interface
 * @return {Object} Object that contains a logic class and an arguments array that
 *                  should be used to invoke `config` method of provided logic class.
 */
function child(logicClass) {
  return new ChildCmd(logicClass);
}

// Constants
var EMPTY_ARRAY = [];
var EMPTY_FINISHED = Promise.resolve();

function createInternalContext() {
  return {
    processes: {},
    stackCounter: 0,
    emptyStackHandlers: {}
  };
}

/**
 * Iterate through all active child processes and run
 * given iterator for every model.
 * @param  {Process} proc
 */
function forEachChildren(proc, iterator) {
  for (var id in proc.children) {
    iterator(proc.children[id]);
  }
}

function updateChildLogic(proc, model, childCmd) {
  var logicClass = childCmd.logicClass,
      updateMsg = childCmd.updateMsg,
      createArgs = childCmd.createArgs;

  var currProc = procOf(model);

  // Protect update of the model of the same type
  if (updateMsg && (!currProc || !(currProc.logic instanceof logicClass))) {
    proc.logger.onCatchError(new Error('Child model does not exists or have incorrect type'), proc);
    return;
  }

  // Re-run prepare with new config args if proc already running
  // with the same logic class, otherwise destroy the logic
  if (currProc) {
    if (!updateMsg) {
      currProc.destroy();
      currProc = null;
    } else {
      runLogicUpdate(currProc, updateMsg);
    }
  }

  // Run new process for given child definition if no proc was
  // binded to the model or if it was destroyed
  if (!currProc) {
    currProc = new proc.constructor({
      createArgs: createArgs,
      logicClass: logicClass,
      parent: proc,
      logger: proc.logger,
      contexts: proc.contexts,
      internalContext: proc.internalContext
    });
    currProc.bind(model);
    currProc.run();
    proc.children[currProc.id] = currProc;
  }

  return currProc;
}

/**
 * Create process and bind the process to child model
 * with given name. If model field is empty then do nothing.
 * If model already binded to some model do nothing.
 * Return true if model sucessfully binded to the process.
 * Othersise returns false
 *
 * @private
 * @param  {Object} childModel
 * @param  {String} fieldName
 * @return {Boolean}
 */
function bindChild(proc, model, key, childCmd) {
  var currModel = model[key] || {};
  var nextProc = updateChildLogic(proc, currModel, childCmd);
  if (nextProc) {
    model[key] = currModel;
  }
}

/**
 * Bind field in the model with given computed function or dependency
 * object.
 *
 * @private
 * @param  {Process} proc
 * @param  {string} fieldName
 * @param  {function} computeVal
 * @return {Memoize}
 */
function bindComputedFieldCmd(proc, model, key, computeVal) {
  var logger = proc.logger;

  var get = memoize(function () {
    return safeExecFunction(logger, computeVal);
  });
  proc.computedFields[key] = get;

  Object.defineProperty(model, key, {
    enumerable: true,
    configurable: true,
    set: noop,
    get: get
  });

  return get;
}

/**
 * Associate instance of the Process with given model by setting
 * hidden `__proc` field in the model. Also set model and shared model,
 *
 * @private
 * @param  {Process} proc
 * @param  {Object} model
 */
function bindModel(proc, model) {
  proc.model = model || {};
  proc.logic.model = proc.model;

  if (!model.__proc) {
    Object.defineProperty(model, '__proc', {
      value: proc,
      enumerable: false,
      configurable: true
    });
  }
}

function bindContext(proc, model, key, contextCmd) {
  var logger = proc.logger;
  var contextFn = contextCmd.contextFn,
      createArgs = contextCmd.createArgs,
      id = contextCmd.id;


  if (!proc.ownContext[id]) {
    var childDef = child(ContextLogic).create(contextFn, createArgs);
    bindChild(proc, model, key, childDef);
    proc.ownContext[id] = model[key];
  } else {
    logger.onCatchError(new Error('Context "' + contextFn.name + '" already created'));
  }
}

/**
 * Run init command depending on from what state the logic
 * is runinng. Run `logic.rehydrate` if some model was provided
 * and `prepare` if model was not provided and we need to create
 * it from scratch.
 * @param  {Process} proc
 */
function runLogicFunc(proc, name, args) {
  var exec = proc.exec,
      logic = proc.logic;

  var commandFactory = function commandFactory() {
    return logic[name] && logic[name].apply(logic, args);
  };
  exec(commandFactory);
}

function runLogicCreate(proc) {
  runLogicFunc(proc, 'create', proc.createArgs);
}

function runLogicUpdate(proc, msg) {
  runLogicFunc(proc, 'update', [msg]);
}

/**
 * Ren all model observers and returns a Promise which will
 * be resolved when all handlers executed and returned promises
 * is also resolved
 *
 * @private
 * @param  {Process} proc
 * @return {Promise}
 */
function runModelObservers(proc) {
  var observersIterator = function observersIterator(obs) {
    return obs();
  };
  maybeForEach(proc.observers, observersIterator);
  if (proc.parent) {
    maybeForEach(proc.childrenObservers, observersIterator);
  }
}

/**
 * Stop all running observers for all existing computed fields
 *
 * @private
 * @param  {Process} proc
 */
function destroyAttachedProcess(model) {
  var currProc = procOf(model);
  if (currProc) {
    currProc.destroy();
  }
}

function unsubscribeContexts(proc) {
  fastForEach(proc.contextSubs, function (u) {
    return u();
  });
}

function callLogicDestroy(proc) {
  if (!proc.logic.destroy) return;
  var safeDestroyLogic = function safeDestroyLogic() {
    return proc.logic.destroy();
  };
  safeExecFunction(proc.logger, safeDestroyLogic);
}

function findContext(proc, contextCmd) {
  var contexts = proc.contexts;
  var id = contextCmd.id;


  for (var i = 0; i < contexts.length; i++) {
    var ctx = contexts[i][id];
    if (ctx) return ctx;
  }

  return null;
}

function runEmptyStackHandlers(proc) {
  var internalContext = proc.internalContext;

  var handlersObj = internalContext.emptyStackHandlers;
  var handlerIds = Object.keys(handlersObj);
  var queuedObserversIterator = function queuedObserversIterator(handlerId) {
    return handlersObj[handlerId]();
  };

  internalContext.emptyStackHandlers = {};
  fastForEach(handlerIds, queuedObserversIterator);
}

function handleStackPush(proc) {
  var internalContext = proc.internalContext;

  internalContext.stackCounter += 1;
}

function handleStackPop(proc) {
  var internalContext = proc.internalContext;

  internalContext.stackCounter -= 1;

  if (internalContext.stackCounter <= 0) {
    internalContext.stackCounter = 0;
    runEmptyStackHandlers(proc);
  }
}

/**
 * Cancel task with given id in the process if exists
 * and can be cancelled (have `cancel` function defined)
 *
 * @private
 * @param  {Process} proc
 * @param  {nubmer} id
 */
function cancelTask(proc, taskId) {
  var taskProcs = proc.tasks[taskId];
  delete proc.tasks[taskId];

  for (var execId in taskProcs) {
    var taskCall = taskProcs[execId];
    if (taskCall && taskCall.cancel) {
      taskCall.cancel();
    }
  }
}

/**
 * Cancel all executing tasks of the process
 *
 * @private
 * @param  {Process} proc
 */
function cancelAllTasks(proc) {
  for (var taskId in proc.tasks) {
    cancelTask(proc, taskId);
  }
}

/**
 * Execute given task object in scope of given Process.
 * Returns a Promise which will be resolved when task will
 * be resolved or reject with with a command, which should
 * be executed next.
 *
 * @private
 * @param  {Process} proc
 * @param  {Task} taskObj
 * @return {Promise}
 */
function execTask(proc, taskObj) {
  var tasks = proc.tasks,
      logger = proc.logger,
      model = proc.model,
      sharedModel = proc.sharedModel,
      config = proc.config,
      logic = proc.logic;
  var task = taskObj.task,
      executor = taskObj.executor,
      notifyCmd = taskObj.notifyCmd,
      successCmd = taskObj.successCmd,
      failCmd = taskObj.failCmd,
      customArgs = taskObj.customArgs,
      execEvery = taskObj.execEvery,
      customExecId = taskObj.customExecId,
      taskId = taskObj.id;

  // If not in multi-thread mode or just need to cancel a tak –
  // cancel all running task with same identifier (command id)

  if (taskObj.cancelTask || !execEvery) {
    cancelTask(proc, taskId);
    if (taskObj.cancelTask) return;
  }

  // Define next execution id
  var execId = customExecId || nextId();
  var executions = tasks[taskId] = tasks[taskId] || {};
  var cleanup = function cleanup() {
    return delete executions[execId];
  };

  // Do not create any new task if the task with given exec id
  // already exists. Usefull for throttle/debounce tasks
  if (executions[execId]) {
    executions[execId].exec(taskObj);
  } else {
    var taskCall = executor(proc, taskObj);
    executions[execId] = taskCall;
    taskCall.exec(taskObj).then(cleanup, cleanup);
  }
}

function updateModelField(proc, model, key, update) {
  if (update instanceof ChildCmd) {
    bindChild(proc, model, key, update);
    return !update.updateMsg;
  } else if (update instanceof ContextCmd) {
    if (!update.createArgs) return false;
    bindContext(proc, model, key, update);
  } else if (is.func(update)) {
    bindComputedFieldCmd(proc, model, key, update);
  } else if (is.array(update)) {
    var nextModel = model[key] && model[key].slice(0, update.length) || [];
    fastForEach(update, function (nextUpdate, nextKey) {
      updateModelField(proc, nextModel, nextKey, nextUpdate);
    });
    if (model[key] && update.length !== model[key].length) {
      for (var i = update.length - 1; i < model[key].length; i++) {
        destroyAttachedProcess(model[key][i]);
      }
    }
    model[key] = nextModel;
  } else {
    if (model[key] === update) return false;
    destroyAttachedProcess(model[key]);
    model[key] = update;
  }
  return true;
}

/**
 * Update the binded model with update object passed as an argument.
 * Returns true when model was changed, otherwise returns false.
 * Also while updating process destryoy/create processes for
 * removed/added models.
 *
 * @private
 * @param  {?Object} updateObj
 * @return {Boolean}
 */
function updateModel(proc, updateObj) {
  if (!updateObj) return false;
  var modelUpdated = false;

  fastForEach(Object.keys(updateObj), function (k) {
    var res = updateModelField(proc, proc.model, k, updateObj[k]);
    modelUpdated = modelUpdated || res;
  });

  if (modelUpdated) {
    for (var k in proc.computedFields) {
      proc.computedFields[k].reset();
    }
  }

  return modelUpdated;
}

function updateContext(proc, contextCmd) {
  var logger = proc.logger,
      contexts = proc.contexts,
      model = proc.model,
      logic = proc.logic;
  var updateMsg = contextCmd.updateMsg,
      requestFn = contextCmd.requestFn,
      subscribeVal = contextCmd.subscribeVal;


  var ctxModel = findContext(proc, contextCmd);
  var ctxProc = procOf(ctxModel);
  if (!ctxModel || !ctxProc) {
    logger.onCatchError(new Error('Context of given type does not exist'), proc);
    return;
  }

  if (requestFn) {
    var safeExecReceiver = function safeExecReceiver() {
      return requestFn.call(logic, ctxModel);
    };
    proc.exec(safeExecReceiver);
  } else if (updateMsg) {
    forEachChildren(ctxProc, function (p) {
      return runLogicUpdate(p, updateMsg);
    });
  } else if (subscribeVal) {
    var _handle = handle(ctxModel, function (msg) {
      return runLogicUpdate(proc, msg);
    }),
        stopper = _handle.stopper;

    proc.contextSubs.push(stopper);
  }
}

function sendMessageToParents(proc, msg) {
  var updateLogic = function updateLogic(p) {
    return runLogicUpdate(p, msg);
  };
  var notifyHandler = function notifyHandler(h) {
    return h(msg);
  };
  var currParent = proc.parent;

  fastForEach(proc.handlers, notifyHandler);
  while (currParent) {
    updateLogic(currParent);
    fastForEach(currParent.handlers, notifyHandler);
    currParent = currParent.parent;
  }
}

/**
 * Actually execute the command. It iis running "before" handler,
 * then command itself, then "after" handlers and then emit
 * "update" event on the process if model was updated.
 * Returns a promise which will be resolved when a command
 * will be fully executed (command + all handlers).
 *
 * @private
 * @param  {Object} cmd
 */
function doExecCmd(proc, cmd) {
  var logger = proc.logger,
      exec = proc.exec,
      model = proc.model;

  var modelUpdated = false;

  // Prepare and run before handlers
  handleStackPush(proc);
  logger.onStartExec(proc, cmd);

  // Execute command
  if (cmd instanceof TaskCmd) {
    execTask(proc, cmd);
  } else if (cmd instanceof Message) {
    sendMessageToParents(proc, cmd);
  } else if (cmd instanceof ContextCmd) {
    updateContext(proc, cmd);
  } else if (cmd && (is.array(cmd) || is.func(cmd))) {
    fastForEach(cmd, function (x) {
      return exec(x);
    });
  } else if (is.object(cmd)) {
    modelUpdated = updateModel(proc, cmd);
  }

  // Emit model update for view re-rendering
  if (modelUpdated) {
    runModelObservers(proc);
  }

  // Run after handlers
  logger.onEndExec(proc, cmd);
  handleStackPop(proc);
}

/**
 * The main class of MangoJuice that ties model, logic and view together.
 *
 * It works in the following way:
 * - You create a model for your app (object) using `createModel` of
 *   the root block. The result is a plain object.
 * - Then you need to create an instance of Process and pass logic class
 *   in the options object.
 * - Then you `bind` the Process instance to the model object you created on step one.
 *   "To bind Process to a model" means that in the model will be created a hidden
 *   field `__proc` with a reference to the Process instance.
 * - After that you can `run` the Process, which will execute init commands, port.
 *
 * `bind` and `run` also look at the object returned from {@link LogicBase#children}
 * and create/run Process instances for children models.
 *
 * Most of the time you do not need to care about all of these and just use
 * {@link run} and {@link mount}. These small functions do everything described
 * above for you.
 *
 * @class Process
 * @example
 * import { Process, logicOf } from 'mangojuice-core';
 *
 * const ExampleBlock = {
 *   createModel() {
 *     return { a: 10 };
 *   },
 *   Logic: class Example {
 *     \@cmd Increment(amount = 1) {
 *       return { a: this.model.a + amount };
 *     }
 *   }
 * };
 *
 * const model = ExampleBlock.createModel();
 * const proc = new Process({ logic: ExampleBlock.Logic });
 * proc.bind(model);
 * proc.run();
 * proc.exec(logicOf(model).Increment(23));
 */
function Process(opts) {
  var parent = opts.parent,
      logger = opts.logger,
      logicClass = opts.logicClass,
      createArgs = opts.createArgs,
      internalContext = opts.internalContext,
      contexts = opts.contexts;


  this.id = nextId();
  this.logic = new logicClass();
  this.parent = parent;
  this.internalContext = internalContext || createInternalContext();
  this.logger = logger || new DefaultLogger();
  this.createArgs = createArgs || EMPTY_ARRAY;
  this.computedFields = {};
  this.tasks = {};
  this.observers = [];
  this.childrenObservers = [];
  this.children = {};
  this.handlers = [];
  this.contextSubs = [];
  this.ownContext = {};
  this.contexts = [this.ownContext].concat(contexts || EMPTY_ARRAY);
  this.exec = this.exec.bind(this);
}

extend(Process.prototype, /** @lends Process.prototype */{
  /**
   * Bind the process instance to a given model, which means that hidden
   * `__proc` field will be created in the model with a reference to the
   * Process instance.
   *
   * Also bind all children models – go through children definition returned
   * by {@link LogicBase#children}, create Process for each child and bind
   * it to an appropreat child model object.
   *
   * @param  {Object} model  A model object that you want to bind to the Process.
   */
  bind: function bind(model) {
    bindModel(this, model);
  },


  /**
   * Run the process – run children processes, then run port and
   * init commands defined in config.
   * Also run all model observers created by {@link observe}
   */
  run: function run() {
    this.internalContext.processes[this.id] = this;
    runLogicCreate(this);
    runModelObservers(this);
  },


  /**
   * Destroy the process with unbinding from the model and cleaning
   * up all the parts of the process (stop ports, computed fields).
   * `__proc` field will be removed from the model object and all
   * children objects.
   *
   * @param  {Boolean} deep   If true then all children blocks will be destroyed.
   *                          By default, if not provided then considered as true.
   */
  destroy: function destroy(deep) {
    this.computedFields = {};
    this.observers = [];
    this.childrenObservers = [];
    this.handlers = [];
    this.destroyed = true;

    delete this.model.__proc;
    delete this.internalContext.processes[this.id];
    if (this.parent) {
      delete this.parent.children[this.id];
    }
    unsubscribeContexts(this);
    cancelAllTasks(this);
    callLogicDestroy(this);

    if (deep !== false) {
      var childDestroyer = function childDestroyer(proc) {
        return proc.destroy(true);
      };
      forEachChildren(this, childDestroyer);
    }
  },


  /**
   * Exec a command in scope of the Process instance.
   * It will use binded model and logic object to run the
   * command. The command could be function (command factory)
   * or object ({@link Command} instance).
   *
   * A command origin function could return three types of things:
   * - Another {@link Command}/command factory or an array of commands
   *   (an element of the array also could be another array with commands
   *   or null/undefined/false). All commands will be execute in provided order.
   * - An instance of {@link TaskCmd} (which is usually created by {@link task}
   *   helper function). The returned task will be started and do not block
   *   execution of next commands in the stack.
   * - A plain object which is a model update object. The object will be merged
   *   with current model.
   *
   * If command is undefined or `Process` instance not binded
   * to any model it will do nothing.
   *
   * @example
   * class MyLogic {
   *   \@cmd BatchCommand() {
   *     return [
   *       1 > 2 && this.SomeCoomand,
   *       this.AnotheCommand(123),
   *       2 > 1 && [
   *         this.AndSomeOtherCommand,
   *         this.FinalCommand()
   *       ]
   *     ];
   *   }
   *   \@cmd TaskCommand() {
   *     return task(Tasks.SomeTask)
   *       .success(this.SuccessCommand)
   *       .fail(this.FailCommand)
   *   }
   *   \@cmd ModelUpdateCommand() {
   *     if (Math.random() > 0.6) {
   *       return { field: Date.now() };
   *     }
   *   }
   * }
   * @param  {Function|Object} cmd  Command factory or Command instance
   */
  exec: function exec(cmd) {
    var _this = this;

    if (!cmd || this.destroyed) return;

    if (is.func(cmd)) {
      var safeCommandCreate = function safeCommandCreate() {
        return cmd.call(_this.logic);
      };
      var nextCommand = safeExecFunction(this.logger, safeCommandCreate);
      return this.exec(nextCommand);
    }

    doExecCmd(this, cmd);
  },
  update: function update(message) {
    runLogicUpdate(this, message);
  },


  /**
   * Returns a promise which will be resolved when all async
   * tasks and related handlers in the all processes in the app
   * will be finished.
   * @return {Promise}
   */
  finished: function finished() {
    var _this2 = this;

    var promises = [];
    for (var procId in this.internalContext.processes) {
      var proc = this.internalContext.processes[procId];
      for (var taskId in proc.tasks) {
        for (var execId in proc.tasks[taskId]) {
          var execution = proc.tasks[taskId][execId].execution;
          if (is.promise(execution)) {
            promises.push(execution);
          }
        }
      }
    }

    return promises.length ? Promise.all(promises).then(function () {
      return _this2.finished();
    }) : EMPTY_FINISHED;
  }
});

/**
 * Basic class for every logic. It is not necessaty to inherit an actual logic
 * class from it, but for better type-checking it could be really usefull.
 *
 * Logic class defines:
 * - A set of commands that could change an associated model – methods decorated with {@link cmd}
 * - What fields of the model should be associated with which logic class
 *   (children blocks) – {@link LogicBase#children} method
 * - A set of computed model fields – {@link LogicBase#computed} method
 * - A set of rules to handle commands from children logic – {@link LogicBase#hubAfter}
 *   and {@link LogicBase#hubBefore} methods
 * - A logic to handle external events – {@link LogicBase#port} method.
 *
 * The logic class defines all the business-logic of some part of your application.
 * The main goal of the logic – change associated model. Check {@link cmd} and {@link Process#exec}
 * to have a better understanding how command can be defined and how it is executed.
 *
 * @class  LogicBase
 * @example
 * // ./blocks/MyBlock/Logic.js
 * // 
 * import type { Model } from './Model';
 * import ChildBlock from '../ChildBlock';
 * import { Command, cmd } from 'mangojuice-core';
 *
 * export default class MyLogic extends LogicBase<Model> {
 *   children() {
 *     return { child: ChildBlock.Logic };
 *   }
 *   hubAfter(cmd: Command) {
 *     if (cmd.is(ChildBlock.Logic.prototype.SomeCommand)) {
 *       return this.TestCommand(123);
 *     }
 *   }
 *   \@cmd TestCommand(arg) {
 *     return { field: arg + this.model.field };
 *   }
 * }
 * @property {Object} model   A model object which is associated with the
 *                            logic class in parent logic.
 * @property {Object} meta    An object defined in {@link LogicBase#config} method for storing
 *                            some internal logic state.
 * @property {Object} shared  An object with some shared state, defined while
 *                            run process of the app.
 */
function LogicBase() {}

extend(LogicBase.prototype, /** @lends LogicBase.prototype */{
  create: function create() {},
  update: function update() {},
  destroy: function destroy() {}
});

/**
 * Main task executor class, which set as a context for a
 * task function. Provides a way to execute a task, cancel
 * the task and wait for all subtasks to be finished.
 *
 * @private
 * @class AsyncTask
 * @param {Object|AsyncTask}   parent
 * @param {Function}          fn
 * @param {Array<any>}        args
 */
function AsyncTask(proc, taskObj) {
  this.proc = proc;
  this.taskObj = taskObj;
  this.execution = null;
}

extend(AsyncTask.prototype, /** @lends AsyncTask.prototype */{
  /**
   * Execute a task and handle the response. Returns
   * an array which will be executed when the task and all
   * subtasks finished
   * @return {Promise}
   */
  exec: function exec() {
    var _this = this;

    if (this.execution) {
      return this.execution;
    }

    var _taskObj = this.taskObj,
        task = _taskObj.task,
        customArgs = _taskObj.customArgs;

    var tryResult = fastTry(function () {
      return task.apply(_this.proc.logic, customArgs);
    });

    if (tryResult.error) {
      this.handleFail(tryResult.error);
      return Promise.resolve();
    } else if (!is.generator(tryResult.result)) {
      this.handleSuccess(tryResult.result);
      return Promise.resolve();
    } else {
      this.execution = this.iterate(tryResult.result).then(this.handleSuccess.bind(this), this.handleFail.bind(this));
      return this.execution;
    }
  },


  /**
   * Cancel this task and all children subtasks
   */
  cancel: function cancel() {
    this.execution = null;
    this.cancelled = true;
    if (this.defer) {
      this.defer.resolve();
    }
  },
  execCommand: function execCommand(cmdFn, arg) {
    var _this2 = this;

    if (this.cancelled) return;
    this.proc.exec(function () {
      return cmdFn && cmdFn.call(_this2.proc.logic, arg);
    });
  },
  handleSuccess: function handleSuccess(result) {
    this.execCommand(this.taskObj.successCmd, result);
  },
  handleFail: function handleFail(error) {
    var failCmd = this.taskObj.failCmd;

    if (failCmd) {
      this.execCommand(failCmd, error);
    } else {
      this.proc.logger.onCatchError(error, this.proc, this.taskObj);
    }
  },
  iterate: function iterate(gen) {
    var _this3 = this;

    return new Promise(function (resolve, reject) {
      if (!_this3.defer) {
        _this3.defer = { resolve: resolve, reject: reject };
      }
      if (is.func(gen)) {
        var tryResult = fastTry(function () {
          return gen.apply(_this3);
        });
        if (tryResult.error) return reject(tryResult.error);
        gen = tryResult.result;
      }
      if (!gen || !is.generator(gen)) {
        return resolve(gen);
      }

      var onFulfilled = function onFulfilled(res) {
        if (_this3.cancelled) return;

        var _fastTry = fastTry(function () {
          return gen.next(res);
        }),
            error = _fastTry.error,
            result = _fastTry.result;

        if (error) return reject(error);
        next(result);
        return null;
      };

      var onRejected = function onRejected(err) {
        if (_this3.cancelled) return;

        var _fastTry2 = fastTry(function () {
          return gen.throw(err);
        }),
            error = _fastTry2.error,
            result = _fastTry2.result;

        if (error) return reject(error);
        next(result);
      };

      var next = function next(ret) {
        if (_this3.cancelled) return;
        if (ret.done) return resolve(ret.value);
        var value = _this3.toPromise(ret.value);
        if (value && is.promise(value)) return value.then(onFulfilled, onRejected);
        return onRejected(new TypeError('You may only yield a function, promise, generator, array, object or message ' + 'but the following object was passed: "' + String(ret.value) + '"'));
      };

      onFulfilled();
    });
  },
  toPromise: function toPromise(obj) {
    if (!obj) return obj;
    if (is.promise(obj)) return obj;
    if (is.generatorFunc(obj) || is.generator(obj)) return this.iterate(obj);
    if (is.func(obj)) return this.thunkToPromise(obj);
    if (is.array(obj)) return this.arrayToPromise(obj);
    if (obj instanceof Message) return this.emitUpdateMessage(obj);
    if (obj.constructor === Object) return this.objectToPromise(obj);
    return obj;
  },
  emitUpdateMessage: function emitUpdateMessage(msg) {
    this.proc.update(msg);
    return Promise.resolve();
  },
  thunkToPromise: function thunkToPromise(fn) {
    var _this4 = this,
        _arguments = arguments;

    return new Promise(function (resolve, reject) {
      fn.call(_this4.proc.logic, function (err, res) {
        if (err) return reject(err);
        if (_arguments.length > 2) res = slice.call(_arguments, 1);
        resolve(res);
      });
    });
  },
  arrayToPromise: function arrayToPromise(obj) {
    return Promise.all(obj.map(this.toPromise, this));
  },
  objectToPromise: function objectToPromise(obj) {
    var results = new obj.constructor();
    var keys = Object.keys(obj);
    var promises = [];

    var defer = function defer(promise, key) {
      results[key] = undefined;
      promises.push(promise.then(function (res) {
        results[key] = res;
      }));
    };

    for (var i = 0; i < keys.length; i++) {
      var key = keys[i];
      var promise = this.toPromise(obj[key]);
      if (promise && is.promise(promise)) {
        defer(promise, key);
      } else {
        results[key] = obj[key];
      }
    }
    return Promise.all(promises).then(function () {
      return results;
    });
  }
});

function defaultTaskEngine(proc, taskObj) {
  return new AsyncTask(proc, taskObj);
}

/**
 * Creates a {@link TaskCmd} object that could be returned from
 * async task command. It describes the task that should be executed.
 *
 * For more information what the **Task** is and how it should be implemented
 * take a look to {@link TaskCmd}.
 *
 * @property {string} CANCEL  A symbol that you can use to make a promise "cancellable"
 * @param  {Function} taskFn  A function that could return a Promise
 * @return {Object}
 */
function task(taskFn) {
  return new TaskCmd(taskFn, defaultTaskEngine);
}

// Export CANCEL symbol to be able to make cancellable promises
task.CANCEL = CANCEL;

var DELAY_EXEC_ID = 'DELAYED';

function delayedExecTaskEngine(proc, taskObj) {
  return new DebounceTask(proc, taskObj.metaObj);
}

function debounce(wait, func, options) {
  return task(func).multithread().engine(delayedExecTaskEngine).execId(DELAY_EXEC_ID).meta({ wait: wait, options: options });
}

function throttle(wait, func, options) {
  var leading = true;
  var trailing = true;

  if (is.object(options)) {
    leading = 'leading' in options ? !!options.leading : leading;
    trailing = 'trailing' in options ? !!options.trailing : trailing;
  }

  return debounce(wait, func, { maxWait: wait, leading: leading, trailing: trailing });
}

/**
 * Returns a Logic instance attached to a given model object. The logic
 * instance stored in attached {@link Process} instance, so it execute
 * {@link procOf} for the model to get a {@link Process} instance and then
 * get the logic instance form a Process and return it.
 *
 * If Process is not attached to the model, then an Error will be throwed.
 * If the second argument passed then it will also check that the logic
 * instance is instance of some particular class.
 *
 * @param  {Object} model        A model with attached {@link Process}
 * @param  {Class?} logicClass   Optional logic class to check that the logic
 *                               instance is instance of the class
 * @return {LogicBase} Returns a logic instance
 */
function logicOf(model) {
  var proc = procOf(model);
  return proc && proc.logic;
}

/**
 * Run given block. It is a short-hand function for running {@link bind}
 * and then running a Process by {@link Process#run}.
 * Also it returns an additional `finished` Promise which is resolved
 * when all async tasks finished and all handler commands executed.
 *
 * @param  {Block} block  A block that you wan to run
 * @param  {Object} opts  Same options as in {@link bind}
 * @return {{ proc: Process, model: Object, block: Block, finished: Promise }}  An object
 *                  which is almost the same as returned from {@link bind}, but with
 *                  additional `finished` field, which is a Promise that will be resolved
 *                  when all blocks will execute all commands and all async tasks will be
 *                  finished.
 */
function run(block) {
  var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

  var ProcessClass = opts.Process || Process;
  var model = opts.model || {};
  var proc = new ProcessClass({
    logger: opts.logger,
    logicClass: block.Logic || block,
    createArgs: opts.args || []
  });

  proc.bind(model);
  proc.run();

  return model;
}

export { Process, LogicBase, DefaultLogger, child, context, debounce, throttle, message, task, observe, handle, logicOf, procOf, run, utils, config };
