/*
Copyright (c) 2008, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.net/yui/license.txt
version: 2.6.0
*/
/**
 * Provides color conversion and validation utils
 * @class YAHOO.util.Color
 * @namespace YAHOO.util
 */
YAHOO.util.Color = function() {

    var HCHARS="0123456789ABCDEF", lang=YAHOO.lang;

    return {

        /**
         * Converts 0-1 to 0-255
         * @method real2dec
         * @param n {float} the number to convert
         * @return {int} a number 0-255
         */
        real2dec: function(n) {
            return Math.min(255, Math.round(n*256));
        },

        /**
         * Converts HSV (h[0-360], s[0-1]), v[0-1] to RGB [255,255,255]
         * @method hsv2rgb
         * @param h {int|[int, float, float]} the hue, or an
         *        array containing all three parameters
         * @param s {float} the saturation
         * @param v {float} the value/brightness
         * @return {[int, int, int]} the red, green, blue values in
         *          decimal.
         */
        hsv2rgb: function(h, s, v) { 

            if (lang.isArray(h)) {
                return this.hsv2rgb.call(this, h[0], h[1], h[2]);
            }

            var r, g, b, i, f, p, q, t;
            i = Math.floor((h/60)%6);
            f = (h/60)-i;
            p = v*(1-s);
            q = v*(1-f*s);
            t = v*(1-(1-f)*s);
            switch(i) {
                case 0: r=v; g=t; b=p; break;
                case 1: r=q; g=v; b=p; break;
                case 2: r=p; g=v; b=t; break;
                case 3: r=p; g=q; b=v; break;
                case 4: r=t; g=p; b=v; break;
                case 5: r=v; g=p; b=q; break;
            }

            var fn=this.real2dec;

            return [fn(r), fn(g), fn(b)];
        },

        /**
         * Converts to RGB [255,255,255] to HSV (h[0-360], s[0-1]), v[0-1]
         * @method rgb2hsv
         * @param r {int|[int, int, int]} the red value, or an
         *        array containing all three parameters
         * @param g {int} the green value
         * @param b {int} the blue value
         * @return {[int, float, float]} the value converted to hsv
         */
        rgb2hsv: function(r, g, b) {

            if (lang.isArray(r)) {
                return this.rgb2hsv.call(this, r[0], r[1], r[2]);
            }

            r=r/255;
            g=g/255;
            b=b/255;

            var min,max,delta,h,s,v;
            min = Math.min(Math.min(r,g),b);
            max = Math.max(Math.max(r,g),b);
            delta = max-min;

            switch (max) {
                case min: h=0; break;
                case r:   h=60*(g-b)/delta; 
                          if (g<b) {
                              h+=360;
                          }
                          break;
                case g:   h=(60*(b-r)/delta)+120; break;
                case b:   h=(60*(r-g)/delta)+240; break;
            }
            
            s = (max === 0) ? 0 : 1-(min/max);

            var hsv = [Math.round(h), s, max];

            return hsv;

        },

        /**
         * Converts decimal rgb values into a hex string
         * 255,255,255 -> FFFFFF
         * @method rgb2hex
         * @param r {int|[int, int, int]} the red value, or an
         *        array containing all three parameters
         * @param g {int} the green value
         * @param b {int} the blue value
         * @return {string} the hex string
         */
        rgb2hex: function(r, g, b) {
            if (lang.isArray(r)) {
                return this.rgb2hex.call(this, r[0], r[1], r[2]);
            }

            var f=this.dec2hex;
            return f(r) + f(g) + f(b);
        },
     
        /**
         * Converts an int 0...255 to hex pair 00...FF
         * @method dec2hex
         * @param n {int} the number to convert
         * @return {string} the hex equivalent
         */
        dec2hex: function(n) {
            n = parseInt(n, 10);
            n = (lang.isNumber(n)) ? n : 0;
            n = (n > 255 || n < 0) ? 0 : n;

            return HCHARS.charAt((n - n % 16) / 16) + HCHARS.charAt(n % 16);
        },

        /**
         * Converts a hex pair 00...FF to an int 0...255 
         * @method hex2dec
         * @param str {string} the hex pair to convert
         * @return {int} the decimal
         */
        hex2dec: function(str) {
            var f = function(c) {
                return HCHARS.indexOf(c.toUpperCase());
            };

            var s=str.split('');
            
            return ((f(s[0]) * 16) + f(s[1]));
        },

        /**
         * Converts a hex string to rgb
         * @method hex2rgb
         * @param str {string} the hex string
         * @return {[int, int, int]} an array containing the rgb values
         */
        hex2rgb: function(s) { 
            var f = this.hex2dec;
            return [f(s.substr(0, 2)), f(s.substr(2, 2)), f(s.substr(4, 2))];
        },

        /**
         * Returns the closest websafe color to the supplied rgb value.
         * @method websafe
         * @param r {int|[int, int, int]} the red value, or an
         *        array containing all three parameters
         * @param g {int} the green value
         * @param b {int} the blue value
         * @return {[int, int, int]} an array containing the closes
         *                           websafe rgb colors.
         */
        websafe: function(r, g, b) {

            if (lang.isArray(r)) {
                return this.websafe.call(this, r[0], r[1], r[2]);
            }

            // returns the closest match [0, 51, 102, 153, 204, 255]
            var f = function(v) {
                if (lang.isNumber(v)) {
                    v = Math.min(Math.max(0, v), 255);
                    var i, next;
                    for (i=0; i<256; i=i+51) {
                        next = i+51;
                        if (v >= i && v <= next) {
                            return (v-i > 25) ? next : i;
                        }
                    }
 YAHOO.log("Error calculating the websafe value for " + v, "warn");
                }

                return v;
            };

            return [f(r), f(g), f(b)];
        }
    };
}();


