/**
 * Search as you type plugin with some basic smarts.
 *
 * jquery.searchysearch.js
 * @author Nathan Howell <nathan@navigatormm.com>
 */

(function ($) {

    $.fn.searchySearch = function (_options) {
        if (this.length === 0) {
            return;
        }

        var options = {
            only_search_on_match  : false,
            fields_to_clear       : [],
            fields_to_hide        : [],
            threshold_default     : 1250,
            threshold_min         : 250,
            threshold_factor      : 2.5,
            match_delay_default   : 800,
            matcher_factor        : 1.3,
            key_calc_min          : 3,
            key_timer_reset_after : 10000,
            key_delay_min         : 10,
            key_delay_max         : 4500
        };
        $.extend(true, options, _options);

        options.stock_matchers = {
            "isbn": function(str) {
                // ISBN tester
                if (str.match(/^[0-9\s\-]*X?$/i)) {
                    var n = str.replace(/[^0-9X]/gi, '');
                    if (n.length === 10 || n.length === 13) {
                        return [0, n];
                    } else {
                        return [1];
                    }
                } else {
                    return false;
                }
            },
            "postalcode_CA": function(str) {
                // Canadian postal code tester
                if (str.match(/^\s*([a-z][0-9][a-z])[\s\-]*([0-9][a-z][0-9])\s*$/i)) {
                    return [0, (RegExp.$1+' '+RegExp.$2).toUpperCase()];
                } else {
                    var t = str.replace(/[^a-z0-9]/gi, '');
                    if (t.match(/^([a-z][0-9])+[a-z]?$/i)) {
                        return [1];
                    }
                    return false;
                }
            },
            "postalcode_US": function(str) {
                // US zip code tester
                if (str.match(/^\s*([0-9]{5})(?:\s*-?\s*([0-9]{4}))?\s*$/)) {
                    var ret = RegExp.$1;
                    if (RegExp.$2) {
                        ret += '-'+RegExp.$2;
                    }
                    return [0, ret];
                } else if (str.match(/^[0-9\s\-]*$/)) {
                    return [1];
                } else {
                    return false;
                }
            }
        };
        options.handle_matcher = function(res) {
            var $this = $(this);
            var data = $this.data('searchysearch');
            if (res) {
                if (res[0] === 0) {
                    // we matched, so trigger an immediate search
                    data.matched = true;
                    if (res[1]) {
                        data.searchtext = res[1];
                        $this.val(res[1]);
                    }
                    //console.log(' match - '+res);
                    $this.trigger('newsearch');
                    //return false;
                } else if (res[0] === 1) {
                    // this is a partial match, so don't do a search yet
                    data.do_search = false;
                    //console.log(' partial match - '+res);
                }
            }
        };

        options.avg = function(n) {
            var av = 0,
                cnt = 0,
                len = n.length,
                i = 0;
            for (i = 0; i < len; i++) {
                var e = +n[i];
                if(!e && n[i] !== 0 && n[i] !== '0') {
                    e--;
                }
                if (n[i] === e) {
                    av += e;
                    cnt++;
                }
            }
            return av/cnt;
        };

        this.addClass('searchysearch');

        this.bind('newsearch.searchysearch', function() {
            var $this = $(this);
            var data = $this.data('searchysearch');
            //TODO this is a bug workaround for pasting into search field and clicking search. fix properly.
            if (!data.searchtext) {
                data.searchtext = $this.val();
            }
            clearTimeout(data.timer);
            data.options.searcher.call($this, data);
        });
        this.bind('searchcomplete.searchysearch', function() {
            var $this = $(this);
            $this.data('searchysearch').options.searchComplete.call($this, $this.data('searchysearch'));
        });

        if (! this.data('searchysearch')) {
            this.data('searchysearch', {
                results: [],
                times: [],
                lastkey: 0
            });
        }
        var data = this.data('searchysearch');
        data.options = options;

        if (typeof(options.init) === 'function') {
            options.init.call(this, data, options);
        }

        this.bind('keyup change', function(e) {
            // ignore some keys (arrows, esc, ctrl, shift, etc.)
            if (e.which === 38 ||
                e.which === 40 ||
                e.which === 37 ||
                e.which === 39 ||
                e.which === 27 ||
                e.which === 16 ||
                e.which === 17 ||
                e.which === 9)
            {
                return false;
            }

            var now = new Date().getTime();
            var $this = $(this);
            var searchtext = $this.val();
            var data = $this.data('searchysearch');
            data.searchtext = searchtext;

            $this.closest('div').find(options.fields_to_clear.join(',')).val('');
            $this.closest('div').find(options.fields_to_hide.join(',')).hide();

            // enter key is immediate search and do nothing else
            if (e.which === 13 && ! options.only_search_on_match) {
                $this.trigger('newsearch');
                return false;
            }

            // if there has been a long delay since the last keypress, clear the timings
            if (now - data.lastkey > options.key_timer_reset_after) {
                data.times = [];
            }

            if (searchtext === '') {
                // clear recorded key timings and make sure the search timer is not set
                data.times = [];
                clearTimeout(data.timer);
                //clearTimeout(data.matchtimer);
            //} else if (e.which == 8) {
                //data.times.shift;
            } else {
                // log the key timing if it's in the acceptable range
                if (now - data.lastkey < options.key_delay_max && now - data.lastkey > options.key_delay_min) {
                    data.times.push(now - data.lastkey);
                }

                // run the matcher functions to look for recognized search terms
                data.do_search = true;
                data.matched = false;

                if (data.options.matchers) {
                    $.each(data.options.matchers, function(i, m) {
                        if (typeof(data.options.stock_matchers[m]) === 'function') {
                            data.options.handle_matcher.call($this, data.options.stock_matchers[m](searchtext));
                        }
                    });
                }
                if (data.options.custom_matchers) {
                    $.each(data.options.custom_matchers, function(i, m) {
                        if (typeof(m) === 'function') {
                            data.options.handle_matcher.call($this, m(searchtext));
                        }
                    });
                }

                if (! data.matched && ! options.only_search_on_match) {
                    // set the search timer based on the delay between user keypresses or use a default
                    data.threshold = data.times.length > options.key_calc_min ?
                        Number(data.options.avg(data.times)) * options.threshold_factor :
                        options.threshold_default;
                    //console.log('TRIGGER: '+data.threshold);

                    // if the timer is too low, use the minimum setting
                    if (data.threshold < options.threshold_min) {
                        data.threshold = options.threshold_min;
                    }

                    // don't search if the user is still typing or there is a partial match
                    if (now - data.lastkey <= data.threshold || ! data.do_search) {
                        clearTimeout(data.timer);
                        clearTimeout(data.matchtimer);
                    }

                    if (data.do_search) {
                        data.timer = setTimeout(function() {$this.trigger('newsearch');}, data.threshold);
                    }
                }
            }
            data.lastkey = now;
            return false;
        });

    };

}(jQuery));

