/**
 * @class Spinner Control Widget
 *
 * @author Ken Snyder (kendsnyder at gmail dot com)
 * @date 2007-07-15
 * @version 1.0
 * @license Creative Commons Attribution 3.0 (http://creativecommons.org/licenses/by/3.0/)
 * @requires Prototype version 1.5.0 or newer
 * @tested in FF2, IE7, Safari 3, Opera 9
 */
var SpinnerControl = Class.create();

SpinnerControl.prototype = {
    /**
    * @constructor  Create the spinner using an input, an up button, and a down button
    *
    * @param string/Element  inputElement
    * @param string/Element  upElement
    * @param string/Element  downElement
    * @param object          options
    * Available Options:
    *   interval     The amount to increment (default=1)
    *   round        The number of decimal points to which to round (default=0)
    *   min          The lowest allowed value, false for no min (default=false)
    *   max          The highest allowed value, false for no max (default=false)
    *   prefix       String to prepend when updating (default='')
    *   suffix       String to append when updating (default='')
    *   data         An array giving a list of items through which to iterate (default=false)
    *   onIncrement  Function to call after incrementing
    *   onDecrement  Function to call after decrementing
    *   afterUpdate  Function to call after update of the value
    *   onStop       Function to call on click or mouseup
    * @return void
    */
    initialize: function(inputElement, upElement, downElement, options) {
        // store the elements
        this.inputElement = $(inputElement);
        this.upElement = $(upElement);
        this.downElement = $(downElement);
        // store the options
        this.options = Object.extend({
            interval: 1,
            round: 0,
            min: false,
            max: false,
            prefix: '',
            suffix: '',
            data: false,
            onIncrement: Prototype.emptyFunction,
            onDecrement: Prototype.emptyFunction,
            afterUpdate: Prototype.emptyFunction,
            onStop: Prototype.emptyFunction,
            toValueFormatter: function(value) { return value },
            toDisplayFormatter: function(value) { return value }
        }, options);
        // set initial values
        this.reset();
        // build our update function
        this.buildUpdateFunction();
        // define the rate of increasing speed
        if (Prototype.Browser.IE) {
            this.speedHash = { 5: 300, 10: 175, 20: 90, 30: 17 };
        } else {
            this.speedHash = { 5: 250, 10: 85, 20: 35, 30: 10 };
        }
        // attach listeners
        this.observe();
    },

    setOptions: function(options) {
        this.options = Object.extend(this.options, options);
    },
    
    /**
    * Helper function to define the update function
    *
    * @return void
    */
    buildUpdateFunction: function() {
        // do we have a data list?
        if (this.options.data == false) {
            // no, we are an integer or decimal
            this.updateValue = function(multiplier) {
                // parse the value ignoring the substring
                var toValue = new String(this.options.toValueFormatter(this.inputElement.value));
                var value = parseFloat(toValue.replace(/^(.*?)([\-\d\.]+)(.*)$/, '$2'));
                if (isNaN(value)) value = this.options.min || 0;
                // what are we adding
                if (multiplier == 1) {
                    value = (value + this.options.interval).toFixed(this.options.round);
                } else if (multiplier == -1) {
                    value = (value - this.options.interval).toFixed(this.options.round);
                }
                // ensure value falls between the min and max
                if (this.options.min !== false)
                    value = Math.max(this.options.min, value);
                if (this.options.max !== false)
                    value = Math.min(this.options.max, value);
                this.setValue(value);
                // call our afterUpdate function
                this.options.afterUpdate(this);
            } .bind(this);
            // set an initial value if not given
            if (this.inputElement.value === '') {
                this.inputElement.value = this.options.min || 0;
            }
        } else if (this.options.data.constructor == Array && this.options.data.length) {
            // we have a data list
            // set the position pointer to the current or first element
            var current = this.options.data.indexOf(this.inputElement.value);
            this.pos = current == -1 ? 0 : current;
            // define our function
            this.updateValue = function(multiplier) {
                // advance the pointer forward or backward, wrapping between the last and first item
                this.pos = this.pos + multiplier;
                this.pos = this.pos < 0 ? this.options.data.length - 1 : (
          this.pos > this.options.data.length - 1 ? 0 : this.pos
        );
                // update the value to the prefix, plus the rounded number, plus the suffix
                this.setValue(this.options.data[this.pos]);
                // call our afterUpdate function
                this.options.afterUpdate(this);
            } .bind(this);
            // set an initial value if not given
            if (this.inputElement.value === '') {
                this.inputElement.value = this.options.data[0];
            }
        } else {
            // we have an invalid data option
            throw new Error('SpinnerControl.initialize(): invlalid value for options.data');
        }
    },
    setValue: function(value) {
        this.inputElement.value = this.options.prefix + this.options.toDisplayFormatter(value) + this.options.suffix;
    },
    /**
    * Helper function to attach listeners
    */
    observe: function() {
        // define a pre-bound stop function
        var stop = this.stop.bind(this);
        // observe the input
        this.inputElement
        // begin incrementing at start of a keypress
      .observe('keydown', this.keyStart.bindAsEventListener(this))
        // stop incrementing at the end of a keypress
      .observe('keyup', stop)
        // reformat and enforce min-max for typed values
      .observe('blur', this.updateValue.bind(this, 0));
        // observe the up element
        this.upElement
        // begin incrementing at start of click
      .observe('mousedown', this.clickStart.bind(this, 1))
        // stop incrementing at end of click
      .observe('mouseup', stop)
        // in the case of a click and drag, also stop
      .observe('mouseout', stop);
        // observe the down element
        this.downElement
        // begin decrementing at start of click
      .observe('mousedown', this.clickStart.bind(this, -1))
        // stop decrementing at end of click
      .observe('mouseup', stop)
        // in the case of a click and drag, also stop
      .observe('mouseout', stop);
    },
    /**
    * Start incrementing or decrementing based on a pressed key
    *
    * @event keydown on this.inputElement
    * @param object evt
    * @return void
    */
    keyStart: function(evt) {
        if (this.running == false) {

            if (evt.keyCode == Event.KEY_UP) {
                this.running = 'key';
                this.increment();
            }
            else if (evt.keyCode == Event.KEY_DOWN) {
                this.running = 'key';
                this.decrement();
            }
        }
    },
    /**
    * Start incrementing or decrementing based on a mousedown action
    *
    * @param boolean multiplier  If multipler is 1, increment
    * @return void
    */
    clickStart: function(multiplier) {
        this.running = 'mouse';
        if (multiplier == 1) {
            this.increment();
        } else {
            this.decrement();
        }
    },
    /**
    * Set to resting state
    *
    * return @void
    */
    reset: function() {
        // blur the up/down buttons if we got started by clicking
        if (this.running == 'mouse') {
            this.upElement.blur();
            this.downElement.blur();
        }
        this.running = false;
        this.iterations = 0;
    },
    /**
    * Reset and clear timeout
    *
    * @return void
    */
    stop: function() {
        this.reset();
        window.clearTimeout(this.timeout);
        this.options.onStop(this);
    },
    /**
    * Increment the value
    *
    * @return void
    */
    increment: function() {
        this.updateValue(1);
        this.timeout = window.setTimeout(this.increment.bind(this), this.getSpeed());
        this.options.onIncrement(this);
    },
    /**
    * Decrement the value
    *
    * @return void
    */
    decrement: function() {
        this.updateValue(-1);
        this.timeout = window.setTimeout(this.decrement.bind(this), this.getSpeed());
        this.options.onDecrement(this);
    },
    /**
    * Get the delay for the next timeout
    * Overwrite this function for custom speed schemes
    *
    * @return integer
    */
    getSpeed: function() {
        this.iterations++;
        for (var iterations in this.speedHash) {
            if (this.iterations < iterations) {
                return this.speedHash[iterations];
            }
        }
        return this.speedHash[30];
    }
};
