# Source: data/model/enumeration.js

    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;
            }
        }

    });