
/* 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.no_data = true;
		this.model = model;
		this.fields = fields;
        this.fields.__count = {type: 'integer', string:_t('Quantity')};
        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) {
        var row_gb_changed = !_.isEqual(row_groupby, this.rows.groupby),
            col_gb_changed = !_.isEqual(col_groupby, this.cols.groupby);
        
        this.domain = domain;
        this.rows.groupby = row_groupby;
        this.cols.groupby = col_groupby;

        if (row_gb_changed) { this.rows.headers = null; }
        if (col_gb_changed) { this.cols.headers = null; }

        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 cell = _.findWhere(this.cells, {x: Math.min(id1, id2), y: Math.max(id1, id2)});
		return (cell !== undefined) ? cell.values : (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),
			otherRoot = this.get_other_root(header),
			fields = otherRoot.groupby.concat(this.measures);

        if (header.path.length === header.root.groupby.length) {
            header.root.groupby.push(groupby);
        }
        groupby = [groupby].concat(otherRoot.groupby);

        return this.get_groups(groupby, fields, header.domain).then(function (groups) {
            _.each(groups.reverse(), function (group) {
                // make header
                var child = self.make_header(group, header);
                child.expanded = false;
                header.children.splice(0,0, child);
                header.root.headers.splice(header.root.headers.indexOf(header) + 1, 0, child);
                // make cells
                _.each(self.get_ancestors_and_self(group), function (data) {
                    var values = _.map(self.measures, function (m) {
                        return data.attributes.aggregates[m.field];
                    });
                    var other = _.find(otherRoot.headers, function (h) {
                        if (header.root === self.cols) {
                            return _.isEqual(data.path.slice(1), h.path);
                        } else {
                            return _.isEqual(_.rest(data.path), h.path);
                            }
                        });
                    if (other) {
                        self.add_cell(child.id, other.id, values);
                    }
                });
            });
            header.expanded = true;
        });
	},

	make_header: function (group, parent) {
		var title = parent ? group.attributes.value : _t('Total');
        return {
			id: _.uniqueId(),
			path: parent ? parent.path.concat(title) : [],
			title: title,
			children: [],
			domain: parent ? group.model._domain : this.domain,
			root: parent ? parent.root : undefined,
		};
	},

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

	// ----------------------------------------------------------------------
	// Data updating methods
	// ----------------------------------------------------------------------
    // Load the data from the db, using the method this.load_data
    // 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;

		return this.load_data().then (function (result) {
			if (result) {
				self.no_data = false;
				self[self.cols.headers ? 'update_headers' : 'expand_headers'](self.cols, result.col_headers);
				self[self.rows.headers ? 'update_headers' : 'expand_headers'](self.rows, result.row_headers);
			} else {
				self.no_data = true;
			}
		});
	},

	expand_headers: function (root, new_headers) {
		root.headers = new_headers;
		_.each(root.headers, function (header) {
			header.root = root;
			header.expanded = (header.children.length > 0);
		});
	},

	update_headers: function (root, new_headers) {
		_.each(root.headers, function (header) {
			var corresponding_header = _.find(new_headers, function (h) {
				return _.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;
			}
		});
		var updated_headers = _.filter(new_headers, function (header) {
			return (header.expanded !== undefined);
		});
		_.each(updated_headers, function (header) {
			if (!header.expanded) {
				header.children = [];
			}
			header.root = root;
		});
		root.headers = updated_headers;
	},

    // ----------------------------------------------------------------------
    // Data loading methods
    // ----------------------------------------------------------------------

    // To obtain all the values required to draw the full table, we have to do 
    // at least      2 + min(row.groupby.length, col.groupby.length)
    // calls to readgroup. To simplify the code, we will always do 
    // 2 + row.groupby.length calls. For example, if row.groupby = [r1, r2, r3] 
    // and col.groupby = [c1, c2], then we will make the call with the following 
    // groupbys: [r1,r2,r3], [c1,r1,r2,r3], [c1,c2,r1,r2,r3], [].
    load_data: function () {
        var self = this,
            cols = this.cols.groupby,
            rows = this.rows.groupby,
            visible_fields = rows.concat(cols, self.measures);

        if (this.measures.length === 0) {
            return $.Deferred.resolve().promise();
        }

        var groupbys = _.map(_.range(cols.length + 1), function (i) {
            return cols.slice(0, i).concat(rows);
        });
        groupbys.push([]);

        var get_data_requests = _.map(groupbys, function (groupby) {
            return self.get_groups(groupby, visible_fields, self.domain);
        });

        return $.when.apply(null, get_data_requests).then(function () {
            var data = Array.prototype.slice.call(arguments),
                row_data = data[0],
                col_data = (cols.length !== 0) ? data[data.length - 2] : [],
                has_data = data[data.length - 1][0];

            return has_data && self.format_data(col_data, row_data, data);
        });
    },

	get_groups: function (groupbys, fields, domain, path) {
		var self = this,
            groupby = (groupbys.length) ? groupbys[0] : [];
        path = path || [];

		return this._query_db(groupby, fields, domain, path).then(function (groups) {
            if (groupbys.length > 1) {
                var get_subgroups = $.when.apply(null, _.map(groups, function (group) {
                    return self.get_groups(_.rest(groupbys), fields, group.model._domain, path.concat(group.attributes.value)).then(function (subgroups) {
                        group.children = subgroups;
                    });
                }));
                 return get_subgroups.then(function () {
                     return groups;
                 });
            } else {
                return groups;
            }
		});

	},

	_query_db: function (groupby, fields, domain, path) {
		var self = this,
            field_ids = _.without(_.pluck(fields, 'field'), '__count'),
            fields = _.map(field_ids, function(f) { return self.raw_field(f); });

		return this.model.query(field_ids)
			.filter(domain)
			.group_by(groupby.field)
			.then(function (results) {
				var groups = _.filter(results, function (group) {
					return group.attributes.length > 0;
				});
				return _.map(groups, function (g) { return self.format_group(g, path); });
			});
	},

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

    // add the path to the group and sanitize the value...
    format_group: function (group, current_path) {
        var attrs = group.attributes,
            value = attrs.value,
            grouped_on = attrs.grouped_on ? this.raw_field(attrs.grouped_on) : false;

        if (value === false) {
            group.attributes.value = _t('Undefined');
        } else if (grouped_on && this.fields[grouped_on].type === 'selection') {
            var selection = this.fields[grouped_on].selection,
                value_lookup = _.where(selection, {0:value}); 
            group.attributes.value = value_lookup ? value_lookup[0][1] : _t('Undefined');
        } else if (value instanceof Array) {
            group.attributes.value = value[1];
        }

        group.path = (value !== undefined) ? (current_path || []).concat(group.attributes.value) : [];
        group.attributes.aggregates.__count = group.attributes.length;

        return group;
    },

	format_data: function (col_data, row_data, cell_data) {
		var self = this,
			dim_row = this.rows.groupby.length,
			dim_col = this.cols.groupby.length,
            col_headers = this.get_ancestors_and_self(this.make_headers(col_data, dim_col)),
            row_headers = this.get_ancestors_and_self(this.make_headers(row_data, dim_row));

        this.cells = [];
		_.each(cell_data, function (data, index) {
			self.make_cells(data, index, [], row_headers, col_headers);
		}); // not pretty. make it more functional?

		return {col_headers: col_headers, row_headers: row_headers};
	},

	make_headers: function (data, depth, parent) {
		var self = this,
            main = this.make_header(data, parent);

		if (main.path.length < depth) {
			main.children = _.map(data.children || data, function (data_pt) {
				return self.make_headers (data_pt, depth, main);
			});
		}
		return main;
	},

	make_cells: function (data, index, current_path, rows, cols) {
		var self = this;
		_.each(data, function (group) {
			var attr = group.attributes,
				path = attr.grouped_on ? current_path.concat(attr.value) : current_path,
				values = _.map(self.measures, function (measure) { return attr.aggregates[measure.field]; }),
                row = _.find(rows, function (header) { return _.isEqual(header.path, path.slice(index)); }),
                col = _.find(cols, function (header) { return _.isEqual(header.path, path.slice(0, index)); });

			self.add_cell(row.id, col.id, values);
			if (group.children) {
				self.make_cells (group.children, index, path, rows, cols);
			}
		});
	},

});

})();


