(function ($) {
    function init(target) {
        var state = $.data(target, "findbar");
        var opts = state.options;

        function _unhighlight() {
            $(target).findbar("unhighlight");
        }

        function _createFinder(jq, type, options) {
            var Finder = function () { if (this.initialize) { this.initialize.apply(this, arguments); }};
            Finder.prototype = opts.finders[type];
            Finder.prototype.constructor = Finder;
            return new Finder(jq, $.extend(options, {
                remote: opts.remote,
                labels: opts.labels,
                invalidCls: opts.invalidCls,
                onSubmit: function () { $(jq).findbar("search"); },
                onCancel: function () { $(jq).findbar("hide"); },
                onChange: function () { if (!$(jq).findbar("options").remote) { $(jq).findbar("search"); }}
            }));
        }

        target = $(target).hide().addClass(opts.cls);
        if (target.length) {
            target.empty();
        }

        if (opts.fields.length) {
            var byField = $('<input type="checkbox" name="byfield"/>')
                    .prependTo($('<label/>').text(opts.labels.FIELD).css("padding-right", "5px").appendTo(target));
            byField.change(function () {
                var fieldName = field.combobox("getValue");
                if ($(this).is(":checked")) {
                    field.combobox("enable").combobox("options").onChange.call(field, fieldName, null);
                } else {
                    field.combobox("disable").combobox("options").onChange.call(field, null, fieldName);
                }
            });

            var field = $('<select name="field"></select>').appendTo(target);
            field.combobox({
                editable: false,
                disabled: true,
                data: opts.fields,
                width: 120,
                panelHeight: "auto",
                onChange: function (newValue, oldValue) {
                    if ($(this).combobox("options").disabled) {
                        newValue = null;
                    }

                    var valueField = $(this).combobox("options").valueField;
                    var finderOptions = {field: newValue};
                    var newType, oldType, oldQuery;

                    $.each($(this).combobox("getData"), function (_, entry) {
                        if (entry[valueField] === newValue) {
                            if ($.isPlainObject(entry.finder)) {
                                newType = entry.finder.type;
                                $.extend(finderOptions, entry.finder.options);
                            } else if (typeof entry.finder === "string") {
                                newType = entry.finder;
                            }
                        }
                    });

                    if (state.finder) {
                        oldQuery = state.finder.getQuery.call(state.finder);
                        oldType = state.finderType;
                        (state.finder.destroy || $.noop).call(state.finder);
                        state.finder = null;
                    }

                    state.finderType = newType || "text";
                    state.finder = _createFinder(target, state.finderType, finderOptions);
                    if (oldQuery && state.finderType === oldType) {
                        state.finder.setQuery.call(state.finder, $.extend(oldQuery, {field: newValue}));
                    }
                    _unhighlight();
                    if (!opts.remote) {
                        $(target).findbar("search");
                    }
                }
            });
        }

        if (opts.closable) {
            $('<a/>').linkbutton({plain: true, iconCls: "panel-tool-close", onClick: function () {
                var fb = $(this).parents("div.datagrid-findbar");
                setTimeout(function() { $(fb).findbar("hide"); fb = null; }, 50);
            }}).css({float: "right"}).appendTo(target);
        }

        $(target).click(function(e) {
            e.stopPropagation(); //prevent handling by datagrid.headerEvents.click
        });
    }

    function show(jq, e) {
        if (!$(jq).is(":visible")) {
            $(jq).show();
            var opts = $.data(jq, "findbar").options;
            opts.onShow.call(jq, e);
        }
        $(jq).find(":input:text")
                .add($(jq).find("input[type=search]"))
                .filter(":not(:disabled):first").focus();
    }

    function hide(target, e) {
        var state = $.data(target, "findbar");

        if (state.finder) {
            state.finder.destroy.call(state.finder);
            state.finder = null;
            state.finderType = null;
        }

        if (state.options.closable) {
            $(target).findbar("unhighlight").hide().closest("tr.datagrid-findbar-row").remove();
            state.options.onHide.call(target, e);
        }
    }

    function search(jq, query) {
        function _equals(o1, o2) {
            if (!o1 || !o2) { return false; }
            for (var p in o1) {
                if (!o2.hasOwnProperty(p) || o1[p] !== o2[p]) {
                    return false;
                }
            }
            return true;
        }

        var opts = $.data(jq, "findbar").options;
        var oldQuery = opts.query;
        opts.query = query;

        opts.onSearch.call(jq, query, !_equals(oldQuery, query));
    }

    function test(jq, data) {
        var query = $(jq).findbar("getQuery");
        if (query && query.field && query.field !== data.field) {
            return false;
        }
        var state = $.data(jq, "findbar");
        var matcher = state.finder ? state.finder.matcher.call(state.finder) : null;
        return matcher && matcher.find(data.text, data.value);
    }

    function highlight(jq, node) {
        var state = $.data(jq, "findbar");
        var matcher = state.finder ? state.finder.matcher.call(state.finder) : null;
        if (matcher) {
            node.each(function () { _highlight(this, matcher, state.options.highlightCls); });
        }

        function _highlight(target, matcher, cls) {
            if (target.nodeType == 3) {
                var matches = matcher.matches(target.data);
                if (matches) {
                    var node = target.splitText(matches.index);
                    node.splitText(matches[0].length);
                    var wrapper = document.createElement("span");
                    wrapper.className = cls;
                    wrapper.appendChild(node.cloneNode(true));
                    node.parentNode.replaceChild(wrapper, node);
                    return true;
                }
            } else if (target.nodeType == 1 && target.hasChildNodes() && !/(script|style)/i.test(target.tagName)) {
                for (var i = 0, size = target.childNodes.length; i < size; ++i) {
                    if (_highlight(target.childNodes[i], matcher, cls)) {
                        i++;
                    }
                }
            }
            return false;
        }
    }

    function unhighlight(jq, node) {
        var cssClass = $.data(jq, "findbar").options.highlightCls;
        $(node).find("." + cssClass).replaceWith(function() { return $(this).text(); });
    }

    function getQuery(jq) {
        var state = $.data(jq, "findbar");
        return state && state.finder ? state.finder.getQuery.call(state.finder) : {};
    }

    function setQuery(jq, query) {
        var state = $.data(jq, "findbar");
        state.finder && state.finder.setQuery(query);
        $(jq).find("input[name=byfield]:checkbox").prop("checked", !!query.field);
    }

    $.fn.findbar = function (opts) {
        var args = Array.prototype.slice.call(arguments, 1);
        if (typeof opts == "string") {
            return $.fn.findbar.methods[opts].apply(this, args);
        }
        opts = opts || {};
        return this.each(function () {
            var state = $.data(this, "findbar");
            if (state) {
                $.extend(state.options, opts);
            } else {
                opts = $.extend({}, $.fn.findbar.defaults, $.fn.findbar.parseOptions(this), opts);
                $.data(this, "findbar", {options: opts});
            }
            init.apply(null, [this].concat(args));
        });
    };

    $.fn.findbar.methods = {

        options: function () {
            return $.data(this[0], "findbar").options;
        },

        show: function (e) {
            return this.each(function () { show(this, e); });
        },

        hide: function (e) {
            return this.each(function () { hide(this, e); });
        },

        search: function () {
            return this.each(function () { search(this, getQuery(this)); });
        },

        test: function (data) {
            return test(this[0], typeof data === "string" ? {text: data, value: data, field: null} : data);
        },

        highlight: function (node) {
            node = node ? $(node) : this.parents(".datagrid");
            return this.each(function () { highlight(this, node); });
        },

        unhighlight: function (node) {
            node = node ? node : this.parents(".datagrid");
            return this.each(function () { unhighlight(this, node); });
        },

        getQuery: function () {
            return getQuery(this[0]);
        },

        setQuery: function (query) {
            return this.each(function () { setQuery(this, query); })
        }
    };

    $.fn.findbar.parseOptions = function (t) {
        return $.extend({}, $.parser.parseOptions(t, [{showMatchCase: "boolean", showRegexp: "boolean", showFields: "boolean", ignoreCase: "boolean", regexp: "boolean"}]));
    };

    $.fn.findbar.defaults = {
        query: null,
        ignoreCase: true,
        regexp: false,
        field: null,
        fields: [],
        closable: true,
        remote: false,
        cls: "datagrid-findbar", invalidCls: "query-invalid", highlightCls: "highlight",
        labels: {MATCH_CASE: "Match case", REGEXP: "Regexp", FIELD: "Field", SINCE: "Since", UNTIL: "Until"},
        onShow: function (e) {},
        onHide: function (e) {},
        onSearch: function () {},
        onChange: function (query) {},
        finders: {
            text: {
                initialize: function (target, options) {
                    this.options = options;
                    this.container = $('<span />').appendTo(target);
                    this.text = $('<input name="query" type="search" size="30" maxlength="255"/>').prependTo($('<label/>').appendTo(this.container));
                    this.text.off(".findbar")
                            .on("keydown.findbar", $.proxy(function (e) {
                                    if (e.keyCode == 13) {
                                        e.preventDefault();
                                        this._onSubmit();
                                        return false;
                                    } else if (e.keyCode == 27) {
                                        this._onCancel();
                                        return false;
                                    } else {
                                        if (this.timer) { clearTimeout(this.timer); }
                                        this.timer = setTimeout($.proxy(this._onChange, this), 200);
                                    }
                                }, this));

                    if (options.remote) {
                        this.regexp = $('<input type="checkbox" name="regexp"/>')
                                .change($.proxy(this._onChange, this))
                                .prependTo($('<label/>').text(options.labels.REGEXP).appendTo(this.container));

                        $("<a />").linkbutton({
                                                  plain: true,
                                                  iconCls: "icon-search",
                                                  onClick: $.proxy(this._onSubmit, this)
                                              })
                                .appendTo(this.container);
                    } else {
                        this.matchCase = $('<input type="checkbox" name="matchcase"/>')
                                .change($.proxy(this._onChange, this))
                                .prependTo($('<label/>').text(options.labels.MATCH_CASE).appendTo(this.container));

                        this.regexp = $('<input type="checkbox" name="regexp"/>')
                                .change($.proxy(this._onChange, this))
                                .prependTo($('<label/>').text(options.labels.REGEXP).appendTo(this.container));
                    }
                },
                destroy: function () {
                    this.container.remove();
                    this.container = null;
                    this.text = null;
                    this.matchCase = null;
                    this.regexp = null;
                    this.options = null;
                },
                getQuery: function () {
                    return {
                        query: this.text.val(),
                        field: this.options.field,
                        regexp: this.regexp.is(":checked"),
                        ignoreCase: this.matchCase && !this.matchCase.is(":checked")
                    };
                },
                setQuery: function (query) {
                    this.text.val(query.query);
                    this.options.field = query.field || this.options.field;
                    this.regexp.prop("checked", !!query.regexp);
                    if (this.matchCase) {
                        this.matchCase.prop("checked", !query.ignoreCase);
                    }
                },
                matcher: function () {
                    try {
                        var query = this.getQuery();
                        var pattern = query.regexp ? query.query : query.query.replace(/([.*+?^${}()|\[\]\/\\])/g,
                                                                                       "\\$1");
                        var re = new RegExp(pattern, query.ignoreCase ? "i" : "");
                        return {
                            find: function (data) { return re.test(data); },
                            matches: function (data) { return re.exec(data); }
                        };
                    } catch (e) {
                        return null;
                    }
                },
                _validate: function () {
                    if (this.matcher() == null) {
                        this.text.addClass(this.options.invalidCls);
                        return false;
                    } else {
                        this.text.removeClass(this.options.invalidCls);
                        return true;
                    }
                },
                _onChange: function () {
                    if (this._validate()) {
                        this.options.onChange();
                    }
                },
                _onSubmit: function () {
                    if (this._validate()) {
                        this.options.onSubmit();
                    }
                },
                _onCancel: function () {
                    this.options.onCancel();
                }
            },
            datebox: {
                initialize: function (target, options) {
                    this.options = options;
                    this.container = $('<span />').appendTo(target);
                    this.since = $('<input name="startdate" type="text" />')
                            .appendTo($('<label />').text(options.labels.SINCE + " ").appendTo(this.container))
                            .datebox($.extend({}, options, {onChange: $.proxy(this._onChange, this)}));
                    this.till = $('<input name="enddate" type="text" />')
                            .appendTo($('<label />').text(options.labels.UNTIL + " ").appendTo(this.container))
                            .datebox($.extend({}, options, {onChange: $.proxy(this._onChange, this)}));
                    if (options.remote) {
                        $("<a />").linkbutton({
                                                  plain: true,
                                                  iconCls: "icon-search",
                                                  onClick: $.proxy(this._onSubmit, this)
                                              })
                                .appendTo(this.container);
                    }
                },
                destroy: function () {
                    this.since.datebox("destroy");
                    this.since = null;
                    this.till.datebox("destroy");
                    this.till = null;
                    this.container.remove();
                    this.container = null;
                    this.options = null;
                },
                getQuery: function () {
                    var since = this.since.datebox("getValue");
                    var startDate = since.trim().length ? this.since.datebox("options").parser(since).getTime() : null;
                    var till = this.till.datebox("getValue");
                    var endDate = till.trim().length ? this.till.datebox("options").parser(till).getTime() : null;

                    return {field: this.options.field, startDate: startDate, endDate: endDate};
                },
                setQuery: function (query) {
                    this.options.field = query.field || this.options.field;
                    if ($.isNumeric(query.startDate)) {
                        this.since.datebox("setValue",
                                           this.since.datebox("options").formatter(new Date(query.startDate)));
                    }
                    if ($.isNumeric(query.endDate)) {
                        this.till.datebox("setValue", this.till.datebox("options").formatter(new Date(query.endDate)));
                    }
                },
                matcher: function () {
                    var query = this.getQuery();
                    return {
                        find: function (_, value) {
                            if (query.startDate == null && query.endDate == null) {
                                return true;
                            }
                            if (query.startDate && query.endDate && query.startDate < query.endDate) {
                                return query.startDate <= value && value <= query.endDate;
                            }
                            return query.startDate
                                    ? query.startDate <= value
                                    : query.endDate ? value <= query.endDate : false;
                        },
                        matches: function (value) {
                            return null;
                        }
                    };
                },
                _validate: function () {
                    var query = this.getQuery();
                    if (query.startDate && query.endDate && query.startDate >= query.endDate) {
                        this.since.datebox("textbox").addClass(this.options.invalidCls);
                        this.till.datebox("textbox").addClass(this.options.invalidCls);
                        return false;
                    } else {
                        this.since.datebox("textbox").removeClass(this.options.invalidCls);
                        this.till.datebox("textbox").removeClass(this.options.invalidCls);
                        return true;
                    }
                },
                _onChange: function () {
                    if (this._validate()) {
                        this.options.onChange();
                    }
                },
                _onSubmit: function () {
                    if (this._validate()) {
                        this.options.onSubmit();
                    }
                }
            },

            combobox: {
                initialize: function (target, options) {
                    this.options = options;
                    this.container = $('<span />').appendTo(target);
                    this.combo = $('<input type="text" />')
                            .prependTo($('<label />').appendTo(this.container))
                            .combobox($.extend({editable: false}, options, {onChange: $.proxy(this._onChange, this)}));
                    if (options.remote) {
                        $("<a />").linkbutton({plain: true, iconCls: "icon-search", onClick: function () { $(target).findbar("search"); }})
                                .appendTo(this.container);
                    }
                },

                destroy: function () {
                    this.combo.combobox("destroy");
                    this.combo = null;
                    this.container.remove();
                    this.container = null;
                    this.options = null;
                },

                getQuery: function () {
                    var opts = this.combo.combobox("options");
                    var query = opts.multiple
                            ? this.combo.combobox("getValues").join(opts.separator)
                            : this.combo.combobox("getValue");

                    return {field: this.options.field, query: query, ignoreCase: true};
                },

                setQuery: function (query) {
                    this.options.field = query.field || this.options.field;
                    var opts = this.combo.combobox("options");
                    if (opts.multiple) {
                        if (query.query) {
                            this.combo.combobox("setValues", query.query.split(opts.separator));
                        } else {
                            this.combo.combobox("clear");
                        }
                    } else {
                        this.combo.combobox("select", query.query);
                    }
                },
                matcher: function () {
                    var opts = this.combo.combobox("options");
                    var text = opts.multiple
                            ? this.combo.combobox("getText").split(opts.separator)
                            : [this.combo.combobox("getText")];
                    try {
                        var re = new RegExp(text.length == 1 ? text[0] : "[" + text.join("|") + "]");
                        return {
                            find: function (data) { return re.test(data); },
                            matches: function (data) { return re.exec(data); }
                        };
                    } catch (e) {
                        return null;
                    }
                },
                _onChange: function () {
                    (this.options.onChange || $.noop)();
                }
            }
        }
    };

})(jQuery);

