/*!
 * jQuery QueryBuilder 2.5.2
 * Copyright 2014-2018 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
 * Licensed under MIT (https://opensource.org/licenses/MIT)
 */
(function(root, factory) {
    if (typeof define == 'function' && define.amd) {
        define(['jquery', 'dot/doT', 'jquery-extendext'], factory);
    }
    else if (typeof module === 'object' && module.exports) {
        module.exports = factory(require('jquery'), require('dot/doT'), require('jquery-extendext'));
    }
    else {
        factory(root.jQuery, root.doT);
    }
}(this, function($, doT) {
    "use strict";

    /**
     * @typedef {object} Filter
     * @memberof QueryBuilder
     * @description See {@link http://querybuilder.js.org/index.html#filters}
     */

    /**
     * @typedef {object} Operator
     * @memberof QueryBuilder
     * @description See {@link http://querybuilder.js.org/index.html#operators}
     */

    /**
     * @param {jQuery} $el
     * @param {object} options - see {@link http://querybuilder.js.org/#options}
     * @constructor
     */
    var QueryBuilder = function($el, options) {
        $el[0].queryBuilder = this;

        /**
         * Element container
         * @member {jQuery}
         * @readonly
         */
        this.$el = $el;

        /**
         * Configuration object
         * @member {object}
         * @readonly
         */
        this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options);

        /**
         * Internal model
         * @member {Model}
         * @readonly
         */
        this.model = new Model();

        /**
         * Internal status
         * @member {object}
         * @property {string} id - id of the container
         * @property {boolean} generated_id - if the container id has been generated
         * @property {int} group_id - current group id
         * @property {int} rule_id - current rule id
         * @property {boolean} has_optgroup - if filters have optgroups
         * @property {boolean} has_operator_optgroup - if operators have optgroups
         * @readonly
         * @private
         */
        this.status = {
            id: null,
            generated_id: false,
            group_id: 0,
            rule_id: 0,
            has_optgroup: false,
            has_operator_optgroup: false
        };

        /**
         * List of filters
         * @member {QueryBuilder.Filter[]}
         * @readonly
         */
        this.filters = this.settings.filters;

        /**
         * List of icons
         * @member {object.<string, string>}
         * @readonly
         */
        this.icons = this.settings.icons;

        /**
         * List of operators
         * @member {QueryBuilder.Operator[]}
         * @readonly
         */
        this.operators = this.settings.operators;

        /**
         * List of templates
         * @member {object.<string, function>}
         * @readonly
         */
        this.templates = this.settings.templates;

        /**
         * Plugins configuration
         * @member {object.<string, object>}
         * @readonly
         */
        this.plugins = this.settings.plugins;

        /**
         * Translations object
         * @member {object}
         * @readonly
         */
        this.lang = null;

        // translations : english << 'lang_code' << custom
        if (QueryBuilder.regional['en'] === undefined) {
            Utils.error('Config', '"i18n/en.js" not loaded.');
        }
        this.lang = $.extendext(true, 'replace', {}, QueryBuilder.regional['en'], QueryBuilder.regional[this.settings.lang_code], this.settings.lang);

        // "allow_groups" can be boolean or int
        if (this.settings.allow_groups === false) {
            this.settings.allow_groups = 0;
        }
        else if (this.settings.allow_groups === true) {
            this.settings.allow_groups = -1;
        }

        // init templates
        Object.keys(this.templates).forEach(function(tpl) {
            if (!this.templates[tpl]) {
                this.templates[tpl] = QueryBuilder.templates[tpl];
            }
            if (typeof this.templates[tpl] == 'string') {
                this.templates[tpl] = doT.template(this.templates[tpl]);
            }
        }, this);

        // ensure we have a container id
        if (!this.$el.attr('id')) {
            this.$el.attr('id', 'qb_' + Math.floor(Math.random() * 99999));
            this.status.generated_id = true;
        }
        this.status.id = this.$el.attr('id');

        // INIT
        this.$el.addClass('query-builder form-inline');

        this.filters = this.checkFilters(this.filters);
        this.operators = this.checkOperators(this.operators);
        this.bindEvents();
        this.initPlugins();
    };

    $.extend(QueryBuilder.prototype, /** @lends QueryBuilder.prototype */ {
        /**
         * Triggers an event on the builder container
         * @param {string} type
         * @returns {$.Event}
         */
        trigger: function(type) {
            var event = new $.Event(this._tojQueryEvent(type), {
                builder: this
            });

            this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 1));

            return event;
        },

        /**
         * Triggers an event on the builder container and returns the modified value
         * @param {string} type
         * @param {*} value
         * @returns {*}
         */
        change: function(type, value) {
            var event = new $.Event(this._tojQueryEvent(type, true), {
                builder: this,
                value: value
            });

            this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 2));

            return event.value;
        },

        /**
         * Attaches an event listener on the builder container
         * @param {string} type
         * @param {function} cb
         * @returns {QueryBuilder}
         */
        on: function(type, cb) {
            this.$el.on(this._tojQueryEvent(type), cb);
            return this;
        },

        /**
         * Removes an event listener from the builder container
         * @param {string} type
         * @param {function} [cb]
         * @returns {QueryBuilder}
         */
        off: function(type, cb) {
            this.$el.off(this._tojQueryEvent(type), cb);
            return this;
        },

        /**
         * Attaches an event listener called once on the builder container
         * @param {string} type
         * @param {function} cb
         * @returns {QueryBuilder}
         */
        once: function(type, cb) {
            this.$el.one(this._tojQueryEvent(type), cb);
            return this;
        },

        /**
         * Appends `.queryBuilder` and optionally `.filter` to the events names
         * @param {string} name
         * @param {boolean} [filter=false]
         * @returns {string}
         * @private
         */
        _tojQueryEvent: function(name, filter) {
            return name.split(' ').map(function(type) {
                return type + '.queryBuilder' + (filter ? '.filter' : '');
            }).join(' ');
        }
    });


    /**
     * Allowed types and their internal representation
     * @type {object.<string, string>}
     * @readonly
     * @private
     */
    QueryBuilder.types = {
        'string':   'string',
        'integer':  'number',
        'double':   'number',
        'date':     'datetime',
        'time':     'datetime',
        'datetime': 'datetime',
        'boolean':  'boolean'
    };

    /**
     * Allowed inputs
     * @type {string[]}
     * @readonly
     * @private
     */
    QueryBuilder.inputs = [
        'text',
        'number',
        'textarea',
        'radio',
        'checkbox',
        'select'
    ];

    /**
     * Runtime modifiable options with `setOptions` method
     * @type {string[]}
     * @readonly
     * @private
     */
    QueryBuilder.modifiable_options = [
        'display_errors',
        'allow_groups',
        'allow_empty',
        'default_condition',
        'default_filter'
    ];

    /**
     * CSS selectors for common components
     * @type {object.<string, string>}
     * @readonly
     */
    QueryBuilder.selectors = {
        group_container:      '.rules-group-container',
        rule_container:       '.rule-container',
        filter_container:     '.rule-filter-container',
        operator_container:   '.rule-operator-container',
        value_container:      '.rule-value-container',
        error_container:      '.error-container',
        condition_container:  '.rules-group-header .group-conditions',

        rule_header:          '.rule-header',
        group_header:         '.rules-group-header',
        group_actions:        '.group-actions',
        rule_actions:         '.rule-actions',

        rules_list:           '.rules-group-body>.rules-list',

        group_btns:           '.btn',
        group_condition:      '.rules-group-header [name$=_cond]',
        rule_filter:          '.rule-filter-container [name$=_filter]',
        rule_operator:        '.rule-operator-container [name$=_operator]',
        rule_value:           '.rule-value-container :input:not(.icon-help)',

        add_rule:             '[data-add=rule]',
        delete_rule:          '[data-delete=rule]',
        add_group:            '[data-add=group]',
        delete_group:         '[data-delete=group]'
    };

    /**
     * Template strings (see template.js)
     * @type {object.<string, string>}
     * @readonly
     */
    QueryBuilder.templates = {};

    /**
     * Localized strings (see i18n/)
     * @type {object.<string, object>}
     * @readonly
     */
    QueryBuilder.regional = {};

    /**
     * Default operators
     * @type {object.<string, object>}
     * @readonly
     */
    QueryBuilder.OPERATORS = {
        equal:            { type: 'equal',            nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] },
        not_equal:        { type: 'not_equal',        nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] },
        in:               { type: 'in',               nb_inputs: 1, multiple: true,  apply_to: ['string', 'number', 'datetime'] },
        not_in:           { type: 'not_in',           nb_inputs: 1, multiple: true,  apply_to: ['string', 'number', 'datetime'] },
        less:             { type: 'less',             nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
        less_or_equal:    { type: 'less_or_equal',    nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
        greater:          { type: 'greater',          nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
        greater_or_equal: { type: 'greater_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
        between:          { type: 'between',          nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] },
        not_between:      { type: 'not_between',      nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] },
        begins_with:      { type: 'begins_with',      nb_inputs: 1, multiple: false, apply_to: ['string'] },
        not_begins_with:  { type: 'not_begins_with',  nb_inputs: 1, multiple: false, apply_to: ['string'] },
        contains:         { type: 'contains',         nb_inputs: 1, multiple: false, apply_to: ['string'] },
        not_contains:     { type: 'not_contains',     nb_inputs: 1, multiple: false, apply_to: ['string'] },
        ends_with:        { type: 'ends_with',        nb_inputs: 1, multiple: false, apply_to: ['string'] },
        not_ends_with:    { type: 'not_ends_with',    nb_inputs: 1, multiple: false, apply_to: ['string'] },
        is_empty:         { type: 'is_empty',         nb_inputs: 0, multiple: false, apply_to: ['string'] },
        is_not_empty:     { type: 'is_not_empty',     nb_inputs: 0, multiple: false, apply_to: ['string'] },
        is_null:          { type: 'is_null',          nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] },
        is_not_null:      { type: 'is_not_null',      nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }
    };

    /**
     * Default configuration
     * @type {object}
     * @readonly
     */
    QueryBuilder.DEFAULTS = {
        filters: [],
        plugins: [],

        sort_filters: false,
        display_errors: true,
        allow_groups: -1,
        allow_empty: false,
        conditions: ['AND', 'OR'],
        default_condition: 'AND',
        inputs_separator: ' , ',
        select_placeholder: '------',
        display_empty_filter: true,
        default_filter: null,
        optgroups: {},

        default_rule_flags: {
            filter_readonly: false,
            operator_readonly: false,
            value_readonly: false,
            no_delete: false
        },

        default_group_flags: {
            condition_readonly: false,
            no_add_rule: false,
            no_add_group: false,
            no_delete: false
        },

        templates: {
            group: null,
            rule: null,
            filterSelect: null,
            operatorSelect: null,
            ruleValueSelect: null
        },

        lang_code: 'en',
        lang: {},

        operators: [
            'equal',
            'not_equal',
            'in',
            'not_in',
            'less',
            'less_or_equal',
            'greater',
            'greater_or_equal',
            'between',
            'not_between',
            'begins_with',
            'not_begins_with',
            'contains',
            'not_contains',
            'ends_with',
            'not_ends_with',
            'is_empty',
            'is_not_empty',
            'is_null',
            'is_not_null'
        ],

        icons: {
            add_group:    'glyphicon glyphicon-plus-sign',
            add_rule:     'glyphicon glyphicon-plus',
            remove_group: 'glyphicon glyphicon-remove',
            remove_rule:  'glyphicon glyphicon-remove',
            error:        'glyphicon glyphicon-warning-sign'
        }
    };


    /**
     * @module plugins
     */

    /**
     * Definition of available plugins
     * @type {object.<String, object>}
     */
    QueryBuilder.plugins = {};

    /**
     * Gets or extends the default configuration
     * @param {object} [options] - new configuration
     * @returns {undefined|object} nothing or configuration object (copy)
     */
    QueryBuilder.defaults = function(options) {
        if (typeof options == 'object') {
            $.extendext(true, 'replace', QueryBuilder.DEFAULTS, options);
        }
        else if (typeof options == 'string') {
            if (typeof QueryBuilder.DEFAULTS[options] == 'object') {
                return $.extend(true, {}, QueryBuilder.DEFAULTS[options]);
            }
            else {
                return QueryBuilder.DEFAULTS[options];
            }
        }
        else {
            return $.extend(true, {}, QueryBuilder.DEFAULTS);
        }
    };

    /**
     * Registers a new plugin
     * @param {string} name
     * @param {function} fct - init function
     * @param {object} [def] - default options
     */
    QueryBuilder.define = function(name, fct, def) {
        QueryBuilder.plugins[name] = {
            fct: fct,
            def: def || {}
        };
    };

    /**
     * Adds new methods to QueryBuilder prototype
     * @param {object.<string, function>} methods
     */
    QueryBuilder.extend = function(methods) {
        $.extend(QueryBuilder.prototype, methods);
    };

    /**
     * Initializes plugins for an instance
     * @throws ConfigError
     * @private
     */
    QueryBuilder.prototype.initPlugins = function() {
        if (!this.plugins) {
            return;
        }

        if ($.isArray(this.plugins)) {
            var tmp = {};
            this.plugins.forEach(function(plugin) {
                tmp[plugin] = null;
            });
            this.plugins = tmp;
        }

        Object.keys(this.plugins).forEach(function(plugin) {
            if (plugin in QueryBuilder.plugins) {
                this.plugins[plugin] = $.extend(true, {},
                    QueryBuilder.plugins[plugin].def,
                    this.plugins[plugin] || {}
                );

                QueryBuilder.plugins[plugin].fct.call(this, this.plugins[plugin]);
            }
            else {
                Utils.error('Config', 'Unable to find plugin "{0}"', plugin);
            }
        }, this);
    };

    /**
     * Returns the config of a plugin, if the plugin is not loaded, returns the default config.
     * @param {string} name
     * @param {string} [property]
     * @throws ConfigError
     * @returns {*}
     */
    QueryBuilder.prototype.getPluginOptions = function(name, property) {
        var plugin;
        if (this.plugins && this.plugins[name]) {
            plugin = this.plugins[name];
        }
        else if (QueryBuilder.plugins[name]) {
            plugin = QueryBuilder.plugins[name].def;
        }

        if (plugin) {
            if (property) {
                return plugin[property];
            }
            else {
                return plugin;
            }
        }
        else {
            Utils.error('Config', 'Unable to find plugin "{0}"', name);
        }
    };


    /**
     * Final initialisation of the builder
     * @param {object} [rules]
     * @fires QueryBuilder.afterInit
     * @private
     */
    QueryBuilder.prototype.init = function(rules) {
        /**
         * When the initilization is done, just before creating the root group
         * @event afterInit
         * @memberof QueryBuilder
         */
        this.trigger('afterInit');

        if (rules) {
            this.setRules(rules);
            delete this.settings.rules;
        }
        else {
            this.setRoot(true);
        }
    };

    /**
     * Checks the configuration of each filter
     * @param {QueryBuilder.Filter[]} filters
     * @returns {QueryBuilder.Filter[]}
     * @throws ConfigError
     */
    QueryBuilder.prototype.checkFilters = function(filters) {
        var definedFilters = [];

        if (!filters || filters.length === 0) {
            Utils.error('Config', 'Missing filters list');
        }

        filters.forEach(function(filter, i) {
            if (!filter.id) {
                Utils.error('Config', 'Missing filter {0} id', i);
            }
            if (definedFilters.indexOf(filter.id) != -1) {
                Utils.error('Config', 'Filter "{0}" already defined', filter.id);
            }
            definedFilters.push(filter.id);

            if (!filter.type) {
                filter.type = 'string';
            }
            else if (!QueryBuilder.types[filter.type]) {
                Utils.error('Config', 'Invalid type "{0}"', filter.type);
            }

            if (!filter.input) {
                filter.input = QueryBuilder.types[filter.type] === 'number' ? 'number' : 'text';
            }
            else if (typeof filter.input != 'function' && QueryBuilder.inputs.indexOf(filter.input) == -1) {
                Utils.error('Config', 'Invalid input "{0}"', filter.input);
            }

            if (filter.operators) {
                filter.operators.forEach(function(operator) {
                    if (typeof operator != 'string') {
                        Utils.error('Config', 'Filter operators must be global operators types (string)');
                    }
                });
            }

            if (!filter.field) {
                filter.field = filter.id;
            }
            if (!filter.label) {
                filter.label = filter.field;
            }

            if (!filter.optgroup) {
                filter.optgroup = null;
            }
            else {
                this.status.has_optgroup = true;

                // register optgroup if needed
                if (!this.settings.optgroups[filter.optgroup]) {
                    this.settings.optgroups[filter.optgroup] = filter.optgroup;
                }
            }

            switch (filter.input) {
                case 'radio':
                case 'checkbox':
                    if (!filter.values || filter.values.length < 1) {
                        Utils.error('Config', 'Missing filter "{0}" values', filter.id);
                    }
                    break;

                case 'select':
                    var cleanValues = [];
                    filter.has_optgroup = false;

                    Utils.iterateOptions(filter.values, function(value, label, optgroup) {
                        cleanValues.push({
                            value: value,
                            label: label,
                            optgroup: optgroup || null
                        });

                        if (optgroup) {
                            filter.has_optgroup = true;

                            // register optgroup if needed
                            if (!this.settings.optgroups[optgroup]) {
                                this.settings.optgroups[optgroup] = optgroup;
                            }
                        }
                    }.bind(this));

                    if (filter.has_optgroup) {
                        filter.values = Utils.groupSort(cleanValues, 'optgroup');
                    }
                    else {
                        filter.values = cleanValues;
                    }

                    if (filter.placeholder) {
                        if (filter.placeholder_value === undefined) {
                            filter.placeholder_value = -1;
                        }

                        filter.values.forEach(function(entry) {
                            if (entry.value == filter.placeholder_value) {
                                Utils.error('Config', 'Placeholder of filter "{0}" overlaps with one of its values', filter.id);
                            }
                        });
                    }
                    break;
            }
        }, this);

        if (this.settings.sort_filters) {
            if (typeof this.settings.sort_filters == 'function') {
                filters.sort(this.settings.sort_filters);
            }
            else {
                var self = this;
                filters.sort(function(a, b) {
                    return self.translate(a.label).localeCompare(self.translate(b.label));
                });
            }
        }

        if (this.status.has_optgroup) {
            filters = Utils.groupSort(filters, 'optgroup');
        }

        return filters;
    };

    /**
     * Checks the configuration of each operator
     * @param {QueryBuilder.Operator[]} operators
     * @returns {QueryBuilder.Operator[]}
     * @throws ConfigError
     */
    QueryBuilder.prototype.checkOperators = function(operators) {
        var definedOperators = [];

        operators.forEach(function(operator, i) {
            if (typeof operator == 'string') {
                if (!QueryBuilder.OPERATORS[operator]) {
                    Utils.error('Config', 'Unknown operator "{0}"', operator);
                }

                operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator]);
            }
            else {
                if (!operator.type) {
                    Utils.error('Config', 'Missing "type" for operator {0}', i);
                }

                if (QueryBuilder.OPERATORS[operator.type]) {
                    operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator.type], operator);
                }

                if (operator.nb_inputs === undefined || operator.apply_to === undefined) {
                    Utils.error('Config', 'Missing "nb_inputs" and/or "apply_to" for operator "{0}"', operator.type);
                }
            }

            if (definedOperators.indexOf(operator.type) != -1) {
                Utils.error('Config', 'Operator "{0}" already defined', operator.type);
            }
            definedOperators.push(operator.type);

            if (!operator.optgroup) {
                operator.optgroup = null;
            }
            else {
                this.status.has_operator_optgroup = true;

                // register optgroup if needed
                if (!this.settings.optgroups[operator.optgroup]) {
                    this.settings.optgroups[operator.optgroup] = operator.optgroup;
                }
            }
        }, this);

        if (this.status.has_operator_optgroup) {
            operators = Utils.groupSort(operators, 'optgroup');
        }

        return operators;
    };

    /**
     * Adds all events listeners to the builder
     * @private
     */
    QueryBuilder.prototype.bindEvents = function() {
        var self = this;
        var Selectors = QueryBuilder.selectors;

        // group condition change
        this.$el.on('change.queryBuilder', Selectors.group_condition, function() {
            if ($(this).is(':checked')) {
                var $group = $(this).closest(Selectors.group_container);
                self.getModel($group).condition = $(this).val();
            }
        });

        // rule filter change
        this.$el.on('change.queryBuilder', Selectors.rule_filter, function() {
            var $rule = $(this).closest(Selectors.rule_container);
            self.getModel($rule).filter = self.getFilterById($(this).val());
        });

        // rule operator change
        this.$el.on('change.queryBuilder', Selectors.rule_operator, function() {
            var $rule = $(this).closest(Selectors.rule_container);
            self.getModel($rule).operator = self.getOperatorByType($(this).val());
        });

        // add rule button
        this.$el.on('click.queryBuilder', Selectors.add_rule, function() {
            var $group = $(this).closest(Selectors.group_container);
            self.addRule(self.getModel($group));
        });

        // delete rule button
        this.$el.on('click.queryBuilder', Selectors.delete_rule, function() {
            var $rule = $(this).closest(Selectors.rule_container);
            self.deleteRule(self.getModel($rule));
        });

        if (this.settings.allow_groups !== 0) {
            // add group button
            this.$el.on('click.queryBuilder', Selectors.add_group, function() {
                var $group = $(this).closest(Selectors.group_container);
                self.addGroup(self.getModel($group));
            });

            // delete group button
            this.$el.on('click.queryBuilder', Selectors.delete_group, function() {
                var $group = $(this).closest(Selectors.group_container);
                self.deleteGroup(self.getModel($group));
            });
        }

        // model events
        this.model.on({
            'drop': function(e, node) {
                node.$el.remove();
                self.refreshGroupsConditions();
            },
            'add': function(e, parent, node, index) {
                if (index === 0) {
                    node.$el.prependTo(parent.$el.find('>' + QueryBuilder.selectors.rules_list));
                }
                else {
                    node.$el.insertAfter(parent.rules[index - 1].$el);
                }
                self.refreshGroupsConditions();
            },
            'move': function(e, node, group, index) {
                node.$el.detach();

                if (index === 0) {
                    node.$el.prependTo(group.$el.find('>' + QueryBuilder.selectors.rules_list));
                }
                else {
                    node.$el.insertAfter(group.rules[index - 1].$el);
                }
                self.refreshGroupsConditions();
            },
            'update': function(e, node, field, value, oldValue) {
                if (node instanceof Rule) {
                    switch (field) {
                        case 'error':
                            self.updateError(node);
                            break;

                        case 'flags':
                            self.applyRuleFlags(node);
                            break;

                        case 'filter':
                            self.updateRuleFilter(node, oldValue);
                            break;

                        case 'operator':
                            self.updateRuleOperator(node, oldValue);
                            break;

                        case 'value':
                            self.updateRuleValue(node, oldValue);
                            break;
                    }
                }
                else {
                    switch (field) {
                        case 'error':
                            self.updateError(node);
                            break;

                        case 'flags':
                            self.applyGroupFlags(node);
                            break;

                        case 'condition':
                            self.updateGroupCondition(node, oldValue);
                            break;
                    }
                }
            }
        });
    };

    /**
     * Creates the root group
     * @param {boolean} [addRule=true] - adds a default empty rule
     * @param {object} [data] - group custom data
     * @param {object} [flags] - flags to apply to the group
     * @returns {Group} root group
     * @fires QueryBuilder.afterAddGroup
     */
    QueryBuilder.prototype.setRoot = function(addRule, data, flags) {
        addRule = (addRule === undefined || addRule === true);

        var group_id = this.nextGroupId();
        var $group = $(this.getGroupTemplate(group_id, 1));
        $group.find(QueryBuilder.selectors.group_btns).addClass(this.settings.readonly ? 'disabled' : '')

        this.$el.append($group);
        this.model.root = new Group(null, $group);
        this.model.root.model = this.model;

        this.model.root.data = data;
        this.model.root.flags = $.extend({}, this.settings.default_group_flags, flags);
        this.model.root.condition = this.settings.default_condition;

        this.trigger('afterAddGroup', this.model.root);

        if (addRule) {
            this.addRule(this.model.root);
        }

        return this.model.root;
    };

    /**
     * Adds a new group
     * @param {Group} parent
     * @param {boolean} [addRule=true] - adds a default empty rule
     * @param {object} [data] - group custom data
     * @param {object} [flags] - flags to apply to the group
     * @returns {Group}
     * @fires QueryBuilder.beforeAddGroup
     * @fires QueryBuilder.afterAddGroup
     */
    QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) {
        addRule = (addRule === undefined || addRule === true);

        var level = parent.level + 1;

        /**
         * Just before adding a group, can be prevented.
         * @event beforeAddGroup
         * @memberof QueryBuilder
         * @param {Group} parent
         * @param {boolean} addRule - if an empty rule will be added in the group
         * @param {int} level - nesting level of the group, 1 is the root group
         */
        var e = this.trigger('beforeAddGroup', parent, addRule, level);
        if (e.isDefaultPrevented()) {
            return null;
        }

        var group_id = this.nextGroupId();
        var $group = $(this.getGroupTemplate(group_id, level));
        var model = parent.addGroup($group);

        model.data = data;
        model.flags = $.extend({}, this.settings.default_group_flags, flags);
        model.condition = this.settings.default_condition;

        /**
         * Just after adding a group
         * @event afterAddGroup
         * @memberof QueryBuilder
         * @param {Group} group
         */
        this.trigger('afterAddGroup', model);

        /**
         * After any change in the rules
         * @event rulesChanged
         * @memberof QueryBuilder
         */
        this.trigger('rulesChanged');

        if (addRule) {
            this.addRule(model);
        }

        return model;
    };

    /**
     * Tries to delete a group. The group is not deleted if at least one rule is flagged `no_delete`.
     * @param {Group} group
     * @returns {boolean} if the group has been deleted
     * @fires QueryBuilder.beforeDeleteGroup
     * @fires QueryBuilder.afterDeleteGroup
     */
    QueryBuilder.prototype.deleteGroup = function(group) {
        if (group.isRoot()) {
            return false;
        }

        /**
         * Just before deleting a group, can be prevented
         * @event beforeDeleteGroup
         * @memberof QueryBuilder
         * @param {Group} parent
         */
        var e = this.trigger('beforeDeleteGroup', group);
        if (e.isDefaultPrevented()) {
            return false;
        }

        var del = true;

        group.each('reverse', function(rule) {
            del &= this.deleteRule(rule);
        }, function(group) {
            del &= this.deleteGroup(group);
        }, this);

        if (del) {
            group.drop();

            /**
             * Just after deleting a group
             * @event afterDeleteGroup
             * @memberof QueryBuilder
             */
            this.trigger('afterDeleteGroup');

            this.trigger('rulesChanged');
        }

        return del;
    };

    /**
     * Performs actions when a group's condition changes
     * @param {Group} group
     * @param {object} previousCondition
     * @fires QueryBuilder.afterUpdateGroupCondition
     * @private
     */
    QueryBuilder.prototype.updateGroupCondition = function(group, previousCondition) {
        group.$el.find('>' + QueryBuilder.selectors.group_condition).each(function() {
            var $this = $(this);
            $this.prop('checked', $this.val() === group.condition);
            $this.parent().toggleClass('active', $this.val() === group.condition);
        });

        /**
         * After the group condition has been modified
         * @event afterUpdateGroupCondition
         * @memberof QueryBuilder
         * @param {Group} group
         * @param {object} previousCondition
         */
        this.trigger('afterUpdateGroupCondition', group, previousCondition);

        this.trigger('rulesChanged');
    };

    /**
     * Updates the visibility of conditions based on number of rules inside each group
     * @private
     */
    QueryBuilder.prototype.refreshGroupsConditions = function() {
        (function walk(group) {
            if (!group.flags || (group.flags && !group.flags.condition_readonly)) {
                group.$el.find('>' + QueryBuilder.selectors.group_condition).prop('disabled', group.rules.length <= 1)
                    .parent().toggleClass('disabled', group.rules.length <= 1);
            }

            group.each(null, function(group) {
                walk(group);
            }, this);
        }(this.model.root));
    };

    /**
     * Adds a new rule
     * @param {Group} parent
     * @param {object} [data] - rule custom data
     * @param {object} [flags] - flags to apply to the rule
     * @returns {Rule}
     * @fires QueryBuilder.beforeAddRule
     * @fires QueryBuilder.afterAddRule
     * @fires QueryBuilder.changer:getDefaultFilter
     */
    QueryBuilder.prototype.addRule = function(parent, data, flags) {
        /**
         * Just before adding a rule, can be prevented
         * @event beforeAddRule
         * @memberof QueryBuilder
         * @param {Group} parent
         */
        var e = this.trigger('beforeAddRule', parent);
        if (e.isDefaultPrevented()) {
            return null;
        }

        var rule_id = this.nextRuleId();
        var $rule = $(this.getRuleTemplate(rule_id));
        var model = parent.addRule($rule);

        model.data = data;
        model.flags = $.extend({}, this.settings.default_rule_flags, flags);

        /**
         * Just after adding a rule
         * @event afterAddRule
         * @memberof QueryBuilder
         * @param {Rule} rule
         */
        this.trigger('afterAddRule', model);

        this.trigger('rulesChanged');

        this.createRuleFilters(model);

        if (this.settings.default_filter || !this.settings.display_empty_filter) {
            /**
             * Modifies the default filter for a rule
             * @event changer:getDefaultFilter
             * @memberof QueryBuilder
             * @param {QueryBuilder.Filter} filter
             * @param {Rule} rule
             * @returns {QueryBuilder.Filter}
             */
            model.filter = this.change('getDefaultFilter',
                this.getFilterById(this.settings.default_filter || this.filters[0].id),
                model
            );
        }

        return model;
    };

    /**
     * Tries to delete a rule
     * @param {Rule} rule
     * @returns {boolean} if the rule has been deleted
     * @fires QueryBuilder.beforeDeleteRule
     * @fires QueryBuilder.afterDeleteRule
     */
    QueryBuilder.prototype.deleteRule = function(rule) {
        if (rule.flags.no_delete) {
            return false;
        }

        /**
         * Just before deleting a rule, can be prevented
         * @event beforeDeleteRule
         * @memberof QueryBuilder
         * @param {Rule} rule
         */
        var e = this.trigger('beforeDeleteRule', rule);
        if (e.isDefaultPrevented()) {
            return false;
        }

        rule.drop();

        /**
         * Just after deleting a rule
         * @event afterDeleteRule
         * @memberof QueryBuilder
         */
        this.trigger('afterDeleteRule');

        this.trigger('rulesChanged');

        return true;
    };

    /**
     * Creates the filters for a rule
     * @param {Rule} rule
     * @fires QueryBuilder.changer:getRuleFilters
     * @fires QueryBuilder.afterCreateRuleFilters
     * @private
     */
    QueryBuilder.prototype.createRuleFilters = function(rule) {
        /**
         * Modifies the list a filters available for a rule
         * @event changer:getRuleFilters
         * @memberof QueryBuilder
         * @param {QueryBuilder.Filter[]} filters
         * @param {Rule} rule
         * @returns {QueryBuilder.Filter[]}
         */
        var filters = this.change('getRuleFilters', this.filters, rule);
        var $filterSelect = $(this.getRuleFilterSelect(rule, filters));

        rule.$el.find(QueryBuilder.selectors.filter_container).html($filterSelect);

        /**
         * After creating the dropdown for filters
         * @event afterCreateRuleFilters
         * @memberof QueryBuilder
         * @param {Rule} rule
         */
        this.trigger('afterCreateRuleFilters', rule);

        this.applyRuleFlags(rule);
    };

    /**
     * Creates the operators for a rule and init the rule operator
     * @param {Rule} rule
     * @fires QueryBuilder.afterCreateRuleOperators
     * @private
     */
    QueryBuilder.prototype.createRuleOperators = function(rule) {
        var $operatorContainer = rule.$el.find(QueryBuilder.selectors.operator_container).empty();

        if (!rule.filter) {
            return;
        }

        var operators = this.getOperators(rule.filter);
        var $operatorSelect = $(this.getRuleOperatorSelect(rule, operators));

        $operatorContainer.html($operatorSelect);

        // set the operator without triggering update event
        if (rule.filter.default_operator) {
            rule.__.operator = this.getOperatorByType(rule.filter.default_operator);
        }
        else {
            rule.__.operator = operators[0];
        }

        rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);

        /**
         * After creating the dropdown for operators
         * @event afterCreateRuleOperators
         * @memberof QueryBuilder
         * @param {Rule} rule
         * @param {QueryBuilder.Operator[]} operators - allowed operators for this rule
         */
        this.trigger('afterCreateRuleOperators', rule, operators);

        this.applyRuleFlags(rule);
    };

    /**
     * Creates the main input for a rule
     * @param {Rule} rule
     * @fires QueryBuilder.afterCreateRuleInput
     * @private
     */
    QueryBuilder.prototype.createRuleInput = function(rule) {
        var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container).empty();

        rule.__.value = undefined;

        if (!rule.filter || !rule.operator || rule.operator.nb_inputs === 0) {
            return;
        }

        var self = this;
        var $inputs = $();
        var filter = rule.filter;

        for (var i = 0; i < rule.operator.nb_inputs; i++) {
            var $ruleInput = $(this.getRuleInput(rule, i));
            if (i > 0) $valueContainer.append(this.settings.inputs_separator);
            $valueContainer.append($ruleInput);
            $inputs = $inputs.add($ruleInput);
        }

        $valueContainer.css('display', '');

        $inputs.on('change ' + (filter.input_event || ''), function() {
            if (!rule._updating_input) {
                rule._updating_value = true;
                rule.value = self.getRuleInputValue(rule);
                rule._updating_value = false;
            }
        });

        if (filter.plugin) {
            $inputs[filter.plugin](filter.plugin_config || {});
        }

        /**
         * After creating the input for a rule and initializing optional plugin
         * @event afterCreateRuleInput
         * @memberof QueryBuilder
         * @param {Rule} rule
         */
        this.trigger('afterCreateRuleInput', rule);

        if (filter.default_value !== undefined) {
            rule.value = filter.default_value;
        }
        else {
            rule._updating_value = true;
            rule.value = self.getRuleInputValue(rule);
            rule._updating_value = false;
        }

        this.applyRuleFlags(rule);
    };

    /**
     * Performs action when a rule's filter changes
     * @param {Rule} rule
     * @param {object} previousFilter
     * @fires QueryBuilder.afterUpdateRuleFilter
     * @private
     */
    QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) {
        this.createRuleOperators(rule);
        this.createRuleInput(rule);

        rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');

        // clear rule data if the filter changed
        if (previousFilter && rule.filter && previousFilter.id !== rule.filter.id) {
            rule.data = undefined;
        }

        /**
         * After the filter has been updated and the operators and input re-created
         * @event afterUpdateRuleFilter
         * @memberof QueryBuilder
         * @param {Rule} rule
         * @param {object} previousFilter
         */
        this.trigger('afterUpdateRuleFilter', rule, previousFilter);

        this.trigger('rulesChanged');
    };

    /**
     * Performs actions when a rule's operator changes
     * @param {Rule} rule
     * @param {object} previousOperator
     * @fires QueryBuilder.afterUpdateRuleOperator
     * @private
     */
    QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) {
        var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container);

        if (!rule.operator || rule.operator.nb_inputs === 0) {
            $valueContainer.hide();

            rule.__.value = undefined;
        }
        else {
            $valueContainer.css('display', '');

            if ($valueContainer.is(':empty') || !previousOperator ||
                rule.operator.nb_inputs !== previousOperator.nb_inputs ||
                rule.operator.optgroup !== previousOperator.optgroup
            ) {
                this.createRuleInput(rule);
            }
        }

        if (rule.operator) {
            rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);

            // refresh value if the format changed for this operator
            rule.__.value = this.getRuleInputValue(rule);
        }

        /**
         *  After the operator has been updated and the input optionally re-created
         * @event afterUpdateRuleOperator
         * @memberof QueryBuilder
         * @param {Rule} rule
         * @param {object} previousOperator
         */
        this.trigger('afterUpdateRuleOperator', rule, previousOperator);

        this.trigger('rulesChanged');
    };

    /**
     * Performs actions when rule's value changes
     * @param {Rule} rule
     * @param {object} previousValue
     * @fires QueryBuilder.afterUpdateRuleValue
     * @private
     */
    QueryBuilder.prototype.updateRuleValue = function(rule, previousValue) {
        if (!rule._updating_value) {
            this.setRuleInputValue(rule, rule.value);
        }

        /**
         * After the rule value has been modified
         * @event afterUpdateRuleValue
         * @memberof QueryBuilder
         * @param {Rule} rule
         * @param {*} previousValue
         */
        this.trigger('afterUpdateRuleValue', rule, previousValue);

        this.trigger('rulesChanged');
    };

    /**
     * Changes a rule's properties depending on its flags
     * @param {Rule} rule
     * @fires QueryBuilder.afterApplyRuleFlags
     * @private
     */
    QueryBuilder.prototype.applyRuleFlags = function(rule) {
        var flags = rule.flags;
        var Selectors = QueryBuilder.selectors;

        rule.$el.find(Selectors.rule_filter).prop('disabled', flags.filter_readonly);
        rule.$el.find(Selectors.rule_operator).prop('disabled', flags.operator_readonly);
        rule.$el.find(Selectors.rule_value).prop('disabled', flags.value_readonly);

        if (flags.no_delete) {
            rule.$el.find(Selectors.delete_rule).remove();
        }

        /**
         * After rule's flags has been applied
         * @event afterApplyRuleFlags
         * @memberof QueryBuilder
         * @param {Rule} rule
         */
        this.trigger('afterApplyRuleFlags', rule);
    };

    /**
     * Changes group's properties depending on its flags
     * @param {Group} group
     * @fires QueryBuilder.afterApplyGroupFlags
     * @private
     */
    QueryBuilder.prototype.applyGroupFlags = function(group) {
        var flags = group.flags;
        var Selectors = QueryBuilder.selectors;

        group.$el.find('>' + Selectors.group_condition).prop('disabled', flags.condition_readonly)
            .parent().toggleClass('readonly', flags.condition_readonly);

        if (flags.no_add_rule) {
            group.$el.find(Selectors.add_rule).remove();
        }
        if (flags.no_add_group) {
            group.$el.find(Selectors.add_group).remove();
        }
        if (flags.no_delete) {
            group.$el.find(Selectors.delete_group).remove();
        }

        /**
         * After group's flags has been applied
         * @event afterApplyGroupFlags
         * @memberof QueryBuilder
         * @param {Group} group
         */
        this.trigger('afterApplyGroupFlags', group);
    };

    /**
     * Clears all errors markers
     * @param {Node} [node] default is root Group
     */
    QueryBuilder.prototype.clearErrors = function(node) {
        node = node || this.model.root;

        if (!node) {
            return;
        }

        node.error = null;

        if (node instanceof Group) {
            node.each(function(rule) {
                rule.error = null;
            }, function(group) {
                this.clearErrors(group);
            }, this);
        }
    };

    /**
     * Adds/Removes error on a Rule or Group
     * @param {Node} node
     * @fires QueryBuilder.changer:displayError
     * @private
     */
    QueryBuilder.prototype.updateError = function(node) {
        if (this.settings.display_errors) {
            if (node.error === null) {
                node.$el.removeClass('has-error');
            }
            else {
                var errorMessage = this.translate('errors', node.error[0]);
                errorMessage = Utils.fmt(errorMessage, node.error.slice(1));

                /**
                 * Modifies an error message before display
                 * @event changer:displayError
                 * @memberof QueryBuilder
                 * @param {string} errorMessage - the error message (translated and formatted)
                 * @param {array} error - the raw error array (error code and optional arguments)
                 * @param {Node} node
                 * @returns {string}
                 */
                errorMessage = this.change('displayError', errorMessage, node.error, node);

                node.$el.addClass('has-error')
                    .find(QueryBuilder.selectors.error_container).eq(0)
                    .attr('title', errorMessage);
            }
        }
    };

    /**
     * Triggers a validation error event
     * @param {Node} node
     * @param {string|array} error
     * @param {*} value
     * @fires QueryBuilder.validationError
     * @private
     */
    QueryBuilder.prototype.triggerValidationError = function(node, error, value) {
        if (!$.isArray(error)) {
            error = [error];
        }

        /**
         * Fired when a validation error occurred, can be prevented
         * @event validationError
         * @memberof QueryBuilder
         * @param {Node} node
         * @param {string} error
         * @param {*} value
         */
        var e = this.trigger('validationError', node, error, value);
        if (!e.isDefaultPrevented()) {
            node.error = error;
        }
    };


    /**
     * Destroys the builder
     * @fires QueryBuilder.beforeDestroy
     */
    QueryBuilder.prototype.destroy = function() {
        /**
         * Before the {@link QueryBuilder#destroy} method
         * @event beforeDestroy
         * @memberof QueryBuilder
         */
        this.trigger('beforeDestroy');

        if (this.status.generated_id) {
            this.$el.removeAttr('id');
        }

        this.clear();
        this.model = null;

        this.$el
            .off('.queryBuilder')
            .removeClass('query-builder')
            .removeData('queryBuilder');

        delete this.$el[0].queryBuilder;
    };

    /**
     * Clear all rules and resets the root group
     * @fires QueryBuilder.beforeReset
     * @fires QueryBuilder.afterReset
     */
    QueryBuilder.prototype.reset = function() {
        /**
         * Before the {@link QueryBuilder#reset} method, can be prevented
         * @event beforeReset
         * @memberof QueryBuilder
         */
        var e = this.trigger('beforeReset');
        if (e.isDefaultPrevented()) {
            return;
        }

        this.status.group_id = 1;
        this.status.rule_id = 0;

        this.model.root.empty();

        this.model.root.data = undefined;
        this.model.root.flags = $.extend({}, this.settings.default_group_flags);
        this.model.root.condition = this.settings.default_condition;

        this.addRule(this.model.root);

        /**
         * After the {@link QueryBuilder#reset} method
         * @event afterReset
         * @memberof QueryBuilder
         */
        this.trigger('afterReset');

        this.trigger('rulesChanged');
    };

    /**
     * Clears all rules and removes the root group
     * @fires QueryBuilder.beforeClear
     * @fires QueryBuilder.afterClear
     */
    QueryBuilder.prototype.clear = function() {
        /**
         * Before the {@link QueryBuilder#clear} method, can be prevented
         * @event beforeClear
         * @memberof QueryBuilder
         */
        var e = this.trigger('beforeClear');
        if (e.isDefaultPrevented()) {
            return;
        }

        this.status.group_id = 0;
        this.status.rule_id = 0;

        if (this.model.root) {
            this.model.root.drop();
            this.model.root = null;
        }

        /**
         * After the {@link QueryBuilder#clear} method
         * @event afterClear
         * @memberof QueryBuilder
         */
        this.trigger('afterClear');

        this.trigger('rulesChanged');
    };

    /**
     * Modifies the builder configuration.<br>
     * Only options defined in QueryBuilder.modifiable_options are modifiable
     * @param {object} options
     */
    QueryBuilder.prototype.setOptions = function(options) {
        $.each(options, function(opt, value) {
            if (QueryBuilder.modifiable_options.indexOf(opt) !== -1) {
                this.settings[opt] = value;
            }
        }.bind(this));
    };

    /**
     * Returns the model associated to a DOM object, or the root model
     * @param {jQuery} [target]
     * @returns {Node}
     */
    QueryBuilder.prototype.getModel = function(target) {
        if (!target) {
            return this.model.root;
        }
        else if (target instanceof Node) {
            return target;
        }
        else {
            return $(target).data('queryBuilderModel');
        }
    };

    /**
     * Validates the whole builder
     * @param {object} [options]
     * @param {boolean} [options.skip_empty=false] - skips validating rules that have no filter selected
     * @returns {boolean}
     * @fires QueryBuilder.changer:validate
     */
    QueryBuilder.prototype.validate = function(options) {
        options = $.extend({
            skip_empty: false
        }, options);

        this.clearErrors();

        var self = this;

        var valid = (function parse(group) {
            var done = 0;
            var errors = 0;

            group.each(function(rule) {
                if (!rule.filter && options.skip_empty) {
                    return;
                }

                if (!rule.filter) {
                    self.triggerValidationError(rule, 'no_filter', null);
                    errors++;
                    return;
                }

                if (!rule.operator) {
                    self.triggerValidationError(rule, 'no_operator', null);
                    errors++;
                    return;
                }

                if (rule.operator.nb_inputs !== 0) {
                    var valid = self.validateValue(rule, rule.value);

                    if (valid !== true) {
                        self.triggerValidationError(rule, valid, rule.value);
                        errors++;
                        return;
                    }
                }

                done++;

            }, function(group) {
                var res = parse(group);
                if (res === true) {
                    done++;
                }
                else if (res === false) {
                    errors++;
                }
            });

            if (errors > 0) {
                return false;
            }
            else if (done === 0 && !group.isRoot() && options.skip_empty) {
                return null;
            }
            else if (done === 0 && (!self.settings.allow_empty || !group.isRoot())) {
                self.triggerValidationError(group, 'empty_group', null);
                return false;
            }

            return true;

        }(this.model.root));

        /**
         * Modifies the result of the {@link QueryBuilder#validate} method
         * @event changer:validate
         * @memberof QueryBuilder
         * @param {boolean} valid
         * @returns {boolean}
         */
        return this.change('validate', valid);
    };

    /**
     * Gets an object representing current rules
     * @param {object} [options]
     * @param {boolean|string} [options.get_flags=false] - export flags, true: only changes from default flags or 'all'
     * @param {boolean} [options.allow_invalid=false] - returns rules even if they are invalid
     * @param {boolean} [options.skip_empty=false] - remove rules that have no filter selected
     * @returns {object}
     * @fires QueryBuilder.changer:ruleToJson
     * @fires QueryBuilder.changer:groupToJson
     * @fires QueryBuilder.changer:getRules
     */
    QueryBuilder.prototype.getRules = function(options) {
        options = $.extend({
            get_flags: false,
            allow_invalid: false,
            skip_empty: false
        }, options);

        var valid = this.validate(options);
        if (!valid && !options.allow_invalid) {
            return null;
        }

        var self = this;

        var out = (function parse(group) {
            var groupData = {
                condition: group.condition,
                rules: []
            };

            if (group.data) {
                groupData.data = $.extendext(true, 'replace', {}, group.data);
            }

            if (options.get_flags) {
                var flags = self.getGroupFlags(group.flags, options.get_flags === 'all');
                if (!$.isEmptyObject(flags)) {
                    groupData.flags = flags;
                }
            }

            group.each(function(rule) {
                if (!rule.filter && options.skip_empty) {
                    return;
                }

                var value = null;
                if (!rule.operator || rule.operator.nb_inputs !== 0) {
                    value = rule.value;
                }

                var ruleData = {
                    id: rule.filter ? rule.filter.id : null,
                    field: rule.filter ? rule.filter.field : null,
                    type: rule.filter ? rule.filter.type : null,
                    input: rule.filter ? rule.filter.input : null,
                    operator: rule.operator ? rule.operator.type : null,
                    value: value
                };

                if (rule.filter && rule.filter.data || rule.data) {
                    ruleData.data = $.extendext(true, 'replace', {}, rule.filter.data, rule.data);
                }

                if (options.get_flags) {
                    var flags = self.getRuleFlags(rule.flags, options.get_flags === 'all');
                    if (!$.isEmptyObject(flags)) {
                        ruleData.flags = flags;
                    }
                }

                /**
                 * Modifies the JSON generated from a Rule object
                 * @event changer:ruleToJson
                 * @memberof QueryBuilder
                 * @param {object} json
                 * @param {Rule} rule
                 * @returns {object}
                 */
                groupData.rules.push(self.change('ruleToJson', ruleData, rule));

            }, function(model) {
                var data = parse(model);
                if (data.rules.length !== 0 || !options.skip_empty) {
                    groupData.rules.push(data);
                }
            }, this);

            /**
             * Modifies the JSON generated from a Group object
             * @event changer:groupToJson
             * @memberof QueryBuilder
             * @param {object} json
             * @param {Group} group
             * @returns {object}
             */
            return self.change('groupToJson', groupData, group);

        }(this.model.root));

        out.valid = valid;

        /**
         * Modifies the result of the {@link QueryBuilder#getRules} method
         * @event changer:getRules
         * @memberof QueryBuilder
         * @param {object} json
         * @returns {object}
         */
        return this.change('getRules', out);
    };

    /**
     * Sets rules from object
     * @param {object} data
     * @param {object} [options]
     * @param {boolean} [options.allow_invalid=false] - silent-fail if the data are invalid
     * @throws RulesError, UndefinedConditionError
     * @fires QueryBuilder.changer:setRules
     * @fires QueryBuilder.changer:jsonToRule
     * @fires QueryBuilder.changer:jsonToGroup
     * @fires QueryBuilder.afterSetRules
     */
    QueryBuilder.prototype.setRules = function(data, options) {
        options = $.extend({
            allow_invalid: false
        }, options);

        if ($.isArray(data)) {
            data = {
                condition: this.settings.default_condition,
                rules: data
            };
        }

        if (!data || !data.rules || (data.rules.length === 0 && !this.settings.allow_empty)) {
            Utils.error('RulesParse', 'Incorrect data object passed');
        }

        this.clear();
        this.setRoot(false, data.data, this.parseGroupFlags(data));

        /**
         * Modifies data before the {@link QueryBuilder#setRules} method
         * @event changer:setRules
         * @memberof QueryBuilder
         * @param {object} json
         * @param {object} options
         * @returns {object}
         */
        data = this.change('setRules', data, options);

        var self = this;

        (function add(data, group) {
            if (group === null) {
                return;
            }

            if (data.condition === undefined) {
                data.condition = self.settings.default_condition;
            }
            else if (self.settings.conditions.indexOf(data.condition) == -1) {
                Utils.error(!options.allow_invalid, 'UndefinedCondition', 'Invalid condition "{0}"', data.condition);
                data.condition = self.settings.default_condition;
            }

            group.condition = data.condition;

            data.rules.forEach(function(item) {
                var model;

                if (item.rules !== undefined) {
                    if (self.settings.allow_groups !== -1 && self.settings.allow_groups < group.level) {
                        Utils.error(!options.allow_invalid, 'RulesParse', 'No more than {0} groups are allowed', self.settings.allow_groups);
                        self.reset();
                    }
                    else {
                        model = self.addGroup(group, false, item.data, self.parseGroupFlags(item));
                        if (model === null) {
                            return;
                        }

                        add(item, model);
                    }
                }
                else {
                    if (!item.empty) {
                        if (item.id === undefined) {
                            Utils.error(!options.allow_invalid, 'RulesParse', 'Missing rule field id');
                            item.empty = true;
                        }
                        if (item.operator === undefined) {
                            item.operator = 'equal';
                        }
                    }

                    model = self.addRule(group, item.data, self.parseRuleFlags(item));
                    if (model === null) {
                        return;
                    }

                    if (!item.empty) {
                        model.filter = self.getFilterById(item.id, !options.allow_invalid);
                    }

                    if (model.filter) {
                        model.operator = self.getOperatorByType(item.operator, !options.allow_invalid);

                        if (!model.operator) {
                            model.operator = self.getOperators(model.filter)[0];
                        }
                    }

                    if (model.operator && model.operator.nb_inputs !== 0) {
                        if (item.value !== undefined) {
                            model.value = item.value;
                        }
                        else if (model.filter.default_value !== undefined) {
                            model.value = model.filter.default_value;
                        }
                    }

                    /**
                     * Modifies the Rule object generated from the JSON
                     * @event changer:jsonToRule
                     * @memberof QueryBuilder
                     * @param {Rule} rule
                     * @param {object} json
                     * @returns {Rule} the same rule
                     */
                    if (self.change('jsonToRule', model, item) != model) {
                        Utils.error('RulesParse', 'Plugin tried to change rule reference');
                    }
                }
            });

            /**
             * Modifies the Group object generated from the JSON
             * @event changer:jsonToGroup
             * @memberof QueryBuilder
             * @param {Group} group
             * @param {object} json
             * @returns {Group} the same group
             */
            if (self.change('jsonToGroup', group, data) != group) {
                Utils.error('RulesParse', 'Plugin tried to change group reference');
            }

        }(data, this.model.root));

        /**
         * After the {@link QueryBuilder#setRules} method
         * @event afterSetRules
         * @memberof QueryBuilder
         */
        this.trigger('afterSetRules');
    };


    /**
     * Performs value validation
     * @param {Rule} rule
     * @param {string|string[]} value
     * @returns {array|boolean} true or error array
     * @fires QueryBuilder.changer:validateValue
     */
    QueryBuilder.prototype.validateValue = function(rule, value) {
        var validation = rule.filter.validation || {};
        var result = true;

        if (validation.callback) {
            result = validation.callback.call(this, value, rule);
        }
        else {
            result = this._validateValue(rule, value);
        }

        /**
         * Modifies the result of the rule validation method
         * @event changer:validateValue
         * @memberof QueryBuilder
         * @param {array|boolean} result - true or an error array
         * @param {*} value
         * @param {Rule} rule
         * @returns {array|boolean}
         */
        return this.change('validateValue', result, value, rule);
    };

    /**
     * Default validation function
     * @param {Rule} rule
     * @param {string|string[]} value
     * @returns {array|boolean} true or error array
     * @throws ConfigError
     * @private
     */
    QueryBuilder.prototype._validateValue = function(rule, value) {
        var filter = rule.filter;
        var operator = rule.operator;
        var validation = filter.validation || {};
        var result = true;
        var tmp, tempValue;

        if (rule.operator.nb_inputs === 1) {
            value = [value];
        }

        for (var i = 0; i < operator.nb_inputs; i++) {
            if (!operator.multiple && $.isArray(value[i]) && value[i].length > 1) {
                result = ['operator_not_multiple', operator.type, this.translate('operators', operator.type)];
                break;
            }

            switch (filter.input) {
                case 'radio':
                    if (value[i] === undefined || value[i].length === 0) {
                        if (!validation.allow_empty_value) {
                            result = ['radio_empty'];
                        }
                        break;
                    }
                    break;

                case 'checkbox':
                    if (value[i] === undefined || value[i].length === 0) {
                        if (!validation.allow_empty_value) {
                            result = ['checkbox_empty'];
                        }
                        break;
                    }
                    break;

                case 'select':
                    if (value[i] === undefined || value[i].length === 0 || (filter.placeholder && value[i] == filter.placeholder_value)) {
                        if (!validation.allow_empty_value) {
                            result = ['select_empty'];
                        }
                        break;
                    }
                    break;

                default:
                    tempValue = $.isArray(value[i]) ? value[i] : [value[i]];

                    for (var j = 0; j < tempValue.length; j++) {
                        switch (QueryBuilder.types[filter.type]) {
                            case 'string':
                                if (tempValue[j] === undefined || tempValue[j].length === 0) {
                                    if (!validation.allow_empty_value) {
                                        result = ['string_empty'];
                                    }
                                    break;
                                }
                                if (validation.min !== undefined) {
                                    if (tempValue[j].length < parseInt(validation.min)) {
                                        result = [this.getValidationMessage(validation, 'min', 'string_exceed_min_length'), validation.min];
                                        break;
                                    }
                                }
                                if (validation.max !== undefined) {
                                    if (tempValue[j].length > parseInt(validation.max)) {
                                        result = [this.getValidationMessage(validation, 'max', 'string_exceed_max_length'), validation.max];
                                        break;
                                    }
                                }
                                if (validation.format) {
                                    if (typeof validation.format == 'string') {
                                        validation.format = new RegExp(validation.format);
                                    }
                                    if (!validation.format.test(tempValue[j])) {
                                        result = [this.getValidationMessage(validation, 'format', 'string_invalid_format'), validation.format];
                                        break;
                                    }
                                }
                                break;

                            case 'number':
                                if (tempValue[j] === undefined || tempValue[j].length === 0) {
                                    if (!validation.allow_empty_value) {
                                        result = ['number_nan'];
                                    }
                                    break;
                                }
                                if (isNaN(tempValue[j])) {
                                    result = ['number_nan'];
                                    break;
                                }
                                if (filter.type == 'integer') {
                                    if (parseInt(tempValue[j]) != tempValue[j]) {
                                        result = ['number_not_integer'];
                                        break;
                                    }
                                }
                                else {
                                    if (parseFloat(tempValue[j]) != tempValue[j]) {
                                        result = ['number_not_double'];
                                        break;
                                    }
                                }
                                if (validation.min !== undefined) {
                                    if (tempValue[j] < parseFloat(validation.min)) {
                                        result = [this.getValidationMessage(validation, 'min', 'number_exceed_min'), validation.min];
                                        break;
                                    }
                                }
                                if (validation.max !== undefined) {
                                    if (tempValue[j] > parseFloat(validation.max)) {
                                        result = [this.getValidationMessage(validation, 'max', 'number_exceed_max'), validation.max];
                                        break;
                                    }
                                }
                                if (validation.step !== undefined && validation.step !== 'any') {
                                    var v = (tempValue[j] / validation.step).toPrecision(14);
                                    if (parseInt(v) != v) {
                                        result = [this.getValidationMessage(validation, 'step', 'number_wrong_step'), validation.step];
                                        break;
                                    }
                                }
                                break;

                            case 'datetime':
                                if (tempValue[j] === undefined || tempValue[j].length === 0) {
                                    if (!validation.allow_empty_value) {
                                        result = ['datetime_empty'];
                                    }
                                    break;
                                }

                                // we need MomentJS
                                if (validation.format) {
                                    if (!('moment' in window)) {
                                        Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
                                    }

                                    var datetime = moment(tempValue[j], validation.format);
                                    if (!datetime.isValid()) {
                                        result = [this.getValidationMessage(validation, 'format', 'datetime_invalid'), validation.format];
                                        break;
                                    }
                                    else {
                                        if (validation.min) {
                                            if (datetime < moment(validation.min, validation.format)) {
                                                result = [this.getValidationMessage(validation, 'min', 'datetime_exceed_min'), validation.min];
                                                break;
                                            }
                                        }
                                        if (validation.max) {
                                            if (datetime > moment(validation.max, validation.format)) {
                                                result = [this.getValidationMessage(validation, 'max', 'datetime_exceed_max'), validation.max];
                                                break;
                                            }
                                        }
                                    }
                                }
                                break;

                            case 'boolean':
                                if (tempValue[j] === undefined || tempValue[j].length === 0) {
                                    if (!validation.allow_empty_value) {
                                        result = ['boolean_not_valid'];
                                    }
                                    break;
                                }
                                tmp = ('' + tempValue[j]).trim().toLowerCase();
                                if (tmp !== 'true' && tmp !== 'false' && tmp !== '1' && tmp !== '0' && tempValue[j] !== 1 && tempValue[j] !== 0) {
                                    result = ['boolean_not_valid'];
                                    break;
                                }
                        }

                        if (result !== true) {
                            break;
                        }
                    }
            }

            if (result !== true) {
                break;
            }
        }

        if ((rule.operator.type === 'between' || rule.operator.type === 'not_between') && value.length === 2) {
            switch (QueryBuilder.types[filter.type]) {
                case 'number':
                    if (value[0] > value[1]) {
                        result = ['number_between_invalid', value[0], value[1]];
                    }
                    break;

                case 'datetime':
                    // we need MomentJS
                    if (validation.format) {
                        if (!('moment' in window)) {
                            Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
                        }

                        if (moment(value[0], validation.format).isAfter(moment(value[1], validation.format))) {
                            result = ['datetime_between_invalid', value[0], value[1]];
                        }
                    }
                    break;
            }
        }

        return result;
    };

    /**
     * Returns an incremented group ID
     * @returns {string}
     * @private
     */
    QueryBuilder.prototype.nextGroupId = function() {
        return this.status.id + '_group_' + (this.status.group_id++);
    };

    /**
     * Returns an incremented rule ID
     * @returns {string}
     * @private
     */
    QueryBuilder.prototype.nextRuleId = function() {
        return this.status.id + '_rule_' + (this.status.rule_id++);
    };

    /**
     * Returns the operators for a filter
     * @param {string|object} filter - filter id or filter object
     * @returns {object[]}
     * @fires QueryBuilder.changer:getOperators
     * @private
     */
    QueryBuilder.prototype.getOperators = function(filter) {
        if (typeof filter == 'string') {
            filter = this.getFilterById(filter);
        }

        var result = [];

        for (var i = 0, l = this.operators.length; i < l; i++) {
            // filter operators check
            if (filter.operators) {
                if (filter.operators.indexOf(this.operators[i].type) == -1) {
                    continue;
                }
            }
            // type check
            else if (this.operators[i].apply_to.indexOf(QueryBuilder.types[filter.type]) == -1) {
                continue;
            }

            result.push(this.operators[i]);
        }

        // keep sort order defined for the filter
        if (filter.operators) {
            result.sort(function(a, b) {
                return filter.operators.indexOf(a.type) - filter.operators.indexOf(b.type);
            });
        }

        /**
         * Modifies the operators available for a filter
         * @event changer:getOperators
         * @memberof QueryBuilder
         * @param {QueryBuilder.Operator[]} operators
         * @param {QueryBuilder.Filter} filter
         * @returns {QueryBuilder.Operator[]}
         */
        return this.change('getOperators', result, filter);
    };

    /**
     * Returns a particular filter by its id
     * @param {string} id
     * @param {boolean} [doThrow=true]
     * @returns {object|null}
     * @throws UndefinedFilterError
     * @private
     */
    QueryBuilder.prototype.getFilterById = function(id, doThrow) {
        if (id == '-1') {
            return null;
        }

        for (var i = 0, l = this.filters.length; i < l; i++) {
            if (this.filters[i].id == id) {
                return this.filters[i];
            }
        }

        Utils.error(doThrow !== false, 'UndefinedFilter', 'Undefined filter "{0}"', id);

        return null;
    };

    /**
     * Returns a particular operator by its type
     * @param {string} type
     * @param {boolean} [doThrow=true]
     * @returns {object|null}
     * @throws UndefinedOperatorError
     * @private
     */
    QueryBuilder.prototype.getOperatorByType = function(type, doThrow) {
        if (type == '-1') {
            return null;
        }

        for (var i = 0, l = this.operators.length; i < l; i++) {
            if (this.operators[i].type == type) {
                return this.operators[i];
            }
        }

        Utils.error(doThrow !== false, 'UndefinedOperator', 'Undefined operator "{0}"', type);

        return null;
    };

    /**
     * Returns rule's current input value
     * @param {Rule} rule
     * @returns {*}
     * @fires QueryBuilder.changer:getRuleValue
     * @private
     */
    QueryBuilder.prototype.getRuleInputValue = function(rule) {
        var filter = rule.filter;
        var operator = rule.operator;
        var value = [];

        if (filter.valueGetter) {
            value = filter.valueGetter.call(this, rule);
        }
        else {
            var $value = rule.$el.find(QueryBuilder.selectors.value_container);

            for (var i = 0; i < operator.nb_inputs; i++) {
                var name = Utils.escapeElementId(rule.id + '_value_' + i);
                var tmp;

                switch (filter.input) {
                    case 'radio':
                        value.push($value.find('[name=' + name + ']:checked').val());
                        break;

                    case 'checkbox':
                        tmp = [];
                        // jshint loopfunc:true
                        $value.find('[name=' + name + ']:checked').each(function() {
                            tmp.push($(this).val());
                        });
                        // jshint loopfunc:false
                        value.push(tmp);
                        break;

                    case 'select':
                        if (filter.multiple) {
                            tmp = [];
                            // jshint loopfunc:true
                            $value.find('[name=' + name + '] option:selected').each(function() {
                                tmp.push($(this).val());
                            });
                            // jshint loopfunc:false
                            value.push(tmp);
                        }
                        else {
                            value.push($value.find('[name=' + name + '] option:selected').val());
                        }
                        break;

                    default:
                        value.push($value.find('[name=' + name + ']').val());
                }
            }

            value = value.map(function(val) {
                if (operator.multiple && filter.value_separator && typeof val == 'string') {
                    val = val.split(filter.value_separator);
                }

                if ($.isArray(val)) {
                    return val.map(function(subval) {
                        return Utils.changeType(subval, filter.type);
                    });
                }
                else {
                    return Utils.changeType(val, filter.type);
                }
            });

            if (operator.nb_inputs === 1) {
                value = value[0];
            }

            // @deprecated
            if (filter.valueParser) {
                value = filter.valueParser.call(this, rule, value);
            }
        }

        /**
         * Modifies the rule's value grabbed from the DOM
         * @event changer:getRuleValue
         * @memberof QueryBuilder
         * @param {*} value
         * @param {Rule} rule
         * @returns {*}
         */
        return this.change('getRuleValue', value, rule);
    };

    /**
     * Sets the value of a rule's input
     * @param {Rule} rule
     * @param {*} value
     * @private
     */
    QueryBuilder.prototype.setRuleInputValue = function(rule, value) {
        var filter = rule.filter;
        var operator = rule.operator;

        if (!filter || !operator) {
            return;
        }

        rule._updating_input = true;

        if (filter.valueSetter) {
            filter.valueSetter.call(this, rule, value);
        }
        else {
            var $value = rule.$el.find(QueryBuilder.selectors.value_container);

            if (operator.nb_inputs == 1) {
                value = [value];
            }

            for (var i = 0; i < operator.nb_inputs; i++) {
                var name = Utils.escapeElementId(rule.id + '_value_' + i);

                switch (filter.input) {
                    case 'radio':
                        $value.find('[name=' + name + '][value="' + value[i] + '"]').prop('checked', true).trigger('change');
                        break;

                    case 'checkbox':
                        if (!$.isArray(value[i])) {
                            value[i] = [value[i]];
                        }
                        // jshint loopfunc:true
                        value[i].forEach(function(value) {
                            $value.find('[name=' + name + '][value="' + value + '"]').prop('checked', true).trigger('change');
                        });
                        // jshint loopfunc:false
                        break;

                    default:
                        if (operator.multiple && filter.value_separator && $.isArray(value[i])) {
                            value[i] = value[i].join(filter.value_separator);
                        }
                        $value.find('[name=' + name + ']').val(value[i]).trigger('change');
                        break;
                }
            }
        }

        rule._updating_input = false;
    };

    /**
     * Parses rule flags
     * @param {object} rule
     * @returns {object}
     * @fires QueryBuilder.changer:parseRuleFlags
     * @private
     */
    QueryBuilder.prototype.parseRuleFlags = function(rule) {
        var flags = $.extend({}, this.settings.default_rule_flags);

        if (rule.readonly) {
            $.extend(flags, {
                header_readonly: true,
                filter_readonly: true,
                operator_readonly: true,
                value_readonly: true,
                no_delete: true,
                no_sortable: true
            });
        }

        if (rule.flags) {
            $.extend(flags, rule.flags);
        }

        /**
         * Modifies the consolidated rule's flags
         * @event changer:parseRuleFlags
         * @memberof QueryBuilder
         * @param {object} flags
         * @param {object} rule - <b>not</b> a Rule object
         * @returns {object}
         */
        return this.change('parseRuleFlags', flags, rule);
    };

    /**
     * Gets a copy of flags of a rule
     * @param {object} flags
     * @param {boolean} [all=false] - return all flags or only changes from default flags
     * @returns {object}
     * @private
     */
    QueryBuilder.prototype.getRuleFlags = function(flags, all) {
        if (all) {
            return $.extend({}, flags);
        }
        else {
            var ret = {};
            $.each(this.settings.default_rule_flags, function(key, value) {
                if (flags[key] !== value) {
                    ret[key] = flags[key];
                }
            });
            return ret;
        }
    };

    /**
     * Parses group flags
     * @param {object} group
     * @returns {object}
     * @fires QueryBuilder.changer:parseGroupFlags
     * @private
     */
    QueryBuilder.prototype.parseGroupFlags = function(group) {
        var flags = $.extend({}, this.settings.default_group_flags);

        if (group.readonly) {
            $.extend(flags, {
                condition_readonly: true,
                no_add_rule: true,
                no_add_group: true,
                no_delete: true
            });
        }

        if (group.flags) {
            $.extend(flags, group.flags);
        }

        /**
         * Modifies the consolidated group's flags
         * @event changer:parseGroupFlags
         * @memberof QueryBuilder
         * @param {object} flags
         * @param {object} group - <b>not</b> a Group object
         * @returns {object}
         */
        return this.change('parseGroupFlags', flags, group);
    };

    /**
     * Gets a copy of flags of a group
     * @param {object} flags
     * @param {boolean} [all=false] - return all flags or only changes from default flags
     * @returns {object}
     * @private
     */
    QueryBuilder.prototype.getGroupFlags = function(flags, all) {
        if (all) {
            return $.extend({}, flags);
        }
        else {
            var ret = {};
            $.each(this.settings.default_group_flags, function(key, value) {
                if (flags[key] !== value) {
                    ret[key] = flags[key];
                }
            });
            return ret;
        }
    };

    /**
     * Translate a label either by looking in the `lang` object or in itself if it's an object where keys are language codes
     * @param {string} [category]
     * @param {string|object} key
     * @returns {string}
     * @fires QueryBuilder.changer:translate
     */
    QueryBuilder.prototype.translate = function(category, key) {
        if (!key) {
            key = category;
            category = undefined;
        }

        var translation;
        if (typeof key === 'object') {
            translation = key[this.settings.lang_code] || key['en'];
        }
        else {
            translation = (category ? this.lang[category] : this.lang)[key] || key;
        }

        /**
         * Modifies the translated label
         * @event changer:translate
         * @memberof QueryBuilder
         * @param {string} translation
         * @param {string|object} key
         * @param {string} [category]
         * @returns {string}
         */
        return this.change('translate', translation, key, category);
    };

    /**
     * Returns a validation message
     * @param {object} validation
     * @param {string} type
     * @param {string} def
     * @returns {string}
     * @private
     */
    QueryBuilder.prototype.getValidationMessage = function(validation, type, def) {
        return validation.messages && validation.messages[type] || def;
    };


    QueryBuilder.templates.group = '\
<div id="{{= it.group_id }}" class="rules-group-container"> \
  <div class="rules-group-header"> \
    <div class="btn-group pull-right group-actions"> \
      <button type="button" class="btn btn-xs btn-success" data-add="rule"> \
        <i class="{{= it.icons.add_rule }}"></i> {{= it.translate("add_rule") }} \
      </button> \
      {{? it.settings.allow_groups===-1 || it.settings.allow_groups>=it.level }} \
        <button type="button" class="btn btn-xs btn-success" data-add="group"> \
          <i class="{{= it.icons.add_group }}"></i> {{= it.translate("add_group") }} \
        </button> \
      {{?}} \
      {{? it.level>1 }} \
        <button type="button" class="btn btn-xs btn-danger" data-delete="group"> \
          <i class="{{= it.icons.remove_group }}"></i> {{= it.translate("delete_group") }} \
        </button> \
      {{?}} \
    </div> \
    <div class="btn-group group-conditions"> \
      {{~ it.conditions: condition }} \
        <label class="btn btn-xs btn-primary"> \
          <input type="radio" name="{{= it.group_id }}_cond" value="{{= condition }}"> {{= it.translate("conditions", condition) }} \
        </label> \
      {{~}} \
    </div> \
    {{? it.settings.display_errors }} \
      <div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
    {{?}} \
  </div> \
  <div class=rules-group-body> \
    <div class=rules-list></div> \
  </div> \
