
(function() {

var instance = openerp;
openerp.web.data = {};

/**
 * Serializes the sort criterion array of a dataset into a form which can be
 * consumed by OpenERP's RPC APIs.
 *
 * @param {Array} criterion array of fields, from first to last criteria, prefixed with '-' for reverse sorting
 * @returns {String} SQL-like sorting string (``ORDER BY``) clause
 */
instance.web.serialize_sort = function (criterion) {
    return _.map(criterion,
        function (criteria) {
            if (criteria[0] === '-') {
                return criteria.slice(1) + ' DESC';
            }
            return criteria + ' ASC';
        }).join(', ');
};

instance.web.Query = instance.web.Class.extend({
    init: function (model, fields) {
        this._model = model;
        this._fields = fields;
        this._filter = [];
        this._context = {};
        this._limit = false;
        this._offset = 0;
        this._order_by = [];
    },
    clone: function (to_set) {
        to_set = to_set || {};
        var q = new instance.web.Query(this._model, this._fields);
        q._context = this._context;
        q._filter = this._filter;
        q._limit = this._limit;
        q._offset = this._offset;
        q._order_by = this._order_by;

        for(var key in to_set) {
            if (!to_set.hasOwnProperty(key)) { continue; }
            switch(key) {
            case 'filter':
                q._filter = new instance.web.CompoundDomain(
                        q._filter, to_set.filter);
                break;
            case 'context':
                q._context = new instance.web.CompoundContext(
                        q._context, to_set.context);
                break;
            case 'limit':
            case 'offset':
            case 'order_by':
                q['_' + key] = to_set[key];
            }
        }
        return q;
    },
    _execute: function () {
        var self = this;
        return instance.session.rpc('/web/dataset/search_read', {
            model: this._model.name,
            fields: this._fields || false,
            domain: instance.web.pyeval.eval('domains',
                    [this._model.domain(this._filter)]),
            context: instance.web.pyeval.eval('contexts',
                    [this._model.context(this._context)]),
            offset: this._offset,
            limit: this._limit,
            sort: instance.web.serialize_sort(this._order_by)
        }).then(function (results) {
            self._count = results.length;
            return results.records;
        }, null);
    },
    /**
     * Fetches the first record matching the query, or null
     *
     * @returns {jQuery.Deferred<Object|null>}
     */
    first: function () {
        var self = this;
        return this.clone({limit: 1})._execute().then(function (records) {
            delete self._count;
            if (records.length) { return records[0]; }
            return null;
        });
    },
    /**
     * Fetches all records matching the query
     *
     * @returns {jQuery.Deferred<Array<>>}
     */
    all: function () {
        return this._execute();
    },
    /**
     * Fetches the number of records matching the query in the database
     *
     * @returns {jQuery.Deferred<Number>}
     */
    count: function () {
        if (this._count !== undefined) { return $.when(this._count); }
        return this._model.call(
            'search_count', [this._filter], {
                context: this._model.context(this._context)});
    },
    /**
     * Performs a groups read according to the provided grouping criterion
     *
     * @param {String|Array<String>} grouping
     * @returns {jQuery.Deferred<Array<openerp.web.QueryGroup>> | null}
     */
    group_by: function (grouping) {
        var ctx = instance.web.pyeval.eval(
            'context', this._model.context(this._context));

        // undefined passed in explicitly (!)
        if (_.isUndefined(grouping)) {
            grouping = [];
        }

        if (!(grouping instanceof Array)) {
            grouping = _.toArray(arguments);
        }
        if (_.isEmpty(grouping) && !ctx['group_by_no_leaf']) {
            return null;
        }
        var raw_fields = _.map(grouping.concat(this._fields || []), function (field) {
            return field.split(':')[0];
        });

        var self = this;
        return this._model.call('read_group', {
            groupby: grouping,
            fields: _.uniq(raw_fields),
            domain: this._model.domain(this._filter),
            context: ctx,
            offset: this._offset,
            limit: this._limit,
            orderby: instance.web.serialize_sort(this._order_by) || false
        }).then(function (results) {
            return _(results).map(function (result) {
                // FIX: querygroup initialization
                result.__context = result.__context || {};
                result.__context.group_by = result.__context.group_by || [];
                _.defaults(result.__context, ctx);
                return new instance.web.QueryGroup(
                    self._model.name, grouping[0], result);
            });
        });
    },
    /**
     * Creates a new query with the union of the current query's context and
     * the new context.
     *
     * @param context context data to add to the query
     * @returns {openerp.web.Query}
     */
    context: function (context) {
        if (!context) { return this; }
        return this.clone({context: context});
    },
    /**
     * Creates a new query with the union of the current query's filter and
     * the new domain.
     *
     * @param domain domain data to AND with the current query filter
     * @returns {openerp.web.Query}
     */
    filter: function (domain) {
        if (!domain) { return this; }
        return this.clone({filter: domain});
    },
    /**
     * Creates a new query with the provided limit replacing the current
     * query's own limit
     *
     * @param {Number} limit maximum number of records the query should retrieve
     * @returns {openerp.web.Query}
     */
    limit: function (limit) {
        return this.clone({limit: limit});
    },
    /**
     * Creates a new query with the provided offset replacing the current
     * query's own offset
     *
     * @param {Number} offset number of records the query should skip before starting its retrieval
     * @returns {openerp.web.Query}
     */
    offset: function (offset) {
        return this.clone({offset: offset});
    },
    /**
     * Creates a new query with the provided ordering parameters replacing
     * those of the current query
     *
     * @param {String...} fields ordering clauses
     * @returns {openerp.web.Query}
     */
    order_by: function (fields) {
        if (fields === undefined) { return this; }
        if (!(fields instanceof Array)) {
            fields = _.toArray(arguments);
        }
        if (_.isEmpty(fields)) { return this; }
        return this.clone({order_by: fields});
    }
});

instance.web.QueryGroup = instance.web.Class.extend({
    init: function (model, grouping_field, read_group_group) {
        // In cases where group_by_no_leaf and no group_by, the result of
        // read_group has aggregate fields but no __context or __domain.
        // Create default (empty) values for those so that things don't break
        var fixed_group = _.extend(
            {__context: {group_by: []}, __domain: []},
            read_group_group);

        var aggregates = {};
        _(fixed_group).each(function (value, key) {
            if (key.indexOf('__') === 0
                    || key === grouping_field
                    || key === grouping_field + '_count') {
                return;
            }
            aggregates[key] = value || 0;
        });

        this.model = new instance.web.Model(
            model, fixed_group.__context, fixed_group.__domain);

        var raw_field = grouping_field && grouping_field.split(':')[0];
        var group_size = fixed_group[raw_field + '_count'] || fixed_group.__count || 0;
        var leaf_group = fixed_group.__context.group_by.length === 0;

        this.attributes = {
            folded: !!(fixed_group.__fold),
            grouped_on: grouping_field,
            // if terminal group (or no group) and group_by_no_leaf => use group.__count
            length: group_size,
            value: fixed_group[raw_field],
            // A group is open-able if it's not a leaf in group_by_no_leaf mode
            has_children: !(leaf_group && fixed_group.__context['group_by_no_leaf']),

            aggregates: aggregates
        };
    },
    get: function (key) {
        return this.attributes[key];
    },
    subgroups: function () {
        return this.model.query().group_by(this.model.context().group_by);
    },
    query: function () {
        return this.model.query.apply(this.model, arguments);
    }
});

instance.web.Model.include({
    /**
    new openerp.web.Model([session,] model_name[, context[, domain]])

    @constructs instance.web.Model
    @extends instance.web.Class
    
    @param {openerp.web.Session} [session] The session object used to communicate with
    the server.
    @param {String} model_name name of the OpenERP model this object is bound to
    @param {Object} [context]
    @param {Array} [domain]
    */
    init: function() {
        var session, model_name, context, domain;
        var args = _.toArray(arguments);
        args.reverse();
        session = args.pop();
        if (session && ! (session instanceof openerp.web.Session)) {
            model_name = session;
            session = null;
        } else {
            model_name = args.pop();
        }
        context = args.length > 0 ? args.pop() : null;
        domain = args.length > 0 ? args.pop() : null;

        this._super(session, model_name, context, domain);
        this._context = context || {};
        this._domain = domain || [];
    },
    session: function() {
        if (! this._session)
            return instance.session;
        return this._super();
    },
    /**
     * @deprecated does not allow to specify kwargs, directly use call() instead
     */
    get_func: function (method_name) {
        var self = this;
        return function () {
            return self.call(method_name, _.toArray(arguments));
        };
    },
    /**
     * Call a method (over RPC) on the bound OpenERP model.
     *
     * @param {String} method name of the method to call
     * @param {Array} [args] positional arguments
     * @param {Object} [kwargs] keyword arguments
     * @param {Object} [options] additional options for the rpc() method
     * @returns {jQuery.Deferred<>} call result
     */
    call: function (method, args, kwargs, options) {
        args = args || [];
        kwargs = kwargs || {};
        if (!_.isArray(args)) {
            // call(method, kwargs)
            kwargs = args;
            args = [];
        }
        instance.web.pyeval.ensure_evaluated(args, kwargs);
        return this._super(method, args, kwargs, options);
    },
    /**
     * Fetches a Query instance bound to this model, for searching
     *
     * @param {Array<String>} [fields] fields to ultimately fetch during the search
     * @returns {instance.web.Query}
     */
    query: function (fields) {
        return new instance.web.Query(this, fields);
    },
    /**
     * Executes a signal on the designated workflow, on the bound OpenERP model
     *
     * @param {Number} id workflow identifier
     * @param {String} signal signal to trigger on the workflow
     */
    exec_workflow: function (id, signal) {
        return this.session().rpc('/web/dataset/exec_workflow', {
            model: this.name,
            id: id,
            signal: signal
        });
    },
    /**
     * Fetches the model's domain, combined with the provided domain if any
     *
     * @param {Array} [domain] to combine with the model's internal domain
     * @returns {instance.web.CompoundDomain} The model's internal domain, or the AND-ed union of the model's internal domain and the provided domain
     */
    domain: function (domain) {
        if (!domain) { return this._domain; }
        return new instance.web.CompoundDomain(
            this._domain, domain);
    },
    /**
     * Fetches the combination of the user's context and the domain context,
     * combined with the provided context if any
     *
     * @param {Object} [context] to combine with the model's internal context
     * @returns {instance.web.CompoundContext} The union of the user's context and the model's internal context, as well as the provided context if any. In that order.
     */
    context: function (context) {
        return new instance.web.CompoundContext(
            instance.session.user_context, this._context, context || {});
    },
    /**
     * Button action caller, needs to perform cleanup if an action is returned
     * from the button (parsing of context and domain, and fixup of the views
     * collection for act_window actions)
     *
     * FIXME: remove when evaluator integrated
     */
    call_button: function (method, args) {
        instance.web.pyeval.ensure_evaluated(args, {});
        return this.session().rpc('/web/dataset/call_button', {
            model: this.name,
            method: method,
            // Should not be necessary anymore. Integrate remote in this?
            domain_id: null,
            context_id: args.length - 1,
            args: args || []
        });
    },
});

instance.web.DataSet =  instance.web.Class.extend(instance.web.PropertiesMixin, {
    /**
     * Collection of OpenERP records, used to share records and the current selection between views.
     *
     * @constructs instance.web.DataSet
     *
     * @param {String} model the OpenERP model this dataset will manage
     */
    init: function(parent, model, context) {
        instance.web.PropertiesMixin.init.call(this);
        this.model = model;
        this.context = context || {};
        this.index = null;
        this._sort = [];
        this._model = new instance.web.Model(model, context);
    },
    previous: function () {
        this.index -= 1;
        if (!this.ids.length) {
            this.index = null;
        } else if (this.index < 0) {
            this.index = this.ids.length - 1;
        }
        return this;
    },
    next: function () {
        this.index += 1;
        if (!this.ids.length) {
            this.index = null;
        } else if (this.index >= this.ids.length) {
            this.index = 0;
        }
        return this;
    },
    select_id: function(id) {
        var idx = this.get_id_index(id);
        if (idx === null) {
            return false;
        } else {
            this.index = idx;
            return true;
        }
    },
    get_id_index: function(id) {
        for (var i=0, ii=this.ids.length; i<ii; i++) {
            // Here we use type coercion because of the mess potentially caused by
            // OpenERP ids fetched from the DOM as string. (eg: dhtmlxcalendar)
            // OpenERP ids can be non-numeric too ! (eg: recursive events in calendar)
            if (id == this.ids[i]) {
                return i;
            }
        }
        return null;
    },
    /**
     * Read records.
     *
     * @param {Array} ids identifiers of the records to read
     * @param {Array} fields fields to read and return, by default all fields are returned
     * @returns {$.Deferred}
     */
    read_ids: function (ids, fields, options) {
        if (_.isEmpty(ids))
            return $.Deferred().resolve([]);
            
        options = options || {};
        // TODO: reorder results to match ids list
        return this._model.call('read',
            [ids, fields || false],
            {context: this.get_context(options.context)});
    },
    /**
     * Read a slice of the records represented by this DataSet, based on its
     * domain and context.
     *
     * @param {Array} [fields] fields to read and return, by default all fields are returned
     * @params {Object} [options]
     * @param {Number} [options.offset=0] The index from which selected records should be returned
     * @param {Number} [options.limit=null] The maximum number of records to return
     * @returns {$.Deferred}
     */
    read_slice: function (fields, options) {
        var self = this;
        options = options || {};
        return this._model.query(fields)
                .limit(options.limit || false)
                .offset(options.offset || 0)
                .all().done(function (records) {
            self.ids = _(records).pluck('id');
        });
    },
    /**
     * Reads the current dataset record (from its index)
     *
     * @params {Array} [fields] fields to read and return, by default all fields are returned
     * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
     * @returns {$.Deferred}
     */
    read_index: function (fields, options) {
        options = options || {};
        return this.read_ids([this.ids[this.index]], fields, options).then(function (records) {
            if (_.isEmpty(records)) { return $.Deferred().reject().promise(); }
            return records[0];
        });
    },
    /**
     * Reads default values for the current model
     *
     * @param {Array} [fields] fields to get default values for, by default all defaults are read
     * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
     * @returns {$.Deferred}
     */
    default_get: function(fields, options) {
        options = options || {};
        return this._model.call('default_get',
            [fields], {context: this.get_context(options.context)});
    },
    /**
     * Creates a new record in db
     *
     * @param {Object} data field values to set on the new record
     * @param {Object} options Dictionary that can contain the following keys:
     *   - readonly_fields: Values from readonly fields that were updated by
     *     on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
     * @returns {$.Deferred}
     */
    create: function(data, options) {
        var self = this;
        return this._model.call('create', [data], {
            context: this.get_context()
        }).done(function () {
            self.trigger('dataset_changed', data, options);
        });
    },
    /**
     * Saves the provided data in an existing db record
     *
     * @param {Number|String} id identifier for the record to alter
     * @param {Object} data field values to write into the record
     * @param {Object} options Dictionary that can contain the following keys:
     *   - context: The context to use in the server-side call.
     *   - readonly_fields: Values from readonly fields that were updated by
     *     on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
     * @returns {$.Deferred}
     */
    write: function (id, data, options) {
        options = options || {};
        var self = this;
        return this._model.call('write', [[id], data], {
            context: this.get_context(options.context)
        }).done(function () {
            self.trigger('dataset_changed', id, data, options);
        });
    },
    /**
     * Deletes an existing record from the database
     *
     * @param {Number|String} ids identifier of the record to delete
     */
    unlink: function(ids) {
        var self = this;
        return this._model.call('unlink', [ids], {
            context: this.get_context()
        }).done(function () {
            self.trigger('dataset_changed', ids);
        });
    },
    /**
     * Calls an arbitrary RPC method
     *
     * @param {String} method name of the method (on the current model) to call
     * @param {Array} [args] arguments to pass to the method
     * @param {Function} callback
     * @param {Function} error_callback
     * @returns {$.Deferred}
     */
    call: function (method, args) {
        return this._model.call(method, args);
    },
    /**
     * Calls a button method, usually returning some sort of action
     *
     * @param {String} method
     * @param {Array} [args]
     * @returns {$.Deferred}
     */
    call_button: function (method, args) {
        return this._model.call_button(method, args);
    },
    /**
     * Fetches the "readable name" for records, based on intrinsic rules
     *
     * @param {Array} ids
     * @returns {$.Deferred}
     */
    name_get: function(ids) {
        return this._model.call('name_get', [ids], {context: this.get_context()});
    },
    /**
     * 
     * @param {String} name name to perform a search for/on
     * @param {Array} [domain=[]] filters for the objects returned, OpenERP domain
     * @param {String} [operator='ilike'] matching operator to use with the provided name value
     * @param {Number} [limit=0] maximum number of matches to return
     * @param {Function} callback function to call with name_search result
     * @returns {$.Deferred}
     */
    name_search: function (name, domain, operator, limit) {
        return this._model.call('name_search', {
            name: name || '',
            args: domain || false,
            operator: operator || 'ilike',
            context: this._model.context(),
            limit: limit || 0
        });
    },
    /**
     * @param name
     */
    name_create: function(name) {
        return this._model.call('name_create', [name], {context: this.get_context()});
    },
    exec_workflow: function (id, signal) {
        return this._model.exec_workflow(id, signal);
    },
    get_context: function(request_context) {
        return this._model.context(request_context);
    },
    /**
     * Reads or changes sort criteria on the dataset.
     *
     * If not provided with any argument, serializes the sort criteria to
     * an SQL-like form usable by OpenERP's ORM.
     *
     * If given a field, will set that field as first sorting criteria or,
     * if the field is already the first sorting criteria, will reverse it.
     *
     * @param {String} [field] field to sort on, reverses it (toggle from ASC to DESC) if already the main sort criteria
     * @param {Boolean} [force_reverse=false] forces inserting the field as DESC
     * @returns {String|undefined}
     */
    sort: function (field, force_reverse) {
        if (!field) {
            return instance.web.serialize_sort(this._sort);
        }
        var reverse = force_reverse || (this._sort[0] === field);
        this._sort.splice.apply(
            this._sort, [0, this._sort.length].concat(
                _.without(this._sort, field, '-' + field)));

        this._sort.unshift((reverse ? '-' : '') + field);
        return undefined;
    },
    size: function () {
        return this.ids.length;
    },
    alter_ids: function(n_ids) {
        this.ids = n_ids;
    },
    remove_ids: function (ids) {
        this.alter_ids(_(this.ids).difference(ids));
    },
    add_ids: function(ids, at) {
        var args = [at, 0].concat(_.difference(ids, this.ids));
        this.ids.splice.apply(this.ids, args);
    },
    /**
     * Resequence records.
     *
     * @param {Array} ids identifiers of the records to resequence
     * @returns {$.Deferred}
     */
    resequence: function (ids, options) {
        options = options || {};
        return instance.session.rpc('/web/dataset/resequence', {
            model: this.model,
            ids: ids,
            context: instance.web.pyeval.eval(
                'context', this.get_context(options.context)),
        }).then(function (results) {
            return results;
        });
    },
});

instance.web.DataSetStatic =  instance.web.DataSet.extend({
    init: function(parent, model, context, ids) {
        var self = this;
        this._super(parent, model, context);
        // all local records
        this.ids = ids || [];
    },
    read_slice: function (fields, options) {
        options = options || {};
        fields = fields || {};
        var offset = options.offset || 0,
            limit = options.limit || false;
        var end_pos = limit && limit !== -1 ? offset + limit : this.ids.length;
        return this.read_ids(this.ids.slice(offset, end_pos), fields);
    },
    set_ids: function (ids) {
        this.ids = ids;
        if (ids.length === 0) {
            this.index = null;
        } else if (this.index >= ids.length - 1) {
            this.index = ids.length - 1;
        }
    },
    unlink: function(ids) {
        this.set_ids(_.without.apply(null, [this.ids].concat(ids)));
        this.trigger('unlink', ids);
        return $.Deferred().resolve({result: true});
    },
});

instance.web.DataSetSearch =  instance.web.DataSet.extend({
    /**
     * @constructs instance.web.DataSetSearch
     * @extends instance.web.DataSet
     *
     * @param {Object} parent
     * @param {String} model
     * @param {Object} context
     * @param {Array} domain
     */
    init: function(parent, model, context, domain) {
        this._super(parent, model, context);
        this.domain = domain || [];
        this._length = null;
        this.ids = [];
        this._model = new instance.web.Model(model, context, domain);
    },
    /**
     * Read a slice of the records represented by this DataSet, based on its
     * domain and context.
     *
     * @params {Object} options
     * @param {Array} [options.fields] fields to read and return, by default all fields are returned
     * @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
     * @param {Array} [options.domain] domain data to add to the request payload, ANDed with the dataset's domain
     * @param {Number} [options.offset=0] The index from which selected records should be returned
     * @param {Number} [options.limit=null] The maximum number of records to return
     * @returns {$.Deferred}
     */
    read_slice: function (fields, options) {
        options = options || {};
        var self = this;
        var q = this._model.query(fields || false)
            .filter(options.domain)
            .context(options.context)
            .offset(options.offset || 0)
            .limit(options.limit || false);
        q = q.order_by.apply(q, this._sort);

        return q.all().done(function (records) {
            // FIXME: not sure about that one, *could* have discarded count
            q.count().done(function (count) { self._length = count; });
            self.ids = _(records).pluck('id');
        });
    },
    get_domain: function (other_domain) {
        return this._model.domain(other_domain);
    },
    alter_ids: function (ids) {
        this._super(ids);
        if (this.index !== null && this.index >= this.ids.length) {
            this.index = this.ids.length > 0 ? this.ids.length - 1 : 0;
        }
    },
    remove_ids: function (ids) {
        var before = this.ids.length;
        this._super(ids);
        if (this._length) {
            this._length -= (before - this.ids.length);
        }
    },
    unlink: function(ids, callback, error_callback) {
        var self = this;
        return this._super(ids).done(function(result) {
            self.remove_ids( ids);
            self.trigger("dataset_changed", ids, callback, error_callback);
        });
    },
    size: function () {
        if (this._length !== null) {
            return this._length;
        }
        return this._super();
    }
});

instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
    virtual_id_prefix: "one2many_v_id_",
    debug_mode: true,
    init: function() {
        this._super.apply(this, arguments);
        this.reset_ids([]);
        this.last_default_get = {};
        this.running_reads = [];
    },
    default_get: function(fields, options) {
        var self = this;
        return this._super(fields, options).done(function(res) {
            self.last_default_get = res;
        });
    },
    create: function(data, options) {
        var cached = {
            id:_.uniqueId(this.virtual_id_prefix),
            values: _.extend({}, data, (options || {}).readonly_fields || {}),
            defaults: this.last_default_get
        };
        this.to_create.push(_.extend(_.clone(cached), {values: _.clone(data)}));
        this.cache.push(cached);
        return $.Deferred().resolve(cached.id).promise();
    },
    write: function (id, data, options) {
        var self = this;
        var record = _.detect(this.to_create, function(x) {return x.id === id;});
        record = record || _.detect(this.to_write, function(x) {return x.id === id;});
        var dirty = false;
        if (record) {
            for (var k in data) {
                if (record.values[k] === undefined || record.values[k] !== data[k]) {
                    dirty = true;
                    break;
                }
            }
            $.extend(record.values, data);
        } else {
            dirty = true;
            record = {id: id, values: data};
            self.to_write.push(record);
        }
        var cached = _.detect(this.cache, function(x) {return x.id === id;});
        if (!cached) {
            cached = {id: id, values: {}};
            this.cache.push(cached);
        }
        $.extend(cached.values, _.extend({}, record.values, (options || {}).readonly_fields || {}));
        if (dirty)
            this.trigger("dataset_changed", id, data, options);
        return $.Deferred().resolve(true).promise();
    },
    unlink: function(ids, callback, error_callback) {
        var self = this;
        _.each(ids, function(id) {
            if (! _.detect(self.to_create, function(x) { return x.id === id; })) {
                self.to_delete.push({id: id});
            }
        });
        this.to_create = _.reject(this.to_create, function(x) { return _.include(ids, x.id);});
        this.to_write = _.reject(this.to_write, function(x) { return _.include(ids, x.id);});
        this.cache = _.reject(this.cache, function(x) { return _.include(ids, x.id);});
        this.set_ids(_.without.apply(_, [this.ids].concat(ids)));
        this.trigger("dataset_changed", ids, callback, error_callback);
        return $.async_when({result: true}).done(callback);
    },
    reset_ids: function(ids) {
        this.set_ids(ids);
        this.to_delete = [];
        this.to_create = [];
        this.to_write = [];
        this.cache = [];
        this.delete_all = false;
        _.each(_.clone(this.running_reads), function(el) {
            el.reject();
        });
    },
    read_ids: function (ids, fields, options) {
        var self = this;
        var to_get = [];
        _.each(ids, function(id) {
            var cached = _.detect(self.cache, function(x) {return x.id === id;});
            var created = _.detect(self.to_create, function(x) {return x.id === id;});
            if (created) {
                _.each(fields, function(x) {if (cached.values[x] === undefined)
                    cached.values[x] = created.defaults[x] || false;});
            } else {
                if (!cached || !_.all(fields, function(x) {return cached.values[x] !== undefined;}))
                    to_get.push(id);
            }
        });
        var return_records = function() {
            var records = _.map(ids, function(id) {
                var c = _.find(self.cache, function(c) {return c.id === id;});
                return _.isUndefined(c) ? c : _.extend({}, c.values, {"id": id});
            });
            if (self.debug_mode) {
                if (_.include(records, undefined)) {
                    throw "Record not correctly loaded";
                }
            }
            var sort_fields = self._sort,
                    compare = function (v1, v2) {
                        return (v1 < v2) ? -1
                             : (v1 > v2) ? 1
                             : 0;
                    };
            // Array.sort is not necessarily stable. We must be careful with this because
            // sorting an array where all items are considered equal is a worst-case that
            // will randomize the array with an unstable sort! Therefore we must avoid
            // sorting if there are no sort_fields (i.e. all items are considered equal)
            // See also: http://ecma262-5.com/ELS5_Section_15.htm#Section_15.4.4.11 
            //           http://code.google.com/p/v8/issues/detail?id=90
            if (sort_fields.length) {
                records.sort(function (a, b) {
                    return _.reduce(sort_fields, function (acc, field) {
                        if (acc) { return acc; }
                        var sign = 1;
                        if (field[0] === '-') {
                            sign = -1;
                            field = field.slice(1);
                        }
                        return sign * compare(a[field], b[field]);
                    }, 0);
                });
            }
            return $.when(records);
        };
        if(to_get.length > 0) {
            var def = $.Deferred();
            self.running_reads.push(def);
            def.always(function() {
                self.running_reads = _.without(self.running_reads, def);
            });
            this._super(to_get, fields, options).then(function() {
                def.resolve.apply(def, arguments);
            }, function() {
                def.reject.apply(def, arguments);
            });
            return def.then(function(records) {
                _.each(records, function(record, index) {
                    var id = to_get[index];
                    var cached = _.detect(self.cache, function(x) {return x.id === id;});
                    if (!cached) {
                        self.cache.push({id: id, values: record});
                    } else {
                        // I assume cache value is prioritary
                        cached.values = _.defaults(_.clone(cached.values), record);
                    }
                });
                return return_records();
            });
        } else {
            return return_records();
        }
    },
    /**
     * Invalidates caching of a record in the dataset to ensure the next read
     * of that record will hit the server.
     *
     * Of use when an action is going to remote-alter a record which will then
     * need to be reloaded, e.g. action button.
     *
     * @param {Object} id record to remove from the BDS's cache
     */
    evict_record: function (id) {
        // Don't evict records which haven't yet been saved: there is no more
        // recent data on the server (and there potentially isn't any data),
        // and this breaks the assumptions of other methods (that the data
        // for new and altered records is both in the cache and in the to_write
        // or to_create collection)
        if (_(this.to_create.concat(this.to_write)).find(function (record) {
                return record.id === id; })) {
            return;
        }
        for(var i=0, len=this.cache.length; i<len; ++i) {
            var record = this.cache[i];
            // if record we call the button upon is in the cache
            if (record.id === id) {
                // evict it so it gets reloaded from server
                this.cache.splice(i, 1);
                break;
            }
        }
    },
    call_button: function (method, args) {
        this.evict_record(args[0][0]);
        return this._super(method, args);
    },
    exec_workflow: function (id, signal) {
        this.evict_record(id);
        return this._super(id, signal);
    },
    alter_ids: function(n_ids) {
        this._super(n_ids);
        this.trigger("dataset_changed", n_ids);
    },
});
instance.web.BufferedDataSet.virtual_id_regex = /^one2many_v_id_.*$/;

