/*jshint undef: true, unused:true */
/*global jQuery: true */

/*!=========================================================================
 *  Bootstrap TouchSpin
 *  v2.5.0
 *
 *  A mobile and touch friendly input spinner component for Bootstrap 3.
 *
 *      https://github.com/istvan-meszaros/bootstrap-touchspin
 *      http://www.virtuosoft.eu/code/bootstrap-touchspin/
 *
 *  Copyright 2013 István Ujj-Mészáros
 *
 *  Thanks for the contributors:
 *      Stefan Bauer - https://github.com/sba
 *      amid2887 - https://github.com/amid2887
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
* ====================================================================== */

(function($) {
    "use strict";

    $.fn.TouchSpin = function(options) {

        var defaults = {
            min: 0,
            max: 100,
            initval: "",
            step: 1,
            decimals: 0,
            stepinterval: 100,
            forcestepdivisibility: 'round',  // none | floor | round | ceil
            stepintervaldelay: 500,
            prefix: "",
            postfix: "",
            prefix_extraclass: "",
            postfix_extraclass: "",
            booster: true,
            boostat: 10,
            maxboostedstep: false,
            mousewheel: true,
            buttondown_class: "btn btn-default",
            buttonup_class: "btn btn-default"
        };

        return this.each(function() {

            var settings,
                originalinput = $(this),
                originalinput_data = originalinput.data(),
                container,
                elements,
                value,
                downSpinTimer,
                upSpinTimer,
                downDelayTimeout,
                upDelayTimeout,
                spincount = 0,
                spinning = false;

            init();

            function init() {
                if (originalinput.data("alreadyinitialized")) {
                    return;
                }

                originalinput.data("alreadyinitialized", true);

                if (!originalinput.is("input")) {
                    console.log("Must be an input.");
                    return;
                }

                _initSettings();
                _setInitval();
                _checkValue();
                _buildHtml();
                _initElements();
                _bindEvents();
                _bindEventsInterface();
            }

            function _setInitval() {
                if (settings.initval !== "" && originalinput.val() === "") {
                    originalinput.val(settings.initval);
                }
            }

            function changeSettings(newsettings) {
                _updateSettings(newsettings);
                _checkValue();

                var value = Number(elements.input.val());
                elements.input.val(value.toFixed(settings.decimals));
            }

            function _initSettings() {
                settings = $.extend({}, defaults, originalinput_data, options);
            }

            function _updateSettings(newsettings) {
                settings = $.extend({}, settings, newsettings);
            }

            function _buildHtml() {
                var initval = originalinput.val(),
                    parentelement = originalinput.parent();

                if (initval !== "") {
                    initval = Number(initval).toFixed(settings.decimals);
                }

                originalinput.data("initvalue", initval).val(initval);
                originalinput.addClass("form-control");

                $("<style type='text/css'>.bootstrap-touchspin-prefix:empty,.bootstrap-touchspin-postfix:empty{display:none;}</style>").appendTo("head");

                if (parentelement.hasClass("input-group")) {
                    _advanceInputGroup(parentelement);
                }
                else {
                    _buildInputGroup();
                }

            }

            function _advanceInputGroup(parentelement) {
                parentelement.addClass("bootstrap-touchspin");

                var prev = originalinput.prev(),
                    next = originalinput.next();

                var downhtml,
                    uphtml,
                    prefixhtml = '<span class="input-group-addon bootstrap-touchspin-prefix">' + settings.prefix + '</span>',
                    postfixhtml = '<span class="input-group-addon bootstrap-touchspin-postfix">' + settings.postfix + '</span>';

                if (prev.hasClass("input-group-btn")) {
                    downhtml = '<button class="' + settings.buttondown_class + ' bootstrap-touchspin-down" type="button">-</button>',
                    prev.append(downhtml);
                }
                else {
                    downhtml = '<span class="input-group-btn"><button class="' + settings.buttondown_class + ' bootstrap-touchspin-down" type="button">-</button></span>';
                    $(downhtml).insertBefore(originalinput);
                }

                if (next.hasClass("input-group-btn")) {
                    uphtml = '<button class="' + settings.buttonup_class + ' bootstrap-touchspin-up" type="button">+</button>',
                    next.prepend(uphtml);
                }
                else {
                    uphtml = '<span class="input-group-btn"><button class="' + settings.buttonup_class + ' bootstrap-touchspin-up" type="button">+</button></span>';
                    $(uphtml).insertAfter(originalinput);
                }

                $(prefixhtml).insertBefore(originalinput);
                $(postfixhtml).insertAfter(originalinput);

                container = parentelement;
            }

            function _buildInputGroup() {
                var html = '<div class="input-group bootstrap-touchspin"><span class="input-group-btn"><button class="' + settings.buttondown_class + ' bootstrap-touchspin-down" type="button">-</button></span><span class="input-group-addon bootstrap-touchspin-prefix">' + settings.prefix + '</span><span class="input-group-addon bootstrap-touchspin-postfix">' + settings.postfix + '</span><span class="input-group-btn"><button class="' + settings.buttonup_class + ' bootstrap-touchspin-up" type="button">+</button></span></div>';

                container = $(html).insertBefore(originalinput);

                $(".bootstrap-touchspin-prefix", container).after(originalinput);

                if (originalinput.hasClass("input-sm")) {
                    container.addClass("input-group-sm");
                }
                else if (originalinput.hasClass("input-lg")) {
                    container.addClass("input-group-lg");
                }
            }

            function _initElements() {
                elements = {
                    down: $(".bootstrap-touchspin-down", container),
                    up: $(".bootstrap-touchspin-up", container),
                    input: $("input", container),
                    prefix: $(".bootstrap-touchspin-prefix", container).addClass(settings.prefix_extraclass),
                    postfix: $(".bootstrap-touchspin-postfix", container).addClass(settings.postfix_extraclass)
                };
            }

            function _bindEvents() {
                originalinput.on("keydown", function(ev) {
                    var code = ev.keyCode || ev.which;

                    if (code === 38) {
                        if (spinning !== "up") {
                            upOnce();
                            startUpSpin();
                        }
                        ev.preventDefault();
                    }
                    else if (code === 40) {
                        if (spinning !== "down") {
                            downOnce();
                            startDownSpin();
                        }
                        ev.preventDefault();
                    }
                });

                originalinput.on("keyup", function(ev) {
                    var code = ev.keyCode || ev.which;

                    if (code === 38) {
                        stopSpin();
                    }
                    else if (code === 40) {
                        stopSpin();
                    }
                });

                originalinput.on("blur", function() {
                    _checkValue();
                });

                elements.down.on("keydown", function(ev) {
                    var code = ev.keyCode || ev.which;

                    if (code === 32 || code === 13) {
                        if (spinning !== "down") {
                            downOnce();
                            startDownSpin();
                        }
                        ev.preventDefault();
                    }
                });

                elements.down.on("keyup", function(ev) {
                    var code = ev.keyCode || ev.which;

                    if (code === 32 || code === 13) {
                        stopSpin();
                    }
                });

                elements.up.on("keydown", function(ev) {
                    var code = ev.keyCode || ev.which;

                    if (code === 32 || code === 13) {
                        if (spinning !== "up") {
                            upOnce();
                            startUpSpin();
                        }
                        ev.preventDefault();
                    }
                });

                elements.up.on("keyup", function(ev) {
                    var code = ev.keyCode || ev.which;

                    if (code === 32 || code === 13) {
                        stopSpin();
                    }
                });

                elements.down.on("mousedown touchstart", function(ev) {
                    downOnce();
                    startDownSpin();

                    ev.preventDefault();
                    ev.stopPropagation();
                });

                elements.up.on("mousedown touchstart", function(ev) {
                    upOnce();
                    startUpSpin();

                    ev.preventDefault();
                    ev.stopPropagation();
                });

                elements.up.on("mouseout touchleave touchend touchcancel", function(ev) {
                    if (!spinning) {
                        return;
                    }

                    ev.stopPropagation();
                    stopSpin();
                });

                elements.down.on("mouseout touchleave touchend touchcancel", function(ev) {
                    if (!spinning) {
                        return;
                    }

                    ev.stopPropagation();
                    stopSpin();
                });

                elements.down.on("mousemove touchmove", function(ev) {
                    if (!spinning) {
                        return;
                    }

                    ev.stopPropagation();
                    ev.preventDefault();
                });

                elements.up.on("mousemove touchmove", function(ev) {
                    if (!spinning) {
                        return;
                    }

                    ev.stopPropagation();
                    ev.preventDefault();
                });

                $(document).on("mouseup touchend touchcancel", function(ev) {
                    if (!spinning) {
                        return;
                    }

                    ev.preventDefault();
                    stopSpin();
                });

                $(document).on("mousemove touchmove scroll scrollstart", function(ev) {
                    if (!spinning) {
                        return;
                    }

                    ev.preventDefault();
                    stopSpin();
                });

                if (settings.mousewheel) {
                    originalinput.on("mousewheel DOMMouseScroll", function(ev) {
                        var delta = ev.originalEvent.wheelDelta || -ev.originalEvent.detail;

                        ev.stopPropagation();
                        ev.preventDefault();

                        if (delta < 0) {
                            downOnce();
                        }
                        else {
                            upOnce();
                        }
                    });
                }
            }

            function _bindEventsInterface() {
                originalinput.on('touchspin.uponce', function() {
                    stopSpin();
                    upOnce();
                });

                originalinput.on('touchspin.downonce', function() {
                    stopSpin();
                    downOnce();
                });

                originalinput.on('touchspin.startupspin', function() {
                    startUpSpin();
                });

                originalinput.on('touchspin.startdownspin', function() {
                    startDownSpin();
                });

                originalinput.on('touchspin.stopspin', function() {
                    stopSpin();
                });

                originalinput.on('touchspin.updatesettings', function(e, newsettings) {
                    changeSettings(newsettings);
                });
            }

            function _forcestepdivisibility(value) {
                switch(settings.forcestepdivisibility) {
                    case 'round':
                        return (Math.round(value / settings.step) * settings.step).toFixed(settings.decimals);
                        break;
                    case 'floor':
                        return (Math.floor(value / settings.step) * settings.step).toFixed(settings.decimals);
                        break;
                    case 'ceil':
                        return (Math.ceil(value / settings.step) * settings.step).toFixed(settings.decimals);
                        break;
                    default:
                        return value;
                }
            }

            function _checkValue() {
                var val, parsedval, returnval;

                val = originalinput.val();

                if (val === "") {
                    return;
                }

                if (settings.decimals > 0 && val === ".") {
                    return;
                }

                parsedval = parseFloat(val);

                if (isNaN(parsedval)) {
                    parsedval = 0;
                }

                returnval = parsedval;

                if (parsedval.toString() !== val) {
                    returnval = parsedval;
                }

                if (parsedval < settings.min) {
                    returnval = settings.min;
                }

                if (parsedval > settings.max) {
                    returnval = settings.max;
                }

                returnval = _forcestepdivisibility(returnval);

                if (Number(val).toString() !== returnval.toString()) {
                    originalinput.val(returnval);
                    originalinput.trigger("change");
                }
            }

            function _getBoostedStep() {
                if (!settings.booster) {
                    return settings.step;
                }
                else {
                    var boosted = Math.pow(2,Math.floor(spincount / settings.boostat)) * settings.step;

                    if (settings.maxboostedstep) {
                        if (boosted > settings.maxboostedstep) {
                            boosted = settings.maxboostedstep;
                            value = Math.round((value / boosted) * boosted);
                        }
                    }

                    return Math.max(settings.step, boosted);
                }
            }

            function upOnce() {
                _checkValue();

                value = parseFloat(elements.input.val());
                if (isNaN(value)) {
                    value = 0;
                }

                var initvalue = value,
                    boostedstep = _getBoostedStep();

                value =  value + boostedstep;

                if (value > settings.max) {
                    stopSpin();
                    value = settings.max;
                    originalinput.trigger("touchspin.max");
                }

                elements.input.val(Number(value).toFixed(settings.decimals));

                if (initvalue !== value) {
                    originalinput.trigger("change");
                }
            }

            function downOnce() {
                _checkValue();

                value = parseFloat(elements.input.val());
                if (isNaN(value)) {
                    value = 0;
                }

                var initvalue = value,
                    boostedstep = _getBoostedStep();

                value = value - boostedstep;

                if (value < settings.min) {
                    stopSpin();
                    value = settings.min;
                    originalinput.trigger("touchspin.min");
                }

                elements.input.val(value.toFixed(settings.decimals));

                if (initvalue !== value) {
                    originalinput.trigger("change");
                }
            }

            function startDownSpin() {
                stopSpin();

                spincount = 0;
                spinning = "down";

                downDelayTimeout = setTimeout(function() {
                    downSpinTimer = setInterval(function() {
                        spincount++;
                        downOnce();
                    }, settings.stepinterval);
                }, settings.stepintervaldelay);
            }

            function startUpSpin() {
                stopSpin();

                spincount = 0;
                spinning = "up";

                upDelayTimeout = setTimeout(function() {
                    upSpinTimer = setInterval(function() {
                        spincount++;
                        upOnce();
                    }, settings.stepinterval);
                }, settings.stepintervaldelay);
            }

            function stopSpin() {
                clearTimeout(downDelayTimeout);
                clearTimeout(upDelayTimeout);
                clearInterval(downSpinTimer);
                clearInterval(upSpinTimer);

                spincount = 0;
                spinning = false;
            }

        });

    };

})(jQuery);