</div>';

    QueryBuilder.templates.rule = '\
<div id="{{= it.rule_id }}" class="rule-container"> \
  <div class="rule-header"> \
    <div class="btn-group pull-right rule-actions"> \
      <button type="button" class="btn btn-xs btn-danger" data-delete="rule"> \
        <i class="{{= it.icons.remove_rule }}"></i> {{= it.translate("delete_rule") }} \
      </button> \
    </div> \
  </div> \
  {{? it.settings.display_errors }} \
    <div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
  {{?}} \
  <div class="rule-filter-container"></div> \
  <div class="rule-operator-container"></div> \
  <div class="rule-value-container"></div> \
</div>';

    QueryBuilder.templates.filterSelect = '\
{{ var optgroup = null; }} \
<select class="form-control" name="{{= it.rule.id }}_filter"> \
  {{? it.settings.display_empty_filter }} \
    <option value="-1">{{= it.settings.select_placeholder }}</option> \
  {{?}} \
  {{~ it.filters: filter }} \
    {{? optgroup !== filter.optgroup }} \
      {{? optgroup !== null }}</optgroup>{{?}} \
      {{? (optgroup = filter.optgroup) !== null }} \
        <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
      {{?}} \
    {{?}} \
    <option value="{{= filter.id }}" {{? filter.icon}}data-icon="{{= filter.icon}}"{{?}}>{{= it.translate(filter.label) }}</option> \
  {{~}} \
  {{? optgroup !== null }}</optgroup>{{?}} \
