/* vim:ts=2:sts=2:sw=2:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

module.metadata = {
  "stability": "unstable"
};

/**
 * Returns `true` if `value` is `undefined`.
 * @examples
 *    var foo; isUndefined(foo); // true
 *    isUndefined(0); // false
 */
function isUndefined(value) {
  return value === undefined;
}
exports.isUndefined = isUndefined;

/**
 * Returns `true` if value is `null`.
 * @examples
 *    isNull(null); // true
 *    isNull(undefined); // false
 */
function isNull(value) {
  return value === null;
}
exports.isNull = isNull;

/**
 * Returns `true` if value is a string.
 * @examples
 *    isString("moe"); // true
 */
function isString(value) {
  return typeof value === "string";
}
exports.isString = isString;

/**
 * Returns `true` if `value` is a number.
 * @examples
 *    isNumber(8.4 * 5); // true
 */
function isNumber(value) {
  return typeof value === "number";
}
exports.isNumber = isNumber;

/**
 * Returns `true` if `value` is a `RegExp`.
 * @examples
 *    isRegExp(/moe/); // true
 */
function isRegExp(value) {
  return isObject(value) && instanceOf(value, RegExp);
}
exports.isRegExp = isRegExp;

/**
 * Returns true if `value` is a `Date`.
 * @examples
 *    isDate(new Date()); // true
 */
function isDate(value) {
  return isObject(value) && instanceOf(value, Date);
}
exports.isDate = isDate;

/**
 * Returns true if object is a Function.
 * @examples
 *    isFunction(function foo(){}) // true
 */
function isFunction(value) {
    return typeof value === "function";
}
exports.isFunction = isFunction;

/**
 * Returns `true` if `value` is an object (please note that `null` is considered
 * to be an atom and not an object).
 * @examples
 *    isObject({}) // true
 *    isObject(null) // false
 */
function isObject(value) {
    return typeof value === "object" && value !== null;
}
exports.isObject = isObject;

/**
 * Returns true if `value` is an Array.
 * @examples
 *    isArray([1, 2, 3])  // true
 *    isArray({ 0: 'foo', length: 1 }) // false
 */
var isArray = Array.isArray || function isArray(value) {
  Object.prototype.toString.call(value) === "[object Array]";
}
exports.isArray = isArray;

/**
 * Returns `true` if `value` is an Arguments object.
 * @examples
 *    (function(){ return isArguments(arguments); })(1, 2, 3); // true
 *    isArguments([1,2,3]); // false
 */
function isArguments(value) {
  Object.prototype.toString.call(value) === "[object Arguments]";
}
exports.isArguments = isArguments;

/**
 * Returns true if it is a primitive `value`. (null, undefined, number,
 * boolean, string)
 * @examples
 *    isPrimitive(3) // true
 *    isPrimitive('foo') // true
 *    isPrimitive({ bar: 3 }) // false
 */
function isPrimitive(value) {
  return !isFunction(value) && !isObject(value);
}
exports.isPrimitive = isPrimitive;

/**
 * Returns `true` if given `object` is flat (it is direct decedent of
 * `Object.prototype` or `null`).
 * @examples
 *    isFlat({}) // true
 *    isFlat(new Type()) // false
 */
function isFlat(object) {
  return isObject(object) && (isNull(Object.getPrototypeOf(object)) ||
                              isNull(Object.getPrototypeOf(
                                     Object.getPrototypeOf(object))));
}
exports.isFlat = isFlat;

/**
 * Returns `true` if object contains no values.
 */
function isEmpty(object) {
  if (isObject(object)) {
    for (var key in object)
      return false;
    return true;
  }
  return false;
}
exports.isEmpty = isEmpty;

/**
 * Returns `true` if `value` is an array / flat object containing only atomic
 * values and other flat objects.
 */
function isJSON(value, visited) {
    // Adding value to array of visited values.
    (visited || (visited = [])).push(value);
            // If `value` is an atom return `true` cause it's valid JSON.
    return  isPrimitive(value) ||
            // If `value` is an array of JSON values that has not been visited
            // yet.
            (isArray(value) &&  value.every(function(element) {
                                  return isJSON(element, visited);
                                })) ||
            // If `value` is a plain object containing properties with a JSON
            // values it's a valid JSON.
            (isFlat(value) && Object.keys(value).every(function(key) {
                var $ = Object.getOwnPropertyDescriptor(value, key);
                // Check every proprety of a plain object to verify that
                // it's neither getter nor setter, but a JSON value, that
                // has not been visited yet.
                return  ((!isObject($.value) || !~visited.indexOf($.value)) &&
                        !('get' in $) && !('set' in $) &&
                        isJSON($.value, visited));
            }));
}
exports.isJSON = function (value) {
  return isJSON(value);
};