instance.web.ProxyDataSet = instance.web.DataSetSearch.extend({
    init: function() {
        this._super.apply(this, arguments);
        this.create_function = null;
        this.write_function = null;
        this.read_function = null;
        this.default_get_function = null;
        this.unlink_function = null;
    },
    read_ids: function (ids, fields, options) {
        if (this.read_function) {
            return this.read_function(ids, fields, options, this._super);
        } else {
            return this._super.apply(this, arguments);
        }
    },
    default_get: function(fields, options) {
        if (this.default_get_function) {
            return this.default_get_function(fields, options, this._super);
        } else {
            return this._super.apply(this, arguments);
        }
    },
    create: function(data, options) {
        if (this.create_function) {
            return this.create_function(data, options, this._super);
        } else {
            return this._super.apply(this, arguments);
        }
    },
    write: function (id, data, options) {
        if (this.write_function) {
            return this.write_function(id, data, options, this._super);
        } else {
            return this._super.apply(this, arguments);
        }
    },
    unlink: function(ids) {
        if (this.unlink_function) {
            return this.unlink_function(ids, this._super);
        } else {
            return this._super.apply(this, arguments);
        }
    },
});

instance.web.CompoundContext = instance.web.Class.extend({
    init: function () {
        this.__ref = "compound_context";
        this.__contexts = [];
        this.__eval_context = null;
        var self = this;
        _.each(arguments, function(x) {
            self.add(x);
        });
    },
    add: function (context) {
        this.__contexts.push(context);
        return this;
    },
    set_eval_context: function (eval_context) {
        this.__eval_context = eval_context;
        return this;
    },
    get_eval_context: function () {
        return this.__eval_context;
    },
    eval: function() {
        return instance.web.pyeval.eval('context', this, undefined, {no_user_context: true});
    },
});

