/* 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": "deprecated"
};

// `var` is being used in the module in order to make it reusable in
// environments in which `let` is not yet supported.

// Shortcut to `Object.prototype.hasOwnProperty.call`.
// owns(object, name) would be the same as
// Object.prototype.hasOwnProperty.call(object, name);
var owns = Function.prototype.call.bind(Object.prototype.hasOwnProperty);

/**
 * Whether or not given property descriptors are equivalent. They are
 * equivalent either if both are marked as 'conflict' or 'required' property
 * or if all the properties of descriptors are equal.
 * @param {Object} actual
 * @param {Object} expected
 */
function equivalentDescriptors(actual, expected) {
  return (actual.conflict && expected.conflict) ||
         (actual.required && expected.required) ||
         equalDescriptors(actual, expected);
}
/**
 * Whether or not given property descriptors define equal properties.
 */
function equalDescriptors(actual, expected) {
  return actual.get === expected.get &&
         actual.set === expected.set &&
         actual.value === expected.value &&
         !!actual.enumerable === !!expected.enumerable &&
         !!actual.configurable === !!expected.configurable &&
         !!actual.writable === !!expected.writable;
}

// Utilities that throwing exceptions for a properties that are marked
// as "required" or "conflict" properties.
function throwConflictPropertyError(name) {
  throw new Error("Remaining conflicting property: `" + name + "`");
}
function throwRequiredPropertyError(name) {
  throw new Error("Missing required property: `" + name + "`");
}

/**
 * Generates custom **required** property descriptor. Descriptor contains
 * non-standard property `required` that is equal to `true`.
 * @param {String} name
 *    property name to generate descriptor for.
 * @returns {Object}
 *    custom property descriptor
 */
function RequiredPropertyDescriptor(name) {
  // Creating function by binding first argument to a property `name` on the
  // `throwConflictPropertyError` function. Created function is used as a
  // getter & setter of the created property descriptor. This way we ensure
  // that we throw exception late (on property access) if object with
  // `required` property was instantiated using built-in `Object.create`.
  var accessor = throwRequiredPropertyError.bind(null, name);
  return { get: accessor, set: accessor, required: true };
}

/**
 * Generates custom **conflicting** property descriptor. Descriptor contains
 * non-standard property `conflict` that is equal to `true`.
 * @param {String} name
 *    property name to generate descriptor for.
 * @returns {Object}
 *    custom property descriptor
 */
function ConflictPropertyDescriptor(name) {
  // For details see `RequiredPropertyDescriptor` since idea is same.
  var accessor = throwConflictPropertyError.bind(null, name);
  return { get: accessor, set: accessor, conflict: true };
}

/**
 * Tests if property is marked as `required` property.
 */
function isRequiredProperty(object, name) {
  return !!object[name].required;
}

/**
 * Tests if property is marked as `conflict` property.
 */
function isConflictProperty(object, name) {
  return !!object[name].conflict;
}

/**
 * Function tests whether or not method of the `source` object with a given
 * `name` is inherited from `Object.prototype`.
 */
function isBuiltInMethod(name, source) {
  var target = Object.prototype[name];

  // If methods are equal then we know it's `true`.
  return target == source ||
  // If `source` object comes form a different sandbox `==` will evaluate
  // to `false`, in that case we check if functions names and sources match.
         (String(target) === String(source) && target.name === source.name);
}

/**
 * Function overrides `toString` and `constructor` methods of a given `target`
 * object with a same-named methods of a given `source` if methods of `target`
 * object are inherited / copied from `Object.prototype`.
 * @see create
 */
function overrideBuiltInMethods(target, source) {
  if (isBuiltInMethod("toString", target.toString)) {
    Object.defineProperty(target, "toString",  {
      value: source.toString,
      configurable: true,
      enumerable: false
    });
  }

  if (isBuiltInMethod("constructor", target.constructor)) {
    Object.defineProperty(target, "constructor", {
      value: source.constructor,
      configurable: true,
      enumerable: false
    });
  }
}

/**
 * Composes new trait with the same own properties as the original trait,
 * except that all property names appearing in the first argument are replaced
 * by "required" property descriptors.
 * @param {String[]} keys
 *    Array of strings property names.
 * @param {Object} trait
 *    A trait some properties of which should be excluded.
 * @returns {Object}
 * @example
 *    var newTrait = exclude(["name", ...], trait)
 */
function exclude(names, trait) {
  var map = {};

  Object.keys(trait).forEach(function(name) {

    // If property is not excluded (the array of names does not contain it),
    // or it is a "required" property, copy it to the property descriptor `map`
    // that will be used for creation of resulting trait.
    if (!~names.indexOf(name) || isRequiredProperty(trait, name))
      map[name] = { value: trait[name], enumerable: true };

    // For all the `names` in the exclude name array we create required
    // property descriptors and copy them to the `map`.
    else
      map[name] = { value: RequiredPropertyDescriptor(name), enumerable: true };
  });

  return Object.create(Trait.prototype, map);
}