// integration into datagrid plugin
(function ($) {

    function init(target) {
        var state = $.data(target, "datagrid");
        var header = state.dc.header2.find("table.datagrid-htable");
        header.find("tr.datagrid-findbar-row").remove();

        var dg = $(target);
        var fields = dg.datagrid("getColumnFields");
        var tr = $('<tr class="datagrid-header-row datagrid-findbar-row"></tr>');
        var td = $('<td' + (fields.length ? ' colspan="' + fields.length + '"' : '') + '></td>');
        var bar = $('<div/>').hide().appendTo(td.appendTo(tr.appendTo(header.find("tbody"))));

        return bar.findbar({
            remote: !!state.options.remoteSearch,
            fields: $.map(fields, function (field) {
                var col = dg.datagrid("getColumnOption", field);
                return false === col.searchable
                       ? undefined
                       : {text: (col && col.title) || field, value: field, finder: col.finder || "text"};
            }),

            onSearch: function (query, firstPage) {
                var opts = dg.datagrid("options");
                opts.pageNumber = firstPage ? 1 : opts.pageNumber;
                dg.datagrid("reload", query);
            },

            onShow: function () {
                var opts = dg.datagrid("options");
                var loadFilter = opts.loadFilter;
                var onBeforeLoad = opts.onBeforeLoad;
                var onLoadSuccess = opts.onLoadSuccess || $.noop;
                opts.oldOptions = {
                    loadFilter: opts.loadFilter,
                    onBeforeLoad: opts.onBeforeLoad,
                    onLoadSuccess: opts.onLoadSuccess,
                    pageNumber: opts.pageNumber,
                    pageSize: opts.pageSize,
                    queryParams: opts.queryParams,
                    checkOnSelect: opts.checkOnSelect
                };

                $.extend(opts, {
                    checkOnSelect: false,

                    loadFilter: function (data) {
                        data = $(this).datagrid("options").remoteSearch ? data : searchFilter.call(this, data);
                        return loadFilter.call(this, data);
                    },

                    onBeforeLoad: function() {
                        var ret = onBeforeLoad.apply(this, arguments);
                        if (ret) {
                            $(this).datagrid("options").searchQuery = $(this).datagrid("getFindbar").findbar("getQuery");
                        }
                        return ret;
                    },

                    onLoadSuccess: function () {
                        $(this).datagrid("clearSelections");
                        onLoadSuccess.apply(this, arguments);

                        var query = $(this).datagrid("options").searchQuery;
                        if (query) {
                            var fb = this;
                            setTimeout(function() {
                                // restore checkboxes state in the datagrid header
                                $(fb).datagrid("getFindbar").findbar("setQuery", query);
                                highlightSearch.call(fb, query);
                                fb = null;
                            }, 0);
                        }
                    }
                });

                dg.datagrid("resize");
            },

            onHide: function () {
                var opts = dg.datagrid("options");
                $.extend(opts, opts.oldOptions);
                delete opts.oldOptions;

                dg.datagrid("resize").datagrid("reload");
            }
        });
    }

    function searchFilter(data) {
        if ($.isArray(data)) {
            data = {total: data.length, rows: data};
        }
        var rows = matches.call(this, [].concat(data.rows));

        return {total: rows.length, rows: rows};
    }

    function matches(rows) {
        var dg = $(this);
        var fb = dg.datagrid("getFindbar");
        var columns = $.map($(this).datagrid("getColumnFields", true).concat($(this).datagrid("getColumnFields", false)),
                            function (field) { return dg.datagrid("getColumnOption",  field); });

        for (var i = 0; i < rows.length;) {
            var row = rows[i];
            var found = false;

            for (var j = 0; j < columns.length; ++j) {
                var col = columns[j];
                var data = {
                    text: col.formatter ? col.formatter(row[col.field], row, i) : row[col.field],
                    value: row[col.field], field: col.field
                };
                if (fb.findbar("test", data)) {
                    found = true;
                    break;
                }
            }
            if (found) {
                ++i;
            } else {
                rows.splice(i, 1);
            }
        }
        return rows;
    }

    function highlightSearch(query) {
        var dg = $(this);
        var fb = dg.datagrid("getFindbar");
        var cells = $.data(this, "datagrid").dc.view.find(".datagrid-body").find("td");
        var fields = dg.datagrid("getColumnFields", true).concat(dg.datagrid("getColumnFields", false));
        var nonSearchableFields = $.map(fields, function (field) { return dg.datagrid("getColumnOption",  field).searchable === false ? field : null; });

        fb.findbar("unhighlight", cells);

        if (query && query.field) {
            cells = cells.filter("[field=" + query.field + "]");
        } else if (nonSearchableFields.length != 0) {
            cells = cells.filter(function() { return $.inArray($(this).attr("field"), nonSearchableFields) == -1; });
        }

        fb.findbar("highlight", cells);
    }

    $.extend($.fn.datagrid.methods, {
        getFindbar: function (jq) {
            var bar = jq.datagrid("getPanel").find("div.datagrid-findbar");
            if (bar.length == 0) {
                bar = init(jq[0]);
            }
            return bar;
        }
    });

    $.extend($.fn.datagrid.defaults, {search: false, remoteSearch: false});

})(jQuery);