odoo.define("web.Domain", function (require) {
"use strict";

var collections = require("web.collections");
var pyUtils = require("web.py_utils");
var py = window.py; // look py.js

/**
 * The Domain Class allows to work with a domain as a tree and provides tools
 * to manipulate array and string representations of domains.
 */
var Domain = collections.Tree.extend({
    /**
     * @constructor
     * @param {string|Array|boolean|undefined} domain
     *        The given domain can be:
     *            * a string representation of the Python prefix-array
     *              representation of the domain.
     *            * a JS prefix-array representation of the domain.
     *            * a boolean where the "true" domain match all records and the
     *              "false" domain does not match any records.
     *            * undefined, considered as the false boolean.
     *            * a number, considered as true except 0 considered as false.
     * @param {Object} [evalContext] - in case the given domain is a string, an
     *                               evaluation context might be needed
     */
    init: function (domain, evalContext) {
        this._super.apply(this, arguments);
        if (_.isArray(domain) || _.isString(domain)) {
            this._parse(this.normalizeArray(_.clone(this.stringToArray(domain, evalContext))));
        } else {
            this._data = !!domain;
        }
    },

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * Evaluates the domain with a set of values.
     *
     * @param {Object} values - a mapping {fieldName -> fieldValue} (note: all
     *                        the fields used in the domain should be given a
     *                        value otherwise the computation will break)
     * @returns {boolean}
     */
    compute: function (values) {
        if (this._data === true || this._data === false) {
            // The domain is a always-true or a always-false domain
            return this._data;
        } else if (_.isArray(this._data)) {
            // The domain is a [name, operator, value] entity
            // First check if we have the field value in the field values set
            // and if the first part of the domain contains 'parent.field'
            // get the value from the parent record.
            var isParentField = false;
            var fieldName = this._data[0];
            // We split the domain first part and check if it's a match
            // for the syntax 'parent.field'.
            var parentField = this._data[0].split('.');
            if ('parent' in values && parentField.length === 2) {
                fieldName = parentField[1];
                isParentField = parentField[0] === 'parent' &&
                    fieldName in values.parent;
            }
            if (!(this._data[0] in values) && !(isParentField)) {
                throw new Error(_.str.sprintf(
                    "Unknown field %s in domain",
                    this._data[0]
                ));
            }
            var fieldValue;
            if (!isParentField) {
                fieldValue = values[fieldName];
            } else {
                fieldValue = values.parent[fieldName];
            }

            switch (this._data[1]) {
                case "=":
                case "==":
                    return _.isEqual(fieldValue, this._data[2]);
                case "!=":
                case "<>":
                    return !_.isEqual(fieldValue, this._data[2]);
                case "<":
                    return (fieldValue < this._data[2]);
                case ">":
                    return (fieldValue > this._data[2]);
                case "<=":
                    return (fieldValue <= this._data[2]);
                case ">=":
                    return (fieldValue >= this._data[2]);
                case "in":
                    return _.contains(
                        _.isArray(this._data[2]) ? this._data[2] : [this._data[2]],
                        fieldValue
                    );
                case "not in":
                    return !_.contains(
                        _.isArray(this._data[2]) ? this._data[2] : [this._data[2]],
                        fieldValue
                    );
                case "like":
                    return (fieldValue.toLowerCase().indexOf(this._data[2].toLowerCase()) >= 0);
                case "ilike":
                    return (fieldValue.indexOf(this._data[2]) >= 0);
                default:
                    throw new Error(_.str.sprintf(
                        "Domain %s uses an unsupported operator",
                        this._data
                    ));
            }
        } else { // The domain is a set of [name, operator, value] entitie(s)
            switch (this._data) {
                case "&":
                    return _.every(this._children, function (child) {
                        return child.compute(values);
                    });
                case "|":
                    return _.some(this._children, function (child) {
                        return child.compute(values);
                    });
                case "!":
                    return !this._children[0].compute(values);
            }
        }
    },
    /**
     * Return the JS prefix-array representation of this domain. Note that all
     * domains that use the "false" domain cannot be represented as such.
     *
     * @returns {Array} JS prefix-array representation of this domain
     */
    toArray: function () {
        if (this._data === false) {
            throw new Error("'false' domain cannot be converted to array");
        } else if (this._data === true) {
            return [];
        } else {
            var arr = [this._data];
            return arr.concat.apply(arr, _.map(this._children, function (child) {
                return child.toArray();
            }));
        }
    },
    /**
     * @returns {string} representation of the Python prefix-array
     *                   representation of the domain
     */
    toString: function () {
        return Domain.prototype.arrayToString(this.toArray());
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * Initializes the tree representation of the domain according to its given
     * JS prefix-array representation. Note: the given array is considered
     * already normalized.
     *
     * @private
     * @param {Array} domain - normalized JS prefix-array representation of
     *                       the domain
     */
    _parse: function (domain) {
        this._data = (domain.length === 0 ? true : domain[0]);
        if (domain.length <= 1) return;

        var expected = 1;
        for (var i = 1 ; i < domain.length ; i++) {
            if (domain[i] === "&" || domain[i] === "|") {
                expected++;
            } else if (domain[i] !== "!") {
                expected--;
            }

            if (!expected) {
                i++;
                this._addSubdomain(domain.slice(1, i));
                this._addSubdomain(domain.slice(i));
                break;
            }
        }
    },

    /**
     * Adds a domain as a child (e.g. if the current domain is ["|", A, B],
     * using this method with a ["&", C, D] domain will result in a
     * ["|", "|", A, B, "&", C, D]).
     * Note: the internal tree representation is automatically simplified.
     *
     * @param {Array} domain - normalized JS prefix-array representation of a
     *                       domain to add
     */
    _addSubdomain: function (domain) {
        if (!domain.length) return;
        var subdomain = new Domain(domain);

        if (!subdomain._children.length || subdomain._data !== this._data) {
            this._children.push(subdomain);
        } else {
            var self = this;
            _.each(subdomain._children, function (childDomain) {
                self._children.push(childDomain);
            });
        }
    },

    //--------------------------------------------------------------------------
    // Static
    //--------------------------------------------------------------------------

    /**
     * Converts JS prefix-array representation of a domain to a string
     * representation of the Python prefix-array representation of this domain.
     *
     * @static
     * @param {Array|string} domain
     * @returns {string}
     */
    arrayToString: function (domain) {
        if (_.isString(domain)) return domain;
        return JSON.stringify(domain || [])
            .replace(/null/g, "None")
            .replace(/false/g, "False")
            .replace(/true/g, "True");
    },
    /*
     * @param {string} fieldName
     * @param {string} period
     * @param {string} type ('date' or 'datetime')
     * @param {boolean} forTooltip indicates if domain is used to print a tooltip
     * @param {string} comparisonPeriod
     * @returns {string} a domain in string form
     */
    constructDomain: function (fieldName, period, type, forTooltip, comparisonPeriod) {
        var leftBoundaryParams, rightBoundaryParams;
        var offsetPeriodParams;
        var t = forTooltip || false;
        function makeInterval () {
            switch (comparisonPeriod) {
                case 'previous_period':
                    _.each(offsetPeriodParams, function (value, key) {
                        if (!leftBoundaryParams[key] ||_.isNumber(leftBoundaryParams[key])) {
                            leftBoundaryParams[key] = value + (leftBoundaryParams[key] || 0);
                        } else {
                            leftBoundaryParams[key] = value + ' + ' + leftBoundaryParams[key];
                        }
                        if (!rightBoundaryParams[key] || _.isNumber(rightBoundaryParams[key])) {
                            rightBoundaryParams[key] = value + (rightBoundaryParams[key] || 0);
                        } else {
                            rightBoundaryParams[key] = value + ' + ' + rightBoundaryParams[key];
                        }
                    });
                    break;
                case 'previous_year':
                    leftBoundaryParams.years = leftBoundaryParams.years ? leftBoundaryParams.years - 1 : -1;
                    rightBoundaryParams.years = rightBoundaryParams.years ? rightBoundaryParams.years - 1 : -1;
                  break;
            }

            var stringifyParams = function (value, key) {
                return key + '=' + value;
            };
            var leftBoundaryStringifyParams = _.map(leftBoundaryParams, stringifyParams).join(', ');
            var rightBoundaryStringifyParams = _.map(rightBoundaryParams, stringifyParams).join(', ');

            if (type === 'date') {
                return "['&'," +
                    "('" + fieldName + "', '>=', (context_today() + relativedelta(" + leftBoundaryStringifyParams + ")).strftime('%Y-%m-%d'))," +
                    "('" + fieldName + "', '<', (context_today() + relativedelta(" + rightBoundaryStringifyParams + ")).strftime('%Y-%m-%d'))"+
                    "]";
            }
            else {
                return "['&'," +
                    "('" + fieldName + "', '>=', " +
                    "(datetime.datetime.combine(context_today() + relativedelta(" + leftBoundaryStringifyParams + "), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S'))," +
                    "('" + fieldName + "', '<', " +
                    "(datetime.datetime.combine(context_today() + relativedelta(" + rightBoundaryStringifyParams + "), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S'))"+
                    "]";
            }
        }
        switch (period) {
            case 'today':
                leftBoundaryParams = {};
                rightBoundaryParams = t ? {} : {days: 1};
                offsetPeriodParams = {days: -1};
                return makeInterval();
            case 'this_week':
                leftBoundaryParams = {weeks: -1, days: 1, weekday: 0};
                rightBoundaryParams = t ? {weekday: 6} : {days: 1, weekday: 0};
                offsetPeriodParams = {weeks: -1};
                return makeInterval();
            case 'this_month':
                leftBoundaryParams = {day: 1};
                rightBoundaryParams = _.extend(t ? {days: -1} : {},
                    {day: 1, months: 1});
                offsetPeriodParams = {months: -1};
                return makeInterval();
            case 'this_quarter':
                leftBoundaryParams = {months: '- (context_today().month - 1) % 3', day: 1};
                rightBoundaryParams = _.extend(t ? {days: -1} : {},
                    {months: '3 - (context_today().month - 1) % 3', day: 1});
                offsetPeriodParams = {months: -3};
                return makeInterval();
            case 'this_year':
                leftBoundaryParams = {month: 1, day: 1};
                rightBoundaryParams = _.extend(t ? {days: -1} : {},
                    {month: 1, day: 1, years: 1});
                offsetPeriodParams = {years: -1};
                return makeInterval();
            case 'yesterday':
                leftBoundaryParams = {days: -1};
                rightBoundaryParams = (t ? {days: -1} : {});
                offsetPeriodParams = {days: -1};
                return makeInterval();
            case 'last_week':
                leftBoundaryParams = {weeks: -2, days: 1, weekday: 0};
                rightBoundaryParams = t ? {weeks: -1, weekday: 6} : {weeks: -1, days: 1, weekday: 0};
                offsetPeriodParams = {weeks: -1};
                return makeInterval();
            case 'last_month':
                leftBoundaryParams = {months: -1, day: 1};
                rightBoundaryParams = _.extend(t ? {days: -1} : {}, {day: 1});
                offsetPeriodParams = {months: -1};
                return makeInterval();
            case 'last_quarter':
                leftBoundaryParams = {months: '- 3 - (context_today().month - 1) % 3', day: 1};
                rightBoundaryParams = _.extend(t ? {days: -1} : {},
                    {months: '- (context_today().month - 1) % 3', day: 1});
                offsetPeriodParams = {months: -3};
                return makeInterval();
            case 'last_year':
                leftBoundaryParams = {month: 1, day: 1, years: -1};
                rightBoundaryParams = _.extend(t ? {days: -1} : {}, {month: 1, day: 1});
                offsetPeriodParams = {years: -1};
                return makeInterval();
            case 'last_7_days':
                leftBoundaryParams = {days: -7};
                rightBoundaryParams = t ? {days: -1} : {};
                offsetPeriodParams = {days: -7};
                return makeInterval();
            case 'last_30_days':
                leftBoundaryParams = {days: -30};
                rightBoundaryParams = t ? {days: -1} : {};
                offsetPeriodParams = {days: -30};
                return makeInterval();
            case 'last_365_days':
                leftBoundaryParams = {days: -365};
                rightBoundaryParams = t ? {days: -1} : {};
                offsetPeriodParams = {days: -365};
                return makeInterval();
        }
    },
    /**
     * Converts a string representation of the Python prefix-array
     * representation of a domain to a JS prefix-array representation of this
     * domain.
     *
     * @static
     * @param {string|Array} domain
     * @param {Object} [evalContext]
     * @returns {Array}
     */
    stringToArray: function (domain, evalContext) {
        if (!_.isString(domain)) return _.clone(domain);
        return pyUtils.eval("domain", domain ? domain.replace(/%%/g, '%') : "[]", evalContext);
    },
    /**
     * Makes implicit "&" operators explicit in the given JS prefix-array
     * representation of domain (e.g [A, B] -> ["&", A, B])
     *
     * @static
     * @param {Array} domain - the JS prefix-array representation of the domain
     *                       to normalize (! will be normalized in-place)
     * @returns {Array} the normalized JS prefix-array representation of the
     *                  given domain
     */
    normalizeArray: function (domain) {
        var expected = 1;
        _.each(domain, function (item) {
            if (item === "&" || item === "|") {
                expected++;
            } else if (item !== "!") {
                expected--;
            }
        });
        if (expected < 0) {
            domain.unshift.apply(domain, _.times(Math.abs(expected), _.constant("&")));
        }
        return domain;
    },
    /**
     * Converts JS prefix-array representation of a domain to a python condition
     *
     * @static
     * @param {Array} domain
     * @returns {string}
     */
    domainToCondition: function (domain) {
        if (!domain.length) {
            return 'True';
        }
        var self = this;
        function consume(stack) {
            var len = stack.length;
            if (len <= 1) {
                return stack;
            } else if (stack[len-1] === '|' || stack[len-1] === '&' || stack[len-2] === '|' || stack[len-2] === '&') {
                return stack;
            } else if (len == 2) {
                stack.splice(-2, 2, stack[len-2] + ' and ' + stack[len-1]);
            } else if (stack[len-3] == '|') {
                if (len === 3) {
                    stack.splice(-3, 3, stack[len-2] + ' or ' + stack[len-1]);
                } else {
                    stack.splice(-3, 3, '(' + stack[len-2] + ' or ' + stack[len-1] + ')');
                }
            } else {
                stack.splice(-3, 3, stack[len-2] + ' and ' + stack[len-1]);
            }
            consume(stack);
        }

        var stack = [];
        _.each(domain, function (dom) {
            if (dom === '|' || dom === '&') {
                stack.push(dom);
            } else {
                var operator = dom[1] === '=' ? '==' : dom[1];
                if (!operator) {
                    throw new Error('Wrong operator for this domain');
                }
                if (operator === '!=' && dom[2] === false) { // the field is set
                    stack.push(dom[0]);
                } else if (dom[2] === null || dom[2] === true || dom[2] === false) {
                    stack.push(dom[0] + ' ' + (operator === '!=' ? 'is not ' : 'is ') + (dom[2] === null ? 'None' : (dom[2] ? 'True' : 'False')));
                } else {
                    stack.push(dom[0] + ' ' + operator + ' ' + JSON.stringify(dom[2]));
                }
                consume(stack);
            }
        });

        if (stack.length !== 1) {
            throw new Error('Wrong domain');
        }

        return stack[0];
    },
    /**
     * Converts python condition to a JS prefix-array representation of a domain
     *
     * @static
     * @param {string} condition
     * @returns {Array}
     */
    conditionToDomain: function (condition) {
        if (!condition || condition.match(/^\s*(True)?\s*$/)) {
            return [];
        }

        var ast = py.parse(py.tokenize(condition));


        function astToStackValue (node) {
            switch (node.id) {
                case '(name)': return node.value;
                case '.': return astToStackValue(node.first) + '.' + astToStackValue(node.second);
                case '(string)': return node.value;
                case '(number)': return node.value;
                case '(constant)': return node.value === 'None' ? null : node.value === 'True' ? true : false;
                case '[': return _.map(node.first, function (node) {return astToStackValue(node);});
            }
        }
        function astToStack (node) {
            switch (node.id) {
                case '(name)': return [[astToStackValue(node), '!=', false]];
                case '.': return [[astToStackValue(node.first) + '.' + astToStackValue(node.second), '!=', false]];
                case 'not': return [[astToStackValue(node.first), '=', false]];

                case 'or': return ['|'].concat(astToStack(node.first)).concat(astToStack(node.second));
                case 'and': return ['&'].concat(astToStack(node.first)).concat(astToStack(node.second));
                case '(comparator)':
                    if (node.operators.length !== 1) {
                        throw new Error('Wrong condition to convert in domain');
                    }
                    var right = astToStackValue(node.expressions[0]);
                    var left = astToStackValue(node.expressions[1]);
                    var operator = node.operators[0];
                    switch (operator) {
                        case 'is': operator = '='; break;
                        case 'is not': operator = '!='; break;
                        case '==': operator = '='; break;
                    }
                    return [[right, operator, left]];
                default:
                    throw "Condition cannot be transformed into domain";
            }
        }

        return astToStack(ast);
    },
});

return Domain;
});
