/**
 * @license
 * jQuery Tools @VERSION Rangeinput - HTML5 <input type="range" /> for humans
 *
 * NO COPYRIGHTS OR LICENSES. DO WHAT YOU LIKE.
 *
 * http://flowplayer.org/tools/rangeinput/
 *
 * Since: Mar 2010
 * Date: @DATE
 */
(function($) {

    $.tools = $.tools || {version: '@VERSION'};

    var tool;

    tool = $.tools.rangeinput = {

        conf: {
            min: 0,
            max: 100,		// as defined in the standard
            step: 'any', 	// granularity of the value. a non-zero float or int (or "any")
            steps: 0,
            value: 0,
            precision: undefined,
            vertical: 0,
            keyboard: true,
            progress: false,
            speed: 100,

            // set to null if not needed
            css: {
                input:		'range',
                slider: 		'slider',
                progress: 	'progress',
                handle: 		'handle'
            }

        }
    };

//{{{ fn.drag

    /*
     FULL featured drag and drop. 0.7 kb minified, 0.3 gzipped. done.
     Who told d'n'd is rocket science? Usage:

     $(".myelement").drag({y: false}).on("drag", function(event, x, y) {
     // do your custom thing
     });

     Configuration:
     x: true, 		// enable horizontal drag
     y: true, 		// enable vertical drag
     drag: true 		// true = perform drag, false = only fire events

     Events: dragStart, drag, dragEnd.
     */
    var doc, draggable;

    $.fn.drag = function(conf) {

        conf = $.extend({x: true, y: true, drag: true}, conf);

        doc = doc || $(document).on("mousedown mouseup", function(e) {

            var el = $(e.target);

            // start
            if (e.type == "mousedown" && el.data("drag")) {

                var offset = el.position(),
                    x0 = e.pageX - offset.left,
                    y0 = e.pageY - offset.top,
                    start = true;

                doc.on("mousemove.drag", function(e) {
                    var x = e.pageX -x0,
                        y = e.pageY -y0,
                        props = {};

                    if (conf.x) { props.left = x; }
                    if (conf.y) { props.top = y; }

                    if (start) {
                        el.trigger("dragStart");
                        start = false;
                    }
                    if (conf.drag) { el.css(props); }
                    el.trigger("drag", [y, x]);
                    draggable = el;
                });

                e.preventDefault();

            } else {

                try {
                    if (draggable) {
                        draggable.trigger("dragEnd");
                    }
                } finally {
                    doc.off("mousemove.drag");
                    draggable = null;
                }
            }

        });

        return this.data("drag", true);
    };

//}}}



    function round(value, precision) {
        var n = Math.pow(10, precision);
        return Math.round(value * n) / n;
    }

    // get hidden element's width or height even though it's hidden
    function dim(el, key) {
        var v = parseInt(el.css(key), 10);
        if (v) { return v; }
        var s = el[0].currentStyle;
        return s && s.width && parseInt(s.width, 10);
    }

    function hasEvent(el) {
        var e = $._data(el[0], "events");
        return e && e.onSlide;
    }

    function RangeInput(input, conf) {

        // private variables
        var self = this,
            css = conf.css,
            root = $("<div><div/><a href='#'/></div>").data("rangeinput", self),
            vertical,
            value,			// current value
            origo,			// handle's start point
            len,				// length of the range
            pos;				// current position of the handle

        // create range
        input.before(root);

        var handle = root.addClass(css.slider).find("a").addClass(css.handle),
            progress = root.find("div").addClass(css.progress);

        // get (HTML5) attributes into configuration
        $.each("min,max,step,value".split(","), function(i, key) {
            var val = input.attr(key);
            if (parseFloat(val)) {
                conf[key] = parseFloat(val, 10);
            }
        });

        var range = conf.max - conf.min,
            step = conf.step == 'any' ? 0 : conf.step,
            precision = conf.precision;

        if (precision === undefined) {
            precision = step.toString().split(".");
            precision = precision.length === 2 ? precision[1].length : 0;
        }

        // Replace built-in range input (type attribute cannot be changed)
        if (input.attr("type") == 'range') {
            var def = input.clone().wrap("<div/>").parent().html(),
                clone = $(def.replace(/type/i, "type=text data-orig-type"));

            clone.val(conf.value);
            input.replaceWith(clone);
            input = clone;
        }

        input.addClass(css.input);

        var fire = $(self).add(input), fireOnSlide = true;


        /**
         The flesh and bone of this tool. All sliding is routed trough this.

         @param evt types include: click, keydown, blur and api (setValue call)
         @param isSetValue when called trough setValue() call (keydown, blur, api)

         vertical configuration gives additional complexity.
         */
        function slide(evt, x, val, isSetValue) {

            // calculate value based on slide position
            if (val === undefined) {
                val = x / len * range;

                // x is calculated based on val. we need to strip off min during calculation
            } else if (isSetValue) {
                val -= conf.min;
            }

            // increment in steps
            if (step) {
                val = Math.round(val / step) * step;
            }

            // count x based on value or tweak x if stepping is done
            if (x === undefined || step) {
                x = val * len / range;
            }

            // crazy value?
            if (isNaN(val)) { return self; }

            // stay within range
            x = Math.max(0, Math.min(x, len));
            val = x / len * range;

            if (isSetValue || !vertical) {
                val += conf.min;
            }

            // in vertical ranges value rises upwards
            if (vertical) {
                if (isSetValue) {
                    x = len -x;
                } else {
                    val = conf.max - val;
                }
            }

            // precision
            val = round(val, precision);

            // onSlide
            var isClick = evt.type == "click";
            if (fireOnSlide && value !== undefined && !isClick) {
                evt.type = "onSlide";
                fire.trigger(evt, [val, x]);
                if (evt.isDefaultPrevented()) { return self; }
            }

            // speed & callback
            var speed = isClick ? conf.speed : 0,
                callback = isClick ? function()  {
                    evt.type = "change";
                    fire.trigger(evt, [val]);
                } : null;

            if (vertical) {
                handle.animate({top: x}, speed, callback);
                if (conf.progress) {
                    progress.animate({height: len - x + handle.height() / 2}, speed);
                }

            } else {
                handle.animate({left: x}, speed, callback);
                if (conf.progress) {
                    progress.animate({width: x + handle.width() / 2}, speed);
                }
            }

            // store current value
            value = val;
            pos = x;

            // se input field's value
            input.val(val);

            return self;
        }


        $.extend(self, {

            getValue: function() {
                return value;
            },

            setValue: function(val, e) {
                init();
                return slide(e || $.Event("api"), undefined, val, true);
            },

            getConf: function() {
                return conf;
            },

            getProgress: function() {
                return progress;
            },

            getHandle: function() {
                return handle;
            },

            getInput: function() {
                return input;
            },

            step: function(am, e) {
                e = e || $.Event();
                var step = conf.step == 'any' ? 1 : conf.step;
                self.setValue(value + step * (am || 1), e);
            },

            // HTML5 compatible name
            stepUp: function(am) {
                return self.step(am || 1);
            },

            // HTML5 compatible name
            stepDown: function(am) {
                return self.step(-am || -1);
            }

        });

        // callbacks
        $.each("onSlide,change".split(","), function(i, name) {

            // from configuration
            if ($.isFunction(conf[name]))  {
                $(self).on(name, conf[name]);
            }

            // API methods
            self[name] = function(fn) {
                if (fn) { $(self).on(name, fn); }
                return self;
            };
        });


        // dragging
        handle.drag({drag: false}).on("dragStart", function() {

            /* do some pre- calculations for seek() function. improves performance */
            init();

            // avoid redundant event triggering (= heavy stuff)
            fireOnSlide = hasEvent($(self)) || hasEvent(input);


        }).on("drag", function(e, y, x) {

            if (input.is(":disabled")) { return false; }
            slide(e, vertical ? y : x);

        }).on("dragEnd", function(e) {
            if (!e.isDefaultPrevented()) {
                e.type = "change";
                fire.trigger(e, [value]);
            }

        }).click(function(e) {
            return e.preventDefault();
        });

        // clicking
        root.click(function(e) {
            if (input.is(":disabled") || e.target == handle[0]) {
                return e.preventDefault();
            }
            init();
            var fix = vertical ? handle.height() / 2 : handle.width() / 2;
            slide(e, vertical ? len-origo-fix + e.pageY  : e.pageX -origo -fix);
        });

        if (conf.keyboard) {

            input.keydown(function(e) {

                if (input.attr("readonly")) { return; }

                var key = e.keyCode,
                    up = $([75, 76, 38, 33, 39]).index(key) != -1,
                    down = $([74, 72, 40, 34, 37]).index(key) != -1;

                if ((up || down) && !(e.shiftKey || e.altKey || e.ctrlKey)) {

                    // UP: 	k=75, l=76, up=38, pageup=33, right=39
                    if (up) {
                        self.step(key == 33 ? 10 : 1, e);

                        // DOWN:	j=74, h=72, down=40, pagedown=34, left=37
                    } else if (down) {
                        self.step(key == 34 ? -10 : -1, e);
                    }
                    return e.preventDefault();
                }
            });
        }


        input.blur(function(e) {
            var val = $(this).val();
            if (val !== value) {
                self.setValue(val, e);
            }
        });


        // HTML5 DOM methods
        $.extend(input[0], { stepUp: self.stepUp, stepDown: self.stepDown});


        // calculate all dimension related stuff
        function init() {
            vertical = conf.vertical || dim(root, "height") > dim(root, "width");

            if (vertical) {
                len = dim(root, "height") - dim(handle, "height");
                origo = root.offset().top + len;

            } else {
                len = dim(root, "width") - dim(handle, "width");
                origo = root.offset().left;
            }
        }

        function begin() {
            init();
            self.setValue(conf.value !== undefined ? conf.value : conf.min);
        }
        begin();

        // some browsers cannot get dimensions upon initialization
        if (!len) {
            $(window).load(begin);
        }
    }

    $.expr[':'].range = function(el) {
        var type = el.getAttribute("type");
        return type && type == 'range' || !!$(el).filter("input").data("rangeinput");
    };


    // jQuery plugin implementation
    $.fn.rangeinput = function(conf) {

        // already installed
        if (this.data("rangeinput")) { return this; }

        // extend configuration with globals
        conf = $.extend(true, {}, tool.conf, conf);

        var els;

        this.each(function() {
            var el = new RangeInput($(this), $.extend(true, {}, conf));
            var input = el.getInput().data("rangeinput", el);
            els = els ? els.add(input) : input;
        });

        return els ? els : this;
    };


}) (jQuery);