(function() {

    var _pickercount = 0;

    /**
     * The colorpicker module provides a widget for selecting colors
     * @module colorpicker
     * @requires yahoo, dom, event, element, slider
     */


    /**
     * Creates the host element if it doesn't exist
     * @method _createHostElement
     * @private
     */
    var _createHostElement = function() {
        var el = document.createElement('div');

        if (this.CSS.BASE) {
            el.className = this.CSS.BASE;
        }
        
        return el;
    };

    /**
     * A widget to select colors
     * @namespace YAHOO.widget
     * @class YAHOO.widget.ColorPicker
     * @extends YAHOO.util.Element
     * @constructor
     * @param {HTMLElement | String | Object} el(optional) The html 
     * element that represents the colorpicker, or the attribute object to use. 
     * An element will be created if none provided.
     * @param {Object} attr (optional) A key map of the colorpicker's 
     * initial attributes.  Ignored if first arg is attributes object.
     */
    YAHOO.widget.ColorPicker = function(el, attr) {
        _pickercount = _pickercount + 1;
        this.logger = new YAHOO.widget.LogWriter("ColorPicker");
        attr = attr || {};
        if (arguments.length === 1 && !YAHOO.lang.isString(el) && !el.nodeName) {
            attr = el; // treat first arg as attr object
            el = attr.element || null;
        }
        
        if (!el && !attr.element) { // create if we dont have one
            this.logger.log("creating host element");
            el = _createHostElement.call(this, attr);
        }

    	YAHOO.widget.ColorPicker.superclass.constructor.call(this, el, attr); 
    };

    YAHOO.extend(YAHOO.widget.ColorPicker, YAHOO.util.Element);
    
    var proto = YAHOO.widget.ColorPicker.prototype,
        Slider=YAHOO.widget.Slider,
        Color=YAHOO.util.Color,
        Dom = YAHOO.util.Dom,
        Event = YAHOO.util.Event,
        lang = YAHOO.lang,
        sub = lang.substitute;
    

    var b = "yui-picker";

    /**
     * The element ids used by this control
     * @property ID
     * @final
     */
    proto.ID = {

        /**
         * The id for the "red" form field
         * @property ID.R
         * @type String
         * @final
         * @default yui-picker-r
         */
        R: b + "-r",

        /**
         * The id for the "red" hex pair output
         * @property ID.R_HEX
         * @type String
         * @final
         * @default yui-picker-rhex
         */
        R_HEX: b + "-rhex",

        /**
         * The id for the "green" form field
         * @property ID.G
         * @type String
         * @final
         * @default yui-picker-g
         */
        G: b + "-g",

        /**
         * The id for the "green" hex pair output
         * @property ID.G_HEX
         * @type String
         * @final
         * @default yui-picker-ghex
         */
        G_HEX: b + "-ghex",


        /**
         * The id for the "blue" form field
         * @property ID.B
         * @type String
         * @final
         * @default yui-picker-b
         */
        B: b + "-b",

        /**
         * The id for the "blue" hex pair output
         * @property ID.B_HEX
         * @type String
         * @final
         * @default yui-picker-bhex
         */
        B_HEX: b + "-bhex",

        /**
         * The id for the "hue" form field
         * @property ID.H
         * @type String
         * @final
         * @default yui-picker-h
         */
        H: b + "-h",

        /**
         * The id for the "saturation" form field
         * @property ID.S
         * @type String
         * @final
         * @default yui-picker-s
         */
        S: b + "-s",

        /**
         * The id for the "value" form field
         * @property ID.V
         * @type String
         * @final
         * @default yui-picker-v
         */
        V: b + "-v",

        /**
         * The id for the picker region slider
         * @property ID.PICKER_BG
         * @type String
         * @final
         * @default yui-picker-bg
         */
        PICKER_BG:      b + "-bg",

        /**
         * The id for the picker region thumb
         * @property ID.PICKER_THUMB
         * @type String
         * @final
         * @default yui-picker-thumb
         */
        PICKER_THUMB:   b + "-thumb",

        /**
         * The id for the hue slider
         * @property ID.HUE_BG
         * @type String
         * @final
         * @default yui-picker-hue-bg
         */
        HUE_BG:         b + "-hue-bg",

        /**
         * The id for the hue thumb
         * @property ID.HUE_THUMB
         * @type String
         * @final
         * @default yui-picker-hue-thumb
         */
        HUE_THUMB:      b + "-hue-thumb",

        /**
         * The id for the hex value form field
         * @property ID.HEX
         * @type String
         * @final
         * @default yui-picker-hex
         */
        HEX:            b + "-hex",

        /**
         * The id for the color swatch
         * @property ID.SWATCH
         * @type String
         * @final
         * @default yui-picker-swatch
         */
        SWATCH:         b + "-swatch",

        /**
         * The id for the websafe color swatch
         * @property ID.WEBSAFE_SWATCH
         * @type String
         * @final
         * @default yui-picker-websafe-swatch
         */
        WEBSAFE_SWATCH: b + "-websafe-swatch",

        /**
         * The id for the control details
         * @property ID.CONTROLS
         * @final
         * @default yui-picker-controls
         */
        CONTROLS: b + "-controls",

        /**
         * The id for the rgb controls
         * @property ID.RGB_CONTROLS
         * @final
         * @default yui-picker-rgb-controls
         */
        RGB_CONTROLS: b + "-rgb-controls",

        /**
         * The id for the hsv controls
         * @property ID.HSV_CONTROLS
         * @final
         * @default yui-picker-hsv-controls
         */
        HSV_CONTROLS: b + "-hsv-controls",
        
        /**
         * The id for the hsv controls
         * @property ID.HEX_CONTROLS
         * @final
         * @default yui-picker-hex-controls
         */
        HEX_CONTROLS: b + "-hex-controls",

        /**
         * The id for the hex summary
         * @property ID.HEX_SUMMARY
         * @final
         * @default yui-picker-hex-summary
         */
        HEX_SUMMARY: b + "-hex-summary",

        /**
         * The id for the controls section header
         * @property ID.CONTROLS_LABEL
         * @final
         * @default yui-picker-controls-label
         */
        CONTROLS_LABEL: b + "-controls-label"
    };

    /**
     * Constants for any script-generated messages.  The values here
     * are the default messages.  They can be updated by providing
     * the complete list to the constructor for the "txt" attribute.
     * @property TXT
     * @final
     */
    proto.TXT = {
        ILLEGAL_HEX: "Illegal hex value entered",
        SHOW_CONTROLS: "Show color details",
        HIDE_CONTROLS: "Hide color details",
        CURRENT_COLOR: "Currently selected color: {rgb}",
        CLOSEST_WEBSAFE: "Closest websafe color: {rgb}. Click to select.",
        R: "R",
        G: "G",
        B: "B",
        H: "H",
        S: "S",
        V: "V",
        HEX: "#",
        DEG: "\u00B0",
        PERCENT: "%"
    };

    /**
     * Constants for the default image locations for img tags that are
     * generated by the control.  They can be modified by passing the
     * complete list to the contructor for the "images" attribute
     * @property IMAGE
     * @final
     */
    proto.IMAGE = {
        PICKER_THUMB: "../../build/colorpicker/assets/picker_thumb.png",
        HUE_THUMB: "../../build/colorpicker/assets/hue_thumb.png"
    };

    /*
     * Constants for the control's custom event names.  subscribe
     * to the rgbChange event instead.
     * @property EVENT
     * @final
     */
    //proto.EVENT = {
        //CHANGE: "change"
    //};

    //proto.CSS = { };

    /**
     * Constants for the control's default default values
     * @property DEFAULT
     * @final
     */
    proto.DEFAULT = {
        PICKER_SIZE: 180
    };

    /**
     * Constants for the control's configuration attributes
     * @property OPT
     * @final
     */
    proto.OPT = {
        HUE: "hue",
        SATURATION: "saturation",
        VALUE: "value",
        RED: "red",
        GREEN: "green",
        BLUE: "blue",
        HSV: "hsv",
        RGB: "rgb",
        WEBSAFE: "websafe",
        HEX: "hex",
        PICKER_SIZE: "pickersize",
        SHOW_CONTROLS: "showcontrols",
        SHOW_RGB_CONTROLS: "showrgbcontrols",
        SHOW_HSV_CONTROLS: "showhsvcontrols",
        SHOW_HEX_CONTROLS: "showhexcontrols",
        SHOW_HEX_SUMMARY: "showhexsummary",
        SHOW_WEBSAFE: "showwebsafe",
        //SHOW_SUBMIT: "showsubmit",
        CONTAINER: "container",
        IDS: "ids",
        ELEMENTS: "elements",
        TXT: "txt",
        IMAGES: "images",
        ANIMATE: "animate"
    };

    /**
     * Moves the hue slider into the position dictated by the current state
     * of the control
     * @method _updateHueSlider
     * @private
     */
    var _updateHueSlider = function() {
        var size = this.get(this.OPT.PICKER_SIZE),
            h = this.get(this.OPT.HUE);

        h = size - Math.round(h / 360 * size);
        
        // 0 is at the top and bottom of the hue slider.  Always go to
        // the top so we don't end up sending the thumb to the bottom
        // when the value didn't actually change (e.g., a conversion
        // produced 360 instead of 0 and the value was already 0).
        if (h === size) {
            h = 0;
        }
        this.logger.log("Hue slider is being set to " + h);

        this.hueSlider.setValue(h);
    };

    /**
     * Moves the picker slider into the position dictated by the current state
     * of the control
     * @method _updatePickerSlider
     * @private
     */
    var _updatePickerSlider = function() {
        var size = this.get(this.OPT.PICKER_SIZE),
            s = this.get(this.OPT.SATURATION),
            v = this.get(this.OPT.VALUE);

        s = Math.round(s * size / 100);
        v = Math.round(size - (v * size / 100));

        this.logger.log("Setting picker slider to " + [s, v]);

        this.pickerSlider.setRegionValue(s, v);
    };

    /**
     * Moves the sliders into the position dictated by the current state
     * of the control
     * @method _updateSliders
     * @private
     */
    var _updateSliders = function() {
        _updateHueSlider.call(this);
        _updatePickerSlider.call(this);
    };

    /**
     * Sets the control to the specified rgb value and
     * moves the sliders to the proper positions
     * @method setValue
     * @param rgb {[int, int, int]} the rgb value
     * @param silent {boolean} whether or not to fire the change event
     */
    proto.setValue = function(rgb, silent) {
        silent = (silent) || false;
        this.set(this.OPT.RGB, rgb, silent);
        _updateSliders.call(this);
    };

    /**
     * The hue slider
     * @property hueSlider
     * @type YAHOO.widget.Slider
     */
    proto.hueSlider = null; 
    
    /**
     * The picker region
     * @property pickerSlider
     * @type YAHOO.widget.Slider
     */
    proto.pickerSlider = null;

    /**
     * Translates the slider value into hue, int[0,359]
     * @method _getH
     * @private
     * @return {int} the hue from 0 to 359
     */
    var _getH = function() {
        var size = this.get(this.OPT.PICKER_SIZE),
            h = (size - this.hueSlider.getValue()) / size;
        h = Math.round(h*360);
        return (h === 360) ? 0 : h;
    };

    /**
     * Translates the slider value into saturation, int[0,1], left to right
     * @method _getS
     * @private
     * @return {int} the saturation from 0 to 1
     */
    var _getS = function() {
        return this.pickerSlider.getXValue() / this.get(this.OPT.PICKER_SIZE);
    };

    /**
     * Translates the slider value into value/brightness, int[0,1], top
     * to bottom
     * @method _getV
     * @private
     * @return {int} the value from 0 to 1
     */
    var _getV = function() {
        var size = this.get(this.OPT.PICKER_SIZE);
        return (size - this.pickerSlider.getYValue()) / size;
    };

    /**
     * Updates the background of the swatch with the current rbg value.
     * Also updates the websafe swatch to the closest websafe color
     * @method _updateSwatch
     * @private
     */
    var _updateSwatch = function() {
        var rgb = this.get(this.OPT.RGB),
            websafe = this.get(this.OPT.WEBSAFE),
            el = this.getElement(this.ID.SWATCH),
            color = rgb.join(","),
            txt = this.get(this.OPT.TXT);

        Dom.setStyle(el, "background-color", "rgb(" + color  + ")");
        el.title = lang.substitute(txt.CURRENT_COLOR, {
                "rgb": "#" + this.get(this.OPT.HEX)
            });


        el = this.getElement(this.ID.WEBSAFE_SWATCH);
        color = websafe.join(",");

        Dom.setStyle(el, "background-color", "rgb(" + color + ")");
        el.title = lang.substitute(txt.CLOSEST_WEBSAFE, {
                "rgb": "#" + Color.rgb2hex(websafe)
            });

    };

    /**
     * Reads the sliders and converts the values to RGB, updating the
     * internal state for all the individual form fields
     * @method _getValuesFromSliders
     * @private
     */
    var _getValuesFromSliders = function() {
        var h=_getH.call(this), s=_getS.call(this), v=_getV.call(this);
        YAHOO.log("hsv " + [h, s, v]);

        var rgb = Color.hsv2rgb(h, s, v);
        //var websafe = Color.websafe(rgb);
        //var hex = Color.rgb2hex(rgb[0], rgb[1], rgb[2]);

        this.set(this.OPT.RGB, rgb);
    };

    /**
     * Updates the form field controls with the state data contained
     * in the control.
     * @method _updateFormFields
     * @private
     */
    var _updateFormFields = function() {
        this.getElement(this.ID.H).value = this.get(this.OPT.HUE);
        this.getElement(this.ID.S).value = this.get(this.OPT.SATURATION);
        this.getElement(this.ID.V).value = this.get(this.OPT.VALUE);
        this.getElement(this.ID.R).value = this.get(this.OPT.RED);
        this.getElement(this.ID.R_HEX).innerHTML = Color.dec2hex(this.get(this.OPT.RED));
        this.getElement(this.ID.G).value = this.get(this.OPT.GREEN);
        this.getElement(this.ID.G_HEX).innerHTML = Color.dec2hex(this.get(this.OPT.GREEN));
        this.getElement(this.ID.B).value = this.get(this.OPT.BLUE);
        this.getElement(this.ID.B_HEX).innerHTML = Color.dec2hex(this.get(this.OPT.BLUE));
        this.getElement(this.ID.HEX).value = this.get(this.OPT.HEX);
    };

    /**
     * Event handler for the hue slider.
     * @method _onHueSliderChange
     * @param newOffset {int} pixels from the start position
     * @private
     */
    var _onHueSliderChange = function(newOffset) {
        this.logger.log("hue update: " + newOffset , "warn");

        var h = _getH.call(this);
        this.set(this.OPT.HUE, h, true);

        // set picker background to the hue
        var rgb = Color.hsv2rgb(h, 1, 1);
        var styleDef = "rgb(" + rgb.join(",") + ")";

        Dom.setStyle(this.getElement(this.ID.PICKER_BG), "background-color", styleDef);

        if (this.hueSlider.valueChangeSource === this.hueSlider.SOURCE_UI_EVENT) {
            _getValuesFromSliders.call(this);
        }

        _updateFormFields.call(this);
        _updateSwatch.call(this);
    };

    /**
     * Event handler for the picker slider, which controls the
     * saturation and value/brightness.
     * @method _onPickerSliderChange
     * @param newOffset {{x: int, y: int}} x/y pixels from the start position
     * @private
     */
    var _onPickerSliderChange = function(newOffset) {
        this.logger.log(sub("picker update [{x}, {y}]", newOffset));

        var s=_getS.call(this), v=_getV.call(this);
        this.set(this.OPT.SATURATION, Math.round(s*100), true);
        this.set(this.OPT.VALUE, Math.round(v*100), true);

        if (this.pickerSlider.valueChangeSource === this.pickerSlider.SOURCE_UI_EVENT) {
            _getValuesFromSliders.call(this);
        }

        _updateFormFields.call(this);
        _updateSwatch.call(this);
    };


    /**
     * Key map to well-known commands for txt field input
     * @method _getCommand
     * @param e {Event} the keypress or keydown event
     * @return {int} a command code
     * <ul>
     * <li>0 = not a number, letter in range, or special key</li>
     * <li>1 = number</li>
     * <li>2 = a-fA-F</li>
     * <li>3 = increment (up arrow)</li>
     * <li>4 = decrement (down arrow)</li>
     * <li>5 = special key (tab, delete, return, escape, left, right)</li> 
     * <li>6 = return</li>
     * </ul>
     * @private
     */
    var _getCommand = function(e) {
        var c = Event.getCharCode(e);

        //alert(Event.getCharCode(e) + ", " + e.keyCode + ", " + e.charCode);

        // special keys
        if (c === 38) { // up arrow
            return 3;
        } else if (c === 13) { // return
            return 6;
        } else if (c === 40) { // down array
            return 4;
        } else if (c >= 48 && c<=57) { // 0-9
            return 1;
        } else if (c >= 97 && c<=102) { // a-f
            return 2;
        } else if (c >= 65 && c<=70) { // A-F
            return 2;
        //} else if ("8, 9, 13, 27, 37, 39".indexOf(c) > -1 || 
        //              (c >= 112 && c <=123)) { // including F-keys
        // tab, delete, return, escape, left, right
        } else if ("8, 9, 13, 27, 37, 39".indexOf(c) > -1) { // special chars
            return 5;
        } else { // something we probably don't want
            return 0;
        }
    };

    /**
     * Use the value of the text field to update the control
     * @method _hexFieldKeypress
     * @param e {Event} an event
     * @param el {HTMLElement} the field
     * @param prop {string} the key to the linked property
     * @private
     */
    var _useFieldValue = function(e, el, prop) {
        var val = el.value;

        if (prop !== this.OPT.HEX) {
            val = parseInt(val, 10);
        }

        if (val !== this.get(prop)) {
            this.set(prop, val);
        }
    };

    /**
     * Handle keypress on one of the rgb or hsv fields.
     * @method _rgbFieldKeypress
     * @param e {Event} the keypress event
     * @param el {HTMLElement} the field
     * @param prop {string} the key to the linked property
     * @private
     */
    var _rgbFieldKeypress = function(e, el, prop) {
        var command = _getCommand(e);
        var inc = (e.shiftKey) ? 10 : 1;
        switch (command) {
            case 6: // return, update the value
                _useFieldValue.apply(this, arguments);
                break;
                        
            case 3: // up arrow, increment
                this.set(prop, Math.min(this.get(prop)+inc, 255));
                _updateFormFields.call(this);
                //Event.stopEvent(e);
                break;
            case 4: // down arrow, decrement
                this.set(prop, Math.max(this.get(prop)-inc, 0));
                _updateFormFields.call(this);
                //Event.stopEvent(e);
                break;

            default:
        }

    };

    /**
     * Handle keydown on the hex field
     * @method _hexFieldKeypress
     * @param e {Event} the keypress event
     * @param el {HTMLElement} the field
     * @param prop {string} the key to the linked property
     * @private
     */
    var _hexFieldKeypress = function(e, el, prop) {
        var command = _getCommand(e);
        if (command === 6) { // return, update the value
            _useFieldValue.apply(this, arguments);
        }
    };

    /** 
     * Allows numbers and special chars, and by default allows a-f.  
     * Used for the hex field keypress handler.
     * @method _hexOnly
     * @param e {Event} the event
     * @param numbersOnly omits a-f if set to true
     * @private
     * @return {boolean} false if we are canceling the event
     */
    var _hexOnly = function(e, numbersOnly) {
        var command = _getCommand(e);
        switch (command) {
            case 6: // return
            case 5: // special char
            case 1: // number
                break;
            case 2: // hex char (a-f)
                if (numbersOnly !== true) {
                    break;
                }

                // fallthrough is intentional

            default: // prevent alpha and punctuation
                Event.stopEvent(e);
                return false;
        }
    };

    /** 
     * Allows numbers and special chars only.  Used for the
     * rgb and hsv fields keypress handler.
     * @method _numbersOnly
     * @param e {Event} the event
     * @private
     * @return {boolean} false if we are canceling the event
     */
    var _numbersOnly = function(e) {
        return _hexOnly(e, true);
    };

    /**
     * Returns the element reference that is saved.  The id can be either
     * the element id, or the key for this id in the "id" config attribute.
     * For instance, the host element id can be obtained by passing its
     * id (default: "yui_picker") or by its key "YUI_PICKER".
     * @param id {string} the element id, or key 
     * @return {HTMLElement} a reference to the element
     */
    proto.getElement = function(id) { 
        return this.get(this.OPT.ELEMENTS)[this.get(this.OPT.IDS)[id]]; 
    };

    var _createElements = function() {
        this.logger.log("Building markup");
        var el, child, img, fld, i, 
            ids = this.get(this.OPT.IDS),
            txt = this.get(this.OPT.TXT),
            images = this.get(this.OPT.IMAGES),
            Elem = function(type, o) {
                var n = document.createElement(type);
                if (o) {
                    lang.augmentObject(n, o, true);
                }
                return n;
            },
            RGBElem = function(type, obj) {
                var o = lang.merge({
                        //type: "txt",
                        autocomplete: "off",
                        value: "0",
                        size: 3,
                        maxlength: 3
                    }, obj);

                o.name = o.id;
                return new Elem(type, o);
            };

        var p = this.get("element");

        // Picker slider (S and V) ---------------------------------------------

        el = new Elem("div", {
            id: ids[this.ID.PICKER_BG],
            className: "yui-picker-bg",
            tabIndex: -1,
            hideFocus: true
        });

        child = new Elem("div", {
            id: ids[this.ID.PICKER_THUMB],
            className: "yui-picker-thumb"
        });

        img = new Elem("img", {
            src: images.PICKER_THUMB
        });

        child.appendChild(img);
        el.appendChild(child);
        p.appendChild(el);
        
        // Hue slider ---------------------------------------------
        el = new Elem("div", {
            id: ids[this.ID.HUE_BG],
            className: "yui-picker-hue-bg",
            tabIndex: -1,
            hideFocus: true
        });

        child = new Elem("div", {
            id: ids[this.ID.HUE_THUMB],
            className: "yui-picker-hue-thumb"
        });

        img = new Elem("img", {
            src: images.HUE_THUMB
        });

        child.appendChild(img);
        el.appendChild(child);
        p.appendChild(el);


        // controls ---------------------------------------------

        el = new Elem("div", {
            id: ids[this.ID.CONTROLS],
            className: "yui-picker-controls"
        });

        p.appendChild(el);
        p = el;

            // controls header
            el = new Elem("div", {
                className: "hd"
            });

            child = new Elem("a", {
                id: ids[this.ID.CONTROLS_LABEL],
                //className: "yui-picker-controls-label",
                href: "#"
            });
            el.appendChild(child);
            p.appendChild(el);

            // bd
            el = new Elem("div", {
                className: "bd"
            });

            p.appendChild(el);
            p = el;

                // rgb
                el = new Elem("ul", {
                    id: ids[this.ID.RGB_CONTROLS],
                    className: "yui-picker-rgb-controls"
                });

                child = new Elem("li");
                child.appendChild(document.createTextNode(txt.R + " "));

                fld = new RGBElem("input", {
                    id: ids[this.ID.R],
                    className: "yui-picker-r"
                });

                child.appendChild(fld);
                el.appendChild(child);

                child = new Elem("li");
                child.appendChild(document.createTextNode(txt.G + " "));

                fld = new RGBElem("input", {
                    id: ids[this.ID.G],
                    className: "yui-picker-g"
                });

                child.appendChild(fld);
                el.appendChild(child);

                child = new Elem("li");
                child.appendChild(document.createTextNode(txt.B + " "));

                fld = new RGBElem("input", {
                    id: ids[this.ID.B],
                    className: "yui-picker-b"
                });

                child.appendChild(fld);
                el.appendChild(child);

                p.appendChild(el);

                // hsv
                el = new Elem("ul", {
                    id: ids[this.ID.HSV_CONTROLS],
                    className: "yui-picker-hsv-controls"
                });

                child = new Elem("li");
                child.appendChild(document.createTextNode(txt.H + " "));

                fld = new RGBElem("input", {
                    id: ids[this.ID.H],
                    className: "yui-picker-h"
                });

                child.appendChild(fld);
                child.appendChild(document.createTextNode(" " + txt.DEG));

                el.appendChild(child);

                child = new Elem("li");
                child.appendChild(document.createTextNode(txt.S + " "));

                fld = new RGBElem("input", {
                    id: ids[this.ID.S],
                    className: "yui-picker-s"
                });

                child.appendChild(fld);
                child.appendChild(document.createTextNode(" " + txt.PERCENT));

                el.appendChild(child);

                child = new Elem("li");
                child.appendChild(document.createTextNode(txt.V + " "));

                fld = new RGBElem("input", {
                    id: ids[this.ID.V],
                    className: "yui-picker-v"
                });

                child.appendChild(fld);
                child.appendChild(document.createTextNode(" " + txt.PERCENT));

                el.appendChild(child);
                p.appendChild(el);


                // hex summary

                el = new Elem("ul", {
                    id: ids[this.ID.HEX_SUMMARY],
                    className: "yui-picker-hex_summary"
                });

                child = new Elem("li", {
                    id: ids[this.ID.R_HEX]
                });
                el.appendChild(child);

                child = new Elem("li", {
                    id: ids[this.ID.G_HEX]
                });
                el.appendChild(child);

                child = new Elem("li", {
                    id: ids[this.ID.B_HEX]
                });
                el.appendChild(child);
                p.appendChild(el);

                // hex field
                el = new Elem("div", {
                    id: ids[this.ID.HEX_CONTROLS],
                    className: "yui-picker-hex-controls"
                });
                el.appendChild(document.createTextNode(txt.HEX + " "));

                child = new RGBElem("input", {
                    id: ids[this.ID.HEX],
                    className: "yui-picker-hex",
                    size: 6,
                    maxlength: 6
                });

                el.appendChild(child);
                p.appendChild(el);

                p = this.get("element");

                // swatch
                el = new Elem("div", {
                    id: ids[this.ID.SWATCH],
                    className: "yui-picker-swatch"
                });

                p.appendChild(el);

                // websafe swatch
                el = new Elem("div", {
                    id: ids[this.ID.WEBSAFE_SWATCH],
                    className: "yui-picker-websafe-swatch"
                });

                p.appendChild(el);

    };

    var _attachRGBHSV = function(id, config) {
        Event.on(this.getElement(id), "keydown", function(e, me) {
                _rgbFieldKeypress.call(me, e, this, config);
            }, this);
        Event.on(this.getElement(id), "keypress", _numbersOnly, this);
        Event.on(this.getElement(id), "blur", function(e, me) {
                _useFieldValue.call(me, e, this, config);
            }, this);
    };


    /**
     * Updates the rgb attribute with the current state of the r,g,b
     * fields.  This is invoked from change listeners on these
     * attributes to facilitate updating these values from the
     * individual form fields
     * @method _updateRGB
     * @private
     */
    var _updateRGB = function() {
        var rgb = [this.get(this.OPT.RED), 
                   this.get(this.OPT.GREEN),
                   this.get(this.OPT.BLUE)];

        this.logger.log("RGB value set to " + rgb);
        this.set(this.OPT.RGB, rgb);

        _updateSliders.call(this);
    };

    /**
     * Sets the initial state of the sliders
     * @method initPicker
     */
    proto.initPicker = function () {

        // bind all of our elements
        var o=this.OPT, 
            ids = this.get(o.IDS), 
            els = this.get(o.ELEMENTS), 
                  i, el, id;

        // Add the default value as a key for each element for easier lookup
        for (i in this.ID) {
            if (lang.hasOwnProperty(this.ID, i)) {
                ids[this.ID[i]] = ids[i];
            }
        }

        // Check for picker element, if not there, create all of them
        el = Dom.get(ids[this.ID.PICKER_BG]);
        if (!el) {
            _createElements.call(this);
        } else {
            this.logger.log("Using pre-existing markup");
        }

        for (i in ids) {
            if (lang.hasOwnProperty(ids, i)) {
                // look for element
                el = Dom.get(ids[i]);

                // generate an id if the implementer passed in an element reference,
                // and the element did not have an id already
                id = Dom.generateId(el);

                // update the id in case we generated the id
                ids[i] = id; // key is WEBSAFE_SWATCH
                ids[ids[i]] = id; // key is websafe_swatch

                // store the dom ref
                els[id] = el;
            }
        }

        // set the initial visibility state of our controls
            els = [o.SHOW_CONTROLS, 
                   o.SHOW_RGB_CONTROLS,
                   o.SHOW_HSV_CONTROLS,
                   o.SHOW_HEX_CONTROLS,
                   o.SHOW_HEX_SUMMARY,
                   o.SHOW_WEBSAFE
                   ];

        for (i=0; i<els.length; i=i+1) {
            this.set(els[i], this.get(els[i]));
        }

        var s = this.get(o.PICKER_SIZE);
        this.logger.log("picker size" + s);

        this.hueSlider = Slider.getVertSlider(this.getElement(this.ID.HUE_BG), 
                                              this.getElement(this.ID.HUE_THUMB), 0, s);
        this.hueSlider.subscribe("change", _onHueSliderChange, this, true);

        this.pickerSlider = Slider.getSliderRegion(this.getElement(this.ID.PICKER_BG), 
                                                   this.getElement(this.ID.PICKER_THUMB), 0, s, 0, s);
        this.pickerSlider.subscribe("change", _onPickerSliderChange, this, true);

        // Set the animate state
        this.set(o.ANIMATE,this.get(o.ANIMATE));

        //_onHueSliderChange.call(this, 0);

        Event.on(this.getElement(this.ID.WEBSAFE_SWATCH), "click", function(e) {
               this.setValue(this.get(o.WEBSAFE));
               //_updateSliders
           }, this, true);

        Event.on(this.getElement(this.ID.CONTROLS_LABEL), "click", function(e) {
               this.set(o.SHOW_CONTROLS, !this.get(o.SHOW_CONTROLS));
               Event.preventDefault(e);
           }, this, true);

        _attachRGBHSV.call(this, this.ID.R, this.OPT.RED); 
        _attachRGBHSV.call(this, this.ID.G, this.OPT.GREEN); 
        _attachRGBHSV.call(this, this.ID.B, this.OPT.BLUE); 
        _attachRGBHSV.call(this, this.ID.H, this.OPT.HUE); 
        _attachRGBHSV.call(this, this.ID.S, this.OPT.SATURATION); 
        _attachRGBHSV.call(this, this.ID.V, this.OPT.VALUE); 

        Event.on(this.getElement(this.ID.HEX), "keydown", function(e, me) {
                _hexFieldKeypress.call(me, e, this, me.OPT.HEX);
            }, this);

        Event.on(this.getElement(this.ID.HEX), "keypress", _hexOnly, this);
        Event.on(this.getElement(this.ID.HEX), "blur", function(e, me) {
                _useFieldValue.call(me, e, this, me.OPT.HEX);
            }, this);

        _updateRGB.call(this);
    };


    /**
     * Updates the RGB values from the current state of the HSV
     * values.  Executed when the one of the HSV form fields are
     * updated
     * _updateRGBFromHSV
     * @private
     */
    var _updateRGBFromHSV = function() {
        var hsv = [this.get(this.OPT.HUE), 
                   this.get(this.OPT.SATURATION)/100,
                   this.get(this.OPT.VALUE)/100];

        var rgb = Color.hsv2rgb(hsv);

        this.logger.log("HSV converted to RGB " + hsv + " : " + rgb);
        this.set(this.OPT.RGB, rgb);

        _updateSliders.call(this);
    };

    /**
     * Parses the hex string to normalize shorthand values, converts
     * the hex value to rgb and updates the rgb attribute (which
     * updates the state for all of the other values)
     * method _updateHex
     * @private
     */
    var _updateHex = function() {
       
        var hex = this.get(this.OPT.HEX), l=hex.length;

        // support #369 -> #336699 shorthand
        if (l === 3) {
            var c = hex.split(""), i;
            for (i=0; i<l; i=i+1) {
                c[i] = c[i] + c[i];
            }

            hex = c.join("");
        }

        if (hex.length !== 6) {
            this.logger.log(this.get(this.TXT.ILLEGAL_HEX), "error");
            return false;
        }

        var rgb = Color.hex2rgb(hex);

        this.logger.log(sub("Hex value set to {hex} ({rgb})", {
                hex: hex, rgb: rgb
            }));

        this.setValue(rgb);

        //_updateSliders.call(this);

    };



    /**
     * Sets up the config attributes and the change listeners for this
     * properties
     * @method initAttributes
     * @param attr An object containing default attribute values
     */
    proto.initAttributes = function(attr) {

        attr = attr || {};
        YAHOO.widget.ColorPicker.superclass.initAttributes.call(this, attr);
        
        /**
         * The size of the picker. Trying to change this is not recommended.
         * @attribute pickersize
         * @default 180
         * @type int
         */
        this.setAttributeConfig(this.OPT.PICKER_SIZE, {
                value: attr.size || this.DEFAULT.PICKER_SIZE
            });

        /**
         * The current hue value 0-360
         * @attribute hue
         * @type int
         */
        this.setAttributeConfig(this.OPT.HUE, {
                value: attr.hue || 0,
                validator: lang.isNumber
            });

        /**
         * The current saturation value 0-100
         * @attribute saturation
         * @type int
         */
        this.setAttributeConfig(this.OPT.SATURATION, {
                value: attr.saturation || 0,
                validator: lang.isNumber
            });

        /**
         * The current value/brightness value 0-100
         * @attribute value
         * @type int
         */
        this.setAttributeConfig(this.OPT.VALUE, {
                value: lang.isNumber(attr.value) ? attr.value : 100,
                validator: lang.isNumber
            });

        /**
         * The current red value 0-255
         * @attribute red
         * @type int
         */
        this.setAttributeConfig(this.OPT.RED, {
                value: lang.isNumber(attr.red) ? attr.red : 255,
                validator: lang.isNumber
            });

        /**
         * The current green value 0-255
         * @attribute green 
         * @type int
         */
        this.setAttributeConfig(this.OPT.GREEN, {
                value: lang.isNumber(attr.green) ? attr.green : 255,
                validator: lang.isNumber
            });

        /**
         * The current blue value 0-255
         * @attribute blue
         * @type int
         */
        this.setAttributeConfig(this.OPT.BLUE, {
                value: lang.isNumber(attr.blue) ? attr.blue : 255,
                validator: lang.isNumber
            });

        /**
         * The current hex value #000000-#FFFFFF, without the #
         * @attribute hex
         * @type string
         */
        this.setAttributeConfig(this.OPT.HEX, {
                value: attr.hex || "FFFFFF",
                validator: lang.isString
            });

        /**
         * The current rgb value.  Updates the state of all of the
         * other value fields.  Read-only: use setValue to set the
         * controls rgb value.
         * @attribute hex
         * @type [int, int, int]
         * @readonly
         */
        this.setAttributeConfig(this.OPT.RGB, {
                value: attr.rgb || [255,255,255],
                method: function(rgb) {

                    this.set(this.OPT.RED, rgb[0], true);
                    this.set(this.OPT.GREEN, rgb[1], true);
                    this.set(this.OPT.BLUE, rgb[2], true);

                    var websafe = Color.websafe(rgb);
                    this.set(this.OPT.WEBSAFE, websafe, true);

                    var hex = Color.rgb2hex(rgb);
                    this.set(this.OPT.HEX, hex, true);

                    var hsv = Color.rgb2hsv(rgb);

                    this.logger.log(sub("RGB value set to {rgb} (hsv: {hsv})", {
                            "hsv": hsv, "rgb": rgb
                        }));

                    this.set(this.OPT.HUE, hsv[0], true);
                    this.set(this.OPT.SATURATION, Math.round(hsv[1]*100), true);
                    this.set(this.OPT.VALUE, Math.round(hsv[2]*100), true);
                },
                readonly: true
            });

        /**
         * If the color picker will live inside of a container object,
         * set, provide a reference to it so the control can use the
         * container's events.
         * @attribute container
         * @type YAHOO.widget.Panel
         */
        this.setAttributeConfig(this.OPT.CONTAINER, {
                    value: null,
                    method: function(container) {
                        if (container) {
                            // Position can get out of sync when the
                            // control is manipulated while display is
                            // none.  Resetting the slider constraints
                            // when it is visible gets the state back in
                            // order.
                            container.showEvent.subscribe(function() {
                                // this.pickerSlider.thumb.resetConstraints();
                                // this.hueSlider.thumb.resetConstraints();
                                this.pickerSlider.focus();
                            }, this, true);
                        }
                    }
                });
        /**
         * The closest current websafe value
         * @attribute websafe
         * @type int
         */
        this.setAttributeConfig(this.OPT.WEBSAFE, {
                value: attr.websafe || [255,255,255]
            });


        var ids = attr.ids || lang.merge({}, this.ID);

        if (!attr.ids && _pickercount > 1) {
            for (var i in ids) {
                if (lang.hasOwnProperty(ids, i)) {
                    ids[i] = ids[i] + _pickercount;
                }
            }
        }


        /**
         * A list of element ids and/or element references used by the 
         * control.  The default is the this.ID list, and can be customized
         * by passing a list in the contructor
         * @attribute ids
         * @type {referenceid: realid}
         * @writeonce
         */
        this.setAttributeConfig(this.OPT.IDS, {
                value: ids,
                writeonce: true
            });

        /**
         * A list of txt strings for internationalization.  Default
         * is this.TXT
         * @attribute txt
         * @type {key: txt}
         * @writeonce
         */
        this.setAttributeConfig(this.OPT.TXT, {
                value: attr.txt || this.TXT,
                writeonce: true
            });

        /**
         * The img src default list
         * is this.IMAGES
         * @attribute images
         * @type {key: image}
         * @writeonce
         */
        this.setAttributeConfig(this.OPT.IMAGES, {
                value: attr.images || this.IMAGE,
                writeonce: true
            });
        /**
         * The element refs used by this control.  Set at initialization
         * @attribute elements
         * @type {id: HTMLElement}
         * @readonly
         */
        this.setAttributeConfig(this.OPT.ELEMENTS, {
                value: {},
                readonly: true
            });

        /**
         * Returns the cached element reference.  If the id is not a string, it
         * is assumed that it is an element and this is returned.
         * @param id {string|HTMLElement} the element key, id, or ref
         * @param on {boolean} hide or show.  If true, show
         * @private */
        var _hideShowEl = function(id, on) {
            var el = (lang.isString(id) ? this.getElement(id) : id);
            //Dom.setStyle(id, "visibility", (on) ? "" : "hidden");
            Dom.setStyle(el, "display", (on) ? "" : "none");
        };

        /**
         * Hide/show the entire set of controls
         * @attribute showcontrols
         * @type boolean
         * @default true
         */
        this.setAttributeConfig(this.OPT.SHOW_CONTROLS, {
                value: lang.isBoolean(attr.showcontrols) ? attr.showcontrols : true,
                method: function(on) {

                    var el = Dom.getElementsByClassName("bd", "div", 
                            this.getElement(this.ID.CONTROLS))[0];

                    _hideShowEl.call(this, el, on);

                    this.getElement(this.ID.CONTROLS_LABEL).innerHTML = 
                        (on) ? this.get(this.OPT.TXT).HIDE_CONTROLS :
                               this.get(this.OPT.TXT).SHOW_CONTROLS;

                }
            });

        /**
         * Hide/show the rgb controls
         * @attribute showrgbcontrols
         * @type boolean
         * @default true
         */
        this.setAttributeConfig(this.OPT.SHOW_RGB_CONTROLS, {
                value: lang.isBoolean(attr.showrgbcontrols) ? attr.showrgbcontrols : true,
                method: function(on) {
                    //Dom.setStyle(this.getElement(this.ID.RBG_CONTROLS), "visibility", (on) ? "" : "hidden");
                    _hideShowEl.call(this, this.ID.RGB_CONTROLS, on);
                }
            });

        /**
         * Hide/show the hsv controls
         * @attribute showhsvcontrols
         * @type boolean
         * @default false
         */
        this.setAttributeConfig(this.OPT.SHOW_HSV_CONTROLS, {
                value: lang.isBoolean(attr.showhsvcontrols) ?
                                      attr.showhsvcontrols : false,
                method: function(on) {
                    //Dom.setStyle(this.getElement(this.ID.HSV_CONTROLS), "visibility", (on) ? "" : "hidden");
                    _hideShowEl.call(this, this.ID.HSV_CONTROLS, on);

                    // can't show both the hsv controls and the rbg hex summary
                    if (on && this.get(this.OPT.SHOW_HEX_SUMMARY)) {
                        this.set(this.OPT.SHOW_HEX_SUMMARY, false);
                    }
                }
            });

        /**
         * Hide/show the hex controls
         * @attribute showhexcontrols
         * @type boolean
         * @default true
         */
        this.setAttributeConfig(this.OPT.SHOW_HEX_CONTROLS, {
                value: lang.isBoolean(attr.showhexcontrols) ?
                                      attr.showhexcontrols : false,
                method: function(on) {
                    _hideShowEl.call(this, this.ID.HEX_CONTROLS, on);
                }
            });

        /**
         * Hide/show the websafe swatch
         * @attribute showwebsafe
         * @type boolean
         * @default true
         */
        this.setAttributeConfig(this.OPT.SHOW_WEBSAFE, {
                value: lang.isBoolean(attr.showwebsafe) ? attr.showwebsafe : true,
                method: function(on) {
                    _hideShowEl.call(this, this.ID.WEBSAFE_SWATCH, on);
                }
            });

        /**
         * Hide/show the hex summary
         * @attribute showhexsummary
         * @type boolean
         * @default true
         */
        this.setAttributeConfig(this.OPT.SHOW_HEX_SUMMARY, {
                value: lang.isBoolean(attr.showhexsummary) ? attr.showhexsummary : true,
                method: function(on) {
                    _hideShowEl.call(this, this.ID.HEX_SUMMARY, on);

                    // can't show both the hsv controls and the rbg hex summary
                    if (on && this.get(this.OPT.SHOW_HSV_CONTROLS)) {
                        this.set(this.OPT.SHOW_HSV_CONTROLS, false);
                    }
                }
            });
        this.setAttributeConfig(this.OPT.ANIMATE, {
                value: lang.isBoolean(attr.animate) ? attr.animate : true,
                method: function(on) {
                    this.pickerSlider.animate = on;
                    this.hueSlider.animate = on;
                }
            });

        this.on(this.OPT.HUE + "Change", _updateRGBFromHSV, this, true);
        this.on(this.OPT.SATURATION + "Change", _updateRGBFromHSV, this, true);
        this.on(this.OPT.VALUE + "Change", _updatePickerSlider, this, true);

        this.on(this.OPT.RED + "Change", _updateRGB, this, true);
        this.on(this.OPT.GREEN + "Change", _updateRGB, this, true);
        this.on(this.OPT.BLUE + "Change", _updateRGB, this, true);

        this.on(this.OPT.HEX + "Change", _updateHex, this, true);

        this.initPicker();
    };

})();
YAHOO.register("colorpicker", YAHOO.widget.ColorPicker, {version: "2.6.0", build: "1321"});
