/**
 * 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 (i = 0; i < length; ++i) {
                    ret = me.apply(operations[i], ret);
                }
            }

            entry[operation] = ret;
        }

        return ret;
    }
});
