module.exports = new (function() {

  /**
   * Given an element name, returns that element object
   *
   * @param {Object} parent The parent page or section
   * @param {string} elementName Name of element
   * @returns {Object} The element object
   */
  function getElement(parent, elementName) {
    elementName = elementName.substring(1);
    if (!(elementName in parent.elements)) {
      throw new Error(elementName + ' was not found in "' + parent.name +
        '". Available elements: ' + Object.keys(parent.elements));
    }
    return parent.elements[elementName];
  }

  /**
   * Given a section name, returns that section object
   *
   * @param {Object} parent The parent page or section
   * @param {string} sectionName Name of section
   * @returns {Object} The section object
   */
  function getSection(parent, sectionName) {
    sectionName = sectionName.substring(1);
    if (!(sectionName in parent.section)) {
      throw new Error(sectionName + ' was not found in "' + parent.name +
        '". Available sections: ' + Object.keys(parent.sections));
    }
    return parent.section[sectionName];
  }

  /**
   * Calls use(Css|Xpath|Recursion) command
   *
   * Uses `useXpath`, `useCss`, and `useRecursion` commands.
   *
   * @param {Object} client The Nightwatch instance
   * @param {string} desiredStrategy (css selector|xpath|recursion)
   * @returns {null}
   */
  function setLocateStrategy(client, desiredStrategy) {
    var methodMap = {
      xpath : 'useXpath',
      'css selector' : 'useCss',
      recursion : 'useRecursion'
    };

    if (desiredStrategy in methodMap) {
      client.api[methodMap[desiredStrategy]]();
    }
  }

  /**
   * Creates a closure that enables calling commands and assertions on the page or section.
   * For all element commands and assertions, it fetches element's selector and locate strategy
   *  For elements nested under sections, it sets 'recursion' as the locate strategy and passes as its first argument to the command an array of its ancestors + self
   *  If the command or assertion is not on an element, it calls it with the untouched passed arguments
   *
   * @param {Object} parent The parent page or section
   * @param {function} commandFn The actual command function
   * @param {string} commandName The name of the command ("click", "containsText", etc)
   * @param {Boolean} [isChaiAssertion]
   * @returns {function}
   */
  function makeWrappedCommand(parent, commandFn, commandName, isChaiAssertion) {
    return function() {
      var args = Array.prototype.slice.call(arguments);
      var prevLocateStrategy = parent.client.locateStrategy;
      var elementCommand = isElementCommand(args);

      if (elementCommand) {
        var firstArg;
        var desiredStrategy;
        var callbackIndex;
        var originalCallback;
        var elementOrSectionName = args.shift();
        var getter = (isChaiAssertion && commandName === 'section') ? getSection : getElement;
        var elementOrSection = getter(parent, elementOrSectionName);
        var ancestors = getAncestorsWithElement(elementOrSection);

        if (ancestors.length === 1) {
          firstArg = elementOrSection.selector;
          desiredStrategy = elementOrSection.locateStrategy;
        } else {
          firstArg = ancestors;
          desiredStrategy = 'recursion';
        }

        setLocateStrategy(parent.client, desiredStrategy);
        args.unshift(firstArg);

        // if a callback is being used with this command, wrap it in
        // a function that allows us to restore the locate strategy
        // to its original value before the callback is called

        callbackIndex = findCallbackIndex(args);
        if (callbackIndex !== -1) {
          originalCallback = args[callbackIndex];

          args[callbackIndex] = function callbackWrapper() {

            // restore the locate strategy directly through client.locateStrategy.
            // setLocateStrategy() can't be used since it uses the api commands
            // which get added to the command queue and will not update the
            // strategy in time for the callback which is getting immediately
            // called after

            parent.client.locateStrategy = prevLocateStrategy;
            return originalCallback.apply(parent.client.api, arguments);
          };
        }
      }

      var c = commandFn.apply(parent.client, args);
      if (elementCommand) {
        setLocateStrategy(parent.client, prevLocateStrategy);
      }
      return (isChaiAssertion ? c : parent);
    };
  }

  /**
   *
   * @param {Array} args
   * @return {boolean}
   */
  function isElementCommand(args) {
    return (args.length > 0) && (args[0].toString().indexOf('@') === 0);
  }

  /**
   * Identifies the location of a callback function within an arguments array.
   *
   * @param {Array} args Arguments array in which to find the location of a callback.
   * @returns {number} Index location of the callback in the args array. If not found, -1 is returned.
   */
  function findCallbackIndex(args) {

    if (args.length === 0) {
      return -1;
    }

    // callbacks will usually be the last argument. waitFor methods allow an additional
    // message argument to follow the callback which will also need to be checked for.

    // last argument

    var index = args.length - 1;
    if (typeof args[index] === 'function') {
      return index;
    }

    // second to last argument (waitfor calls)

    index--;
    if (typeof args[index] === 'function') {
      return index;
    }

    return -1;
  }

  /**
   * Retrieves an array of ancestors of the supplied element. The last element in the array is the element object itself
   *
   * @param {Object} element The element
   * @returns {Array}
   */
  function getAncestorsWithElement(element) {
    var elements = [];
    function addElement(e) {
      elements.unshift(e);
      if (e.parent && e.parent.selector) {
        addElement(e.parent);
      }
    }
    addElement(element);
    return elements;
  }

  /**
   * Adds commands (elements commands, assertions, etc) to the page or section
   *
   * @param {Object} parent The parent page or section
   * @param {Object} target What the command is added to (parent|section or assertion object on parent|section)
   * @param {Object} commands
   * @returns {null}
   */
  function applyCommandsToTarget(parent, target, commands) {

    Object.keys(commands).forEach(function(commandName) {
      if (isValidAssertion(commandName)) {
        target[commandName] = target[commandName] || {};

        var isChaiAssertion = commandName === 'expect';
        var assertions = commands[commandName];

        Object.keys(assertions).forEach(function(assertionName) {
          target[commandName][assertionName] = addCommand(target[commandName], assertions[assertionName], assertionName, parent, isChaiAssertion);
        });
      } else {
        target[commandName] = addCommand(target, commands[commandName], commandName, parent, false);
      }
    });
  }

  function addCommand(target, commandFn, commandName, parent, isChaiAssertion) {
    if (target[commandName]) {
      parent.client.results.errors++;
      var error = new Error('The command "' + commandName + '" is already defined!');
      parent.client.errors.push(error.stack);
      throw error;
    }

    return makeWrappedCommand(parent, commandFn, commandName, isChaiAssertion);
  }

  function isValidAssertion(commandName) {
    return ['assert', 'verify', 'expect'].indexOf(commandName) > -1;
  }

  /**
   * Entrypoint to add commands (elements commands, assertions, etc) to the page or section
   *
   * @param {Object} parent The parent page or section
   * @param {function} commandLoader function that retrieves commands
   * @returns {null}
   */
  this.addWrappedCommands = function (parent, commandLoader) {
    var commands = {};
    commands = commandLoader(commands);
    applyCommandsToTarget(parent, parent, commands);
  };

})();
