    /**
     * @extends Ext.ux.ajax.Simlet
     * @alias simlet.json
     * JSON Simlet.
     */
    Ext.define('Ext.ux.ajax.JsonSimlet', function () {

        function makeSortFn (def, cmp) {
            var order = def.direction,
                sign = (order && order.toUpperCase() == 'DESC') ? -1 : 1;

            return function (leftRec, rightRec) {
                var lhs = leftRec[def.property],
                    rhs = rightRec[def.property],
                    c = (lhs < rhs) ? -1 : ((rhs < lhs) ? 1 : 0);

                if (c || !cmp) {
                    return c * sign;
                }

                return cmp(leftRec, rightRec);
            }
        }

        function makeSortFns (defs, cmp) {
            for (var sortFn = cmp, i = defs && defs.length; i; ) {
                sortFn = makeSortFn(defs[--i], sortFn);
            }
            return sortFn;
        }

        return {
            extend: 'Ext.ux.ajax.Simlet',
            alias: 'simlet.json',

            getData: function (ctx) {
                var me = this,
                    data = me.data,
                    params = ctx.params,
                    order = (params.group||'')+'-'+(params.sort||'')+'-'+(params.dir||''),
                    fields,
                    sortFn,
                    i;

                if (!order) {
                    return data;
                }

                ctx.groupSpec = params.group && Ext.decode(params.group);
                if (order == me.currentOrder) {
                    return me.sortedData;
                }

                fields = params.sort;
                if (params.dir) {
                    fields = [{ direction: params.dir, property: fields }];
                } else {
                    fields = Ext.decode(params.sort);
                }

                sortFn = makeSortFns((ctx.sortSpec = fields));
                sortFn = makeSortFns(ctx.groupSpec, sortFn);

                data = data.slice(0); // preserve 'physical' order of raw data...
                if (sortFn) {
                    Ext.Array.sort(data, sortFn);
                }

                me.sortedData = data;
                me.currentOrder = order;

                return data;
            },

            getPage: function (ctx, data) {
                var ret = data,
                    length = data.length,
                    start = ctx.params.start || 0,
                    end = ctx.params.limit ? Math.min(length, start + ctx.params.limit) : length;

                if (start || end < length) {
                    ret = ret.slice(start, end);
                }

                return ret;
            },

            getGroupSummary: function (groupField, rows, ctx) {
                return rows[0];
            },

            getSummary: function (ctx, data, page) {
                var me = this,
                    groupField = ctx.groupSpec[0].property,
                    accum,
                    todo = {},
                    summary = [],
                    fieldValue,
                    lastFieldValue;

                Ext.each(page, function (rec) {
                    fieldValue = rec[groupField];
                    todo[fieldValue] = true;
                });

                function flush () {
                    if (accum) {
                        summary.push(me.getGroupSummary(groupField, accum, ctx));
                        accum = null;
                    }
                }

                // data is ordered primarily by the groupField, so one pass can pick up all
                // the summaries one at a time.
                Ext.each(data, function (rec) {
                    fieldValue = rec[groupField];

                    if (lastFieldValue !== fieldValue) {
                        flush();
                        lastFieldValue = fieldValue;
                    }

                    if (!todo[fieldValue]) {
                        // if we have even 1 summary, we have summarized all that we need
                        // (again because data and page are ordered by groupField)
                        return !summary.length;
                    }

                    if (accum) {
                        accum.push(rec);
                    } else {
                        accum = [rec];
                    }

                    return true;
                });

                flush(); // make sure that last pesky summary goes...

                return summary;
            },

            doGet: function (ctx) {
                var me = this,
                    data = me.getData(ctx),
                    page = me.getPage(ctx, data),
                    reader = ctx.xhr.options.proxy.reader,
                    ret = me.callParent(arguments), // pick up status/statusText
                    response = {};

                if (reader.root) {
                    response[reader.root] = page;
                    response[reader.totalProperty] = data.length;
                } else {
                    response = page;
                }

                if (ctx.groupSpec) {
                    response.summaryData = me.getSummary(ctx, data, page);
                }

                ret.responseText = Ext.encode(response);
                return ret;
            }
        };
    }());