instance.web.CompoundDomain = instance.web.Class.extend({
    init: function () {
        this.__ref = "compound_domain";
        this.__domains = [];
        this.__eval_context = null;
        var self = this;
        _.each(arguments, function(x) {
            self.add(x);
        });
    },
    add: function(domain) {
        this.__domains.push(domain);
        return this;
    },
    set_eval_context: function(eval_context) {
        this.__eval_context = eval_context;
        return this;
    },
    get_eval_context: function() {
        return this.__eval_context;
    },
    eval: function() {
        return instance.web.pyeval.eval('domain', this);
    },
});

instance.web.DropMisordered = instance.web.Class.extend({
    /**
     * @constructs instance.web.DropMisordered
     * @extends instance.web.Class
     *
     * @param {Boolean} [failMisordered=false] whether mis-ordered responses should be failed or just ignored
     */
    init: function (failMisordered) {
        // local sequence number, for requests sent
        this.lsn = 0;
        // remote sequence number, seqnum of last received request
        this.rsn = -1;
        this.failMisordered = failMisordered || false;
    },
    /**
     * Adds a deferred (usually an async request) to the sequencer
     *
     * @param {$.Deferred} deferred to ensure add
     * @returns {$.Deferred}
     */
    add: function (deferred) {
        var res = $.Deferred();

        var self = this, seq = this.lsn++;
        deferred.done(function () {
            if (seq > self.rsn) {
                self.rsn = seq;
                res.resolve.apply(res, arguments);
            } else if (self.failMisordered) {
                res.reject();
            }
        }).fail(function () {
            res.reject.apply(res, arguments);
        });

        return res.promise();
    }
});

