lively.lang_function.js

/* global clearTimeout, setTimeout */

/**
 * Abstractions around first class functions like augmenting and inspecting
 * functions as well as to control function calls like dealing with asynchronous
 * control flows.
 * @module lively.lang/function
 */

import { merge as objectMerge, safeToString } from './object.js';
import Closure from './closure.js';

// -=-=-=-=-=-=-=-=-
// static functions
// -=-=-=-=-=-=-=-=-

function Empty () { /* `function() {}` */ return function () {}; }
function K () { /* `function(arg) { return arg; }` */ return function (arg) { return arg; }; }
function Null () { /* `function() { return null; }` */ return function () { return null; }; }
function False () { /* `function() { return false; }` */ return function () { return false; }; }
function True () { /* `function() { return true; }` */ return function () { return true; }; }

/**
* Throws a well behaved error on purpopse, when accessing unimplemented
* functionality.
* @param { string } what - The thing, which is not implemented.
* @param { boolean } strict - Whether or not to throw an actual error.
* @example
* notYetImplemented('5D rendering', true)
**/
function notYetImplemented (what, strict = false) {
  let error = new Error(`${what} is not yet implemented.`);
  $world.logError(error);
  if (strict) {
    throw error;
  }
}

/**
 * Returns wether or not a given function is a "built in".
 * Built in functions are native to the runtime and their
 * implementation can not be inspected from Javascript.
 * @param { function } fn - The function to check for.
 * @returns { boolean }
 */
function isNativeFunction (fn) {
  return Function.toString.call(fn).indexOf('[native code]') !== -1;
}

// -=-=-=-=-=-
// accessing
// -=-=-=-=-=-

/**
 * Returns all property names of a given object that reference a function.
 * @param { Object } obj - The object to return the property names for.
 * @returns { String[] }
 * @example
 * var obj = {foo: 23, bar: function() { return 42; }};
 * all(obj) // => ["bar"]
 */
function all (object) {
  const a = [];
  for (const name in object) {
    if (!object.__lookupGetter__(name) &&
     typeof object[name] === 'function') a.push(name);
  }
  return a;
}

/**
 * Returns all local (non-prototype) property names of a given object that
 * reference a function.
 * @param { Object } object - The object to return the property names for.
 * @returns { String[] }
 * @example
 * var obj1 = {foo: 23, bar: function() { return 42; }};
 * var obj2 = {baz: function() { return 43; }};
 * obj2.__proto__ = obj1
 * own(obj2) // => ["baz"]
 * all(obj2) // => ["baz","bar"]
 */
function own (object) {
  const a = [];
  for (const name in object) {
    if (!object.__lookupGetter__(name) &&
     object.hasOwnProperty(name) &&
     typeof object[name] === 'function') a.push(name);
  }
  return a;
}

// -=-=-=-=-=-
// inspection
// -=-=-=-=-=-

/**
 * Extract the names of all parameters for a given function object.
 * @param { function } f - The function object to extract the parameter names of.
 * @returns { String[] }
 * @example
 * argumentNames(function(arg1, arg2) {}) // => ["arg1","arg2"]
 * argumentNames(function() {}) // => []
 */
function argumentNames (f) {
  if (f.superclass) return []; // it's a class...
  const src = f.toString(); let names = '';
  const arrowMatch = src.match(/(?:\(([^\)]*)\)|([^\(\)-+!]+))\s*=>/);
  if (arrowMatch) names = arrowMatch[1] || arrowMatch[2] || '';
  else {
    const headerMatch = src.match(/^[\s\(]*function[^(]*\(([^)]*)\)/);
    if (headerMatch && headerMatch[1]) names = headerMatch[1];
  }
  return names.replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '')
    .replace(/\s+/g, '').split(',')
    .map(function (ea) { return ea.trim(); })
    .filter(function (name) { return !!name; });
}

/**
 * Return a qualified name for a given function object.
 * @param { function } f - The function object to determine the name of.
 * @returns { String }
 */
function qualifiedMethodName (f) {
  let objString = '';
  if (f.declaredClass) {
    objString += f.declaredClass + '>>';
  } else if (f.declaredObject) {
    objString += f.declaredObject + '.';
  }
  return objString + (f.methodName || f.displayName || f.name || 'anonymous');
}

/**
 * Useful when you have to stringify code but not want
 * to construct strings by hand.
 * @param { function } func - The function to extract the body from.
 * @returns { String }
 * @example
 * extractBody(function(arg) {
 *   var x = 34;
 *   alert(2 + arg);
 * }) => "var x = 34;\nalert(2 + arg);"
 */