/**
 * Returns if `value` is an instance of a given `Type`. This is exactly same as
 * `value instanceof Type` with a difference that `Type` can be from a scope
 * that has a different top level object. (Like in case where `Type` is a
 * function from different iframe / jetpack module / sandbox).
 */
function instanceOf(value, Type) {
  var isConstructorNameSame;
  var isConstructorSourceSame;

  // If `instanceof` returned `true` we know result right away.
  var isInstanceOf = value instanceof Type;

  // If `instanceof` returned `false` we do ducktype check since `Type` may be
  // from a different sandbox. If a constructor of the `value` or a constructor
  // of the value's prototype has same name and source we assume that it's an
  // instance of the Type.
  if (!isInstanceOf && value) {
    isConstructorNameSame = value.constructor.name === Type.name;
    isConstructorSourceSame = String(value.constructor) == String(Type);
    isInstanceOf = (isConstructorNameSame && isConstructorSourceSame) ||
                    instanceOf(Object.getPrototypeOf(value), Type);
  }
  return isInstanceOf;
}
exports.instanceOf = instanceOf;

/**
 * Function returns textual representation of a value passed to it. Function
 * takes additional `indent` argument that is used for indentation. Also
 * optional `limit` argument may be passed to limit amount of detail returned.
 * @param {Object} value
 * @param {String} [indent="    "]
 * @param {Number} [limit]
 */
function source(value, indent, limit, offset, visited) {
  var result;
  var names;
  var nestingIndex;
  var isCompact = !isUndefined(limit);

  indent = indent || "    ";
  offset = (offset || "");
  result = "";
  visited = visited || [];

  if (isUndefined(value)) {
    result += "undefined";
  }
  else if (isNull(value)) {
    result += "null";
  }
  else if (isString(value)) {
    result += '"' + value + '"';
  }
  else if (isFunction(value)) {
    value = String(value).split("\n");
    if (isCompact && value.length > 2) {
      value = value.splice(0, 2);
      value.push("...}");
    }
    result += value.join("\n" + offset);
  }
  else if (isArray(value)) {
    if ((nestingIndex = (visited.indexOf(value) + 1))) {
      result = "#" + nestingIndex + "#";
    }
    else {
      visited.push(value);

      if (isCompact)
        value = value.slice(0, limit);

      result += "[\n";
      result += value.map(function(value) {
        return offset + indent + source(value, indent, limit, offset + indent,
                                        visited);
      }).join(",\n");
      result += isCompact && value.length > limit ?
                ",\n" + offset + "...]" : "\n" + offset + "]";
    }
  }
  else if (isObject(value)) {
    if ((nestingIndex = (visited.indexOf(value) + 1))) {
      result = "#" + nestingIndex + "#"
    }
    else {
      visited.push(value)

      names = Object.keys(value);

      result += "{ // " + value + "\n";
      result += (isCompact ? names.slice(0, limit) : names).map(function(name) {
        var _limit = isCompact ? limit - 1 : limit;
        var descriptor = Object.getOwnPropertyDescriptor(value, name);
        var result = offset + indent + "// ";
        var accessor;
        if (0 <= name.indexOf(" "))
          name = '"' + name + '"';

        if (descriptor.writable)
          result += "writable ";
        if (descriptor.configurable)
          result += "configurable ";
        if (descriptor.enumerable)
          result += "enumerable ";

        result += "\n";
        if ("value" in descriptor) {
          result += offset + indent + name + ": ";
          result += source(descriptor.value, indent, _limit, indent + offset,
                           visited);
        }
        else {

          if (descriptor.get) {
            result += offset + indent + "get " + name + " ";
            accessor = source(descriptor.get, indent, _limit, indent + offset,
                              visited);
            result += accessor.substr(accessor.indexOf("{"));
          }

          if (descriptor.set) {
            result += offset + indent + "set " + name + " ";
            accessor = source(descriptor.set, indent, _limit, indent + offset,
                              visited);
            result += accessor.substr(accessor.indexOf("{"));
          }
        }
        return result;
      }).join(",\n");

      if (isCompact) {
        if (names.length > limit && limit > 0) {
          result += ",\n" + offset  + indent + "//...";
        }
      }
      else {
        if (names.length)
          result += ",";

        result += "\n" + offset + indent + '"__proto__": ';
        result += source(Object.getPrototypeOf(value), indent, 0,
                         offset + indent);
      }

      result += "\n" + offset + "}";
    }
  }
  else {
    result += String(value);
  }
  return result;
}
exports.source = function (value, indentation, limit) {
  return source(value, indentation, limit);
};
