(function ($) {
    $.fn.scannerDetection = function (options) {

        // If string given, call onComplete callback
        if (typeof options === "string") {
            this.each(function () {
                this.scannerDetectionTest(options);
            });
            return this;
        }

        // If false (boolean) given, deinitialize plugin
        if (options === false) {
            this.each(function () {
                this.scannerDetectionOff();
            });
            return this;
        }

        var defaults = {
            onComplete: false, // Callback after detection of a successfull scanning (scanned string in parameter)
            onError: false, // Callback after detection of a unsuccessfull scanning (scanned string in parameter)
            onReceive: false, // Callback after receiving and processing a char (scanned char in parameter)
            onKeyDetect: false, // Callback after detecting a keyDown (key char in parameter) - in contrast to onReceive, this fires for non-character keys like tab, arrows, etc. too!
            timeBeforeScanTest: 100, // Wait duration (ms) after keypress event to check if scanning is finished
            avgTimeByChar: 30, // Average time (ms) between 2 chars. Used to do difference between keyboard typing and scanning
            minLength: 6, // Minimum length for a scanning
            endChar: [9, 13], // Chars to remove and means end of scanning
            startChar: [], // Chars to remove and means start of scanning
            ignoreIfFocusOn: false, // do not handle scans if the currently focused element matches this selector
            scanButtonKeyCode: false, // Key code of the scanner hardware button (if the scanner button a acts as a key itself) 
            scanButtonLongPressThreshold: 3, // How many times the hardware button should issue a pressed event before a barcode is read to detect a longpress
            onScanButtonLongPressed: false, // Callback after detection of a successfull scan while the scan button was pressed and held down
            stopPropagation: false, // Stop immediate propagation on keypress event
            preventDefault: false // Prevent default action on keypress event
        };
        if (typeof options === "function") {
            options = { onComplete: options }
        }
        if (typeof options !== "object") {
            options = $.extend({}, defaults);
        } else {
            options = $.extend({}, defaults, options);
        }

        this.each(function () {
            var self = this, $self = $(self), firstCharTime = 0, lastCharTime = 0, stringWriting = '', callIsScanner = false, testTimer = false, scanButtonCounter = 0;
            var initScannerDetection = function () {
                firstCharTime = 0;
                stringWriting = '';
                scanButtonCounter = 0;
            };
            self.scannerDetectionOff = function () {
                $self.unbind('keydown.scannerDetection');
                $self.unbind('keypress.scannerDetection');
            }
            self.isFocusOnIgnoredElement = function () {
                if (!options.ignoreIfFocusOn) return false;
                if (typeof options.ignoreIfFocusOn === 'string') return $(':focus').is(options.ignoreIfFocusOn);
                if (typeof options.ignoreIfFocusOn === 'object' && options.ignoreIfFocusOn.length) {
                    var focused = $(':focus');
                    for (var i = 0; i < options.ignoreIfFocusOn.length; i++) {
                        if (focused.is(options.ignoreIfFocusOn[i])) {
                            return true;
                        }
                    }
                }
                return false;
            }
            self.scannerDetectionTest = function (s) {
                // If string is given, test it
                if (s) {
                    firstCharTime = lastCharTime = 0;
                    stringWriting = s;
                }

                if (!scanButtonCounter) {
                    scanButtonCounter = 1;
                }

                // If all condition are good (length, time...), call the callback and re-initialize the plugin for next scanning
                // Else, just re-initialize
                if (stringWriting.length >= options.minLength && lastCharTime - firstCharTime < stringWriting.length * options.avgTimeByChar) {
                    if (options.onScanButtonLongPressed && scanButtonCounter > options.scanButtonLongPressThreshold) options.onScanButtonLongPressed.call(self, stringWriting, scanButtonCounter);
                    else if (options.onComplete) options.onComplete.call(self, stringWriting, scanButtonCounter);
                    $self.trigger('scannerDetectionComplete', { string: stringWriting });
                    initScannerDetection();
                    return true;
                } else {
                    if (options.onError) options.onError.call(self, stringWriting);
                    $self.trigger('scannerDetectionError', { string: stringWriting });
                    initScannerDetection();
                    return false;
                }
            }
            $self.data('scannerDetection', { options: options }).unbind('.scannerDetection').bind('keydown.scannerDetection', function (e) {
                // If it's just the button of the scanner, ignore it and wait for the real input
                if (options.scanButtonKeyCode !== false && e.which == options.scanButtonKeyCode) {
                    scanButtonCounter++;
                    // Cancel default
                    e.preventDefault();
                    e.stopImmediatePropagation();
                }
                // Add event on keydown because keypress is not triggered for non character keys (tab, up, down...)
                // So need that to check endChar and startChar (that is often tab or enter) and call keypress if necessary
                else if ((firstCharTime && options.endChar.indexOf(e.which) !== -1)
                    || (!firstCharTime && options.startChar.indexOf(e.which) !== -1)) {
                    // Clone event, set type and trigger it
                    var e2 = jQuery.Event('keypress', e);
                    e2.type = 'keypress.scannerDetection';
                    $self.triggerHandler(e2);
                    // Cancel default
                    e.preventDefault();
                    e.stopImmediatePropagation();
                }
                // Fire keyDetect event in any case!
                if (options.onKeyDetect) options.onKeyDetect.call(self, e);
                $self.trigger('scannerDetectionKeyDetect', { evt: e });

            }).bind('keypress.scannerDetection', function (e) {
                if (this.isFocusOnIgnoredElement()) return;
                if (options.stopPropagation) e.stopImmediatePropagation();
                if (options.preventDefault) e.preventDefault();

                if (firstCharTime && options.endChar.indexOf(e.which) !== -1) {
                    e.preventDefault();
                    e.stopImmediatePropagation();
                    callIsScanner = true;
                } else if (!firstCharTime && options.startChar.indexOf(e.which) !== -1) {
                    e.preventDefault();
                    e.stopImmediatePropagation();
                    callIsScanner = false;
                } else {
                    if (typeof (e.which) != 'undefined') {
                        stringWriting += String.fromCharCode(e.which);
                    }
                    callIsScanner = false;
                }

                if (!firstCharTime) {
                    firstCharTime = Date.now();
                }
                lastCharTime = Date.now();

                if (testTimer) clearTimeout(testTimer);
                if (callIsScanner) {
                    self.scannerDetectionTest();
                    testTimer = false;
                } else {
                    testTimer = setTimeout(self.scannerDetectionTest, options.timeBeforeScanTest);
                }

                if (options.onReceive) options.onReceive.call(self, e);
                $self.trigger('scannerDetectionReceive', { evt: e });
            });
        });
        return this;
    }
})(jQuery);