/**
 * Composes new instance of `Trait` with a properties of a given `trait`,
 * except that all properties whose name is an own property of `renames` will
 * be renamed to `renames[name]` and a `"required"` property for name will be
 * added instead.
 *
 * For each renamed property, a required property is generated. If
 * the `renames` map two properties to the same name, a conflict is generated.
 * If the `renames` map a property to an existing unrenamed property, a
 * conflict is generated.
 *
 * @param {Object} renames
 *    An object whose own properties serve as a mapping from old names to new
 *    names.
 * @param {Object} trait
 *    A new trait with renamed properties.
 * @returns {Object}
 * @example
 *
 *    // Return trait with `bar` property equal to `trait.foo` and with
 *    // `foo` and `baz` "required" properties.
 *    var renamedTrait = rename({ foo: "bar", baz: null }), trait);
 *
 *    // t1 and t2 are equivalent traits
 *    var t1 = rename({a: "b"}, t);
 *    var t2 = compose(exclude(["a"], t), { a: { required: true }, b: t[a] });
 */
function rename(renames, trait) {
  var map = {};

  // Loop over all the properties of the given `trait` and copy them to a
  // property descriptor `map` that will be used for the creation of the
  // resulting trait.  Also, rename properties in the `map` as specified by
  // `renames`.
  Object.keys(trait).forEach(function(name) {
    var alias;

    // If the property is in the `renames` map, and it isn't a "required"
    // property (which should never need to be aliased because "required"
    // properties never conflict), then we must try to rename it.
    if (owns(renames, name) && !isRequiredProperty(trait, name)) {
      alias = renames[name];

      // If the `map` already has the `alias`, and it isn't a "required"
      // property, that means the `alias` conflicts with an existing name for a
      // provided trait (that can happen if >=2 properties are aliased to the
      // same name). In this case we mark it as a conflicting property.
      // Otherwise, everything is fine, and we copy property with an `alias`
      // name.
      if (owns(map, alias) && !map[alias].value.required) {
        map[alias] = {
          value: ConflictPropertyDescriptor(alias),
          enumerable: true
        };
      }
      else {
        map[alias] = {
          value: trait[name],
          enumerable: true
        };
      }

      // Regardless of whether or not the rename was successful, we check to
      // see if the original `name` exists in the map (such a property
      // could exist if previous another property was aliased to this `name`).
      // If it isn't, we mark it as "required", to make sure the caller
      // provides another value for the old name, which methods of the trait
      // might continue to reference.
      if (!owns(map, name)) {
        map[name] = {
          value: RequiredPropertyDescriptor(name),
          enumerable: true
        };
      }
    }

    // Otherwise, either the property isn't in the `renames` map (thus the
    // caller is not trying to rename it) or it is a "required" property.
    // Either way, we don't have to alias the property, we just have to copy it
    // to the map.
    else {
      // The property isn't in the map yet, so we copy it over.
      if (!owns(map, name)) {
        map[name] = { value: trait[name], enumerable: true };
      }

      // The property is already in the map (that means another property was
      // aliased with this `name`, which creates a conflict if the property is
      // not marked as "required"), so we have to mark it as a "conflict"
      // property.
      else if (!isRequiredProperty(trait, name)) {
        map[name] = {
          value: ConflictPropertyDescriptor(name),
          enumerable: true
        };
      }
    }
  });
  return Object.create(Trait.prototype, map);
}

/**
 * Composes new resolved trait, with all the same properties as the original
 * `trait`, except that all properties whose name is an own property of
 * `resolutions` will be renamed to `resolutions[name]`.
 *
 * If `resolutions[name]` is `null`, the value is mapped to a property
 * descriptor that is marked as a "required" property.
 */
function resolve(resolutions, trait) {
    var renames = {};
    var exclusions = [];

    // Go through each mapping in `resolutions` object and distribute it either
    // to `renames` or `exclusions`.
    Object.keys(resolutions).forEach(function(name) {

      // If `resolutions[name]` is a truthy value then it's a mapping old -> new
      // so we copy it to `renames` map.
      if (resolutions[name])
        renames[name] = resolutions[name];

      // Otherwise it's not a mapping but an exclusion instead in which case we
      // add it to the `exclusions` array.
      else
        exclusions.push(name);
    });

    // First `exclude` **then** `rename` and order is important since
    // `exclude` and `rename` are not associative.
    return rename(renames, exclude(exclusions, trait));
}

