
/* jshint undef: false  */

(function () {
'use strict';

var _lt = openerp.web._lt;
var _t = openerp.web._t;

//  PivotTable requires a call to update_data after initialization
openerp.web_graph.PivotTable = openerp.web.Class.extend({

	init: function (model, domain, fields, options) {
		this.cells = [];
		this.domain = domain;
        this.context = options.context;
		this.no_data = true;
		this.updating = false;
		this.model = model;
		this.fields = fields;
        this.fields.__count = {type: 'integer', string:_t('Count')};
        this.measures = options.measures || [];
        this.rows = { groupby: options.row_groupby, headers: null };
        this.cols = { groupby: options.col_groupby, headers: null };
	},

	// ----------------------------------------------------------------------
	// Configuration methods
	// ----------------------------------------------------------------------
    // this.measures: list of measure [measure], measure = {field: _, string: _, type: _}
    // this.rows.groupby, this.cols.groupby : list of groupbys used for describing rows (...),
    //      a groupby is also {field:_, string:_, type:_}
    //      If its type is date/datetime, field can have the corresponding interval in its description,
    //      for example 'create_date:week'.
	set_measures: function (measures) {
        this.measures = measures;
        return this.update_data();
	},

	toggle_measure: function (measure) {
        var current_measure = _.findWhere(this.measures, measure);
        if (current_measure) {  // remove current_measure
            var index = this.measures.indexOf(current_measure);
            this.measures = _.without(this.measures, current_measure);
            if (this.measures.length === 0) {
                this.no_data = true;
            } else {
                _.each(this.cells, function (cell) {
                    cell.values.splice(index, 1);
                });
            }
            return $.Deferred().resolve();
        } else {  // add a new measure
            this.measures.push(measure);
            return this.update_data();
        }
	},

    set: function (domain, row_groupby, col_groupby, measures_groupby) {
        var self = this;
        if (this.updating) {
            return this.updating.then(function () {
                self.updating = false;
                return self.set(domain, row_groupby, col_groupby, measures_groupby);
            });
        }
        var row_gb_changed = !_.isEqual(row_groupby, this.rows.groupby),
            col_gb_changed = !_.isEqual(col_groupby, this.cols.groupby),
			measures_gb_changed = !_.isEqual(measures_groupby, this.measures);

        this.domain = domain;
        this.rows.groupby = row_groupby;
        this.cols.groupby = col_groupby;

		if (measures_groupby.length) { this.measures = measures_groupby; }

        if (row_gb_changed) { this.rows.headers = null; }
        if (col_gb_changed) { this.cols.headers = null; }
		if (measures_gb_changed && measures_groupby.length) { this.set_measures(measures_groupby); }

        return this.update_data();
    },

	// ----------------------------------------------------------------------
	// Cells manipulation methods
	// ----------------------------------------------------------------------
    // cells are objects {x:_, y:_, values:_} where x < y and values is an array
    //       of values (one for each measure).  The condition x < y might look
    //       unnecessary, but it makes the rest of the code simpler: headers
    //       don't krow if they are rows or cols, they just know their id, so
    //       it is useful that a call get_values(id1, id2) is the same as get_values(id2, id1)
	add_cell : function (id1, id2, values) {
		this.cells.push({x: Math.min(id1, id2), y: Math.max(id1, id2), values: values});
	},

	get_values: function (id1, id2, default_values) {
        var cells = this.cells,
            x = Math.min(id1, id2),
            y = Math.max(id1, id2);
        for (var i = 0; i < cells.length; i++) {
            if (cells[i].x == x && cells[i].y == y) {
                return cells[i].values;
            }
        }
        return (default_values || new Array(this.measures.length));
	},

	// ----------------------------------------------------------------------
	// Headers/Rows/Cols manipulation methods
	// ----------------------------------------------------------------------
    // this.rows.headers, this.cols.headers = [header] describe the tree structure
    //      of rows/cols.  Headers are objects
    //      {
    //          id:_,               (unique id obviously)
    //          path: [...],        (array of all parents title, with its own title at the end)
    //          title:_,            (name of the row/col)
    //          children:[_],       (subrows or sub cols of this row/col)
    //          domain:_,           (domain of data in this row/col)
    //          root:_              (ref to this.rows or this.cols corresponding to the header)
    //          expanded:_          (boolean, true if it has been expanded)
    //      }
	is_row: function (id) {
        return !!_.findWhere(this.rows.headers, {id:id});
    },

	is_col: function (id) {
        return !!_.findWhere(this.cols.headers, {id:id});
	},

	get_header: function (id) {
		return _.findWhere(this.rows.headers.concat(this.cols.headers), {id:id});
	},

    _get_headers_with_depth: function (headers, depth) {
        return _.filter(headers, function (header) {
            return header.path.length === depth;
        });
    },

	// return all columns with a path length of 'depth'
	get_cols_with_depth: function (depth) {
        return this._get_headers_with_depth(this.cols.headers, depth);
	},

	// return all rows with a path length of 'depth'
	get_rows_with_depth: function (depth) {
        return this._get_headers_with_depth(this.rows.headers, depth);
	},

    get_ancestor_leaves: function (header) {
        return _.where(this.get_ancestors_and_self(header), {expanded:false});
    },

	// return all non expanded rows
	get_rows_leaves: function () {
		return _.where(this.rows.headers, {expanded:false});
	},

	// return all non expanded cols
	get_cols_leaves: function () {
		return _.where(this.cols.headers, {expanded:false});
	},

    get_ancestors: function (header) {
        var self = this;
        if (!header.children) return [];
        return  [].concat.apply([], _.map(header.children, function (c) {
            return self.get_ancestors_and_self(c);
        }));
    },

    get_ancestors_and_self: function (header) {
        var self = this;
        return [].concat.apply([header], _.map(header.children, function (c) {
            return self.get_ancestors_and_self(c);
        }));
    },

	get_total: function (header) {
        return (header) ? this.get_values(header.id, this.get_other_root(header).headers[0].id)
                        : this.get_values(this.rows.headers[0].id, this.cols.headers[0].id);
	},

	get_other_root: function (header) {
		return (header.root === this.rows) ? this.cols : this.rows;
	},

    main_row: function () { return this.rows.headers[0]; },

    main_col: function () { return this.cols.headers[0]; },

	// ----------------------------------------------------------------------
	// Table manipulation methods : fold/expand/swap
	// ----------------------------------------------------------------------
	// return true if the folding changed the groupbys, false if not
    fold: function (header) {
		var ancestors = this.get_ancestors(header),
            removed_ids = _.pluck(ancestors, 'id');

		header.root.headers = _.difference(header.root.headers, ancestors);
        header.children = [];
		header.expanded = false;

        this.cells = _.reject(this.cells, function (cell) {
            return (_.contains(removed_ids, cell.x) || _.contains(removed_ids, cell.y));
        });

        var new_groupby_length = _.max(_.pluck(_.pluck(header.root.headers, 'path'), 'length'));
        if (new_groupby_length < header.root.groupby.length) {
			header.root.groupby.splice(new_groupby_length);
            return true;
        }
        return false;
	},

    fold_with_depth: function (root, depth) {
        var self = this;
        _.each(this._get_headers_with_depth(root.headers, depth), function (header) {
            self.fold(header);
        });
    },

    expand_all: function () {
        this.rows.headers = null;
        this.cols.headers = null;
        return this.update_data();
    },

	expand: function (header_id, groupby) {
        var self = this,
            header = this.get_header(header_id),
            other_root = this.get_other_root(header),
            this_gb = [groupby.field],
            other_gbs = _.pluck(other_root.groupby, 'field');

        if (header.path.length === header.root.groupby.length) {
            header.root.groupby.push(groupby);
        }
        return this.perform_requests(this_gb, other_gbs, header.domain).then(function () {
            var data = Array.prototype.slice.call(arguments).slice(other_gbs.length + 1);
            _.each(data, function (data_pt) {
                self.make_headers_and_cell(
                    data_pt, header.root.headers, other_root.headers, 1, header.path, true);
            });
            header.expanded = true;
            header.children.forEach(function (child) {
                child.expanded = false;
                child.root = header.root;
            });
        });
	},

	swap_axis: function () {
		var temp = this.rows;
		this.rows = this.cols;
		this.cols = temp;
	},

	// ----------------------------------------------------------------------
	// Data updating methods
	// ----------------------------------------------------------------------
    // update_data will try to preserve the expand/not expanded status of each
    // column/row.  If you want to expand all, then set this.cols.headers/this.rows.headers
    // to null before calling update_data.
    update_data: function () {
        var self = this;
        this.updating = this.perform_requests().then (function () {
            var data = Array.prototype.slice.call(arguments);
            self.no_data = !data[0].length;
            if (self.no_data) {
                return;
            }
            var row_headers = [],
                col_headers = [];
            self.cells = [];

            var dim_col = self.cols.groupby.length,
                i, j, index;

            for (i = 0; i < self.rows.groupby.length + 1; i++) {
                for (j = 0; j < dim_col + 1; j++) {
                    index = i*(dim_col + 1) + j;
                    self.make_headers_and_cell(data[index], row_headers, col_headers, i);
                }
            }
            self.set_headers(row_headers, self.rows);
            self.set_headers(col_headers, self.cols);
        });
        return this.updating;
    },

    make_headers_and_cell: function (data_pts, row_headers, col_headers, index, prefix, expand) {
        var self = this;
        data_pts.forEach(function (data_pt) {
            var row_value = (prefix || []).concat(data_pt.attributes.value.slice(0,index));
            var col_value = data_pt.attributes.value.slice(index);

            if (expand && !_.find(col_headers, function (hdr) {return self.isEqual(col_value, hdr.path);})) {
                return;
            }
            var row = self.find_or_create_header(row_headers, row_value, data_pt);
            var col = self.find_or_create_header(col_headers, col_value, data_pt);

            var cell_value = _.map(self.measures, function (m) {
                return data_pt.attributes.aggregates[m.field];
            });
            self.cells.push({
                x: Math.min(row.id, col.id),
                y: Math.max(row.id, col.id),
                values: cell_value
            });
        });
    },

    make_header: function (values) {
        return _.extend({
            children: [],
            domain: this.domain,
            expanded: undefined,
            id: _.uniqueId(),
            path: [],
            root: undefined,
            title: undefined
        }, values || {});
    },

    find_or_create_header: function (headers, path, data_pt) {
        var self = this;
        var hdr = _.find(headers, function (header) {
            return self.isEqual(path, header.path);
        });
        if (hdr) {
            return hdr;
        }
        if (!path.length) {
            hdr = this.make_header({title: _t('Total')});
            headers.push(hdr);
            return hdr;
        }
        hdr = this.make_header({
            path:path,
            domain:data_pt.model._domain,
            title: _t(_.last(path))
        });
        var parent = _.find(headers, function (header) {
            return self.isEqual(header.path, _.initial(path, 1));
        });

        var previous = parent.children.length ? _.last(parent.children) : parent;
        headers.splice(headers.indexOf(previous) + 1, 0, hdr);
        parent.children.push(hdr);
        return hdr;
    },

    perform_requests: function (group1, group2, domain) {
        var self = this,
            requests = [],
            row_gbs = _.pluck(this.rows.groupby, 'field'),
            col_gbs = _.pluck(this.cols.groupby, 'field'),
            field_list = row_gbs.concat(col_gbs, _.pluck(this.measures, 'field')),
            fields = field_list.map(function (f) { return self.raw_field(f); });

        group1 = group1 || row_gbs;
        group2 = group2 || col_gbs;

        var i,j, groupbys;
        for (i = 0; i < group1.length + 1; i++) {
            for (j = 0; j < group2.length + 1; j++) {
                groupbys = group1.slice(0,i).concat(group2.slice(0,j));
                requests.push(self.get_groups(groupbys, fields, domain || self.domain));
            }
        }
        return $.when.apply(null, requests);
    },

    // set the 'expanded' status of new_headers more or less like root.headers, with root as root
    set_headers: function(new_headers, root) {
        var self = this;
        if (root.headers) {
            _.each(root.headers, function (header) {
                var corresponding_header = _.find(new_headers, function (h) {
                    return self.isEqual(h.path, header.path);
                });
                if (corresponding_header && header.expanded) {
                    corresponding_header.expanded = true;
                    _.each(corresponding_header.children, function (c) {
                        c.expanded = false;
                    });
                }
                if (corresponding_header && (!header.expanded)) {
                    corresponding_header.expanded = false;
                    corresponding_header.children = [];
                }
            });
            var updated_headers = _.filter(new_headers, function (header) {
                return (header.expanded !== undefined);
            });
            _.each(updated_headers, function (header) {
                header.root = root;
            });
            root.headers = updated_headers;
        } else {
            root.headers = new_headers;
            _.each(root.headers, function (header) {
                header.root = root;
                header.expanded = (header.children.length > 0);
            });
        }
        return new_headers;
    },

    get_groups: function (groupbys, fields, domain) {
        var self = this;
        return this.model.query(_.without(fields, '__count'))
            .filter(domain)
            .context(this.context)
            .lazy(false)
            .group_by(groupbys)
            .then(function (groups) {
                return groups.filter(function (group) {
                    return group.attributes.length > 0;
                }).map(function (group) {
                    var attrs = group.attributes,
                        grouped_on = attrs.grouped_on instanceof Array ? attrs.grouped_on : [attrs.grouped_on],
                        raw_grouped_on = grouped_on.map(function (f) {
                            return self.raw_field(f);
                        });
                    if (grouped_on.length === 1) {
                        attrs.value = [attrs.value];
                    }
                    attrs.value = _.range(grouped_on.length).map(function (i) {
                        var grp = grouped_on[i],
                            field = self.fields[grp];
                        if (attrs.value[i] === false) {
                            return _t('Undefined');
                        } else if (attrs.value[i] instanceof Array) {
                            return attrs.value[i][1];
                        } else if (field && field.type === 'selection') {
                            var selected = _.where(field.selection, {0: attrs.value[i]})[0];
                            return selected ? selected[1] : attrs.value[i];
                        }
                        return attrs.value[i];
                    });
                    attrs.aggregates.__count = group.attributes.length;
                    attrs.grouped_on = raw_grouped_on;
                    return group;
                });
            });
    },

    // if field is a fieldname, returns field, if field is field_id:interval, retuns field_id
    raw_field: function (field) {
        return field.split(':')[0];
    },

    isEqual: function (path1, path2) {
        if (path1.length !== path2.length) { return false; }
        for (var i = 0; i < path1.length; i++) {
            if (path1[i] !== path2[i]) {
                return false;
            }
        }
        return true;
    },

});

})();
