var juggler = require('loopback-datasource-juggler');
var remoting = require('strong-remoting');
var cls = require('continuation-local-storage');
var domain = require('domain');

module.exports = function(loopback) {

  /**
   * Get the current context object. The context is preserved
   * across async calls, it behaves like a thread-local storage.
   *
   * @returns {ChainedContext} The context object or null.
   */
  loopback.getCurrentContext = function() {
    // A placeholder method, see loopback.createContext() for the real version
    return null;
  };

  /**
   * Run the given function in such way that
   * `loopback.getCurrentContext` returns the
   * provided context object.
   *
   * **NOTE**
   *
   * The method is supported on the server only, it does not work
   * in the browser at the moment.
   *
   * @param {Function} fn The function to run, it will receive arguments
   * (currentContext, currentDomain).
   * @param {ChainedContext} context An optional context object.
   *   When no value is provided, then the default global context is used.
   */
  loopback.runInContext = function(fn, context) {
    var currentDomain = domain.create();
    currentDomain.oldBind = currentDomain.bind;
    currentDomain.bind = function(callback, context) {
      return currentDomain.oldBind(ns.bind(callback, context), context);
    };

    var ns = context || loopback.createContext('loopback');

    currentDomain.run(function() {
      ns.run(function executeInContext(context) {
        fn(ns, currentDomain);
      });
    });
  };

  /**
   * Create a new LoopBackContext instance that can be used
   * for `loopback.runInContext`.
   *
   * **NOTES**
   *
   * At the moment, `loopback.getCurrentContext` supports
   * a single global context instance only. If you call `createContext()`
   * multiple times, `getCurrentContext` will return the last context
   * created.
   *
   * The method is supported on the server only, it does not work
   * in the browser at the moment.
   *
   * @param {String} scopeName An optional scope name.
   * @return {ChainedContext} The new context object.
   */
  loopback.createContext = function(scopeName) {
    // Make the namespace globally visible via the process.context property
    process.context = process.context || {};
    var ns = process.context[scopeName];
    if (!ns) {
      ns = cls.createNamespace(scopeName);
      process.context[scopeName] = ns;
      // Set up loopback.getCurrentContext()
      loopback.getCurrentContext = function() {
        return ns && ns.active ? ns : null;
      };

      chain(juggler);
      chain(remoting);
    }
    return ns;
  };

  /**
   * Create a chained context
   * @param {Object} child The child context
   * @param {Object} parent The parent context
   * @private
   * @constructor
   */
  function ChainedContext(child, parent) {
    this.child = child;
    this.parent = parent;
  }

  /**
   * Get the value by name from the context. If it doesn't exist in the child
   * context, try the parent one
   * @param {String} name Name of the context property
   * @returns {*} Value of the context property
   * @private
   */
  ChainedContext.prototype.get = function(name) {
    var val = this.child && this.child.get(name);
    if (val === undefined) {
      return this.parent && this.parent.get(name);
    }
  };

  ChainedContext.prototype.set = function(name, val) {
    if (this.child) {
      return this.child.set(name, val);
    } else {
      return this.parent && this.parent.set(name, val);
    }
  };

  ChainedContext.prototype.reset = function(name, val) {
    if (this.child) {
      return this.child.reset(name, val);
    } else {
      return this.parent && this.parent.reset(name, val);
    }
  };

  function chain(child) {
    if (typeof child.getCurrentContext === 'function') {
      var childContext = new ChainedContext(child.getCurrentContext(),
        loopback.getCurrentContext());
      child.getCurrentContext = function() {
        return childContext;
      };
    } else {
      child.getCurrentContext = loopback.getCurrentContext;
    }
  }
};