/**
 * Create a Trait (a custom property descriptor map) that represents the given
 * `object`'s own properties. Property descriptor map is a "custom", because it
 * inherits from `Trait.prototype` and it's property descriptors may contain
 * two attributes that is not part of the ES5 specification:
 *
 *  - "required" (this property must be provided by another trait
 *    before an instance of this trait can be created)
 *  - "conflict" (when the trait is composed with another trait,
 *    a unique value for this property is provided by two or more traits)
 *
 * Data properties bound to the `Trait.required` singleton exported by
 * this module will be marked as "required" properties.
 *
 * @param {Object} object
 *    Map of properties to compose trait from.
 * @returns {Trait}
 *    Trait / Property descriptor map containing all the own properties of the
 *    given argument.
 */
function trait(object) {
  var map;
  var trait = object;

  if (!(object instanceof Trait)) {
    // If the passed `object` is not already an instance of `Trait`, we create
    // a property descriptor `map` containing descriptors for the own properties
    // of the given `object`.  `map` is then used to create a `Trait` instance
    // after all properties are mapped.  Note that we can't create a trait and
    // then just copy properties into it since that will fail for inherited
    // read-only properties.
    map = {};

    // Each own property of the given `object` is mapped to a data property
    // whose value is a property descriptor.
    Object.keys(object).forEach(function (name) {

      // If property of an `object` is equal to a `Trait.required`, it means
      // that it was marked as "required" property, in which case we map it
      // to "required" property.
      if (Trait.required ==
          Object.getOwnPropertyDescriptor(object, name).value) {
        map[name] = {
          value: RequiredPropertyDescriptor(name),
          enumerable: true
        };
      }
      // Otherwise property is mapped to it's property descriptor.
      else {
        map[name] = {
          value: Object.getOwnPropertyDescriptor(object, name),
          enumerable: true
        };
      }
    });

    trait = Object.create(Trait.prototype, map);
  }
  return trait;
}

/**
 * Compose a property descriptor map that inherits from `Trait.prototype` and
 * contains property descriptors for all the own properties of the passed
 * traits.
 *
 * If two or more traits have own properties with the same name, the returned
 * trait will contain a "conflict" property for that name. Composition is a
 * commutative and associative operation, and the order of its arguments is
 * irrelevant.
 */
function compose(trait1, trait2/*, ...*/) {
  // Create a new property descriptor `map` to which all the own properties
  // of the passed traits are copied.  This map will be used to create a `Trait`
  // instance that will be the result of this composition.
  var map = {};

  // Properties of each passed trait are copied to the composition.
  Array.prototype.forEach.call(arguments, function(trait) {
    // Copying each property of the given trait.
    Object.keys(trait).forEach(function(name) {

      // If `map` already owns a property with the `name` and it is not
      // marked "required".
      if (owns(map, name) && !map[name].value.required) {

        // If the source trait's property with the `name` is marked as
        // "required", we do nothing, as the requirement was already resolved
        // by a property in the `map` (because it already contains a
        // non-required property with that `name`).  But if properties are just
        // different, we have a name clash and we substitute it with a property
        // that is marked "conflict".
        if (!isRequiredProperty(trait, name) &&
            !equivalentDescriptors(map[name].value, trait[name])
        ) {
          map[name] = {
            value: ConflictPropertyDescriptor(name),
            enumerable: true
          };
        }
      }

      // Otherwise, the `map` does not have an own property with the `name`, or
      // it is marked "required".  Either way, the trait's property is copied to
      // the map (if the property of the `map` is marked "required", it is going
      // to be resolved by the property that is being copied).
      else {
        map[name] = { value: trait[name], enumerable: true };
      }
    });
  });

  return Object.create(Trait.prototype, map);
}

/**
 *  `defineProperties` is like `Object.defineProperties`, except that it
 *  ensures that:
 *    - An exception is thrown if any property in a given `properties` map
 *      is marked as "required" property and same named property is not
 *      found in a given `prototype`.
 *    - An exception is thrown if any property in a given `properties` map
 *      is marked as "conflict" property.
 * @param {Object} object
 *    Object to define properties on.
 * @param {Object} properties
 *    Properties descriptor map.
 * @returns {Object}
 *    `object` that was passed as a first argument.
 */
function defineProperties(object, properties) {

  // Create a map into which we will copy each verified property from the given
  // `properties` description map. We use it to verify that none of the
  // provided properties is marked as a "conflict" property and that all
  // "required" properties are resolved by a property of an `object`, so we
  // can throw an exception before mutating object if that isn't the case.
  var verifiedProperties = {};

  // Coping each property from a given `properties` descriptor map to a
  // verified map of property descriptors.
  Object.keys(properties).forEach(function(name) {

    // If property is marked as "required" property and we don't have a same
    // named property in a given `object` we throw an exception. If `object`
    // has same named property just skip this property since required property
    // is was inherited and there for requirement was satisfied.
    if (isRequiredProperty(properties, name)) {
      if (!(name in object))
        throwRequiredPropertyError(name);
    }

    // If property is marked as "conflict" property we throw an exception.
    else if (isConflictProperty(properties, name)) {
      throwConflictPropertyError(name);
    }

    // If property is not marked neither as "required" nor "conflict" property
    // we copy it to verified properties map.
    else {
      verifiedProperties[name] = properties[name];
    }
  });

  // If no exceptions were thrown yet, we know that our verified property
  // descriptor map has no properties marked as "conflict" or "required",
  // so we just delegate to the built-in `Object.defineProperties`.
  return Object.defineProperties(object, verifiedProperties);
}

