( function ($)
{
  /**
   * HEX to RGB
   * @param {String} hex
   * @return Array of RGB values
   * @type Array
   */
  function hexToRGB(hex)
  {
    return [
      parseInt(hex.substring(0,2), 16),
      parseInt(hex.substring(2,4), 16),
      parseInt(hex.substring(4,6), 16)
    ]
  }

  /**
   * Convert RGB values to string
   * @param {Array} rgb
   * @return CSS color
   * @type string
   */
  function cssColor(rgb)
  {
    return "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")"
  }

  /**
   * Build values meta-data and sort them
   * @param {jQuery} items
   * @param {Array} values
   * @return Max value
   * @type Number
   */
  function buildValues(items, values)
  {
    var index = {};

    items.each( function ()
    {
      var item = $(this),
          value = item.attr("value");

      if (typeof index[value] === "undefined")
      {
        index[value] = values.length;
        values.push([value, [item]]);
      }
      else
      {
        values[index[value]][1].push(item);
      }
    });

    values.sort( function (a, b)
    {
      return b[0] - a[0];
    })

    return values.length ? values[0][0] : false;
  }

  /**
   * Set widget settings
   * @param {jQuery} widget
   * @param {Number} size
   * @param {Array} rgb
   */
  function updateWidget(widget, size, rgb)
  {
    widget.css(
    {
      fontSize: size,
      color: cssColor(rgb)
    })
  }

  /**
   * Draw items on page in sortable mode
   * @param {Array} values
   * @param {Number} maxValue
   * @param {Object} conf
   * @param {jQuery} parent
   */
  function sort(values, maxValue, conf, parent)
  {
    var left,
        right,
        widgets,
        total,
        i;

    if (parent)
    {
      right = left = parent;
      i = 0;
    }
    else
    {
      widgets = values[0][1];
      total = widgets.length;

      for (i = 0; i < total; i++)
      {
        updateWidget(widgets[i], conf.fontSizeUp, conf.rgbUp);

        if ( ! left)
        {
          left = widgets[i];
        }
        else if ( ! right)
        {
          right = widgets[i].insertAfter(left);
        }
        else
        {
          right = widgets[i].insertAfter(right);
        }
      }

      if ( ! right)
      {
        right = left;
      }
      
      i = 1;
    }

    for (var length = values.length; i < length; i++)
    {
      widgets = values[i][1];
      total = widgets.length;

      var median = total >> 1,
          value = values[i][0],
          fontSize = conf.fontSizeDown + (conf.fontSizeSub * value / maxValue),
          rgb = [
            conf.rgbDown[0] - Math.round(conf.rgbSub[0] *  value / maxValue),
            conf.rgbDown[1] - Math.round(conf.rgbSub[1] *  value / maxValue),
            conf.rgbDown[2] - Math.round(conf.rgbSub[2] *  value / maxValue)
          ];

      for (var j = 0; j < median; j++)
      {
        updateWidget(widgets[j], fontSize, rgb);
        left = widgets[j].insertBefore(left);
      }

      for (j = median; j < total; j++)
      {
        updateWidget(widgets[j], fontSize, rgb);
        right = widgets[j].insertAfter(right);
      }
    }
  }

  $.fn.hcloud = function (options)
  {
    var defaults =
    {
      fontSizeRange: [8, 25],
      colorRange: ["000000", "cccccc"], // dark to light
      itemsSelector: "> span",
      childrenSelector: "> ul > li"
    };

    var conf = $.extend(defaults, options), // plugin configuration
        // cloud sorting configuration
        sortConf = {
          rgbUp: hexToRGB(conf.colorRange[0]),
          rgbDown: hexToRGB(conf.colorRange[1]),
          fontSizeUp: conf.fontSizeRange[1],
          fontSizeDown: conf.fontSizeRange[0]
        };

        sortConf.rgbSub = [
          sortConf.rgbDown[0] - sortConf.rgbUp[0],
          sortConf.rgbDown[1] - sortConf.rgbUp[1],
          sortConf.rgbDown[2] - sortConf.rgbUp[2]
        ];

        sortConf.fontSizeSub = sortConf.fontSizeUp - sortConf.fontSizeDown;

    return this.each( function ()
    {
      var cloud = $(this),
          items = cloud.find(conf.itemsSelector),
          values = [],
          maxValue = buildValues(items, values);

      /**
       * Collapse a tag with children
       * @param {jQuery} tag
       * @return FALSE if tag collapsed, otherwise array of children
       * @type Boolean|jQuery
       */
      function collapse(tag)
      {
        var children = tag.data("hcloud");

        if ( tag.hasClass("selected") )
        {
          tag.removeClass("selected");

          if (children)
          {
            tag.find("> ul").append( children.trigger("collapse") );
          }

          return false;
        }

        return children;
      }

      cloud.click( function (event)
      {
        if (event.target.nodeName === "LI")
        {
          var tag = $(event.target),
              children = collapse(tag);

          if (children !== false)
          {
            tag.addClass("selected");

            if ( ! children)
            {
              children = tag.find(conf.childrenSelector).bind("collapse", function ()
              {
                collapse( $(this) );
              });
            }

            var values = [],
                maxValue = buildValues(children, values);

            if (maxValue !== false)
            {
              sort(values, maxValue, sortConf, tag);
              tag.data("hcloud", children);
            }
          }
        }
      });

      if (maxValue !== false)
      {
        sort(values, maxValue, sortConf);
      }
    });
  };

}) (jQuery);

