/**
 * @author Alexey Kuzmin <alex.s.kuzmin@gmail.com>
 * @fileoverview JavaScript implementation of JSON Pointer.
 * @see http://tools.ietf.org/html/rfc6901
 */



;(function() {
  'use strict';

  /**
   * List of special characters and their escape sequences.
   * Special characters will be unescaped in order they are listed.
   * Section 3 of spec.
   * @type {Array.<Array.<string>>}
   * @const
   */
  var SPECIAL_CHARACTERS = [
    ['/', '~1'],
    ['~', '~0']
  ];


  /**
   * Tokens' separator in JSON pointer string.
   * Section 3 of spec.
   * @type {string}
   * @const
   */
  var TOKENS_SEPARATOR = '/';


  /**
   * Prefix for error messages.
   * @type {string}
   * @const
   */
  var ERROR_MESSAGE_PREFIX = 'JSON Pointer: ';


  /**
   * Validates non-empty pointer string.
   * @type {RegExp}
   * @const
   */
  var NON_EMPTY_POINTER_REGEXP = /(\/[^\/]*)+/;


  /**
   * List of error messages.
   * Please keep it in alphabetical order.
   * @enum {string}
   */
  var ErrorMessage = {
    HYPHEN_IS_NOT_SUPPORTED_IN_ARRAY_CONTEXT:
        'Implementation does not support "-" token for arrays.',
    INVALID_DOCUMENT: 'JSON document is not valid.',
    INVALID_DOCUMENT_TYPE: 'JSON document must be a string or object.',
    INVALID_POINTER: 'Pointer is not valid.',
    NON_NUMBER_TOKEN_IN_ARRAY_CONTEXT:
        'Non-number tokens cannot be used in array context.',
    TOKEN_WITH_LEADING_ZERO_IN_ARRAY_CONTEXT:
        'Token with leading zero cannot be used in array context.'
  };


  /**
   * Returns |target| object's value pointed by |opt_pointer|, returns undefined
   * if |opt_pointer| points to non-existing value.
   * If pointer is not provided, validates first argument and returns
   * evaluator function that takes pointer as argument.
   * @param {(string|Object|Array)} target Evaluation target.
   * @param {string=} opt_pointer JSON Pointer string.
   * @returns {*} Some value.
   */
  function getPointedValue(target, opt_pointer) {
    // .get() method implementation.

    // First argument must be either string or object.
    if (isString(target)) {

      // If string it must be valid JSON document.
      try {
        // Let's try to parse it as JSON.
        target = JSON.parse(target);
      }
      catch (e) {
        // If parsing failed, an exception will be thrown.
        throw getError(ErrorMessage.INVALID_DOCUMENT);
      }
    }
    else if (!isObject(target)) {
      // If not object or string, an exception will be thrown.
      throw getError(ErrorMessage.INVALID_DOCUMENT_TYPE);
    }

    // |target| is already parsed, let's create evaluator function for it.
    var evaluator = createPointerEvaluator(target);

    if (isUndefined(opt_pointer)) {
      // If pointer was not provided, return evaluator function.
      return evaluator;
    }
    else {
      // If pointer is provided, return evaluation result.
      return evaluator(opt_pointer);
    }
  }


  /**
   * Returns function that takes JSON Pointer as single argument
   * and evaluates it in given |target| context.
   * Returned function throws an exception if pointer is not valid
   * or any error occurs during evaluation.
   * @param {*} target Evaluation target.
   * @returns {Function}
   */
  function createPointerEvaluator(target) {

    // Use cache to store already received values.
    var cache = {};

    return function(pointer) {

      if (!isValidJSONPointer(pointer)) {
        // If it's not, an exception will be thrown.
        throw getError(ErrorMessage.INVALID_POINTER);
      }

      // First, look up in the cache.
      if (cache.hasOwnProperty(pointer)) {
        // If cache entry exists, return it's value.
        return cache[pointer];
      }

      // Now, when all arguments are valid, we can start evaluation.
      // First of all, let's convert JSON pointer string to tokens list.
      var tokensList = parsePointer(pointer);
      var token;
      var value = target;

      // Evaluation will be continued till tokens list is not empty
      // and returned value is not an undefined.
      while (!isUndefined(value) && !isUndefined(token = tokensList.pop())) {
        // Let's evaluate token in current context.
        // `getValue()` might throw an exception, but we won't handle it.
        value = getValue(value, token);
      }

      // Pointer evaluation is done, save value in the cache and return it.
      cache[pointer] = value;
      return value;
    };
  }


  /**
   * Returns true if given |pointer| is valid, returns false otherwise.
   * @param {!string} pointer
   * @returns {boolean} Whether pointer is valid.
   */
  function isValidJSONPointer(pointer) {
    // Validates JSON pointer string.

    if (!isString(pointer)) {
      // If it's not a string, it obviously is not valid.
      return false;
    }

    if ('' === pointer) {
      // If it is string and is an empty string, it's valid.
      return true;
    }

    // If it is non-empty string, it must match spec defined format.
    // Check Section 3 of specification for concrete syntax.
    return NON_EMPTY_POINTER_REGEXP.test(pointer);
  }


  /**
   * Returns tokens list for given |pointer|. List is reversed, e.g.
   *     '/simple/path' -> ['path', 'simple']
   * @param {!string} pointer JSON pointer string.
   * @returns {Array} List of tokens.
   */
  function parsePointer(pointer) {
    // Converts JSON pointer string into tokens list.

    // Let's split pointer string by tokens' separator character.
    // Also we will reverse resulting array to simplify it's further usage.
    var tokens = pointer.split(TOKENS_SEPARATOR).reverse();

    // Last item in resulting array is always an empty string,
    // we don't need it, let's remove it.
    tokens.pop();

    // Now tokens' array is ready to use, let's return it.
    return tokens;
  }


  /**
   * Decodes all escape sequences in given |rawReferenceToken|.
   * @param {!string} rawReferenceToken
   * @returns {string} Unescaped reference token.
   */
  function unescapeReferenceToken(rawReferenceToken) {
    // Unescapes reference token. See Section 3 of specification.

    var referenceToken = rawReferenceToken;
    var character;
    var escapeSequence;
    var pair;
    var replaceRegExp;

    // Order of unescaping does matter.
    // That's why an array is used here and not hash.
    for (var i = 0; i < SPECIAL_CHARACTERS.length; i++) {
      pair = SPECIAL_CHARACTERS[i];
      character = pair[0];
      escapeSequence = pair[1];
      replaceRegExp = new RegExp(escapeSequence, 'g');
      referenceToken = referenceToken.replace(replaceRegExp, character);
    }

    return referenceToken;
  }


  /**
   * Returns value pointed by |token| in evaluation |context|.
   * Throws an exception if any error occurs.
   * @param {*} context Current evaluation context.
   * @param {!string} token Unescaped reference token.
   * @returns {*} Some value or undefined if value if not found.
   */
  function getValue(context, token) {
    // Reference token evaluation. See Section 4 of spec.

    // First of all we should unescape all special characters in token.
    token = unescapeReferenceToken(token);

    // Further actions depend of context of evaluation.

    if (isArray(context)) {
      // In array context there are more strict requirements
      // for token value.

      if ('-' === token) {
        // Token cannot be a "-" character,
        // it has no sense in current implementation.
        throw getError(ErrorMessage.HYPHEN_IS_NOT_SUPPORTED_IN_ARRAY_CONTEXT);
      }
      if (!isNumber(token)) {
        // Token cannot be non-number.
        throw getError(ErrorMessage.NON_NUMBER_TOKEN_IN_ARRAY_CONTEXT);
      }
      if (token.length > 1 && '0' === token[0]) {
        // Token cannot be non-zero number with leading zero.
        throw getError(ErrorMessage.TOKEN_WITH_LEADING_ZERO_IN_ARRAY_CONTEXT);
      }
      // If all conditions are met, simply return element
      // with token's value index.
      // It might be undefined, but it's ok.
      return context[token];
    }

    if (isObject(context)) {
      // In object context we can simply return element w/ key equal to token.
      // It might be undefined, but it's ok.
      return context[token];
    }

    // If context is not an array or an object,
    // token evaluation is not possible.
    // This is the expected situation and so we won't throw an error,
    // undefined value is perfectly suitable here.
    return;
  }


  /**
   * Returns Error instance for throwing.
   * @param {string} message Error message.
   * @returns {Error}
   */
  function getError(message) {
    return new Error(ERROR_MESSAGE_PREFIX + message);
  }


  function isObject(o) {
    return 'object' === typeof o && null !== o;
  }


  function isArray(a) {
    if (Array.isArray) {
      return Array.isArray(a);
    } else {
      return Object.prototype.toString.call(a) === '[object Array]';
    }
  }


  function isNumber(n) {
    return !isNaN(Number(n));
  }


  function isString(s) {
    return 'string' === typeof s || s instanceof String;
  }


  function isUndefined(v) {
    return 'undefined' === typeof v;
  }


  // Let's expose API to the world.

  var jsonpointer = {
    get: getPointedValue
  };

  if ('object' === typeof exports) {
    // If `exports` is an object, we are in Node.js context.
    // We are supposed to act as Node.js package.
    module.exports = jsonpointer;
  } else if ('function' === typeof define && define.amd) {
    // If there is global function `define()` and `define.amd` is defined,
    // we are supposed to act as AMD module.
    define(function() {
      return jsonpointer;
    });
  } else {
    // Last resort.
    // Let's create global `jsonpointer` object.
    this.jsonpointer = jsonpointer;
  }

}).call((function() {
  'use strict';
  return (typeof window !== 'undefined' ? window : this);
})());