instance.web.SimpleIndexedDB = instance.web.Class.extend({
    /** 
     * A simple wrapper around IndexedDB that provides an asynchronous 
     * localStorage-like Key-Value api that persists between browser 
     * restarts.
     *
     * It will not work if the browser doesn't support a recent version 
     * of IndexedDB, and it may fail if the user refuses db access.
     *
     * All instances of SimpleIndexedDB will by default refer to the same
     * IndexedDB database, if you want to pick another one, use the 'name'
     * option on instanciation.
     */
    init: function(opts){
        var self = this;
        var opts = opts || {};

        this.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
        this.db = undefined;
        this._ready = new $.Deferred();

        if(this.indexedDB && this.indexedDB.open){
            var request = this.indexedDB.open( opts.name || "SimpleIndexedDB" ,1);

            request.onerror = function(event){
                self.db = null;
                self._ready.reject(event.target.error);
            };

            request.onsuccess = function(event){
                self.db = request.result;
                self._ready.resolve();
            };

            request.onupgradeneeded = function(event){
                self.db = event.target.result;

                var objectStore = self.db.createObjectStore("keyvals", { keyPath:"key" });
                self._ready.resolve();
            };
        }else{
            this.db = null;
            this._ready.reject({type:'UnknownError', message:'IndexedDB is not supported by your Browser'});
        }
    },

    /**
     * returns true if the browser supports the necessary IndexedDB API 
     * (but doesn't tell if the db can be created, check ready() for that) 
     */
    isSupportedByBrowser: function(){
        return this.indexedDB && this.indexedDB.open;
    },

    /**
     * returns a deferred that resolves when the db has been successfully
     * initialized. done/failed callbacks are optional.
     */
    ready: function(done,failed){
        this._ready.then(done,failed);
        return this._ready.promise();
    },

    /**
     * fetches the value associated to 'key' in the db. if the key doesn't
     * exists, it returns undefined. The returned value is provided as a 
     * deferred, or as the first parameter of the optional 'done' callback.
     */
    getItem: function(key,done,failed){
        var self = this;
        var def = new $.Deferred();
            def.then(done,failed);

        this._ready.then(function(){
                var request = self.db.transaction(["keyvals"],"readwrite")
                                     .objectStore("keyvals")
                                     .get(key);

                request.onsuccess = function(){
                    def.resolve(request.result ? request.result.value : undefined);
                };

                request.onerror = function(event){ 
                    def.reject(event.target.error);
                };
            },function(){
                def.reject({type:'UnknownError', message:'Could not initialize the IndexedDB database'});
            });

        return def.promise();
    },
    
    /**
     * Associates a value to 'key' in the db, overwriting previous value if
     * necessary. Contrary to localStorage, the value is not limited to strings and
     * can be any javascript object, even with cyclic references !
     *
     * Be sure to check for failure as the user may refuse to have data localy stored.
     */
    setItem: function(key,value,done,failed){
        var self = this;
        var def = new $.Deferred();
            def.then(done,failed);

        this._ready.then(function(){
                var request = self.db.transaction(["keyvals"],"readwrite")
                                     .objectStore("keyvals")
                                     .put( {key:key, value:value} );

                request.onsuccess = function(){
                    def.resolve();
                };

                request.onerror = function(event){ 
                    def.reject(event.target.error);
                };
            },function(){
                def.reject({type:'UnknownError', message:'Could not initialize the IndexedDB database'});
            });

        return def.promise();
    },

    /**
     * Removes the value associated with 'key' from the db. 
     */
    removeItem: function(key,done,failed){
        var self = this;
        var def = new $.Deferred();
            def.then(done,failed);

        this._ready.then(function(){
                var request = self.db.transaction(["keyvals"],"readwrite")
                                  .objectStore("keyvals")
                                  .delete(key);

                request.onsuccess = function(){
                    def.resolve();
                };

                request.onerror = function(event){ 
                    def.reject(event.target.error);
                };
            },function(){
                def.reject({type:'UnknownError', message:'Could not initialize the IndexedDB database'});
            });

        return def.promise();
    },
});

})();

// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