function extractBody (func) {
  const codeString = String(func)
    .replace(/^function[^\{]+\{\s*/, '')
    .replace(/\}$/, '')
    .trim();
  const lines = codeString.split(/\n|\r/); let indent;
  for (let i = 0; i < lines.length; i++) {
    const m = lines[i].match(/^(\s+)[^\s]/);
    if (m && (indent === undefined || m[1].length < indent.length)) indent = m[1];
  }
  return indent ? codeString.replace(new RegExp('^' + indent, 'gm'), '') : codeString;
}

// -=-=-=-
// timing
// -=-=-=-

/**
 * Returns synchronous runtime of calling `func` in ms.
 * @param { function } func - The function to time.
 * @returns { number }
 * @example
 * timeToRun(function() { new WebResource("http://google.de").beSync().get() });
 * // => 278 (or something else...)
 */
function timeToRun (func) {
  const startTime = Date.now();
  func();
  return Date.now() - startTime;
}

/**
 * Like `timeToRun` but calls function `n` times instead of once. Returns
 * the average runtime of a call in ms.
 * @see timeToRun
 * @param { function } func - The function to time.
 * @param { number } n - The number of times to run the function.
 * @returns { number }
 */
function timeToRunN (func, n) {
  const startTime = Date.now();
  for (let i = 0; i < n; i++) func();
  return (Date.now() - startTime) / n;
}

/**
 * Delays calling `func` for `timeout` seconds(!).
 * @param { function } func - Function object to delay execution for.
 * @param { number } timeout - The duration in milliseconds to delay the execution for.
 * @example
 * (function() { alert("Run in the future!"); }).delay(1);
 */
function delay (func, timeout/*, arg1...argN */) {
  const args = Array.prototype.slice.call(arguments);
  const __method = args.shift();
  timeout = args.shift() * 1000;
  return setTimeout(function delayed () {
    return __method.apply(__method, args);
  }, timeout);
}

/**
 * Similar to `debounce` but instead of taking a custom time interval we
 * instead wait until the execution of the wrapped function has finished.
 * @param { function } func - The function to guard the execution for.
 * @returns { function } The wrapped function.
 */
function guard (func) {
  let active;
  return function () {
    if (!active) {
      active = true;
      const res = func();
      if (res instanceof Promise) res.then(() => active = false);
      else active = false;
      return res;
    }
  };
}

const _guardByName = {};

/**
 * Like `guard` but remembers the last invocation via a `name`.
 * Repeated calls to `guardNamed` with the same name will therefore be
 * guarded as if we were calling the same guarded closure repeatedly.
 * @param { string } name - The identifier to guard by.
 * @param { function } func - The function to guard.
 * @returns { function } The guarded function resolved via the identifier.
 */
function guardNamed (name, func) {
  const store = _guardByName;
  if (store[name]) return store[name];
  function guardNamedWrapper () {
    // ignore-in-doc, cleaning up
    let res;
    try {
      res = func.apply(this, arguments);
    } catch (err) {}
    if (res instanceof Promise) {
      res.finally(() => {
        delete store[name];
      });
    } else delete store[name];
    return res;
  }
  return store[name] = guard(guardNamedWrapper);
}

// these last two methods are Underscore.js 1.3.3 and are slightly adapted
// Underscore.js license:
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Underscore is distributed under the MIT license.

/**
 * Exec func at most once every wait ms even when called more often
 * useful to calm down eagerly running updaters and such.
 * This may very likely drop the last couple of calls so if
 * you need a guarantee for the last call to "complete successfully"
 * throttle is not the right choice.
 * @param { function } func - The function to be wrapped.
 * @param { number } wait - The duration of time in milliseconds to wait until the throttle is suspended.
 * @example
 * var i = 0;
 * var throttled = throttle(function() { alert(++i + '-' + Date.now()) }, 500);
 * Array.range(0,100).forEach(function(n) { throttled() });
 */
function throttle (func, wait) {
  let context; let args; let timeout; let throttling; let more; let result;
  const whenDone = debounce(wait, function () { more = throttling = false; });
  return function () {
    context = this; args = arguments;
    const later = function () {
      timeout = null;
      if (more) func.apply(context, args);
      whenDone();
    };
    if (!timeout) timeout = setTimeout(later, wait);
    if (throttling) {
      more = true;
    } else {
      result = func.apply(context, args);
    }
    whenDone();
    throttling = true;
    return result;
  };
}

/**
 * Call `func` after `wait` milliseconds elapsed since the last invocation.
 * Unlike `throttle` an invocation will restart the wait period. This is
 * useful if you have a stream of events that you want to wait for to finish
 * and run a subsequent function afterwards. When you pass arguments to the
 * debounced functions then the arguments from the last call will be use for
 * the invocation.
 * @param { number } wait - The duration in milliseconds to wait until the next invocation.
 * @param { function } func - The founction to be wrapped.
 * @param { Boolean } immediate - When set to true, immediately call `func` but when called again during `wait` before wait ms are done nothing happens. E.g. to not exec a user invoked action twice accidentally.
 * @example
 * var start = Date.now();
 * var f = debounce(200, function(arg1) {
 *   alert("running after " + (Date.now()-start) + "ms with arg " + arg1);
 * });
 * f("call1");
 * delay(curry(f, "call2"), 0.1);
 * delay(curry(f, "call3"), 0.15);
 * // => Will eventually output: "running after 352ms with arg call3"
 */
function debounce (wait, func, immediate) {
  let timeout;
  return function () {
    const context = this; const args = arguments;
    const later = function () {
      timeout = null;
      if (!immediate) func.apply(context, args);
    };
    if (immediate && !timeout) func.apply(context, args);
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

const _throttledByName = {};
/**
 * Like `throttle` but remembers the throttled function once created and
 * repeated calls to `throttleNamed` with the identical name will use the same
 * throttled function. This allows to throttle functions in a central place
 * that might be called various times in different contexts without having to
 * manually store the throttled function.
 * @param { String } name - The identifier for the throttled closure.
 * @see throttle.
 */
function throttleNamed (name, wait, func) {
  const store = _throttledByName;
  if (store[name]) return store[name];
  function throttleNamedWrapper () {
    // ignore-in-doc, cleaning up
    debounceNamed(name, wait, function () { delete store[name]; })();
    return func.apply(this, arguments);
  }
  return store[name] = throttle(throttleNamedWrapper, wait);
}

const _debouncedByName = {};
/**
 * Like `debounce` but remembers the debounced function once created and
 * repeated calls to `debounceNamed` with the identical name will use the same
 * debounced function. This allows to debounce functions in a central place
 * that might be called various times in different contexts without having to
 * manually store the debounced function.
 * @param { String } name - The identifier for the debounced closure.
 * @see debounce
 */
function debounceNamed (name, wait, func, immediate) {
  const store = _debouncedByName;
  if (store[name]) return store[name];
  function debounceNamedWrapper () {
    // ignore-in-doc, cleaning up
    delete store[name];
    return func.apply(this, arguments);
  }
  return store[name] = debounce(wait, debounceNamedWrapper, immediate);
}

const _queues = {};

/**
 * @typedef { Object } WorkerQueue
 * @property { function } push - Handles the addition of a single task to the queue.
 * @property { function } pushAll - Handles the addition of multiple tasks to the queue.
 * @property { function } handleError - Callback to handle errors that appear in a task.
  * @property { function } drain - Callback that is run once the queue empties.
 */

/**
 * Creates and initializes a worker queue.
 * @param { string } id - The identifier for the worker queue.
 * @param { function } workerFunc - Asynchronous function to process queued tasks.
 * @returns { WorkerQueue }
 * @example
 * var sum = 0;
 * var q = createQueue("example-queue", function(arg, thenDo) { sum += arg; thenDo(); });
 * q.pushAll([1,2,3]);
 * queues will be remembered by their name
 * createQueue("example-queue").push(4);
 * sum // => 6
 */
function createQueue (id, workerFunc) {
  const store = _queues;

  let queue = store[id] || (store[id] = {
    _workerActive: false,
    worker: workerFunc,
    tasks: [],
    drain: null, // can be overwritten by a function
    push: function (task) {
      queue.tasks.push(task);
      if (!queue._workerActive) queue.activateWorker();
    },
    pushAll: function (tasks) {
      tasks.forEach(function (ea) { queue.tasks.push(ea); });
      if (!queue._workerActive) queue.activateWorker();
    },
    pushNoActivate: function (task) {
      queue.tasks.push(task);
    },
    handleError: function (err) {
      // can be overwritten
      err && console.error('Error in queue: ' + err);
    },
    activateWorker: function () {
      function callback (err) { queue.handleError(err); queue.activateWorker(); }
      const tasks = queue.tasks; const active = queue._workerActive;
      if (tasks.length === 0) {
        if (active) {
          queue._workerActive = false;
          if (typeof queue.drain === 'function') queue.drain();
        }
        delete store[id];
      } else {
        if (!active) queue._workerActive = true;
        try {
          queue.worker(tasks.shift(), callback);
        } catch (err) { callback(err); }
      }
    }
  });

  return queue;
}

const _queueUntilCallbacks = {};
/**
 * This functions helps when you have a long running computation that
 * multiple call sites (independent from each other) depend on. This
 * function does the housekeeping to start the long running computation
 * just once and returns an object that allows to schedule callbacks
 * once the workerFunc is done.
 * This is how it works:
 * If `id` does not exist, workerFunc is called, otherwise ignored.
 * workerFunc is expected to call thenDoFunc with arguments: error, arg1, ..., argN
 * if called subsequently before workerFunc is done, the other thenDoFunc
 * will "pile up" and called with the same arguments as the first
 * thenDoFunc once workerFunc is done.
 * @see createQueue
 * @param { number } optTimeout - The timeout for slow running tasks in milliseconds.
 * @example
 * var worker = workerWithCallbackQueue("example",
 *   function slowFunction(thenDo) {
 *     var theAnswer = 42;
 *     setTimeout(function() { thenDo(null, theAnswer); });
 *   });
 * // all "call sites" depend on `slowFunction` but don't have to know about
 * // each other
 * worker.whenDone(function callsite1(err, theAnswer) { alert("callback1: " + theAnswer); })
 * worker.whenDone(function callsite2(err, theAnswer) { alert("callback2: " + theAnswer); })
 * workerWithCallbackQueue("example").whenDone(function callsite3(err, theAnswer) { alert("callback3: " + theAnswer); })
 * // => Will eventually show: callback1: 42, callback2: 42 and callback3: 42
 */
function workerWithCallbackQueue (id, workerFunc, optTimeout) {
  const store = _queueUntilCallbacks;
  let queueCallbacks = store[id];
  const isRunning = !!queueCallbacks;

  if (isRunning) return queueCallbacks;

  let callbacksRun = false; let canceled = false;

  function cleanup () {
    if (timeoutProc) clearTimeout(timeoutProc);
    callbacksRun = true;
    delete store[id];
  }

  function runCallbacks (args) {
    if (callbacksRun) return;
    cleanup();
    queueCallbacks.callbacks.forEach(function (cb) {
      try { cb.apply(null, args); } catch (e) {
        console.error(
          'Error when invoking callbacks in queueUntil [' +
          id + ']:\n' +
          (String(e.stack || e)));
      }
    });
  }

  // timeout
  let timeoutProc;
  if (optTimeout) {
    timeoutProc = setTimeout(function () {
      if (callbacksRun) return;
      runCallbacks([new Error('timeout')]);
    }, optTimeout);
  }

  // init the store
  queueCallbacks = store[id] = {
    callbacks: [],
    cancel: function () {
      canceled = true;
      cleanup();
    },
    whenDone: function (cb) {
      queueCallbacks.callbacks.push(cb);
      return queueCallbacks;
    }
  };

  // call worker, but delay so we can immediately return
  setTimeout(function () {
    if (canceled) return;
    try {
      workerFunc(function (/* args */) { runCallbacks(arguments); });
    } catch (e) { runCallbacks([e]); }
  }, 0);

  return queueCallbacks;
}

function _composeAsyncDefaultEndCallback (err, arg1/* err + args */) {
  if (err) console.error('lively.lang.composeAsync error', err);
}

/**
 * Composes functions that are asynchronous and expecting continuations to
 * be called in node.js callback style (error is first argument, real
 * arguments follow).
 * A call like `composeAsync(f,g,h)(arg1, arg2)` has a flow of control like:
 *  `f(arg1, arg2, thenDo1)` -> `thenDo1(err, fResult)`
 * -> `g(fResult, thenDo2)` -> `thenDo2(err, gResult)` ->
 * -> `h(fResult, thenDo3)` -> `thenDo2(err, hResult)`
 * @param { ...function } functions - The collections of asynchronous functions to compose.
 * @return { function }
 * @example
 * composeAsync(
 *   function(a,b, thenDo) { thenDo(null, a+b); },
 *   function(x, thenDo) { thenDo(x*4); }
 *  )(3,2, function(err, result) { alert(result); });
 */
function composeAsync (/* functions */) {
  const toArray = Array.prototype.slice;
  const functions = toArray.call(arguments);
  const defaultEndCb = _composeAsyncDefaultEndCallback;
  let endCallback = defaultEndCb;
  let endSuccess; let endFailure;
  const endPromise = new Promise(function (resolve, reject) {
    endSuccess = resolve; endFailure = reject;
  });

  return functions.reverse().reduce(function (prevFunc, funcOrPromise, i) {
    let nextActivated = false;
    return function () {
      const args = toArray.call(arguments);

      // the last arg needs to be function, discard all non-args
      // following it. This allows to have an optional callback func that can
      // even be `undefined`, e.g. when calling this func from a callsite
      // using var args;
      if (endCallback === defaultEndCb && i === functions.length - 1/* first function */) {
        while (args.length && typeof args[args.length - 1] !== 'function') args.pop();
        if (typeof args[args.length - 1] === 'function') endCallback = args.pop();
      }

      function next (/* err and args */) {
        nextActivated = true;
        const args = toArray.call(arguments);
        const err = args.shift();
        if (err) { endCallback(err); endFailure(err); } else prevFunc.apply(null, args);
      }

      if (typeof funcOrPromise === 'function') {
        try {
          const result = funcOrPromise.apply(this, args.concat([next]));
          if (result && typeof result.then === 'function' && typeof result.catch === 'function') {
            result
              .then(function (value) { return next(null, value); })
              .catch(function (err) { return next(err); });
          }
        } catch (e) {
          console.error('composeAsync: ', e.stack || e);
          if (!nextActivated) { endCallback(e); endFailure(e); }
        }
      } else if (funcOrPromise && typeof funcOrPromise.then === 'function' && typeof funcOrPromise.catch === 'function') {
        funcOrPromise
          .then(function (value) { next(null, value); })
          .catch(function (err) { next(err); });
      } else {
        const err = new Error('Invalid argument to composeAsync: ' + funcOrPromise);
        endCallback(err);
        endFailure(err);
      }

      return endPromise;
    };
  }, function () {
    const args = toArray.call(arguments);
    endCallback.apply(null, [null].concat(args));
    endSuccess(args[0]);
  });
}

/**
 * Composes a set of synchronous functions:
 * `compose(f,g,h)(arg1, arg2)` = `h(g(f(arg1, arg2)))`
 * @param { ...function } functions - The collections of functions to compose.
 * @returns { function }
 * @example
 * compose(
 *   function(a,b) { return a+b; },
 *   function(x) {return x*4}
 * )(3,2) // => 20
 */
function compose (/* functions */) {
  const functions = Array.prototype.slice.call(arguments);
  return functions.reverse().reduce(
    function (prevFunc, func) {
      return function () {
        return prevFunc(func.apply(this, arguments));
      };
    }, function (x) { return x; });
}
/**
 * Swaps the first two args
 * @param { function } f - Function to flip the arguments for.
 * @returns { function }
 * @example
 * flip(function(a, b, c) {
 *   return a + b + c; })(' World', 'Hello', '!') // => "Hello World!"
 */
function flip (f) {
  return function flipped (/* args */) {
    const args = Array.prototype.slice.call(arguments);
    const flippedArgs = [args[1], args[0]].concat(args.slice(2));
    return f.apply(null, flippedArgs);
  };
}

/**
 * Returns a modified version of func that will have `null` always curried
 * as first arg. Usful e.g. to make a nodejs-style callback work with a
 * then-able.
 * @param { function } func - The function to modify.
 * @returns { function }
 * @example
 * promise.then(withNull(cb)).catch(cb);
 */
function withNull (func) {
  func = func || function () {};
  return function (/* args */) {
    const args = Array.from(arguments);
    func.apply(null, [null].concat(args));
  };
}

/**
 * Wait for waitTesterFunc to return true, then run thenDo, passing
 * failure/timout err as first parameter. A timout occurs after
 * timeoutMs. During the wait period waitTesterFunc might be called
 * multiple times.
 * @param { number } timeoutMs - The milliseconds to wait for at max.
 * @param { function } waitTesterFunc - The testing function.
 * @param { function } thenDo - Callback that is invoked once the condition is met.
 */
function waitFor (timeoutMs, waitTesterFunc, thenDo) {
  const start = Date.now();
  let timeStep = 50;
  if (!thenDo) {
    thenDo = waitTesterFunc;
    waitTesterFunc = timeoutMs;
    timeoutMs = undefined;
  }
  (function test () {
    if (waitTesterFunc()) return thenDo();
    if (timeoutMs) {
      const duration = Date.now() - start;
      const timeLeft = timeoutMs - duration;
      if (timeLeft <= 0) return thenDo(new Error('timeout'));
      if (timeLeft < timeStep) timeStep = timeLeft;
    }
    setTimeout(test, timeStep);
  })();
}

/**
 * Wait for multiple asynchronous functions. Once all have called the
 * continuation, call `thenDo`.
 * @param { Object } [options] - A set of configuration options.
 * @param { number } options.timeout - How long to wait in milliseconds.
 * @param { function[] } funcs - The set of functions ot wait for.
 * @param { function } thenDo - The callback to invoke after the wait finishes.
 */
function waitForAll (options, funcs, thenDo) {
  if (!thenDo) { thenDo = funcs; funcs = options; options = null; }
  options = options || {};

  const results = funcs.map(function () { return null; });
  if (!funcs.length) { thenDo(null, results); return; }

  const leftFuncs = Array.prototype.slice.call(funcs);

  funcs.forEach(function (f, i) {
    try {
      f(function (/* err and args */) {
        const args = Array.prototype.slice.call(arguments);
        const err = args.shift();
        markAsDone(f, i, err, args);
      });
    } catch (e) { markAsDone(f, i, e, null); }
  });

  if (options.timeout) {
    setTimeout(function () {
      if (!leftFuncs.length) return;
      const missing = results
        .map(function (ea, i) { return ea === null && i; })
        .filter(function (ea) { return typeof ea === 'number'; })
        .join(', ');
      const err = new Error('waitForAll timed out, functions at ' + missing + ' not done');
      markAsDone(null, null, err, null);
    }, options.timeout);
  }

  function markAsDone (f, i, err, result) {
    if (!leftFuncs.length) return;

    let waitForAllErr = null;
    const fidx = leftFuncs.indexOf(f);
    (fidx > -1) && leftFuncs.splice(fidx, 1);
    if (err) {
      leftFuncs.length = 0;
      waitForAllErr = new Error('in waitForAll at' +
        (typeof i === 'number' ? ' ' + i : '') +
        ': \n' + (err.stack || String(err)));
    } else if (result) results[i] = result;
    if (!leftFuncs.length) {
      setTimeout(function () {
        thenDo(waitForAllErr, results);
      }, 0);
    }
  }
}

// -=-=-=-=-
// wrapping
// -=-=-=-=-

/**
 * Return a version of `func` with args applied.
 * @param { function } func - The function to curry.
 * @example
 * var add1 = (function(a, b) { return a + b; }).curry(1);
 * add1(3) // => 4
 */
function curry (func, ...curryArgs) {
  if (arguments.length <= 1) return arguments[0];
  const args = Array.prototype.slice.call(arguments);
  func = args.shift();
  function wrappedFunc () {
    return func.apply(this, args.concat(Array.prototype.slice.call(arguments)));
  }
  wrappedFunc.isWrapper = true;
  wrappedFunc.originalFunction = func;
  return wrappedFunc;
}

/**
 * A `wrapper` is another function that is being called with the arguments
 * of `func` and a proceed function that, when called, runs the originally
 * wrapped function.
 * @param { function } func - The function to wrap.
 * @param { function } wrapper - The function to wrap the other one.
 * @returns { function }
 * @example
 * function original(a, b) { return a+b }
 * var wrapped = wrap(original, function logWrapper(proceed, a, b) {
 *   alert("original called with " + a + "and " + b);
 *   return proceed(a, b);
 * })
 * wrapped(3,4) // => 7 and a message will pop up
 */
function wrap (func, wrapper) {
  const __method = func;
  const wrappedFunc = function wrapped () {
    const args = Array.prototype.slice.call(arguments);
    const wrapperArgs = wrapper.isWrapper
      ? args
      : [__method.bind(this)].concat(args);
    return wrapper.apply(this, wrapperArgs);
  };
  wrappedFunc.isWrapper = true;
  wrappedFunc.originalFunction = __method;
  return wrappedFunc;
}

/**
 * Get the original function that was augmented by `wrap`. `getOriginal`
 * will traversed as many wrappers as necessary.
 * @param { function } wrappedFunc - The wrapped function to retrieve the original from.
 * @returns { function }
 */
function getOriginal (wrappedFunc) {
  while (wrappedFunc.originalFunction) wrappedFunc = wrappedFunc.originalFunction;
  return wrappedFunc;
}

/**
 * Function wrappers used for wrapping, cop, and other method
 * manipulations attach a property "originalFunction" to the wrapper. By
 * convention this property references the wrapped method like wrapper
 * -> cop wrapper -> real method.
 * tThis method gives access to the linked list starting with the outmost
 * wrapper.
 * @param { function } method - A function that has been wrapped potentially multiple times.
 * @returns { function[] }
 */
function wrapperChain (method) {
  const result = [];
  do {
    result.push(method);
    method = method.originalFunction;
  } while (method);
  return result;
}

/**
 * Change an objects method for a single invocation.
 * @param { object } obj -
 * @param { string } methodName -
 * @param { function } replacement -
 * @returns { object }
 * @example
 * var obj = {foo: function() { return "foo"}};
 * lively.lang.replaceMethodForOneCall(obj, "foo", function() { return "bar"; });
 * obj.foo(); // => "bar"
 * obj.foo(); // => "foo"
 */
function replaceMethodForOneCall (obj, methodName, replacement) {
  replacement.originalFunction = obj[methodName];
  const reinstall = obj.hasOwnProperty(methodName);
  obj[methodName] = function () {
    if (reinstall) obj[methodName] = replacement.originalFunction;
    else delete obj[methodName];
    return replacement.apply(this, arguments);
  };
  return obj;
}

/**
 * Ensure that `func` is only executed once. Multiple calls will not call
 * `func` again but will return the original result.
 * @param { function } func - The function to be wrapped to only execute once.
 * @returns { function }
 */
function once (func) {
  if (!func) return undefined;
  if (typeof func !== 'function') { throw new Error('once() expecting a function'); }
  let invoked = false; let result;
  return function () {
    if (invoked) return result;
    invoked = true;
    return result = func.apply(this, arguments);
  };
}

/**
 * Accepts multiple functions and returns an array of wrapped
 * functions. Those wrapped functions ensure that only one of the original
 * function is run (the first on to be invoked).
 *
 * This is useful if you have multiple asynchronous choices of how the
 * control flow might continue but want to ensure that a continuation
 * is  only triggered once, like in a timeout situation:
 *
 * ```js
 * function outerFunction(callback) {
 *   function timeoutAction() { callback(new Error('timeout!')); }
 *   function otherAction() { callback(null, "All OK"); }
 *   setTimeout(timeoutAction, 200);
 *   doSomethingAsync(otherAction);
 * }
 * ```
 *
 * To ensure that `callback` only runs once you would normally have to write boilerplate like this:
 *
 * ```js
 * var ran = false;
 * function timeoutAction() { if (ran) return; ran = true; callback(new Error('timeout!')); }
 * function otherAction() { if (ran) return; ran = true; callback(null, "All OK"); }
 * ```
 *
 * Since this can get tedious an error prone, especially if more than two choices are involved, `either` can be used like this:
 * @example
 * function outerFunction(callback) {
 *   var actions = either(
 *     function() { callback(new Error('timeout!')); },
 *     function() { callback(null, "All OK"); });
 *   setTimeout(actions[0], 200);
 *   doSomethingAsync(actions[1]);
 * }
 */
function either (/* funcs */) {
  const funcs = Array.prototype.slice.call(arguments); let wasCalled = false;
  return funcs.map(function (func) {
    return function () {
      if (wasCalled) return undefined;
      wasCalled = true;
      return func.apply(this, arguments);
    };
  });
}

const _eitherNameRegistry = {};

/**
 * Works like [`either`](#) but usage does not require to wrap all
 * functions at once.
 * @see either
 * @param { string } name -
 * @param { function } func - The function to wrap.
 * @return { function }
 * @example
 * var log = "", name = "either-example-" + Date.now();
 * function a() { log += "aRun"; };
 * function b() { log += "bRun"; };
 * function c() { log += "cRun"; };
 * setTimeout(eitherNamed(name, a), 100);
 * setTimeout(eitherNamed(name, b), 40);
 * setTimeout(eitherNamed(name, c), 80);
 * setTimeout(function() { alert(log); /\* => "bRun" *\/ }, 150);
 */
function eitherNamed (name, func) {
  const funcs = Array.prototype.slice.call(arguments);
  const registry = _eitherNameRegistry;
  name = funcs.shift();
  const eitherCall = registry[name] || (registry[name] = { wasCalled: false, callsLeft: 0 });
  eitherCall.callsLeft++;
  return function () {
    eitherCall.callsLeft--;
    // cleanup the storage if all registered functions fired
    if (eitherCall.callsLeft <= 0) delete registry[name];
    if (eitherCall.wasCalled) return undefined;
    eitherCall.wasCalled = true;
    return func.apply(this, arguments);
  };
}

// -=-=-=-=-
// creation
// -=-=-=-=-
function evalJS (src) { return eval(src); }

/**
 * Creates a function from a string.
 * @param { string|function } funcOrString - A function or string to create a function from.
 * @returns { function }
 * @example
 * fromString("function() { return 3; }")() // => 3
 */
function fromString (funcOrString) {
  return evalJS('(' + funcOrString.toString() + ');');
}

/**
 * Lifts `func` to become a `Closure`, that is that free variables referenced
 * in `func` will be bound to the values of an object that can be passed in as
 * the second parameter. Keys of this object are mapped to the free variables.
 *
 * Please see [`Closure`](#) for a more detailed explanation and examples.
 * @param { function } func - The function to create a closure from.
 * @param { object } [optVarMapping] - The var mapping that defines how the free variables inside the closure are to be bound.
 * @returns { function }
 */
function asScript (func, optVarMapping) {
  return Closure.fromFunction(func, optVarMapping).recreateFunc();
}

const binds = asScript;

/**
 * Like `asScript` but makes `f` a method of `obj` as `optName` or the name
 * of the function.
 * @param { function } f - The function to create a method from.
 * @param { object } obj - The object to attach the method to.
 * @param { string } [optName] - The name of the method once attached to the object.
 * @param { object } [optMapping] - The var mapping that defines how the free variables inside the method are to be bound.
 * @returns { function }
 */
function asScriptOf (f, obj, optName, optMapping) {
  const name = optName || f.name;
  if (!name) {
    throw Error('Function that wants to be a script needs a name: ' + this);
  }
  const proto = Object.getPrototypeOf(obj);
  let mapping = { this: obj };
  if (optMapping) mapping = objectMerge([mapping, optMapping]);
  if (proto && proto[name]) {
    const superFunc = function () {
      try {
        // FIXME super is supposed to be static
        return Object.getPrototypeOf(obj)[name].apply(obj, arguments);
      } catch (e) {
        if (typeof $world !== 'undefined') $world.logError(e, 'Error in $super call');
        else console.error('Error in $super call: ' + e + '\n' + e.stack);
        return null;
      }
    };
    mapping.$super = Closure.fromFunction(superFunc, { obj, name }).recreateFunc();
  }
  return addToObject(asScript(f, mapping), obj, name);
}

// -=-=-=-=-=-=-=-=-
// closure related
// -=-=-=-=-=-=-=-=-

/**
 * Attaches a given function to an object as a method.
 * @param { function } f - The function to create a method from.
 * @param { object } obj - The object to attach the method to.
 * @param { string } name - The name of the method once attached to the object.
 * @returns { function }
 */
function addToObject (f, obj, name) {
  f.displayName = name;

  const methodConnections = obj.attributeConnections
    ? obj.attributeConnections.filter(function (con) {
      return con.getSourceAttrName() === 'update';
    })
    : [];

  if (methodConnections) { methodConnections.forEach(function (ea) { ea.disconnect(); }); }

  obj[name] = f;

  if (typeof obj) f.declaredObject = safeToString(obj);

  // suppport for tracing
  if (typeof lively !== 'undefined' && obj && lively.Tracing && lively.Tracing.stackTracingEnabled) {
    lively.Tracing.instrumentMethod(obj, name, {
      declaredObject: safeToString(obj)
    });
  }

  if (methodConnections) { methodConnections.forEach(function (ea) { ea.connect(); }); }

  return f;
}

/**
 * Given a lively closure, modifies the var binding.
 * @param { function } f - A lively closure whos binding has been instrumented beforehand.
 * @param { string } name - The name of the local variable to adjust.
 * @param { * } value - The value to adjust the local variable in the closure to.
 */
function setLocalVarValue (f, name, value) {
  if (f.hasLivelyClosure) f.livelyClosure.funcProperties[name] = value;
}

/**
 * Returns the var mapping for a given lively closure.
 */
function getVarMapping (f) {
  if (f.hasLivelyClosure) return f.livelyClosure.varMapping;
  if (f.isWrapper) return f.originalFunction.varMapping;
  if (f.varMapping) return f.varMapping;
  return {};
}

/**
 * @see setLocalVarValue
 */
function setProperty (func, name, value) {
  func[name] = value;
  if (func.hasLivelyClosure) func.livelyClosure.funcProperties[name] = value;
}

// -=-=-=-=-=-=-=-=-=-=-=-=-
// class-related functions
// -=-=-=-=-=-=-=-=-=-=-=-=-

/**
 * Treats passed function as class (constructor).
 * @param { function } klass - The function to check for as a class.
 * @returns { string[] }
 * @example
 * var Klass1 = function() {}
 * Klass1.prototype.foo = function(a, b) { return a + b; };
 * Klass1.prototype.bar = function(a) { return this.foo(a, 3); };
 * Klass1.prototype.baz = 23;
 * functionNames(Klass1); // => ["bar","foo"]
 */
function functionNames (klass) {
  let result = []; let lookupObj = klass.prototype;
  while (lookupObj) {
    result = Object.keys(lookupObj).reduce(function (result, name) {
      if (typeof lookupObj[name] === 'function' && result.indexOf(name) === -1) { result.push(name); }
      return result;
    }, result);
    lookupObj = Object.getPrototypeOf(lookupObj);
  }
  return result;
}

/**
 * Return the names of the functions defined on the prototype.
 * @param { function } func - The function whos prototype to check.
 * @return { string[] }
 */
function localFunctionNames (func) {
  return Object.keys(func.prototype)
    .filter(function (name) { return typeof func.prototype[name] === 'function'; });
}

// -=-=-=-=-=-=-=-=-=-=-
// tracing and logging
// -=-=-=-=-=-=-=-=-=-=-

/**
 * Wraps a given function to automatically log all the errors encountered to the console.
 * @param { function } func - The function to wrap.
 * @param { string } prefix - The log prefix to pass to the console.warn() call.
 * @returns { function }
 */
function logErrors (func, prefix) {
  const advice = function logErrorsAdvice (proceed /*, args */) {
    const args = Array.prototype.slice.call(arguments);
    args.shift();
    try {
      return proceed.apply(func, args);
    } catch (er) {
      if (typeof lively !== 'undefined' && lively.morphic && lively.morphic.World && lively.morphic.World.current()) {
        lively.morphic.World.current().logError(er);
        throw er;
      }

      if (prefix) console.warn('ERROR: %s.%s(%s): err: %s %s', func, prefix, args, er, er.stack || '');
      else console.warn('ERROR: %s %s', er, er.stack || '');
      throw er;
    }
  };

  advice.methodName = '$logErrorsAdvice';
  const result = wrap(func, advice);
  result.originalFunction = func;
  result.methodName = '$logErrorsWrapper';
  return result;
}

/**
 * Wrap a function to log to console once it succesfully completes.
 * @params { function } func - The function to wrap.
 * @params { string } module - The message to log once the call completes.
 * @returns { function }
 */
function logCompletion (func, module) {
  const advice = function logCompletionAdvice (proceed) {
    const args = Array.prototype.slice.call(arguments);
    args.shift();
    let result;
    try {
      result = proceed.apply(func, args);
    } catch (er) {
      console.warn('failed to load ' + module + ': ' + er);
      if (typeof lively !== 'undefined' && lively.lang.Execution) { lively.lang.Execution.showStack(); }
      throw er;
    }
    console.log('completed ' + module);
    return result;
  };

  advice.methodName = '$logCompletionAdvice::' + module;

  const result = wrap(func, advice);
  result.methodName = '$logCompletionWrapper::' + module;
  result.originalFunction = func;
  return result;
}

/**
 * Wraps a function to log to the console every time it is applied.
 * @param { function } func - The function to wrap.
 * @param { boolean } isUrgent - Wether or not the applications should logged as warnings or plain logs.
 * @returns { function }
 */
function logCalls (func, isUrgent) {
  const original = func;
  let result;
  const advice = function logCallsAdvice (proceed) {
    const args = Array.prototype.slice.call(arguments);
    args.shift(), result = proceed.apply(func, args);
    if (isUrgent) {
      console.warn('%s(%s) -> %s', qualifiedMethodName(original), args, result);
    } else {
      console.log('%s(%s) -> %s', qualifiedMethodName(original), args, result);
    }
    return result;
  };

  advice.methodName = '$logCallsAdvice::' + qualifiedMethodName(func);

  result = wrap(func, advice);
  result.originalFunction = func;
  result.methodName = '$logCallsWrapper::' + qualifiedMethodName(func);
  return result;
}

/**
 * Wraps a function such that it traces all subsequent function calls to a stack object.
 * @param { function } func - The function to wrap.
 * @param { List } stack - The stack to trace the occuring calls to.
 * @returns { function }
 */
function traceCalls (func, stack) {
  const advice = function traceCallsAdvice (proceed) {
    const args = Array.prototype.slice.call(arguments);
    args.shift();
    stack.push(args);
    const result = proceed.apply(func, args);
    stack.pop();
    return result;
  };
  return wrap(func, advice);
}

/**
 * Returns the current stackframes of the execution as a string.
 * @returns { string }
 */
function webkitStack () {
  // this won't work in every browser
  try {
    throw new Error();
  } catch (e) {
    // remove "Error" and this function from stack, rewrite it nicely
    return String(e.stack)
      .split(/\n/)
      .slice(2)
      .map(function (line) { return line.replace(/^\s*at\s*([^\s]+).*/, '$1'); })
      .join('\n');
  }
}

export {
  isNativeFunction,

  Empty, K, Null, False, True, notYetImplemented, withNull,

  all, own,

  argumentNames, qualifiedMethodName, extractBody,

  timeToRun, timeToRunN,

  delay, throttle, debounce, throttleNamed, debounceNamed, guard, guardNamed,

  createQueue, workerWithCallbackQueue,

  composeAsync, compose,

  waitFor, waitForAll,

  flip, curry, wrap, binds,
  getOriginal, wrapperChain,

  replaceMethodForOneCall,
  once, either, eitherNamed,

  evalJS,

  fromString,

  asScript, asScriptOf, addToObject,
  setLocalVarValue, getVarMapping, setProperty,

  functionNames, localFunctionNames,

  logErrors, logCompletion, logCalls, traceCalls, webkitStack
};