var Montage = require("core/core").Montage;
/**
* Subclasses of this class represent enumerated types. Instances of those
* subclasses represent possible values of those types. Enumerated type
* subclasses and their possible values can be defined as in the following:
*
* exports.Suit = Enumeration.specialize("id", "name", {
* SPADES: [0, "Spades"],
* HEARTS: [1, "Hearts"],
* DIAMONDS: [2, "Diamonds"],
* CLUBS: [3, "Clubs"]
* });
*
* // To be use like this:
* myCard = {value: 12, suit: Suit.HEARTS};
*
* Enumerated types can also be defined as class properties using the
* [getterFor()]{@link Enumeration.getterFor} constructor, as in the following:
*
* exports.Card = Montage.specialize({
*
* value: {
* value: undefined
* },
*
* suit: {
* value: undefined
* }
*
* }, {
*
* Suit: {
* get: Enumeration.getterFor("_Suit", "id", "name", {
* SPADES: [1, "Spade"],
* HEARTS: [2, "Heart"],
* DIAMONDS: [3, "Diamond"],
* CLUBS: [4, "Club"]
* })
* }
*
* });
*
* // To be use like this:
* myCard = new Card();
* myCard.value = 12;
* myCard.suit = Card.Suit.HEARTS;
*
* In addition to being created as shown above, instances of an enumerated
* type subclasses can be created using a `with*()` class method that will be
* automatically generated for each subclass based on the subclass' property
* names, as in the following:
*
* Card.Suit.ROSES = Card.Suit.withIdAndName(5, "Roses");
*
* Once instances of an enumerated type subclass are created they can be looked
* up using `for*()` class methods that will be automatically generated for each
* of the subclass's unique property names, as in the following:
*
* myCard.value = Math.floor(1 + 13 * Math.random());
* myCard.suit = Card.Suit.forId(Math.floor(1 + 4 * Math.random()));
*
* @class
* @extends external:Montage
*
* @todo [Charles] Simplify this extremely, notably by making enumerations
* instances of the Enumeration class instead of subclasses of it and by
* reworking the [getterFor()]{@link Enumeration.getterFor} constructor.
* @todo [Charles] Switch to initial-caps enumeration value names like "Spades"
* instead of all-caps names like "NAMES".
* @todo [Charles] Provide a "values" property to the enumeration similar to
* Java's "values()" Enum method.
*/
exports.Enumeration = Montage.specialize({}, /** @lends Enumeration */ {
/**
* Creates a new enumeration subclass with the specified attributes.
*
* @method
* @argument {Array.<string>|string} [uniquePropertyNames]
* @argument {Array.<string>|...string} [otherPropertyNames]
* @argument {Object} [prototypeDescriptor]
* @argument {Object} [constructorDescriptor]
* @argument {Object} [constants]
* @returns {function()} - The created Enumeration subclass.
*
* @todo [Charles] Simplify this API, possibly by making
* "uniquePropertyNames", "otherPropertyNames", and "constants"
* properties of the derived instance, leaving "specialize()"
* to its "Montage.specialize()" API and meaning.
*/
specialize: {
value: function (uniquePropertyNames, otherPropertyNames,
prototypeDescriptor, constructorDescriptor,
constants) {
return this._specialize(this._parseSpecializeArguments.apply(this, arguments));
}
},
/**
* Creates and returns a getter which, when first called, will create and
* cache a new enumeration subclass with the specified attributes.
*
* @method
* @argument {string} key
* @argument {Array.<string>|string} [uniquePropertyNames]
* @argument {Array.<string>|...string} [otherPropertyNames]
* @argument {Object} [prototypeDescriptor]
* @argument {Object} [constructorDescriptor]
* @argument {Object} [constants]
* @returns {function()} - A getter that will create and cache the desired
* Enumeration subclass.
*
* @todo [Charles] Simplify this API, replacing it with simpler
* methods like Enumeration.withNames(), Enumeration.withValues(),
* and Enumeration.withPrototypeAndValues().
*/
getterFor: {
value: function (key, uniquePropertyNames, otherPropertyNames,
prototypeDescriptor, constructorDescriptor,
constants) {
var specializeArguments = this._parseSpecializeArguments.apply(this, Array.prototype.slice.call(arguments, 1));
// Return a function that will create the desired enumeration.
return function () {
if (!this.hasOwnProperty(key)) {
this[key] = exports.Enumeration._specialize(specializeArguments);
}
return this[key];
};
}
},
_parseSpecializeArguments: {
value: function (/* uniquePropertyNames, otherPropertyNames,
prototypeDescriptor, constructorDescriptor,
constants */) {
var start, unique, other, end, i, n;
// The unique property names array is the first argument if that's
// an array, or an array containing the first argument if that's a
// non-empty string, or an empty array.
start = 0;
if (Array.isArray(arguments[start])) {
unique = arguments[start];
} else if (typeof arguments[start] === "string" && arguments[start].length > 0) {
unique = [arguments[start]];
} else if (arguments[start] === null || arguments[start] === undefined || arguments[start] === "") {
unique = [];
} else {
unique = [];
start -= 1;
}
// The other property names array is the next argument if that's an
// array, or an array containing the next argument and all following
// ones that are strings if there are any, or an empty array.
other = Array.isArray(arguments[start + 1]) && arguments[start + 1];
for (i = start + 1, n = arguments.length; !other; i += 1) {
if (i === n || !arguments[i] || typeof arguments[i] !== "string") {
other = Array.prototype.slice.call(arguments, start + 1, i);
start = i - 2;
}
}
// The remaining argument values come from the remaining arguments.
end = Math.min(arguments.length, start + 5);
return {
uniquePropertyNames: unique,
otherPropertyNames: other,
prototypeDescriptor: end > start + 3 ? arguments[start + 2] : {},
constructorDescriptor: end > start + 4 ? arguments[start + 3] : {},
constantDescriptors: end > start + 2 ? arguments[end - 1] : {}
};
}
},
/**
* @private
* @method
*/
_specialize: {
value: function (parsed) {
var unique = parsed.uniquePropertyNames,
other = parsed.otherPropertyNames,
prototype = parsed.prototypeDescriptor,
constructor = parsed.constructorDescriptor,
constants = parsed.constantDescriptors,
enumeration, create, i, keys, key;
// Create the desired enumeration, including a constructor taking
// only unique property values (like `withId()`), and if there are
// other property values a constructor taking values for those too
// (like `withIdAndName()`).
this._addPropertiesToDescriptor(prototype, unique, other);
this._addLookupFunctionsToDescriptor(constructor, unique);
this._addConstructorFunctionToDescriptor(constructor, unique, []);
if (other.length) {
this._addConstructorFunctionToDescriptor(constructor, unique, other);
}
enumeration = Montage.specialize.call(exports.Enumeration, prototype, constructor);
// Add the requested constants.
create = this._makeCreateFunction(unique, other);
keys = Object.keys(constants);
for (i = 0; (key = keys[i]); ++i) {
enumeration[key] = create.apply(enumeration, constants[key]);
}
// Return the created enumeration.
return enumeration;
}
},
_addPropertiesToDescriptor: {
value: function(prototypeDescriptor, uniquePropertyNames, otherPropertyNames) {
var i, n;
for (i = 0, n = uniquePropertyNames.length; i < n; i += 1) {
if (!prototypeDescriptor[uniquePropertyNames[i]]) {
prototypeDescriptor[uniquePropertyNames[i]] = {value: undefined};
}
}
for (i = 0, n = otherPropertyNames.length; i < n; i += 1) {
if (!prototypeDescriptor[otherPropertyNames[i]]) {
prototypeDescriptor[otherPropertyNames[i]] = {value: undefined};
}
}
}
},
_addLookupFunctionsToDescriptor: {
value: function(constructorDescriptor, uniquePropertyNames) {
var name, lookup, i, n;
for (i = 0, n = uniquePropertyNames.length; i < n; i += 1) {
name = "for" + uniquePropertyNames[i][0].toUpperCase() + uniquePropertyNames[i].slice(1);
lookup = this._makeLookupFunction(uniquePropertyNames[i]);
constructorDescriptor[name] = {value: lookup};
}
}
},
_makeLookupFunction: {
value: function(propertyName) {
return function (propertyValue) {
return this._instances &&
this._instances[propertyName] &&
this._instances[propertyName][propertyValue];
};
}
},
_addConstructorFunctionToDescriptor: {
value: function(constructorDescriptor, uniquePropertyNames, otherPropertyNames) {
var create, names, name, i, n;
if (uniquePropertyNames.length || otherPropertyNames.length) {
create = this._makeCreateFunction(uniquePropertyNames, otherPropertyNames);
names = ["with"];
for (i = 0, n = uniquePropertyNames.length; i < n; i += 1) {
names.push(uniquePropertyNames[i][0].toUpperCase());
names.push(uniquePropertyNames[i].slice(1));
}
for (i = 0, n = otherPropertyNames.length; i < n; i += 1) {
names.push(otherPropertyNames[i][0].toUpperCase());
names.push(otherPropertyNames[i].slice(1));
}
if (names.length > 3) {
names.splice(names.length - 2, 0, "And");
}
constructorDescriptor[names.join("")] = {value: create};
}
}
},
_makeCreateFunction: {
value: function(uniquePropertyNames, otherPropertyNames) {
var self = this;
return function (propertyValues, propertiesDescriptor) {
var createArguments = self._parseCreateArguments(uniquePropertyNames, otherPropertyNames, arguments);
return self._create(this, uniquePropertyNames, otherPropertyNames, createArguments);
};
}
},
_parseCreateArguments: {
value: function(uniquePropertyNames, otherPropertyNames, zArguments) {
var count = uniquePropertyNames.length + otherPropertyNames.length;
return {
propertyValues: Array.prototype.slice.call(zArguments, 0, count),
propertyDescriptors: zArguments[count] || {}
};
}
},
_create: {
value: function(type, uniquePropertyNames, otherPropertyNames, zArguments) {
var instance, name, i, m, n;
// Create the instance.
instance = new type();
// Add to the instance the properties specified in the descriptor.
Montage.defineProperties(instance, zArguments.propertyDescriptors);
// Add to the instance the property values specified.
for (i = 0, n = uniquePropertyNames.length, m = otherPropertyNames.length; i < n + m; i += 1) {
name = i < n ? uniquePropertyNames[i] : otherPropertyNames[i - n];
instance[name] = zArguments.propertyValues[i];
}
// Record the instance in the appropriate lookup maps.
for (i = 0, n = uniquePropertyNames.length; i < n; i += 1) {
type._instances = type._instances || {};
type._instances[uniquePropertyNames[i]] = type._instances[uniquePropertyNames[i]] || {};
type._instances[uniquePropertyNames[i]][zArguments.propertyValues[i]] = instance;
}
// Return the created instance.
return instance;
}
}
});