</select>';

    QueryBuilder.templates.operatorSelect = '\
{{? it.operators.length === 1 }} \
<span> \
{{= it.translate("operators", it.operators[0].type) }} \
</span> \
{{?}} \
{{ var optgroup = null; }} \
<select class="form-control {{? it.operators.length === 1 }}hide{{?}}" name="{{= it.rule.id }}_operator"> \
  {{~ it.operators: operator }} \
    {{? optgroup !== operator.optgroup }} \
      {{? optgroup !== null }}</optgroup>{{?}} \
      {{? (optgroup = operator.optgroup) !== null }} \
        <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
      {{?}} \
    {{?}} \
    <option value="{{= operator.type }}" {{? operator.icon}}data-icon="{{= operator.icon}}"{{?}}>{{= it.translate("operators", operator.type) }}</option> \
  {{~}} \
  {{? optgroup !== null }}</optgroup>{{?}} \
</select>';

    QueryBuilder.templates.ruleValueSelect = '\
{{ var optgroup = null; }} \
<select class="form-control" name="{{= it.name }}" {{? it.rule.filter.multiple }}multiple{{?}}> \
  {{? it.rule.filter.placeholder }} \
    <option value="{{= it.rule.filter.placeholder_value }}" disabled selected>{{= it.rule.filter.placeholder }}</option> \
  {{?}} \
  {{~ it.rule.filter.values: entry }} \
    {{? optgroup !== entry.optgroup }} \
      {{? optgroup !== null }}</optgroup>{{?}} \
      {{? (optgroup = entry.optgroup) !== null }} \
        <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
      {{?}} \
    {{?}} \
    <option value="{{= entry.value }}">{{= entry.label }}</option> \
  {{~}} \
  {{? optgroup !== null }}</optgroup>{{?}} \