/**
 *  `create` is like `Object.create`, except that it ensures that:
 *    - An exception is thrown if any property in a given `properties` map
 *      is marked as "required" property and same named property is not
 *      found in a given `prototype`.
 *    - An exception is thrown if any property in a given `properties` map
 *      is marked as "conflict" property.
 * @param {Object} prototype
 *    prototype of the composed object
 * @param {Object} properties
 *    Properties descriptor map.
 * @returns {Object}
 *    An object that inherits form a given `prototype` and implements all the
 *    properties defined by a given `properties` descriptor map.
 */
function create(prototype, properties) {

  // Creating an instance of the given `prototype`.
  var object = Object.create(prototype);

  // Overriding `toString`, `constructor` methods if they are just inherited
  // from `Object.prototype` with a same named methods of the `Trait.prototype`
  // that will have more relevant behavior.
  overrideBuiltInMethods(object, Trait.prototype);

  // Trying to define given `properties` on the `object`. We use our custom
  // `defineProperties` function instead of build-in `Object.defineProperties`
  // that behaves exactly the same, except that it will throw if any
  // property in the given `properties` descriptor is marked as "required" or
  // "conflict" property.
  return defineProperties(object, properties);
}

/**
 * Composes new trait. If two or more traits have own properties with the
 * same name, the new trait will contain a "conflict" property for that name.
 * "compose" is a commutative and associative operation, and the order of its
 * arguments is not significant.
 *
 * **Note:** Use `Trait.compose` instead of calling this function with more
 * than one argument. The multiple-argument functionality is strictly for
 * backward compatibility.
 *
 * @params {Object} trait
 *    Takes traits as an arguments
 * @returns {Object}
 *    New trait containing the combined own properties of all the traits.
 * @example
 *    var newTrait = compose(trait_1, trait_2, ..., trait_N)
 */
function Trait(trait1, trait2) {

  // If the function was called with one argument, the argument should be
  // an object whose properties are mapped to property descriptors on a new
  // instance of Trait, so we delegate to the trait function.
  // If the function was called with more than one argument, those arguments
  // should be instances of Trait or plain property descriptor maps
  // whose properties should be mixed into a new instance of Trait,
  // so we delegate to the compose function.

  return trait2 === undefined ? trait(trait1) : compose.apply(null, arguments);
}

Object.freeze(Object.defineProperties(Trait.prototype, {
  toString: {
    value: function toString() {
      return "[object " + this.constructor.name + "]";
    }
  },

  /**
   * `create` is like `Object.create`, except that it ensures that:
   *    - An exception is thrown if this trait defines a property that is
   *      marked as required property and same named property is not
   *      found in a given `prototype`.
   *    - An exception is thrown if this trait contains property that is
   *      marked as "conflict" property.
   * @param {Object}
   *    prototype of the compared object
   * @returns {Object}
   *    An object with all of the properties described by the trait.
   */
  create: {
    value: function createTrait(prototype) {
      return create(undefined === prototype ? Object.prototype : prototype,
                    this);
    },
    enumerable: true
  },

  /**
   * Composes a new resolved trait, with all the same properties as the original
   * trait, except that all properties whose name is an own property of
   * `resolutions` will be renamed to the value of `resolutions[name]`. If
   * `resolutions[name]` is `null`, the property is marked as "required".
   * @param {Object} resolutions
   *   An object whose own properties serve as a mapping from old names to new
   *   names, or to `null` if the property should be excluded.
   * @returns {Object}
   *   New trait with the same own properties as the original trait but renamed.
   */
  resolve: {
    value: function resolveTrait(resolutions) {
      return resolve(resolutions, this);
    },
    enumerable: true
  }
}));

/**
 * @see compose
 */
Trait.compose = Object.freeze(compose);
Object.freeze(compose.prototype);

/**
 * Constant singleton, representing placeholder for required properties.
 * @type {Object}
 */
Trait.required = Object.freeze(Object.create(Object.prototype, {
  toString: {
    value: Object.freeze(function toString() {
      return "<Trait.required>";
    })
  }
}));
Object.freeze(Trait.required.toString.prototype);

exports.Trait = Object.freeze(Trait);
