/**
 * This class provides name derivation methods for use by a `Schema`.
 * 
 * # Caching
 * 
 * Because most name derivations are only textual manipulations of input strings, the
 * results can be cached. This is handled by the `apply` method by giving it the name of
 * the method to call. For example:
 * 
 *      var str = namer.capitalize('foo'); //  = "Foo"
 *      
 *      var str = namer.apply('capitalize', 'foo');
 * 
 * The return value of the second call (using `apply`) is the same as the first, however,
 * the results of `capitalize` are cached. This allows repeated calls to `apply` given the
 * same operation and string to avoid the extra string manipulation.
 * 
 * # Usage
 * 
 * This class is not intended to be created by application code. It is created by `Schema`
 * instances as directed by the `namer` config. Application code can derive from this
 * class and set the `namer` config to customize naming conventions used by the `Schema`.
 * 
 * @protected
 */
Ext.define('Ext.data.schema.Namer', {
    mixins: [
        'Ext.mixin.Factoryable'
    ],
 
    requires: [
        'Ext.util.Inflector'
    ],
 
    alias: 'namer.default', // also configures Factoryable 
 
    isNamer: true,
 
    //------------------------------------------------------------------------- 
    // Cacheable methods 
 
    capitalize: function (name) {
        return Ext.String.capitalize(name);
    },
 
    /**
     * Given the name of a foreign key field, return the role of the related entity. For
     * example, fields like "fooId" or "foo_id" this implementation returns "foo".
     * @template
     */
    fieldRole: function (name) {
        var match = name.match(this.endsWithIdRe, '');
        if (match) {
            name = name.substr(0, name.length - (match[1] || match[2]).length);
        }
        return this.apply('uncapitalize', name);
    },
 
    idField: function (name) {
        // ex: User ==> userId 
        return this.apply('uncapitalize,singularize', name) + 'Id';
    },
 
    instanceName: function(roleName) {
        return this.apply('underscore', roleName);
    },
 
    multiRole: function (name) {
        return this.apply('undotted,uncapitalize,pluralize', name);
    },
 
    pluralize: function (name) {
        return Ext.util.Inflector.pluralize(name);
    },
    
    readerRoot: function (roleName) {
        return this.apply('uncapitalize', roleName);
    },
 
    singularize: function (name) {
        return Ext.util.Inflector.singularize(name);
    },
 
    storeName: function (roleName) {
        return this.apply('underscore', roleName);
    },
 
    uncapitalize: function (name) {
        return Ext.String.uncapitalize(name);
    },
 
    underscore: function (name) {
        return '_' + name;
    },
 
    uniRole: function (name) {
        return this.apply('undotted,uncapitalize,singularize', name);
    },
 
    undotted: function (name) {
        if (name.indexOf('.') < 0) {
            return name;
        }
 
        var parts = name.split('.'),
            index = parts.length;
 
        while (index-- > 1) {
            parts[index] = this.apply('capitalize', parts[index]);
        }
 
        return parts.join('');
    },
 
    //------------------------------------------------------------------------- 
    // Non-Cacheable methods 
 
    getterName: function (role) {
        var name = role.role;
 
        if (role && role.isMany) {
            //return this.apply('uncapitalize,pluralize', name); 
            return name;
        }
 
        //return this.apply('capitalize,singularize', name); 
        return 'get' + this.apply('capitalize', name);
    },
 
    inverseFieldRole: function (leftType, unique, rightRole, rightType) {
        // In a FK association, the left side may be unique in which case we have a 
        // one-to-one otherwise we have a one-to-many. If the FK field is just the 
        // name of the right side class (e.g., if it is "order"), then we don't want 
        // to include the field name in the left role. 
        var me = this,
            leftRole = me.apply(unique ? 'uniRole' : 'multiRole', leftType),
            s1 = me.apply('pluralize', rightRole),
            s2 = me.apply('undotted,pluralize', rightType);
 
        if (s1.toLowerCase() !== s2.toLowerCase()) {
            // Otherwise, we have something like "creatorId" on Ticket that holds a 
            // reference to User. This makes the right role "creator" so rather than 
            // make the left role "tickets" we make it "creatorTickets". 
            leftRole = rightRole + me.apply('capitalize', leftRole);
        }
 
        return leftRole;
    },
 
    manyToMany: function (relation, leftType, rightType) {
        var me = this,
            // ex: UserGroups 
            ret = me.apply('undotted,capitalize,singularize', leftType) +
                  me.apply('undotted,capitalize,pluralize', rightType);
 
        if (relation) {
            ret = me.apply('capitalize', relation + ret);
        }
 
        return ret;
    },
 
    /**
     * Returns the name for a one-to-many association given the left and right type and
     * the associating `role`.
     * 
     * In many cases the `role` matches the target type. For example, an OrderItem might
     * have an "orderId" field which would have a `role` of "order". If this is a reference
     * to an Order entity then the association name will be "OrderOrderItems".
     * 
     * When the `role` does not match, it is included in the association name. For example,
     * consider a Ticket entity with a "creatorId" field that references a User entity.
     * The `role` of that field will (by default) be "creator". The returned association
     * name will be "UserCreatorTickets".
     */
    manyToOne: function (leftType, leftRole, rightType, rightRole) {
        // ex: OrderItem -> Order  ==> OrderOrderItems 
        //  Ticket (creator) -> User ==> UserCreatorTickets 
        return this.apply('capitalize,singularize', rightType) +
               this.apply('capitalize', leftRole);
    },
 
    matrixRole: function (relation, entityType) {
        var ret = this.apply(relation ? 'multiRole,capitalize' : 'multiRole', entityType);
        return relation ? relation + ret : ret;
    },
 
    oneToOne: function (leftType, leftRole, rightType, rightRole) {
        return this.apply('undotted,capitalize,singularize', rightType) +
               this.apply('capitalize', leftRole);
    },
 
    setterName: function (role) {
        return 'set' + this.apply('capitalize', role.role);
    },
    
    //------------------------------------------------------------------------- 
    // Private 
 
    endsWithIdRe: /(?:(_id)|[^A-Z](Id))$/,
 
    cache: {},
 
    apply: function (operation, name) {
        var me = this,
            cache = me.cache,
            entry = cache[name] || (cache[name] = {}),
            ret = entry[operation],
            i, length, operations;
 
        if (!ret) {
            if (operation.indexOf(',') < 0) {
                ret = me[operation](name);
            } else {
                length = (operations = operation.split(',')).length;
                ret = name;
                for (= 0; i < length; ++i) {
                    ret = me.apply(operations[i], ret);
                }
            }
 
            entry[operation] = ret;
        }
 
        return ret;
    }
});