</select>';

    /**
     * Returns group's HTML
     * @param {string} group_id
     * @param {int} level
     * @returns {string}
     * @fires QueryBuilder.changer:getGroupTemplate
     * @private
     */
    QueryBuilder.prototype.getGroupTemplate = function(group_id, level) {
        var h = this.templates.group({
            builder: this,
            group_id: group_id,
            level: level,
            conditions: this.settings.conditions,
            icons: this.icons,
            settings: this.settings,
            translate: this.translate.bind(this)
        });

        /**
         * Modifies the raw HTML of a group
         * @event changer:getGroupTemplate
         * @memberof QueryBuilder
         * @param {string} html
         * @param {int} level
         * @returns {string}
         */
        return this.change('getGroupTemplate', h, level);
    };

    /**
     * Returns rule's HTML
     * @param {string} rule_id
     * @returns {string}
     * @fires QueryBuilder.changer:getRuleTemplate
     * @private
     */
    QueryBuilder.prototype.getRuleTemplate = function(rule_id) {
        var h = this.templates.rule({
            builder: this,
            rule_id: rule_id,
            icons: this.icons,
            settings: this.settings,
            translate: this.translate.bind(this)
        });

        /**
         * Modifies the raw HTML of a rule
         * @event changer:getRuleTemplate
         * @memberof QueryBuilder
         * @param {string} html
         * @returns {string}
         */
        return this.change('getRuleTemplate', h);
    };

    /**
     * Returns rule's filter HTML
     * @param {Rule} rule
     * @param {object[]} filters
     * @returns {string}
     * @fires QueryBuilder.changer:getRuleFilterTemplate
     * @private
     */
    QueryBuilder.prototype.getRuleFilterSelect = function(rule, filters) {
        var h = this.templates.filterSelect({
            builder: this,
            rule: rule,
            filters: filters,
            icons: this.icons,
            settings: this.settings,
            translate: this.translate.bind(this)
        });

        /**
         * Modifies the raw HTML of the rule's filter dropdown
         * @event changer:getRuleFilterSelect
         * @memberof QueryBuilder
         * @param {string} html
         * @param {Rule} rule
         * @param {QueryBuilder.Filter[]} filters
         * @returns {string}
         */
        return this.change('getRuleFilterSelect', h, rule, filters);
    };

    /**
     * Returns rule's operator HTML
     * @param {Rule} rule
     * @param {object[]} operators
     * @returns {string}
     * @fires QueryBuilder.changer:getRuleOperatorTemplate
     * @private
     */
    QueryBuilder.prototype.getRuleOperatorSelect = function(rule, operators) {
        var h = this.templates.operatorSelect({
            builder: this,
            rule: rule,
            operators: operators,
            icons: this.icons,
            settings: this.settings,
            translate: this.translate.bind(this)
        });

        /**
         * Modifies the raw HTML of the rule's operator dropdown
         * @event changer:getRuleOperatorSelect
         * @memberof QueryBuilder
         * @param {string} html
         * @param {Rule} rule
         * @param {QueryBuilder.Operator[]} operators
         * @returns {string}
         */
        return this.change('getRuleOperatorSelect', h, rule, operators);
    };

    /**
     * Returns the rule's value select HTML
     * @param {string} name
     * @param {Rule} rule
     * @returns {string}
     * @fires QueryBuilder.changer:getRuleValueSelect
     * @private
     */
    QueryBuilder.prototype.getRuleValueSelect = function(name, rule) {
        var h = this.templates.ruleValueSelect({
            builder: this,
            name: name,
            rule: rule,
            icons: this.icons,
            settings: this.settings,
            translate: this.translate.bind(this)
        });

        /**
         * Modifies the raw HTML of the rule's value dropdown (in case of a "select filter)
         * @event changer:getRuleValueSelect
         * @memberof QueryBuilder
         * @param {string} html
         * @param [string} name
         * @param {Rule} rule
         * @returns {string}
         */
        return this.change('getRuleValueSelect', h, name, rule);
    };

    /**
     * Returns the rule's value HTML
     * @param {Rule} rule
     * @param {int} value_id
     * @returns {string}
     * @fires QueryBuilder.changer:getRuleInput
     * @private
     */
    QueryBuilder.prototype.getRuleInput = function(rule, value_id) {
        var filter = rule.filter;
        var validation = rule.filter.validation || {};
        var name = rule.id + '_value_' + value_id;
        var c = filter.vertical ? ' class=block' : '';
        var h = '';

        if (typeof filter.input == 'function') {
            h = filter.input.call(this, rule, name);
        }
        else {
            switch (filter.input) {
                case 'radio':
                case 'checkbox':
                    Utils.iterateOptions(filter.values, function(key, val) {
                        h += '<label' + c + '><input type="' + filter.input + '" name="' + name + '" value="' + key + '"> ' + val + '</label> ';
                    });
                    break;

                case 'select':
                    h = this.getRuleValueSelect(name, rule);
                    break;

                case 'textarea':
                    h += '<textarea class="form-control" name="' + name + '"';
                    if (filter.size) h += ' cols="' + filter.size + '"';
                    if (filter.rows) h += ' rows="' + filter.rows + '"';
                    if (validation.min !== undefined) h += ' minlength="' + validation.min + '"';
                    if (validation.max !== undefined) h += ' maxlength="' + validation.max + '"';
                    if (filter.placeholder) h += ' placeholder="' + filter.placeholder + '"';
                    h += '></textarea>';
                    break;

                case 'number':
                    h += '<input class="form-control" type="number" name="' + name + '"';
                    if (validation.step !== undefined) h += ' step="' + validation.step + '"';
                    if (validation.min !== undefined) h += ' min="' + validation.min + '"';
                    if (validation.max !== undefined) h += ' max="' + validation.max + '"';
                    if (filter.placeholder) h += ' placeholder="' + filter.placeholder + '"';
                    if (filter.size) h += ' size="' + filter.size + '"';
                    h += '>';
                    break;

                default:
                    h += '<input class="form-control" type="text" name="' + name + '"';
                    if (filter.placeholder) h += ' placeholder="' + filter.placeholder + '"';
                    if (filter.type === 'string' && validation.min !== undefined) h += ' minlength="' + validation.min + '"';
                    if (filter.type === 'string' && validation.max !== undefined) h += ' maxlength="' + validation.max + '"';
                    if (filter.size) h += ' size="' + filter.size + '"';
                    h += '>';
            }
        }

        /**
         * Modifies the raw HTML of the rule's input
         * @event changer:getRuleInput
         * @memberof QueryBuilder
         * @param {string} html
         * @param {Rule} rule
         * @param {string} name - the name that the input must have
         * @returns {string}
         */
        return this.change('getRuleInput', h, rule, name);
    };


    /**
     * @namespace
     */
    var Utils = {};

    /**
     * @member {object}
     * @memberof QueryBuilder
     * @see Utils
     */
    QueryBuilder.utils = Utils;

    /**
     * @callback Utils#OptionsIteratee
     * @param {string} key
     * @param {string} value
     * @param {string} [optgroup]
     */

    /**
     * Iterates over radio/checkbox/selection options, it accept four formats
     *
     * @example
     * // array of values
     * options = ['one', 'two', 'three']
     * @example
     * // simple key-value map
     * options = {1: 'one', 2: 'two', 3: 'three'}
     * @example
     * // array of 1-element maps
     * options = [{1: 'one'}, {2: 'two'}, {3: 'three'}]
     * @example
     * // array of elements
     * options = [{value: 1, label: 'one', optgroup: 'group'}, {value: 2, label: 'two'}]
     *
     * @param {object|array} options
     * @param {Utils#OptionsIteratee} tpl
     */
    Utils.iterateOptions = function(options, tpl) {
        if (options) {
            if ($.isArray(options)) {
                options.forEach(function(entry) {
                    if ($.isPlainObject(entry)) {
                        // array of elements
                        if ('value' in entry) {
                            tpl(entry.value, entry.label || entry.value, entry.optgroup);
                        }
                        // array of one-element maps
                        else {
                            $.each(entry, function(key, val) {
                                tpl(key, val);
                                return false; // break after first entry
                            });
                        }
                    }
                    // array of values
                    else {
                        tpl(entry, entry);
                    }
                });
            }
            // unordered map
            else {
                $.each(options, function(key, val) {
                    tpl(key, val);
                });
            }
        }
    };

    /**
     * Replaces {0}, {1}, ... in a string
     * @param {string} str
     * @param {...*} args
     * @returns {string}
     */
    Utils.fmt = function(str, args) {
        if (!Array.isArray(args)) {
            args = Array.prototype.slice.call(arguments, 1);
        }

        return str.replace(/{([0-9]+)}/g, function(m, i) {
            return args[parseInt(i)];
        });
    };

    /**
     * Throws an Error object with custom name or logs an error
     * @param {boolean} [doThrow=true]
     * @param {string} type
     * @param {string} message
     * @param {...*} args
     */
    Utils.error = function() {
        var i = 0;
        var doThrow = typeof arguments[i] === 'boolean' ? arguments[i++] : true;
        var type = arguments[i++];
        var message = arguments[i++];
        var args = Array.isArray(arguments[i]) ? arguments[i] : Array.prototype.slice.call(arguments, i);

        if (doThrow) {
            var err = new Error(Utils.fmt(message, args));
            err.name = type + 'Error';
            err.args = args;
            throw err;
        }
        else {
            console.error(type + 'Error: ' + Utils.fmt(message, args));
        }
    };

    /**
     * Changes the type of a value to int, float or bool
     * @param {*} value
     * @param {string} type - 'integer', 'double', 'boolean' or anything else (passthrough)
     * @returns {*}
     */
    Utils.changeType = function(value, type) {
        if (value === '' || value === undefined) {
            return undefined;
        }

        switch (type) {
            // @formatter:off
            case 'integer':
                if (typeof value === 'string' && !/^-?\d+$/.test(value)) {
                    return value;
                }
                return parseInt(value);
            case 'double':
                if (typeof value === 'string' && !/^-?\d+\.?\d*$/.test(value)) {
                    return value;
                }
                return parseFloat(value);
            case 'boolean':
                if (typeof value === 'string' && !/^(0|1|true|false){1}$/i.test(value)) {
                    return value;
                }
                return value === true || value === 1 || value.toLowerCase() === 'true' || value === '1';
            default: return value;
            // @formatter:on
        }
    };

    /**
     * Escapes a string like PHP's mysql_real_escape_string does
     * @param {string} value
     * @returns {string}
     */
    Utils.escapeString = function(value) {
        if (typeof value != 'string') {
            return value;
        }

        return value
            .replace(/[\0\n\r\b\\\'\"]/g, function(s) {
                switch (s) {
                    // @formatter:off
                    case '\0': return '\\0';
                    case '\n': return '\\n';
                    case '\r': return '\\r';
                    case '\b': return '\\b';
                    default:   return '\\' + s;
                    // @formatter:off
                }
            })
            // uglify compliant
            .replace(/\t/g, '\\t')
            .replace(/\x1a/g, '\\Z');
    };

    /**
     * Escapes a string for use in regex
     * @param {string} str
     * @returns {string}
     */
    Utils.escapeRegExp = function(str) {
        return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
    };

    /**
     * Escapes a string for use in HTML element id
     * @param {string} str
     * @returns {string}
     */
    Utils.escapeElementId = function(str) {
        // Regex based on that suggested by:
        // https://learn.jquery.com/using-jquery-core/faq/how-do-i-select-an-element-by-an-id-that-has-characters-used-in-css-notation/
        // - escapes : . [ ] ,
        // - avoids escaping already escaped values
        return (str) ? str.replace(/(\\)?([:.\[\],])/g,
            function( $0, $1, $2 ) { return $1 ? $0 : '\\' + $2; }) : str;
    };

    /**
     * Sorts objects by grouping them by `key`, preserving initial order when possible
     * @param {object[]} items
     * @param {string} key
     * @returns {object[]}
     */
    Utils.groupSort = function(items, key) {
        var optgroups = [];
        var newItems = [];

        items.forEach(function(item) {
            var idx;

            if (item[key]) {
                idx = optgroups.lastIndexOf(item[key]);

                if (idx == -1) {
                    idx = optgroups.length;
                }
                else {
                    idx++;
                }
            }
            else {
                idx = optgroups.length;
            }

            optgroups.splice(idx, 0, item[key]);
            newItems.splice(idx, 0, item);
        });

        return newItems;
    };

    /**
     * Defines properties on an Node prototype with getter and setter.<br>
     *     Update events are emitted in the setter through root Model (if any).<br>
     *     The object must have a `__` object, non enumerable property to store values.
     * @param {function} obj
     * @param {string[]} fields
     */
    Utils.defineModelProperties = function(obj, fields) {
        fields.forEach(function(field) {
            Object.defineProperty(obj.prototype, field, {
                enumerable: true,
                get: function() {
                    return this.__[field];
                },
                set: function(value) {
                    var previousValue = (this.__[field] !== null && typeof this.__[field] == 'object') ?
                        $.extend({}, this.__[field]) :
                        this.__[field];

                    this.__[field] = value;

                    if (this.model !== null) {
                        /**
                         * After a value of the model changed
                         * @event model:update
                         * @memberof Model
                         * @param {Node} node
                         * @param {string} field
                         * @param {*} value
                         * @param {*} previousValue
                         */
                        this.model.trigger('update', this, field, value, previousValue);
                    }
                }
            });
        });
    };


    /**
     * Main object storing data model and emitting model events
     * @constructor
     */
    function Model() {
        /**
         * @member {Group}
         * @readonly
         */
        this.root = null;

        /**
         * Base for event emitting
         * @member {jQuery}
         * @readonly
         * @private
         */
        this.$ = $(this);
    }

    $.extend(Model.prototype, /** @lends Model.prototype */ {
        /**
         * Triggers an event on the model
         * @param {string} type
         * @returns {$.Event}
         */
        trigger: function(type) {
            var event = new $.Event(type);
            this.$.triggerHandler(event, Array.prototype.slice.call(arguments, 1));
            return event;
        },

        /**
         * Attaches an event listener on the model
         * @param {string} type
         * @param {function} cb
         * @returns {Model}
         */
        on: function() {
            this.$.on.apply(this.$, Array.prototype.slice.call(arguments));
            return this;
        },

        /**
         * Removes an event listener from the model
         * @param {string} type
         * @param {function} [cb]
         * @returns {Model}
         */
        off: function() {
            this.$.off.apply(this.$, Array.prototype.slice.call(arguments));
            return this;
        },

        /**
         * Attaches an event listener called once on the model
         * @param {string} type
         * @param {function} cb
         * @returns {Model}
         */
        once: function() {
            this.$.one.apply(this.$, Array.prototype.slice.call(arguments));
            return this;
        }
    });


    /**
     * Root abstract object
     * @constructor
     * @param {Node} [parent]
     * @param {jQuery} $el
     */
    var Node = function(parent, $el) {
        if (!(this instanceof Node)) {
            return new Node(parent, $el);
        }

        Object.defineProperty(this, '__', { value: {} });

        $el.data('queryBuilderModel', this);

        /**
         * @name level
         * @member {int}
         * @memberof Node
         * @instance
         * @readonly
         */
        this.__.level = 1;

        /**
         * @name error
         * @member {string}
         * @memberof Node
         * @instance
         */
        this.__.error = null;

        /**
         * @name flags
         * @member {object}
         * @memberof Node
         * @instance
         * @readonly
         */
        this.__.flags = {};

        /**
         * @name data
         * @member {object}
         * @memberof Node
         * @instance
         */
        this.__.data = undefined;

        /**
         * @member {jQuery}
         * @readonly
         */
        this.$el = $el;

        /**
         * @member {string}
         * @readonly
         */
        this.id = $el[0].id;

        /**
         * @member {Model}
         * @readonly
         */
        this.model = null;

        /**
         * @member {Group}
         * @readonly
         */
        this.parent = parent;
    };

    Utils.defineModelProperties(Node, ['level', 'error', 'data', 'flags']);

    Object.defineProperty(Node.prototype, 'parent', {
        enumerable: true,
        get: function() {
            return this.__.parent;
        },
        set: function(value) {
            this.__.parent = value;
            this.level = value === null ? 1 : value.level + 1;
            this.model = value === null ? null : value.model;
        }
    });

    /**
     * Checks if this Node is the root
     * @returns {boolean}
     */
    Node.prototype.isRoot = function() {
        return (this.level === 1);
    };

    /**
     * Returns the node position inside its parent
     * @returns {int}
     */
    Node.prototype.getPos = function() {
        if (this.isRoot()) {
            return -1;
        }
        else {
            return this.parent.getNodePos(this);
        }
    };

    /**
     * Deletes self
     * @fires Model.model:drop
     */
    Node.prototype.drop = function() {
        var model = this.model;

        if (!!this.parent) {
            this.parent.removeNode(this);
        }

        this.$el.removeData('queryBuilderModel');

        if (model !== null) {
            /**
             * After a node of the model has been removed
             * @event model:drop
             * @memberof Model
             * @param {Node} node
             */
            model.trigger('drop', this);
        }
    };

    /**
     * Moves itself after another Node
     * @param {Node} target
     * @fires Model.model:move
     */
    Node.prototype.moveAfter = function(target) {
        if (!this.isRoot()) {
            this.move(target.parent, target.getPos() + 1);
        }
    };

    /**
     * Moves itself at the beginning of parent or another Group
     * @param {Group} [target]
     * @fires Model.model:move
     */
    Node.prototype.moveAtBegin = function(target) {
        if (!this.isRoot()) {
            if (target === undefined) {
                target = this.parent;
            }

            this.move(target, 0);
        }
    };

    /**
     * Moves itself at the end of parent or another Group
     * @param {Group} [target]
     * @fires Model.model:move
     */
    Node.prototype.moveAtEnd = function(target) {
        if (!this.isRoot()) {
            if (target === undefined) {
                target = this.parent;
            }

            this.move(target, target.length() === 0 ? 0 : target.length() - 1);
        }
    };

    /**
     * Moves itself at specific position of Group
     * @param {Group} target
     * @param {int} index
     * @fires Model.model:move
     */
    Node.prototype.move = function(target, index) {
        if (!this.isRoot()) {
            if (typeof target === 'number') {
                index = target;
                target = this.parent;
            }

            this.parent.removeNode(this);
            target.insertNode(this, index, false);

            if (this.model !== null) {
                /**
                 * After a node of the model has been moved
                 * @event model:move
                 * @memberof Model
                 * @param {Node} node
                 * @param {Node} target
                 * @param {int} index
                 */
                this.model.trigger('move', this, target, index);
            }
        }
    };


    /**
     * Group object
     * @constructor
     * @extends Node
     * @param {Group} [parent]
     * @param {jQuery} $el
     */
    var Group = function(parent, $el) {
        if (!(this instanceof Group)) {
            return new Group(parent, $el);
        }

        Node.call(this, parent, $el);

        /**
         * @member {object[]}
         * @readonly
         */
        this.rules = [];

        /**
         * @name condition
         * @member {string}
         * @memberof Group
         * @instance
         */
        this.__.condition = null;
    };

    Group.prototype = Object.create(Node.prototype);
    Group.prototype.constructor = Group;

    Utils.defineModelProperties(Group, ['condition']);

    /**
     * Removes group's content
     */
    Group.prototype.empty = function() {
        this.each('reverse', function(rule) {
            rule.drop();
        }, function(group) {
            group.drop();
        });
    };

    /**
     * Deletes self
     */
    Group.prototype.drop = function() {
        this.empty();
        Node.prototype.drop.call(this);
    };

    /**
     * Returns the number of children
     * @returns {int}
     */
    Group.prototype.length = function() {
        return this.rules.length;
    };

    /**
     * Adds a Node at specified index
     * @param {Node} node
     * @param {int} [index=end]
     * @param {boolean} [trigger=false] - fire 'add' event
     * @returns {Node} the inserted node
     * @fires Model.model:add
     */
    Group.prototype.insertNode = function(node, index, trigger) {
        if (index === undefined) {
            index = this.length();
        }

        this.rules.splice(index, 0, node);
        node.parent = this;

        if (trigger && this.model !== null) {
            /**
             * After a node of the model has been added
             * @event model:add
             * @memberof Model
             * @param {Node} parent
             * @param {Node} node
             * @param {int} index
             */
            this.model.trigger('add', this, node, index);
        }

        return node;
    };

    /**
     * Adds a new Group at specified index
     * @param {jQuery} $el
     * @param {int} [index=end]
     * @returns {Group}
     * @fires Model.model:add
     */
    Group.prototype.addGroup = function($el, index) {
        return this.insertNode(new Group(this, $el), index, true);
    };

    /**
     * Adds a new Rule at specified index
     * @param {jQuery} $el
     * @param {int} [index=end]
     * @returns {Rule}
     * @fires Model.model:add
     */
    Group.prototype.addRule = function($el, index) {
        return this.insertNode(new Rule(this, $el), index, true);
    };

    /**
     * Deletes a specific Node
     * @param {Node} node
     */
    Group.prototype.removeNode = function(node) {
        var index = this.getNodePos(node);
        if (index !== -1) {
            node.parent = null;
            this.rules.splice(index, 1);
        }
    };

    /**
     * Returns the position of a child Node
     * @param {Node} node
     * @returns {int}
     */
    Group.prototype.getNodePos = function(node) {
        return this.rules.indexOf(node);
    };

    /**
     * @callback Model#GroupIteratee
     * @param {Node} node
     * @returns {boolean} stop the iteration
     */

    /**
     * Iterate over all Nodes
     * @param {boolean} [reverse=false] - iterate in reverse order, required if you delete nodes
     * @param {Model#GroupIteratee} cbRule - callback for Rules (can be `null` but not omitted)
     * @param {Model#GroupIteratee} [cbGroup] - callback for Groups
     * @param {object} [context] - context for callbacks
     * @returns {boolean} if the iteration has been stopped by a callback
     */
    Group.prototype.each = function(reverse, cbRule, cbGroup, context) {
        if (typeof reverse !== 'boolean' && typeof reverse !== 'string') {
            context = cbGroup;
            cbGroup = cbRule;
            cbRule = reverse;
            reverse = false;
        }
        context = context === undefined ? null : context;

        var i = reverse ? this.rules.length - 1 : 0;
        var l = reverse ? 0 : this.rules.length - 1;
        var c = reverse ? -1 : 1;
        var next = function() {
            return reverse ? i >= l : i <= l;
        };
        var stop = false;

        for (; next(); i += c) {
            if (this.rules[i] instanceof Group) {
                if (!!cbGroup) {
                    stop = cbGroup.call(context, this.rules[i]) === false;
                }
            }
            else if (!!cbRule) {
                stop = cbRule.call(context, this.rules[i]) === false;
            }

            if (stop) {
                break;
            }
        }

        return !stop;
    };

    /**
     * Checks if the group contains a particular Node
     * @param {Node} node
     * @param {boolean} [recursive=false]
     * @returns {boolean}
     */
    Group.prototype.contains = function(node, recursive) {
        if (this.getNodePos(node) !== -1) {
            return true;
        }
        else if (!recursive) {
            return false;
        }
        else {
            // the loop will return with false as soon as the Node is found
            return !this.each(function() {
                return true;
            }, function(group) {
                return !group.contains(node, true);
            });
        }
    };


    /**
     * Rule object
     * @constructor
     * @extends Node
     * @param {Group} parent
     * @param {jQuery} $el
     */
    var Rule = function(parent, $el) {
        if (!(this instanceof Rule)) {
            return new Rule(parent, $el);
        }

        Node.call(this, parent, $el);

        this._updating_value = false;
        this._updating_input = false;

        /**
         * @name filter
         * @member {QueryBuilder.Filter}
         * @memberof Rule
         * @instance
         */
        this.__.filter = null;

        /**
         * @name operator
         * @member {QueryBuilder.Operator}
         * @memberof Rule
         * @instance
         */
        this.__.operator = null;

        /**
         * @name value
         * @member {*}
         * @memberof Rule
         * @instance
         */
        this.__.value = undefined;
    };

    Rule.prototype = Object.create(Node.prototype);
    Rule.prototype.constructor = Rule;

    Utils.defineModelProperties(Rule, ['filter', 'operator', 'value']);

    /**
     * Checks if this Node is the root
     * @returns {boolean} always false
     */
    Rule.prototype.isRoot = function() {
        return false;
    };


    /**
     * @member {function}
     * @memberof QueryBuilder
     * @see Group
     */
    QueryBuilder.Group = Group;

    /**
     * @member {function}
     * @memberof QueryBuilder
     * @see Rule
     */
    QueryBuilder.Rule = Rule;


    /**
     * The {@link http://learn.jquery.com/plugins/|jQuery Plugins} namespace
     * @external "jQuery.fn"
     */

    /**
     * Instanciates or accesses the {@link QueryBuilder} on an element
     * @function
     * @memberof external:"jQuery.fn"
     * @param {*} option - initial configuration or method name
     * @param {...*} args - method arguments
     *
     * @example
     * $('#builder').queryBuilder({ /** configuration object *\/ });
     * @example
     * $('#builder').queryBuilder('methodName', methodParam1, methodParam2);
     */
    $.fn.queryBuilder = function(option) {
        if (this.length === 0) {
            Utils.error('Config', 'No target defined');
        }
        if (this.length > 1) {
            Utils.error('Config', 'Unable to initialize on multiple target');
        }

        var data = this.data('queryBuilder');
        var options = (typeof option == 'object' && option) || {};

        if (!data && option == 'destroy') {
            return this;
        }
        if (!data) {
            var builder = new QueryBuilder(this, options);
            this.data('queryBuilder', builder);
            builder.init(options.rules);
        }
        if (typeof option == 'string') {
            return data[option].apply(data, Array.prototype.slice.call(arguments, 1));
        }

        return this;
    };

    /**
     * @function
     * @memberof external:"jQuery.fn"
     * @see QueryBuilder
     */
    $.fn.queryBuilder.constructor = QueryBuilder;

    /**
     * @function
     * @memberof external:"jQuery.fn"
     * @see QueryBuilder.defaults
     */
    $.fn.queryBuilder.defaults = QueryBuilder.defaults;

    /**
     * @function
     * @memberof external:"jQuery.fn"
     * @see QueryBuilder.defaults
     */
    $.fn.queryBuilder.extend = QueryBuilder.extend;

    /**
     * @function
     * @memberof external:"jQuery.fn"
     * @see QueryBuilder.define
     */
    $.fn.queryBuilder.define = QueryBuilder.define;

    /**
     * @function
     * @memberof external:"jQuery.fn"
     * @see QueryBuilder.regional
     */
    $.fn.queryBuilder.regional = QueryBuilder.regional;


    /**
     * @class BtCheckbox
     * @memberof module:plugins
     * @description Applies Awesome Bootstrap Checkbox for checkbox and radio inputs.
     * @param {object} [options]
     * @param {string} [options.font='glyphicons']
     * @param {string} [options.color='default']
     */
    QueryBuilder.define('bt-checkbox', function(options) {
        if (options.font == 'glyphicons') {
            this.$el.addClass('bt-checkbox-glyphicons');
        }

        this.on('getRuleInput.filter', function(h, rule, name) {
            var filter = rule.filter;

            if ((filter.input === 'radio' || filter.input === 'checkbox') && !filter.plugin) {
                h.value = '';

                if (!filter.colors) {
                    filter.colors = {};
                }
                if (filter.color) {
                    filter.colors._def_ = filter.color;
                }

                var style = filter.vertical ? ' style="display:block"' : '';
                var i = 0;

                Utils.iterateOptions(filter.values, function(key, val) {
                    var color = filter.colors[key] || filter.colors._def_ || options.color;
                    var id = name + '_' + (i++);

                    h.value+= '\
<div' + style + ' class="' + filter.input + ' ' + filter.input + '-' + color + '"> \
  <input type="' + filter.input + '" name="' + name + '" id="' + id + '" value="' + key + '"> \
  <label for="' + id + '">' + val + '</label> \
</div>';
                });
            }
        });
    }, {
        font: 'glyphicons',
        color: 'default'
    });


    /**
     * @class BtSelectpicker
     * @memberof module:plugins
     * @descriptioon Applies Bootstrap Select on filters and operators combo-boxes.
     * @param {object} [options]
     * @param {string} [options.container='body']
     * @param {string} [options.style='btn-inverse btn-xs']
     * @param {int|string} [options.width='auto']
     * @param {boolean} [options.showIcon=false]
     * @throws MissingLibraryError
     */
    QueryBuilder.define('bt-selectpicker', function(options) {
        if (!$.fn.selectpicker || !$.fn.selectpicker.Constructor) {
            Utils.error('MissingLibrary', 'Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select');
        }

        var Selectors = QueryBuilder.selectors;

        // init selectpicker
        this.on('afterCreateRuleFilters', function(e, rule) {
            rule.$el.find(Selectors.rule_filter).removeClass('form-control').selectpicker(options);
        });

        this.on('afterCreateRuleOperators', function(e, rule) {
            rule.$el.find(Selectors.rule_operator).removeClass('form-control').selectpicker(options);
        });

        // update selectpicker on change
        this.on('afterUpdateRuleFilter', function(e, rule) {
            rule.$el.find(Selectors.rule_filter).selectpicker('render');
        });

        this.on('afterUpdateRuleOperator', function(e, rule) {
            rule.$el.find(Selectors.rule_operator).selectpicker('render');
        });

        this.on('beforeDeleteRule', function(e, rule) {
            rule.$el.find(Selectors.rule_filter).selectpicker('destroy');
            rule.$el.find(Selectors.rule_operator).selectpicker('destroy');
        });
    }, {
        container: 'body',
        style: 'btn-inverse btn-xs',
        width: 'auto',
        showIcon: false
    });


    /**
     * @class BtTooltipErrors
     * @memberof module:plugins
     * @description Applies Bootstrap Tooltips on validation error messages.
     * @param {object} [options]
     * @param {string} [options.placement='right']
     * @throws MissingLibraryError
     */
    QueryBuilder.define('bt-tooltip-errors', function(options) {
        if (!$.fn.tooltip || !$.fn.tooltip.Constructor || !$.fn.tooltip.Constructor.prototype.fixTitle) {
            Utils.error('MissingLibrary', 'Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com');
        }

        var self = this;

        // add BT Tooltip data
        this.on('getRuleTemplate.filter getGroupTemplate.filter', function(h) {
            var $h = $(h.value);
            $h.find(QueryBuilder.selectors.error_container).attr('data-toggle', 'tooltip');
            h.value = $h.prop('outerHTML');
        });

        // init/refresh tooltip when title changes
        this.model.on('update', function(e, node, field) {
            if (field == 'error' && self.settings.display_errors) {
                node.$el.find(QueryBuilder.selectors.error_container).eq(0)
                    .tooltip(options)
                    .tooltip('hide')
                    .tooltip('fixTitle');
            }
        });
    }, {
        placement: 'right'
    });


    /**
     * @class ChangeFilters
     * @memberof module:plugins
     * @description Allows to change available filters after plugin initialization.
     */

    QueryBuilder.extend(/** @lends module:plugins.ChangeFilters.prototype */ {
        /**
         * Change the filters of the builder
         * @param {boolean} [deleteOrphans=false] - delete rules using old filters
         * @param {QueryBuilder[]} filters
         * @fires module:plugins.ChangeFilters.changer:setFilters
         * @fires module:plugins.ChangeFilters.afterSetFilters
         * @throws ChangeFilterError
         */
        setFilters: function(deleteOrphans, filters) {
            var self = this;

            if (filters === undefined) {
                filters = deleteOrphans;
                deleteOrphans = false;
            }

            filters = this.checkFilters(filters);

            /**
             * Modifies the filters before {@link module:plugins.ChangeFilters.setFilters} method
             * @event changer:setFilters
             * @memberof module:plugins.ChangeFilters
             * @param {QueryBuilder.Filter[]} filters
             * @returns {QueryBuilder.Filter[]}
             */
            filters = this.change('setFilters', filters);

            var filtersIds = filters.map(function(filter) {
                return filter.id;
            });

            // check for orphans
            if (!deleteOrphans) {
                (function checkOrphans(node) {
                    node.each(
                        function(rule) {
                            if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
                                Utils.error('ChangeFilter', 'A rule is using filter "{0}"', rule.filter.id);
                            }
                        },
                        checkOrphans
                    );
                }(this.model.root));
            }

            // replace filters
            this.filters = filters;

            // apply on existing DOM
            (function updateBuilder(node) {
                node.each(true,
                    function(rule) {
                        if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
                            rule.drop();

                            self.trigger('rulesChanged');
                        }
                        else {
                            self.createRuleFilters(rule);

                            rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
                            self.trigger('afterUpdateRuleFilter', rule);
                        }
                    },
                    updateBuilder
                );
            }(this.model.root));

            // update plugins
            if (this.settings.plugins) {
                if (this.settings.plugins['unique-filter']) {
                    this.updateDisabledFilters();
                }
                if (this.settings.plugins['bt-selectpicker']) {
                    this.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
                }
            }

            // reset the default_filter if does not exist anymore
            if (this.settings.default_filter) {
                try {
                    this.getFilterById(this.settings.default_filter);
                }
                catch (e) {
                    this.settings.default_filter = null;
                }
            }

            /**
             * After {@link module:plugins.ChangeFilters.setFilters} method
             * @event afterSetFilters
             * @memberof module:plugins.ChangeFilters
             * @param {QueryBuilder.Filter[]} filters
             */
            this.trigger('afterSetFilters', filters);
        },

        /**
         * Adds a new filter to the builder
         * @param {QueryBuilder.Filter|Filter[]} newFilters
         * @param {int|string} [position=#end] - index or '#start' or '#end'
         * @fires module:plugins.ChangeFilters.changer:setFilters
         * @fires module:plugins.ChangeFilters.afterSetFilters
         * @throws ChangeFilterError
         */
        addFilter: function(newFilters, position) {
            if (position === undefined || position == '#end') {
                position = this.filters.length;
            }
            else if (position == '#start') {
                position = 0;
            }

            if (!$.isArray(newFilters)) {
                newFilters = [newFilters];
            }

            var filters = $.extend(true, [], this.filters);

            // numeric position
            if (parseInt(position) == position) {
                Array.prototype.splice.apply(filters, [position, 0].concat(newFilters));
            }
            else {
                // after filter by its id
                if (this.filters.some(function(filter, index) {
                    if (filter.id == position) {
                        position = index + 1;
                        return true;
                    }
                })
                ) {
                    Array.prototype.splice.apply(filters, [position, 0].concat(newFilters));
                }
                // defaults to end of list
                else {
                    Array.prototype.push.apply(filters, newFilters);
                }
            }

            this.setFilters(filters);
        },

        /**
         * Removes a filter from the builder
         * @param {string|string[]} filterIds
         * @param {boolean} [deleteOrphans=false] delete rules using old filters
         * @fires module:plugins.ChangeFilters.changer:setFilters
         * @fires module:plugins.ChangeFilters.afterSetFilters
         * @throws ChangeFilterError
         */
        removeFilter: function(filterIds, deleteOrphans) {
            var filters = $.extend(true, [], this.filters);
            if (typeof filterIds === 'string') {
                filterIds = [filterIds];
            }

            filters = filters.filter(function(filter) {
                return filterIds.indexOf(filter.id) === -1;
            });

            this.setFilters(deleteOrphans, filters);
        }
    });


    /**
     * @class ChosenSelectpicker
     * @memberof module:plugins
     * @descriptioon Applies chosen-js Select on filters and operators combo-boxes.
     * @param {object} [options] Supports all the options for chosen
     * @throws MissingLibraryError
     */
    QueryBuilder.define('chosen-selectpicker', function(options) {

        if (!$.fn.chosen) {
            Utils.error('MissingLibrary', 'chosen is required to use "chosen-selectpicker" plugin. Get it here: https://github.com/harvesthq/chosen');
        }

        if (this.settings.plugins['bt-selectpicker']) {
            Utils.error('Conflict', 'bt-selectpicker is already selected as the dropdown plugin. Please remove chosen-selectpicker from the plugin list');
        }

        var Selectors = QueryBuilder.selectors;

        // init selectpicker
        this.on('afterCreateRuleFilters', function(e, rule) {
            rule.$el.find(Selectors.rule_filter).removeClass('form-control').chosen(options);
        });

        this.on('afterCreateRuleOperators', function(e, rule) {
            rule.$el.find(Selectors.rule_operator).removeClass('form-control').chosen(options);
        });

        // update selectpicker on change
        this.on('afterUpdateRuleFilter', function(e, rule) {
            rule.$el.find(Selectors.rule_filter).trigger('chosen:updated');
        });

        this.on('afterUpdateRuleOperator', function(e, rule) {
            rule.$el.find(Selectors.rule_operator).trigger('chosen:updated');
        });

        this.on('beforeDeleteRule', function(e, rule) {
            rule.$el.find(Selectors.rule_filter).chosen('destroy');
            rule.$el.find(Selectors.rule_operator).chosen('destroy');
        });
    });


    /**
     * @class FilterDescription
     * @memberof module:plugins
     * @description Provides three ways to display a description about a filter: inline, Bootsrap Popover or Bootbox.
     * @param {object} [options]
     * @param {string} [options.icon='glyphicon glyphicon-info-sign']
     * @param {string} [options.mode='popover'] - inline, popover or bootbox
     * @throws ConfigError
     */
    QueryBuilder.define('filter-description', function(options) {
        // INLINE
        if (options.mode === 'inline') {
            this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
                var $p = rule.$el.find('p.filter-description');
                var description = e.builder.getFilterDescription(rule.filter, rule);

                if (!description) {
                    $p.hide();
                }
                else {
                    if ($p.length === 0) {
                        $p = $('<p class="filter-description"></p>');
                        $p.appendTo(rule.$el);
                    }
                    else {
                        $p.css('display', '');
                    }

                    $p.html('<i class="' + options.icon + '"></i> ' + description);
                }
            });
        }
        // POPOVER
        else if (options.mode === 'popover') {
            if (!$.fn.popover || !$.fn.popover.Constructor || !$.fn.popover.Constructor.prototype.fixTitle) {
                Utils.error('MissingLibrary', 'Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com');
            }

            this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
                var $b = rule.$el.find('button.filter-description');
                var description = e.builder.getFilterDescription(rule.filter, rule);

                if (!description) {
                    $b.hide();

                    if ($b.data('bs.popover')) {
                        $b.popover('hide');
                    }
                }
                else {
                    if ($b.length === 0) {
                        $b = $('<button type="button" class="btn btn-xs btn-info filter-description" data-toggle="popover"><i class="' + options.icon + '"></i></button>');
                        $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions));

                        $b.popover({
                            placement: 'left',
                            container: 'body',
                            html: true
                        });

                        $b.on('mouseout', function() {
                            $b.popover('hide');
                        });
                    }
                    else {
                        $b.css('display', '');
                    }

                    $b.data('bs.popover').options.content = description;

                    if ($b.attr('aria-describedby')) {
                        $b.popover('show');
                    }
                }
            });
        }
        // BOOTBOX
        else if (options.mode === 'bootbox') {
            if (!('bootbox' in window)) {
                Utils.error('MissingLibrary', 'Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com');
            }

            this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
                var $b = rule.$el.find('button.filter-description');
                var description = e.builder.getFilterDescription(rule.filter, rule);

                if (!description) {
                    $b.hide();
                }
                else {
                    if ($b.length === 0) {
                        $b = $('<button type="button" class="btn btn-xs btn-info filter-description" data-toggle="bootbox"><i class="' + options.icon + '"></i></button>');
                        $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions));

                        $b.on('click', function() {
                            bootbox.alert($b.data('description'));
                        });
                    }
                    else {
                        $b.css('display', '');
                    }

                    $b.data('description', description);
                }
            });
        }
    }, {
        icon: 'glyphicon glyphicon-info-sign',
        mode: 'popover'
    });

    QueryBuilder.extend(/** @lends module:plugins.FilterDescription.prototype */ {
        /**
         * Returns the description of a filter for a particular rule (if present)
         * @param {object} filter
         * @param {Rule} [rule]
         * @returns {string}
         * @private
         */
        getFilterDescription: function(filter, rule) {
            if (!filter) {
                return undefined;
            }
            else if (typeof filter.description == 'function') {
                return filter.description.call(this, rule);
            }
            else {
                return filter.description;
            }
        }
    });


    /**
     * @class Invert
     * @memberof module:plugins
     * @description Allows to invert a rule operator, a group condition or the entire builder.
     * @param {object} [options]
     * @param {string} [options.icon='glyphicon glyphicon-random']
     * @param {boolean} [options.recursive=true]
     * @param {boolean} [options.invert_rules=true]
     * @param {boolean} [options.display_rules_button=false]
     * @param {boolean} [options.silent_fail=false]
     */
    QueryBuilder.define('invert', function(options) {
        var self = this;
        var Selectors = QueryBuilder.selectors;

        // Bind events
        this.on('afterInit', function() {
            self.$el.on('click.queryBuilder', '[data-invert=group]', function() {
                var $group = $(this).closest(Selectors.group_container);
                self.invert(self.getModel($group), options);
            });

            if (options.display_rules_button && options.invert_rules) {
                self.$el.on('click.queryBuilder', '[data-invert=rule]', function() {
                    var $rule = $(this).closest(Selectors.rule_container);
                    self.invert(self.getModel($rule), options);
                });
            }
        });

        // Modify templates
        if (!options.disable_template) {
            this.on('getGroupTemplate.filter', function(h) {
                var $h = $(h.value);
                $h.find(Selectors.condition_container).after(
                    '<button type="button" class="btn btn-xs btn-default" data-invert="group">' +
                    '<i class="' + options.icon + '"></i> ' + self.translate('invert') +
                    '</button>'
                );
                h.value = $h.prop('outerHTML');
            });

            if (options.display_rules_button && options.invert_rules) {
                this.on('getRuleTemplate.filter', function(h) {
                    var $h = $(h.value);
                    $h.find(Selectors.rule_actions).prepend(
                        '<button type="button" class="btn btn-xs btn-default" data-invert="rule">' +
                        '<i class="' + options.icon + '"></i> ' + self.translate('invert') +
                        '</button>'
                    );
                    h.value = $h.prop('outerHTML');
                });
            }
        }
    }, {
        icon: 'glyphicon glyphicon-random',
        recursive: true,
        invert_rules: true,
        display_rules_button: false,
        silent_fail: false,
        disable_template: false
    });

    QueryBuilder.defaults({
        operatorOpposites: {
            'equal':            'not_equal',
            'not_equal':        'equal',
            'in':               'not_in',
            'not_in':           'in',
            'less':             'greater_or_equal',
            'less_or_equal':    'greater',
            'greater':          'less_or_equal',
            'greater_or_equal': 'less',
            'between':          'not_between',
            'not_between':      'between',
            'begins_with':      'not_begins_with',
            'not_begins_with':  'begins_with',
            'contains':         'not_contains',
            'not_contains':     'contains',
            'ends_with':        'not_ends_with',
            'not_ends_with':    'ends_with',
            'is_empty':         'is_not_empty',
            'is_not_empty':     'is_empty',
            'is_null':          'is_not_null',
            'is_not_null':      'is_null'
        },

        conditionOpposites: {
            'AND': 'OR',
            'OR': 'AND'
        }
    });

    QueryBuilder.extend(/** @lends module:plugins.Invert.prototype */ {
        /**
         * Invert a Group, a Rule or the whole builder
         * @param {Node} [node]
         * @param {object} [options] {@link module:plugins.Invert}
         * @fires module:plugins.Invert.afterInvert
         * @throws InvertConditionError, InvertOperatorError
         */
        invert: function(node, options) {
            if (!(node instanceof Node)) {
                if (!this.model.root) return;
                options = node;
                node = this.model.root;
            }

            if (typeof options != 'object') options = {};
            if (options.recursive === undefined) options.recursive = true;
            if (options.invert_rules === undefined) options.invert_rules = true;
            if (options.silent_fail === undefined) options.silent_fail = false;
            if (options.trigger === undefined) options.trigger = true;

            if (node instanceof Group) {
                // invert group condition
                if (this.settings.conditionOpposites[node.condition]) {
                    node.condition = this.settings.conditionOpposites[node.condition];
                }
                else if (!options.silent_fail) {
                    Utils.error('InvertCondition', 'Unknown inverse of condition "{0}"', node.condition);
                }

                // recursive call
                if (options.recursive) {
                    var tempOpts = $.extend({}, options, { trigger: false });
                    node.each(function(rule) {
                        if (options.invert_rules) {
                            this.invert(rule, tempOpts);
                        }
                    }, function(group) {
                        this.invert(group, tempOpts);
                    }, this);
                }
            }
            else if (node instanceof Rule) {
                if (node.operator && !node.filter.no_invert) {
                    // invert rule operator
                    if (this.settings.operatorOpposites[node.operator.type]) {
                        var invert = this.settings.operatorOpposites[node.operator.type];
                        // check if the invert is "authorized"
                        if (!node.filter.operators || node.filter.operators.indexOf(invert) != -1) {
                            node.operator = this.getOperatorByType(invert);
                        }
                    }
                    else if (!options.silent_fail) {
                        Utils.error('InvertOperator', 'Unknown inverse of operator "{0}"', node.operator.type);
                    }
                }
            }

            if (options.trigger) {
                /**
                 * After {@link module:plugins.Invert.invert} method
                 * @event afterInvert
                 * @memberof module:plugins.Invert
                 * @param {Node} node - the main group or rule that has been modified
                 * @param {object} options
                 */
                this.trigger('afterInvert', node, options);

                this.trigger('rulesChanged');
            }
        }
    });


    /**
     * @class MongoDbSupport
     * @memberof module:plugins
     * @description Allows to export rules as a MongoDB find object as well as populating the builder from a MongoDB object.
     */

    QueryBuilder.defaults({
        mongoOperators: {
            // @formatter:off
            equal:            function(v) { return v[0]; },
            not_equal:        function(v) { return { '$ne': v[0] }; },
            in:               function(v) { return { '$in': v }; },
            not_in:           function(v) { return { '$nin': v }; },
            less:             function(v) { return { '$lt': v[0] }; },
            less_or_equal:    function(v) { return { '$lte': v[0] }; },
            greater:          function(v) { return { '$gt': v[0] }; },
            greater_or_equal: function(v) { return { '$gte': v[0] }; },
            between:          function(v) { return { '$gte': v[0], '$lte': v[1] }; },
            not_between:      function(v) { return { '$lt': v[0], '$gt': v[1] }; },
            begins_with:      function(v) { return { '$regex': '^' + Utils.escapeRegExp(v[0]) }; },
            not_begins_with:  function(v) { return { '$regex': '^(?!' + Utils.escapeRegExp(v[0]) + ')' }; },
            contains:         function(v) { return { '$regex': Utils.escapeRegExp(v[0]) }; },
            not_contains:     function(v) { return { '$regex': '^((?!' + Utils.escapeRegExp(v[0]) + ').)*$', '$options': 's' }; },
            ends_with:        function(v) { return { '$regex': Utils.escapeRegExp(v[0]) + '$' }; },
            not_ends_with:    function(v) { return { '$regex': '(?<!' + Utils.escapeRegExp(v[0]) + ')$' }; },
            is_empty:         function(v) { return ''; },
            is_not_empty:     function(v) { return { '$ne': '' }; },
            is_null:          function(v) { return null; },
            is_not_null:      function(v) { return { '$ne': null }; }
            // @formatter:on
        },

        mongoRuleOperators: {
            $eq: function(v) {
                return {
                    'val': v,
                    'op': v === null ? 'is_null' : (v === '' ? 'is_empty' : 'equal')
                };
            },
            $ne: function(v) {
                v = v.$ne;
                return {
                    'val': v,
                    'op': v === null ? 'is_not_null' : (v === '' ? 'is_not_empty' : 'not_equal')
                };
            },
            $regex: function(v) {
                v = v.$regex;
                if (v.slice(0, 4) == '^(?!' && v.slice(-1) == ')') {
                    return { 'val': v.slice(4, -1), 'op': 'not_begins_with' };
                }
                else if (v.slice(0, 5) == '^((?!' && v.slice(-5) == ').)*$') {
                    return { 'val': v.slice(5, -5), 'op': 'not_contains' };
                }
                else if (v.slice(0, 4) == '(?<!' && v.slice(-2) == ')$') {
                    return { 'val': v.slice(4, -2), 'op': 'not_ends_with' };
                }
                else if (v.slice(-1) == '$') {
                    return { 'val': v.slice(0, -1), 'op': 'ends_with' };
                }
                else if (v.slice(0, 1) == '^') {
                    return { 'val': v.slice(1), 'op': 'begins_with' };
                }
                else {
                    return { 'val': v, 'op': 'contains' };
                }
            },
            between: function(v) {
                return { 'val': [v.$gte, v.$lte], 'op': 'between' };
            },
            not_between: function(v) {
                return { 'val': [v.$lt, v.$gt], 'op': 'not_between' };
            },
            $in: function(v) {
                return { 'val': v.$in, 'op': 'in' };
            },
            $nin: function(v) {
                return { 'val': v.$nin, 'op': 'not_in' };
            },
            $lt: function(v) {
                return { 'val': v.$lt, 'op': 'less' };
            },
            $lte: function(v) {
                return { 'val': v.$lte, 'op': 'less_or_equal' };
            },
            $gt: function(v) {
                return { 'val': v.$gt, 'op': 'greater' };
            },
            $gte: function(v) {
                return { 'val': v.$gte, 'op': 'greater_or_equal' };
            }
        }
    });

    QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ {
        /**
         * Returns rules as a MongoDB query
         * @param {object} [data] - current rules by default
         * @returns {object}
         * @fires module:plugins.MongoDbSupport.changer:getMongoDBField
         * @fires module:plugins.MongoDbSupport.changer:ruleToMongo
         * @fires module:plugins.MongoDbSupport.changer:groupToMongo
         * @throws UndefinedMongoConditionError, UndefinedMongoOperatorError
         */
        getMongo: function(data) {
            data = (data === undefined) ? this.getRules() : data;

            if (!data) {
                return null;
            }

            var self = this;

            return (function parse(group) {
                if (!group.condition) {
                    group.condition = self.settings.default_condition;
                }
                if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
                    Utils.error('UndefinedMongoCondition', 'Unable to build MongoDB query with condition "{0}"', group.condition);
                }

                if (!group.rules) {
                    return {};
                }

                var parts = [];

                group.rules.forEach(function(rule) {
                    if (rule.rules && rule.rules.length > 0) {
                        parts.push(parse(rule));
                    }
                    else {
                        var mdb = self.settings.mongoOperators[rule.operator];
                        var ope = self.getOperatorByType(rule.operator);

                        if (mdb === undefined) {
                            Utils.error('UndefinedMongoOperator', 'Unknown MongoDB operation for operator "{0}"', rule.operator);
                        }

                        if (ope.nb_inputs !== 0) {
                            if (!(rule.value instanceof Array)) {
                                rule.value = [rule.value];
                            }
                        }

                        /**
                         * Modifies the MongoDB field used by a rule
                         * @event changer:getMongoDBField
                         * @memberof module:plugins.MongoDbSupport
                         * @param {string} field
                         * @param {Rule} rule
                         * @returns {string}
                         */
                        var field = self.change('getMongoDBField', rule.field, rule);

                        var ruleExpression = {};
                        ruleExpression[field] = mdb.call(self, rule.value);

                        /**
                         * Modifies the MongoDB expression generated for a rul
                         * @event changer:ruleToMongo
                         * @memberof module:plugins.MongoDbSupport
                         * @param {object} expression
                         * @param {Rule} rule
                         * @param {*} value
                         * @param {function} valueWrapper - function that takes the value and adds the operator
                         * @returns {object}
                         */
                        parts.push(self.change('ruleToMongo', ruleExpression, rule, rule.value, mdb));
                    }
                });

                var groupExpression = {};
                groupExpression['$' + group.condition.toLowerCase()] = parts;

                /**
                 * Modifies the MongoDB expression generated for a group
                 * @event changer:groupToMongo
                 * @memberof module:plugins.MongoDbSupport
                 * @param {object} expression
                 * @param {Group} group
                 * @returns {object}
                 */
                return self.change('groupToMongo', groupExpression, group);
            }(data));
        },

        /**
         * Converts a MongoDB query to rules
         * @param {object} query
         * @returns {object}
         * @fires module:plugins.MongoDbSupport.changer:parseMongoNode
         * @fires module:plugins.MongoDbSupport.changer:getMongoDBFieldID
         * @fires module:plugins.MongoDbSupport.changer:mongoToRule
         * @fires module:plugins.MongoDbSupport.changer:mongoToGroup
         * @throws MongoParseError, UndefinedMongoConditionError, UndefinedMongoOperatorError
         */
        getRulesFromMongo: function(query) {
            if (query === undefined || query === null) {
                return null;
            }

            var self = this;

            /**
             * Custom parsing of a MongoDB expression, you can return a sub-part of the expression, or a well formed group or rule JSON
             * @event changer:parseMongoNode
             * @memberof module:plugins.MongoDbSupport
             * @param {object} expression
             * @returns {object} expression, rule or group
             */
            query = self.change('parseMongoNode', query);

            // a plugin returned a group
            if ('rules' in query && 'condition' in query) {
                return query;
            }

            // a plugin returned a rule
            if ('id' in query && 'operator' in query && 'value' in query) {
                return {
                    condition: this.settings.default_condition,
                    rules: [query]
                };
            }

            var key = self.getMongoCondition(query);
            if (!key) {
                Utils.error('MongoParse', 'Invalid MongoDB query format');
            }

            return (function parse(data, topKey) {
                var rules = data[topKey];
                var parts = [];

                rules.forEach(function(data) {
                    // allow plugins to manually parse or handle special cases
                    data = self.change('parseMongoNode', data);

                    // a plugin returned a group
                    if ('rules' in data && 'condition' in data) {
                        parts.push(data);
                        return;
                    }

                    // a plugin returned a rule
                    if ('id' in data && 'operator' in data && 'value' in data) {
                        parts.push(data);
                        return;
                    }

                    var key = self.getMongoCondition(data);
                    if (key) {
                        parts.push(parse(data, key));
                    }
                    else {
                        var field = Object.keys(data)[0];
                        var value = data[field];

                        var operator = self.getMongoOperator(value);
                        if (operator === undefined) {
                            Utils.error('MongoParse', 'Invalid MongoDB query format');
                        }

                        var mdbrl = self.settings.mongoRuleOperators[operator];
                        if (mdbrl === undefined) {
                            Utils.error('UndefinedMongoOperator', 'JSON Rule operation unknown for operator "{0}"', operator);
                        }

                        var opVal = mdbrl.call(self, value);

                        var id = self.getMongoDBFieldID(field, value);

                        /**
                         * Modifies the rule generated from the MongoDB expression
                         * @event changer:mongoToRule
                         * @memberof module:plugins.MongoDbSupport
                         * @param {object} rule
                         * @param {object} expression
                         * @returns {object}
                         */
                        var rule = self.change('mongoToRule', {
                            id: id,
                            field: field,
                            operator: opVal.op,
                            value: opVal.val
                        }, data);

                        parts.push(rule);
                    }
                });

                /**
                 * Modifies the group generated from the MongoDB expression
                 * @event changer:mongoToGroup
                 * @memberof module:plugins.MongoDbSupport
                 * @param {object} group
                 * @param {object} expression
                 * @returns {object}
                 */
                return self.change('mongoToGroup', {
                    condition: topKey.replace('$', '').toUpperCase(),
                    rules: parts
                }, data);
            }(query, key));
        },

        /**
         * Sets rules a from MongoDB query
         * @see module:plugins.MongoDbSupport.getRulesFromMongo
         */
        setRulesFromMongo: function(query) {
            this.setRules(this.getRulesFromMongo(query));
        },

        /**
         * Returns a filter identifier from the MongoDB field.
         * Automatically use the only one filter with a matching field, fires a changer otherwise.
         * @param {string} field
         * @param {*} value
         * @fires module:plugins.MongoDbSupport:changer:getMongoDBFieldID
         * @returns {string}
         * @private
         */
        getMongoDBFieldID: function(field, value) {
            var matchingFilters = this.filters.filter(function(filter) {
                return filter.field === field;
            });

            var id;
            if (matchingFilters.length === 1) {
                id = matchingFilters[0].id;
            }
            else {
                /**
                 * Returns a filter identifier from the MongoDB field
                 * @event changer:getMongoDBFieldID
                 * @memberof module:plugins.MongoDbSupport
                 * @param {string} field
                 * @param {*} value
                 * @returns {string}
                 */
                id = this.change('getMongoDBFieldID', field, value);
            }

            return id;
        },

        /**
         * Finds which operator is used in a MongoDB sub-object
         * @param {*} data
         * @returns {string|undefined}
         * @private
         */
        getMongoOperator: function(data) {
            if (data !== null && typeof data === 'object') {
                if (data.$gte !== undefined && data.$lte !== undefined) {
                    return 'between';
                }
                if (data.$lt !== undefined && data.$gt !== undefined) {
                    return 'not_between';
                }

                var knownKeys = Object.keys(data).filter(function(key) {
                    return !!this.settings.mongoRuleOperators[key];
                }.bind(this));

                if (knownKeys.length === 1) {
                    return knownKeys[0];
                }
            }
            else {
                return '$eq';
            }
        },


        /**
         * Returns the key corresponding to "$or" or "$and"
         * @param {object} data
         * @returns {string|undefined}
         * @private
         */
        getMongoCondition: function(data) {
            var keys = Object.keys(data);

            for (var i = 0, l = keys.length; i < l; i++) {
                if (keys[i].toLowerCase() === '$or' || keys[i].toLowerCase() === '$and') {
                    return keys[i];
                }
            }
        }
    });


    /**
     * @class NotGroup
     * @memberof module:plugins
     * @description Adds a "Not" checkbox in front of group conditions.
     * @param {object} [options]
     * @param {string} [options.icon_checked='glyphicon glyphicon-checked']
     * @param {string} [options.icon_unchecked='glyphicon glyphicon-unchecked']
     */
    QueryBuilder.define('not-group', function(options) {
        var self = this;

        // Bind events
        this.on('afterInit', function() {
            self.$el.on('click.queryBuilder', '[data-not=group]', function() {
                var $group = $(this).closest(QueryBuilder.selectors.group_container);
                var group = self.getModel($group);
                group.not = !group.not;
            });

            self.model.on('update', function(e, node, field) {
                if (node instanceof Group && field === 'not') {
                    self.updateGroupNot(node);
                }
            });
        });

        // Init "not" property
        this.on('afterAddGroup', function(e, group) {
            group.__.not = false;
        });

        // Modify templates
        if (!options.disable_template) {
            this.on('getGroupTemplate.filter', function(h) {
                var $h = $(h.value);
                $h.find(QueryBuilder.selectors.condition_container).prepend(
                    '<button type="button" class="btn btn-xs btn-default" data-not="group">' +
                    '<i class="' + options.icon_unchecked + '"></i> ' + self.translate('NOT') +
                    '</button>'
                );
                h.value = $h.prop('outerHTML');
            });
        }

        // Export "not" to JSON
        this.on('groupToJson.filter', function(e, group) {
            e.value.not = group.not;
        });

        // Read "not" from JSON
        this.on('jsonToGroup.filter', function(e, json) {
            e.value.not = !!json.not;
        });

        // Export "not" to SQL
        this.on('groupToSQL.filter', function(e, group) {
            if (group.not) {
                e.value = 'NOT ( ' + e.value + ' )';
            }
        });

        // Parse "NOT" function from sqlparser
        this.on('parseSQLNode.filter', function(e) {
            if (e.value.name && e.value.name.toUpperCase() == 'NOT') {
                e.value = e.value.arguments.value[0];

                // if the there is no sub-group, create one
                if (['AND', 'OR'].indexOf(e.value.operation.toUpperCase()) === -1) {
                    e.value = new SQLParser.nodes.Op(
                        self.settings.default_condition,
                        e.value,
                        null
                    );
                }

                e.value.not = true;
            }
        });

        // Request to create sub-group if the "not" flag is set
        this.on('sqlGroupsDistinct.filter', function(e, group, data, i) {
            if (data.not && i > 0) {
                e.value = true;
            }
        });

        // Read "not" from parsed SQL
        this.on('sqlToGroup.filter', function(e, data) {
            e.value.not = !!data.not;
        });

        // Export "not" to Mongo
        this.on('groupToMongo.filter', function(e, group) {
            var key = '$' + group.condition.toLowerCase();
            if (group.not && e.value[key]) {
                e.value = { '$nor': [e.value] };
            }
        });

        // Parse "$nor" operator from Mongo
        this.on('parseMongoNode.filter', function(e) {
            var keys = Object.keys(e.value);

            if (keys[0] == '$nor') {
                e.value = e.value[keys[0]][0];
                e.value.not = true;
            }
        });

        // Read "not" from parsed Mongo
        this.on('mongoToGroup.filter', function(e, data) {
            e.value.not = !!data.not;
        });
    }, {
        icon_unchecked: 'glyphicon glyphicon-unchecked',
        icon_checked: 'glyphicon glyphicon-check',
        disable_template: false
    });

    /**
     * From {@link module:plugins.NotGroup}
     * @name not
     * @member {boolean}
     * @memberof Group
     * @instance
     */
    Utils.defineModelProperties(Group, ['not']);

    QueryBuilder.selectors.group_not = QueryBuilder.selectors.group_header + ' [data-not=group]';

    QueryBuilder.extend(/** @lends module:plugins.NotGroup.prototype */ {
        /**
         * Performs actions when a group's not changes
         * @param {Group} group
         * @fires module:plugins.NotGroup.afterUpdateGroupNot
         * @private
         */
        updateGroupNot: function(group) {
            var options = this.plugins['not-group'];
            group.$el.find('>' + QueryBuilder.selectors.group_not)
                .toggleClass('active', group.not)
                .find('i').attr('class', group.not ? options.icon_checked : options.icon_unchecked);

            /**
             * After the group's not flag has been modified
             * @event afterUpdateGroupNot
             * @memberof module:plugins.NotGroup
             * @param {Group} group
             */
            this.trigger('afterUpdateGroupNot', group);

            this.trigger('rulesChanged');
        }
    });


    /**
     * @class Sortable
     * @memberof module:plugins
     * @description Enables drag & drop sort of rules.
     * @param {object} [options]
     * @param {boolean} [options.inherit_no_drop=true]
     * @param {boolean} [options.inherit_no_sortable=true]
     * @param {string} [options.icon='glyphicon glyphicon-sort']
     * @throws MissingLibraryError, ConfigError
     */
    QueryBuilder.define('sortable', function(options) {
        if (!('interact' in window)) {
            Utils.error('MissingLibrary', 'interact.js is required to use "sortable" plugin. Get it here: http://interactjs.io');
        }

        if (options.default_no_sortable !== undefined) {
            Utils.error(false, 'Config', 'Sortable plugin : "default_no_sortable" options is deprecated, use standard "default_rule_flags" and "default_group_flags" instead');
            this.settings.default_rule_flags.no_sortable = this.settings.default_group_flags.no_sortable = options.default_no_sortable;
        }

        // recompute drop-zones during drag (when a rule is hidden)
        interact.dynamicDrop(true);

        // set move threshold to 10px
        interact.pointerMoveTolerance(10);

        var placeholder;
        var ghost;
        var src;
        var moved;

        // Init drag and drop
        this.on('afterAddRule afterAddGroup', function(e, node) {
            if (node == placeholder) {
                return;
            }

            var self = e.builder;

            // Inherit flags
            if (options.inherit_no_sortable && node.parent && node.parent.flags.no_sortable) {
                node.flags.no_sortable = true;
            }
            if (options.inherit_no_drop && node.parent && node.parent.flags.no_drop) {
                node.flags.no_drop = true;
            }

            // Configure drag
            if (!node.flags.no_sortable) {
                interact(node.$el[0])
                    .draggable({
                        allowFrom: QueryBuilder.selectors.drag_handle,
                        onstart: function(event) {
                            moved = false;

                            // get model of dragged element
                            src = self.getModel(event.target);

                            // create ghost
                            ghost = src.$el.clone()
                                .appendTo(src.$el.parent())
                                .width(src.$el.outerWidth())
                                .addClass('dragging');

                            // create drop placeholder
                            var ph = $('<div class="rule-placeholder">&nbsp;</div>')
                                .height(src.$el.outerHeight());

                            placeholder = src.parent.addRule(ph, src.getPos());

                            // hide dragged element
                            src.$el.hide();
                        },
                        onmove: function(event) {
                            // make the ghost follow the cursor
                            ghost[0].style.top = event.clientY - 15 + 'px';
                            ghost[0].style.left = event.clientX - 15 + 'px';
                        },
                        onend: function(event) {
                            // starting from Interact 1.3.3, onend is called before ondrop
                            if (event.dropzone) {
                                moveSortableToTarget(src, $(event.relatedTarget), self);
                                moved = true;
                            }

                            // remove ghost
                            ghost.remove();
                            ghost = undefined;

                            // remove placeholder
                            placeholder.drop();
                            placeholder = undefined;

                            // show element
                            src.$el.css('display', '');

                            /**
                             * After a node has been moved with {@link module:plugins.Sortable}
                             * @event afterMove
                             * @memberof module:plugins.Sortable
                             * @param {Node} node
                             */
                            self.trigger('afterMove', src);

                            self.trigger('rulesChanged');
                        }
                    });
            }

            if (!node.flags.no_drop) {
                //  Configure drop on groups and rules
                interact(node.$el[0])
                    .dropzone({
                        accept: QueryBuilder.selectors.rule_and_group_containers,
                        ondragenter: function(event) {
                            moveSortableToTarget(placeholder, $(event.target), self);
                        },
                        ondrop: function(event) {
                            if (!moved) {
                                moveSortableToTarget(src, $(event.target), self);
                            }
                        }
                    });

                // Configure drop on group headers
                if (node instanceof Group) {
                    interact(node.$el.find(QueryBuilder.selectors.group_header)[0])
                        .dropzone({
                            accept: QueryBuilder.selectors.rule_and_group_containers,
                            ondragenter: function(event) {
                                moveSortableToTarget(placeholder, $(event.target), self);
                            },
                            ondrop: function(event) {
                                if (!moved) {
                                    moveSortableToTarget(src, $(event.target), self);
                                }
                            }
                        });
                }
            }
        });

        // Detach interactables
        this.on('beforeDeleteRule beforeDeleteGroup', function(e, node) {
            if (!e.isDefaultPrevented()) {
                interact(node.$el[0]).unset();

                if (node instanceof Group) {
                    interact(node.$el.find(QueryBuilder.selectors.group_header)[0]).unset();
                }
            }
        });

        // Remove drag handle from non-sortable items
        this.on('afterApplyRuleFlags afterApplyGroupFlags', function(e, node) {
            if (node.flags.no_sortable) {
                node.$el.find('.drag-handle').remove();
            }
        });

        // Modify templates
        if (!options.disable_template) {
            this.on('getGroupTemplate.filter', function(h, level) {
                if (level > 1) {
                    var $h = $(h.value);
                    $h.find(QueryBuilder.selectors.condition_container).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>');
                    h.value = $h.prop('outerHTML');
                }
            });

            this.on('getRuleTemplate.filter', function(h) {
                var $h = $(h.value);
                $h.find(QueryBuilder.selectors.rule_header).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>');
                h.value = $h.prop('outerHTML');
            });
        }
    }, {
        inherit_no_sortable: true,
        inherit_no_drop: true,
        icon: 'glyphicon glyphicon-sort',
        disable_template: false
    });

    QueryBuilder.selectors.rule_and_group_containers = QueryBuilder.selectors.rule_container + ', ' + QueryBuilder.selectors.group_container;
    QueryBuilder.selectors.drag_handle = '.drag-handle';

    QueryBuilder.defaults({
        default_rule_flags: {
            no_sortable: false,
            no_drop: false
        },
        default_group_flags: {
            no_sortable: false,
            no_drop: false
        }
    });

    /**
     * Moves an element (placeholder or actual object) depending on active target
     * @memberof module:plugins.Sortable
     * @param {Node} node
     * @param {jQuery} target
     * @param {QueryBuilder} [builder]
     * @private
     */
    function moveSortableToTarget(node, target, builder) {
        var parent, method;
        var Selectors = QueryBuilder.selectors;

        // on rule
        parent = target.closest(Selectors.rule_container);
        if (parent.length) {
            method = 'moveAfter';
        }

        // on group header
        if (!method) {
            parent = target.closest(Selectors.group_header);
            if (parent.length) {
                parent = target.closest(Selectors.group_container);
                method = 'moveAtBegin';
            }
        }

        // on group
        if (!method) {
            parent = target.closest(Selectors.group_container);
            if (parent.length) {
                method = 'moveAtEnd';
            }
        }

        if (method) {
            node[method](builder.getModel(parent));

            // refresh radio value
            if (builder && node instanceof Rule) {
                builder.setRuleInputValue(node, node.value);
            }
        }
    }


    /**
     * @class SqlSupport
     * @memberof module:plugins
     * @description Allows to export rules as a SQL WHERE statement as well as populating the builder from an SQL query.
     * @param {object} [options]
     * @param {boolean} [options.boolean_as_integer=true] - `true` to convert boolean values to integer in the SQL output
     */
    QueryBuilder.define('sql-support', function(options) {

    }, {
        boolean_as_integer: true
    });

    QueryBuilder.defaults({
        // operators for internal -> SQL conversion
        sqlOperators: {
            equal: { op: '= ?' },
            not_equal: { op: '!= ?' },
            in: { op: 'IN(?)', sep: ', ' },
            not_in: { op: 'NOT IN(?)', sep: ', ' },
            less: { op: '< ?' },
            less_or_equal: { op: '<= ?' },
            greater: { op: '> ?' },
            greater_or_equal: { op: '>= ?' },
            between: { op: 'BETWEEN ?', sep: ' AND ' },
            not_between: { op: 'NOT BETWEEN ?', sep: ' AND ' },
            begins_with: { op: 'LIKE(?)', mod: '{0}%' },
            not_begins_with: { op: 'NOT LIKE(?)', mod: '{0}%' },
            contains: { op: 'LIKE(?)', mod: '%{0}%' },
            not_contains: { op: 'NOT LIKE(?)', mod: '%{0}%' },
            ends_with: { op: 'LIKE(?)', mod: '%{0}' },
            not_ends_with: { op: 'NOT LIKE(?)', mod: '%{0}' },
            is_empty: { op: '= \'\'' },
            is_not_empty: { op: '!= \'\'' },
            is_null: { op: 'IS NULL' },
            is_not_null: { op: 'IS NOT NULL' }
        },

        // operators for SQL -> internal conversion
        sqlRuleOperator: {
            '=': function(v) {
                return {
                    val: v,
                    op: v === '' ? 'is_empty' : 'equal'
                };
            },
            '!=': function(v) {
                return {
                    val: v,
                    op: v === '' ? 'is_not_empty' : 'not_equal'
                };
            },
            'LIKE': function(v) {
                if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
                    return {
                        val: v.slice(1, -1),
                        op: 'contains'
                    };
                }
                else if (v.slice(0, 1) == '%') {
                    return {
                        val: v.slice(1),
                        op: 'ends_with'
                    };
                }
                else if (v.slice(-1) == '%') {
                    return {
                        val: v.slice(0, -1),
                        op: 'begins_with'
                    };
                }
                else {
                    Utils.error('SQLParse', 'Invalid value for LIKE operator "{0}"', v);
                }
            },
            'NOT LIKE': function(v) {
                if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
                    return {
                        val: v.slice(1, -1),
                        op: 'not_contains'
                    };
                }
                else if (v.slice(0, 1) == '%') {
                    return {
                        val: v.slice(1),
                        op: 'not_ends_with'
                    };
                }
                else if (v.slice(-1) == '%') {
                    return {
                        val: v.slice(0, -1),
                        op: 'not_begins_with'
                    };
                }
                else {
                    Utils.error('SQLParse', 'Invalid value for NOT LIKE operator "{0}"', v);
                }
            },
            'IN': function(v) {
                return { val: v, op: 'in' };
            },
            'NOT IN': function(v) {
                return { val: v, op: 'not_in' };
            },
            '<': function(v) {
                return { val: v, op: 'less' };
            },
            '<=': function(v) {
                return { val: v, op: 'less_or_equal' };
            },
            '>': function(v) {
                return { val: v, op: 'greater' };
            },
            '>=': function(v) {
                return { val: v, op: 'greater_or_equal' };
            },
            'BETWEEN': function(v) {
                return { val: v, op: 'between' };
            },
            'NOT BETWEEN': function(v) {
                return { val: v, op: 'not_between' };
            },
            'IS': function(v) {
                if (v !== null) {
                    Utils.error('SQLParse', 'Invalid value for IS operator');
                }
                return { val: null, op: 'is_null' };
            },
            'IS NOT': function(v) {
                if (v !== null) {
                    Utils.error('SQLParse', 'Invalid value for IS operator');
                }
                return { val: null, op: 'is_not_null' };
            }
        },

        // statements for internal -> SQL conversion
        sqlStatements: {
            'question_mark': function() {
                var params = [];
                return {
                    add: function(rule, value) {
                        params.push(value);
                        return '?';
                    },
                    run: function() {
                        return params;
                    }
                };
            },

            'numbered': function(char) {
                if (!char || char.length > 1) char = '$';
                var index = 0;
                var params = [];
                return {
                    add: function(rule, value) {
                        params.push(value);
                        index++;
                        return char + index;
                    },
                    run: function() {
                        return params;
                    }
                };
            },

            'named': function(char) {
                if (!char || char.length > 1) char = ':';
                var indexes = {};
                var params = {};
                return {
                    add: function(rule, value) {
                        if (!indexes[rule.field]) indexes[rule.field] = 1;
                        var key = rule.field + '_' + (indexes[rule.field]++);
                        params[key] = value;
                        return char + key;
                    },
                    run: function() {
                        return params;
                    }
                };
            }
        },

        // statements for SQL -> internal conversion
        sqlRuleStatement: {
            'question_mark': function(values) {
                var index = 0;
                return {
                    parse: function(v) {
                        return v == '?' ? values[index++] : v;
                    },
                    esc: function(sql) {
                        return sql.replace(/\?/g, '\'?\'');
                    }
                };
            },

            'numbered': function(values, char) {
                if (!char || char.length > 1) char = '$';
                var regex1 = new RegExp('^\\' + char + '[0-9]+$');
                var regex2 = new RegExp('\\' + char + '([0-9]+)', 'g');
                return {
                    parse: function(v) {
                        return regex1.test(v) ? values[v.slice(1) - 1] : v;
                    },
                    esc: function(sql) {
                        return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\'');
                    }
                };
            },

            'named': function(values, char) {
                if (!char || char.length > 1) char = ':';
                var regex1 = new RegExp('^\\' + char);
                var regex2 = new RegExp('\\' + char + '(' + Object.keys(values).join('|') + ')', 'g');
                return {
                    parse: function(v) {
                        return regex1.test(v) ? values[v.slice(1)] : v;
                    },
                    esc: function(sql) {
                        return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\'');
                    }
                };
            }
        }
    });

    /**
     * @typedef {object} SqlQuery
     * @memberof module:plugins.SqlSupport
     * @property {string} sql
     * @property {object} params
     */

    QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ {
        /**
         * Returns rules as a SQL query
         * @param {boolean|string} [stmt] - use prepared statements: false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)'
         * @param {boolean} [nl=false] output with new lines
         * @param {object} [data] - current rules by default
         * @returns {module:plugins.SqlSupport.SqlQuery}
         * @fires module:plugins.SqlSupport.changer:getSQLField
         * @fires module:plugins.SqlSupport.changer:ruleToSQL
         * @fires module:plugins.SqlSupport.changer:groupToSQL
         * @throws UndefinedSQLConditionError, UndefinedSQLOperatorError
         */
        getSQL: function(stmt, nl, data) {
            data = (data === undefined) ? this.getRules() : data;

            if (!data) {
                return null;
            }

            nl = !!nl ? '\n' : ' ';
            var boolean_as_integer = this.getPluginOptions('sql-support', 'boolean_as_integer');

            if (stmt === true) {
                stmt = 'question_mark';
            }
            if (typeof stmt == 'string') {
                var config = getStmtConfig(stmt);
                stmt = this.settings.sqlStatements[config[1]](config[2]);
            }

            var self = this;

            var sql = (function parse(group) {
                if (!group.condition) {
                    group.condition = self.settings.default_condition;
                }
                if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
                    Utils.error('UndefinedSQLCondition', 'Unable to build SQL query with condition "{0}"', group.condition);
                }

                if (!group.rules) {
                    return '';
                }

                var parts = [];

                group.rules.forEach(function(rule) {
                    if (rule.rules && rule.rules.length > 0) {
                        parts.push('(' + nl + parse(rule) + nl + ')' + nl);
                    }
                    else {
                        var sql = self.settings.sqlOperators[rule.operator];
                        var ope = self.getOperatorByType(rule.operator);
                        var value = '';

                        if (sql === undefined) {
                            Utils.error('UndefinedSQLOperator', 'Unknown SQL operation for operator "{0}"', rule.operator);
                        }

                        if (ope.nb_inputs !== 0) {
                            if (!(rule.value instanceof Array)) {
                                rule.value = [rule.value];
                            }

                            rule.value.forEach(function(v, i) {
                                if (i > 0) {
                                    value += sql.sep;
                                }

                                if (rule.type == 'boolean' && boolean_as_integer) {
                                    v = v ? 1 : 0;
                                }
                                else if (!stmt && rule.type !== 'integer' && rule.type !== 'double' && rule.type !== 'boolean') {
                                    v = Utils.escapeString(v);
                                }

                                if (sql.mod) {
                                    v = Utils.fmt(sql.mod, v);
                                }

                                if (stmt) {
                                    value += stmt.add(rule, v);
                                }
                                else {
                                    if (typeof v == 'string') {
                                        v = '\'' + v + '\'';
                                    }

                                    value += v;
                                }
                            });
                        }

                        var sqlFn = function(v) {
                            return sql.op.replace('?', function() {
                                return v;
                            });
                        };

                        /**
                         * Modifies the SQL field used by a rule
                         * @event changer:getSQLField
                         * @memberof module:plugins.SqlSupport
                         * @param {string} field
                         * @param {Rule} rule
                         * @returns {string}
                         */
                        var field = self.change('getSQLField', rule.field, rule);

                        var ruleExpression = field + ' ' + sqlFn(value);

                        /**
                         * Modifies the SQL generated for a rule
                         * @event changer:ruleToSQL
                         * @memberof module:plugins.SqlSupport
                         * @param {string} expression
                         * @param {Rule} rule
                         * @param {*} value
                         * @param {function} valueWrapper - function that takes the value and adds the operator
                         * @returns {string}
                         */
                        parts.push(self.change('ruleToSQL', ruleExpression, rule, value, sqlFn));
                    }
                });

                var groupExpression = parts.join(' ' + group.condition + nl);

                /**
                 * Modifies the SQL generated for a group
                 * @event changer:groupToSQL
                 * @memberof module:plugins.SqlSupport
                 * @param {string} expression
                 * @param {Group} group
                 * @returns {string}
                 */
                return self.change('groupToSQL', groupExpression, group);
            }(data));

            if (stmt) {
                return {
                    sql: sql,
                    params: stmt.run()
                };
            }
            else {
                return {
                    sql: sql
                };
            }
        },

        /**
         * Convert a SQL query to rules
         * @param {string|module:plugins.SqlSupport.SqlQuery} query
         * @param {boolean|string} stmt
         * @returns {object}
         * @fires module:plugins.SqlSupport.changer:parseSQLNode
         * @fires module:plugins.SqlSupport.changer:getSQLFieldID
         * @fires module:plugins.SqlSupport.changer:sqlToRule
         * @fires module:plugins.SqlSupport.changer:sqlToGroup
         * @throws MissingLibraryError, SQLParseError, UndefinedSQLOperatorError
         */
        getRulesFromSQL: function(query, stmt) {
            if (!('SQLParser' in window)) {
                Utils.error('MissingLibrary', 'SQLParser is required to parse SQL queries. Get it here https://github.com/mistic100/sql-parser');
            }

            var self = this;

            if (typeof query == 'string') {
                query = { sql: query };
            }

            if (stmt === true) stmt = 'question_mark';
            if (typeof stmt == 'string') {
                var config = getStmtConfig(stmt);
                stmt = this.settings.sqlRuleStatement[config[1]](query.params, config[2]);
            }

            if (stmt) {
                query.sql = stmt.esc(query.sql);
            }

            if (query.sql.toUpperCase().indexOf('SELECT') !== 0) {
                query.sql = 'SELECT * FROM table WHERE ' + query.sql;
            }

            var parsed = SQLParser.parse(query.sql);

            if (!parsed.where) {
                Utils.error('SQLParse', 'No WHERE clause found');
            }

            /**
             * Custom parsing of an AST node generated by SQLParser, you can return a sub-part of the tree, or a well formed group or rule JSON
             * @event changer:parseSQLNode
             * @memberof module:plugins.SqlSupport
             * @param {object} AST node
             * @returns {object} tree, rule or group
             */
            var data = self.change('parseSQLNode', parsed.where.conditions);

            // a plugin returned a group
            if ('rules' in data && 'condition' in data) {
                return data;
            }

            // a plugin returned a rule
            if ('id' in data && 'operator' in data && 'value' in data) {
                return {
                    condition: this.settings.default_condition,
                    rules: [data]
                };
            }

            // create root group
            var out = self.change('sqlToGroup', {
                condition: this.settings.default_condition,
                rules: []
            }, data);

            // keep track of current group
            var curr = out;

            (function flatten(data, i) {
                if (data === null) {
                    return;
                }

                // allow plugins to manually parse or handle special cases
                data = self.change('parseSQLNode', data);

                // a plugin returned a group
                if ('rules' in data && 'condition' in data) {
                    curr.rules.push(data);
                    return;
                }

                // a plugin returned a rule
                if ('id' in data && 'operator' in data && 'value' in data) {
                    curr.rules.push(data);
                    return;
                }

                // data must be a SQL parser node
                if (!('left' in data) || !('right' in data) || !('operation' in data)) {
                    Utils.error('SQLParse', 'Unable to parse WHERE clause');
                }

                // it's a node
                if (['AND', 'OR'].indexOf(data.operation.toUpperCase()) !== -1) {
                    // create a sub-group if the condition is not the same and it's not the first level

                    /**
                     * Given an existing group and an AST node, determines if a sub-group must be created
                     * @event changer:sqlGroupsDistinct
                     * @memberof module:plugins.SqlSupport
                     * @param {boolean} create - true by default if the group condition is different
                     * @param {object} group
                     * @param {object} AST
                     * @param {int} current group level
                     * @returns {boolean}
                     */
                    var createGroup = self.change('sqlGroupsDistinct', i > 0 && curr.condition != data.operation.toUpperCase(), curr, data, i);

                    if (createGroup) {
                        /**
                         * Modifies the group generated from the SQL expression (this is called before the group is filled with rules)
                         * @event changer:sqlToGroup
                         * @memberof module:plugins.SqlSupport
                         * @param {object} group
                         * @param {object} AST
                         * @returns {object}
                         */
                        var group = self.change('sqlToGroup', {
                            condition: self.settings.default_condition,
                            rules: []
                        }, data);

                        curr.rules.push(group);
                        curr = group;
                    }

                    curr.condition = data.operation.toUpperCase();
                    i++;

                    // some magic !
                    var next = curr;
                    flatten(data.left, i);

                    curr = next;
                    flatten(data.right, i);
                }
                // it's a leaf
                else {
                    if ($.isPlainObject(data.right.value)) {
                        Utils.error('SQLParse', 'Value format not supported for {0}.', data.left.value);
                    }

                    // convert array
                    var value;
                    if ($.isArray(data.right.value)) {
                        value = data.right.value.map(function(v) {
                            return v.value;
                        });
                    }
                    else {
                        value = data.right.value;
                    }

                    // get actual values
                    if (stmt) {
                        if ($.isArray(value)) {
                            value = value.map(stmt.parse);
                        }
                        else {
                            value = stmt.parse(value);
                        }
                    }

                    // convert operator
                    var operator = data.operation.toUpperCase();
                    if (operator == '<>') {
                        operator = '!=';
                    }

                    var sqlrl = self.settings.sqlRuleOperator[operator];
                    if (sqlrl === undefined) {
                        Utils.error('UndefinedSQLOperator', 'Invalid SQL operation "{0}".', data.operation);
                    }

                    var opVal = sqlrl.call(this, value, data.operation);

                    // find field name
                    var field;
                    if ('values' in data.left) {
                        field = data.left.values.join('.');
                    }
                    else if ('value' in data.left) {
                        field = data.left.value;
                    }
                    else {
                        Utils.error('SQLParse', 'Cannot find field name in {0}', JSON.stringify(data.left));
                    }

                    var id = self.getSQLFieldID(field, value);

                    /**
                     * Modifies the rule generated from the SQL expression
                     * @event changer:sqlToRule
                     * @memberof module:plugins.SqlSupport
                     * @param {object} rule
                     * @param {object} AST
                     * @returns {object}
                     */
                    var rule = self.change('sqlToRule', {
                        id: id,
                        field: field,
                        operator: opVal.op,
                        value: opVal.val
                    }, data);

                    curr.rules.push(rule);
                }
            }(data, 0));

            return out;
        },

        /**
         * Sets the builder's rules from a SQL query
         * @see module:plugins.SqlSupport.getRulesFromSQL
         */
        setRulesFromSQL: function(query, stmt) {
            this.setRules(this.getRulesFromSQL(query, stmt));
        },

        /**
         * Returns a filter identifier from the SQL field.
         * Automatically use the only one filter with a matching field, fires a changer otherwise.
         * @param {string} field
         * @param {*} value
         * @fires module:plugins.SqlSupport:changer:getSQLFieldID
         * @returns {string}
         * @private
         */
        getSQLFieldID: function(field, value) {
            var matchingFilters = this.filters.filter(function(filter) {
                return filter.field.toLowerCase() === field.toLowerCase();
            });

            var id;
            if (matchingFilters.length === 1) {
                id = matchingFilters[0].id;
            }
            else {
                /**
                 * Returns a filter identifier from the SQL field
                 * @event changer:getSQLFieldID
                 * @memberof module:plugins.SqlSupport
                 * @param {string} field
                 * @param {*} value
                 * @returns {string}
                 */
                id = this.change('getSQLFieldID', field, value);
            }

            return id;
        }
    });

    /**
     * Parses the statement configuration
     * @memberof module:plugins.SqlSupport
     * @param {string} stmt
     * @returns {Array} null, mode, option
     * @private
     */
    function getStmtConfig(stmt) {
        var config = stmt.match(/(question_mark|numbered|named)(?:\((.)\))?/);
        if (!config) config = [null, 'question_mark', undefined];
        return config;
    }


    /**
     * @class UniqueFilter
     * @memberof module:plugins
     * @description Allows to define some filters as "unique": ie which can be used for only one rule, globally or in the same group.
     */
    QueryBuilder.define('unique-filter', function() {
        this.status.used_filters = {};

        this.on('afterUpdateRuleFilter', this.updateDisabledFilters);
        this.on('afterDeleteRule', this.updateDisabledFilters);
        this.on('afterCreateRuleFilters', this.applyDisabledFilters);
        this.on('afterReset', this.clearDisabledFilters);
        this.on('afterClear', this.clearDisabledFilters);

        // Ensure that the default filter is not already used if unique
        this.on('getDefaultFilter.filter', function(e, model) {
            var self = e.builder;

            self.updateDisabledFilters();

            if (e.value.id in self.status.used_filters) {
                var found = self.filters.some(function(filter) {
                    if (!(filter.id in self.status.used_filters) || self.status.used_filters[filter.id].length > 0 && self.status.used_filters[filter.id].indexOf(model.parent) === -1) {
                        e.value = filter;
                        return true;
                    }
                });

                if (!found) {
                    Utils.error(false, 'UniqueFilter', 'No more non-unique filters available');
                    e.value = undefined;
                }
            }
        });
    });

    QueryBuilder.extend(/** @lends module:plugins.UniqueFilter.prototype */ {
        /**
         * Updates the list of used filters
         * @param {$.Event} [e]
         * @private
         */
        updateDisabledFilters: function(e) {
            var self = e ? e.builder : this;

            self.status.used_filters = {};

            if (!self.model) {
                return;
            }

            // get used filters
            (function walk(group) {
                group.each(function(rule) {
                    if (rule.filter && rule.filter.unique) {
                        if (!self.status.used_filters[rule.filter.id]) {
                            self.status.used_filters[rule.filter.id] = [];
                        }
                        if (rule.filter.unique == 'group') {
                            self.status.used_filters[rule.filter.id].push(rule.parent);
                        }
                    }
                }, function(group) {
                    walk(group);
                });
            }(self.model.root));

            self.applyDisabledFilters(e);
        },

        /**
         * Clear the list of used filters
         * @param {$.Event} [e]
         * @private
         */
        clearDisabledFilters: function(e) {
            var self = e ? e.builder : this;

            self.status.used_filters = {};

            self.applyDisabledFilters(e);
        },

        /**
         * Disabled filters depending on the list of used ones
         * @param {$.Event} [e]
         * @private
         */
        applyDisabledFilters: function(e) {
            var self = e ? e.builder : this;

            // re-enable everything
            self.$el.find(QueryBuilder.selectors.filter_container + ' option').prop('disabled', false);

            // disable some
            $.each(self.status.used_filters, function(filterId, groups) {
                if (groups.length === 0) {
                    self.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true);
                }
                else {
                    groups.forEach(function(group) {
                        group.each(function(rule) {
                            rule.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true);
                        });
                    });
                }
            });

            // update Selectpicker
            if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) {
                self.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
            }
        }
    });


    /*!
 * jQuery QueryBuilder 2.5.2
 * Locale: English (en)
 * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr
 * Licensed under MIT (https://opensource.org/licenses/MIT)
 */

    QueryBuilder.regional['en'] = {
        "__locale": "English (en)",
        "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr",
        "add_rule": t("querybuilder.common.add_rule"),
        "add_group": t("querybuilder.common.add_group"),
        "delete_rule": t("querybuilder.common.delete_rule"),
        "delete_group": t("querybuilder.common.delete_group"),
        "conditions": {
            "AND": t("querybuilder.conditions.AND"),
            "OR": t("querybuilder.conditions.OR")
        },
        "operators": {
            "equal": t("querybuilder.operators.equal"),
            "not_equal": t("querybuilder.operators.not_equal"),
            "in": t("querybuilder.operators.in"),
            "not_in": t("querybuilder.operators.not_in"),
            "less": t("querybuilder.operators.less"),
            "less_or_equal": t("querybuilder.operators.less_or_equal"),
            "greater": t("querybuilder.operators.greater"),
            "greater_or_equal": t("querybuilder.operators.greater_or_equal"),
            "between":  t("querybuilder.operators.between"),
            "not_between": t("querybuilder.operators.not_between"),
            "begins_with": t("querybuilder.operators.begins_with"),
            "not_begins_with": t("querybuilder.operators.not_begins_with"),
            "contains": t("querybuilder.operators.contains"),
            "not_contains": t("querybuilder.operators.not_contains"),
            "ends_with": t("querybuilder.operators.ends_with"),
            "not_ends_with": t("querybuilder.operators.not_ends_with"),
            "is_empty": t("querybuilder.operators.is_empty"),
            "is_not_empty": t("querybuilder.operators.is_not_empty"),
            "is_null": t("querybuilder.operators.is_null"),
            "is_not_null": t("querybuilder.operators.is_not_null"),
            "before": t("querybuilder.operators.before"),
            "after": t("querybuilder.operators.after"),
            "like": "Like",
            "not_like": "Not like"
        },
        "errors": {
            "no_filter": t("querybuilder.errors.no_filter"),
            "empty_group": t("querybuilder.errors.empty_group"),
            "radio_empty": t("querybuilder.errors.radio_empty"),
            "checkbox_empty": t("querybuilder.errors.checkbox_empty"),
            "select_empty": t("querybuilder.errors.select_empty"),
            "string_empty": t("querybuilder.errors.string_empty"),
            "string_exceed_min_length": t("querybuilder.errors.string_exceed_min_length"),
            "string_exceed_max_length": t("querybuilder.errors.string_exceed_max_length"),
            "string_invalid_format": t("querybuilder.errors.string_invalid_format"),
            "number_nan": t("querybuilder.errors.number_nan"),
            "number_not_integer": t("querybuilder.errors.number_not_integer"),
            "number_not_double": t("querybuilder.errors.number_not_double"),
            "number_exceed_min": t("querybuilder.errors.number_exceed_min"),
            "number_exceed_max": t("querybuilder.errors.number_exceed_max"),
            "number_wrong_step": t("querybuilder.errors.number_wrong_step"),
            "number_between_invalid": "Invalid values, {0} is greater than {1}",
            "datetime_empty": t("querybuilder.errors.datetime_empty"),
            "datetime_invalid": t("querybuilder.errors.datetime_invalid"),
            "datetime_exceed_min": t("querybuilder.errors.datetime_exceed_min"),
            "datetime_exceed_max": t("querybuilder.errors.datetime_exceed_max"),
            "datetime_between_invalid": "Invalid values, {0} is greater than {1}",
            "boolean_not_valid": t("querybuilder.errors.boolean_not_valid"),
            "operator_not_multiple": t("querybuilder.errors.operator_not_multiple")
        },
        "invert":  t("querybuilder.common.invert"),
        "NOT": "NOT"
    };

    QueryBuilder.defaults({ lang_code: 'en' });
    return QueryBuilder;

}));
