// This code bridges jQuery and Raphael.
//
// It acts as a plugin for both jQuery and Raphael.js.
//
// On jQuery objects it adds .R and .draw:
// .R() 
//     returns (and creates if none) a Raphael's canvas.
// .R($obj) 
//     defines $obj.R() as the canvas for the current jQuery object, 
//     returns the current jQuery object.
// .R(function(paper) { /* this === paper too */ }) 
//     executes the callback with the paper (creating it if needed), 
//     returns the current jQuery object.
// .R("name1 .. nameN") 
//     returns a Raphael group of objects with the specified names.
// .R("name1 .. nameN", function(group, paper) { /* this === group too */})
//     executes the supplied function on the specified group and returns 
//     the current jQuery Object.
//
// .draw(name, raphael-object) 
//     adds the exisiting object under this name abd returns this object.
// .draw(name, drawing-primitive, args)
//     drawing-primitive is any Rapahel's canvas method (circle, path, rect...)
//     args is teh argument array to pass to the method,
//     returns the drawn raphael object.
// .draw(name, drawing-primitive, args, attrs)
//     ditto .draw(name, drawing-primitive, args) and apply attributes attrs
//     to the drawn object. Returns the drawn raphael object.
// .draw(name, drawing-primitive, args, function(obj) { /* this === obj */ })
//     ditto .draw(name, drawing-primitive, args) and call the function on the
//     drawn object. Returns the drawn raphael object.
// .draw(name1, ..., nameN, ...)
//     draws N shapes (specified as above) and returns the group of 
//     drawn objects.
//
//
// On Raphael's objects it adds:
// .$ -- on elements, groups and canvas
// .$() 
//     returns a jQuery object of the Raphael object
// .$(function($obj, obj) { /* $obj === obj.$() && this === $obj */})
//     calls the supplied function, returns the raphael object
//
// .each -- on elements and groups
// .each(function(i, obj) { /* this === obj */ })
//     iterates through the current object, returns it.
//
// .setClass -- on elements
// .setClass("class1 .. classN")
//     tries to set the specified classes to the selection (see css2attrs),
//     returns the current object.
//
// .group -- on canvas, MUST be preferred to .set
//     like .set but adds bridge methods (Set.prototype isn't exposed by Raph)
//
// .outgrow -- on canvas
//     resize the canvas and apply negative margins so it can outgrow it's 
//     original bounding box.

jQuery(function($) {
  Raphael.fn.$ = function(f) { 
   var $canvas = $(this.canvas);
   if (!f) return $canvas;
   f.call($canvas, $canvas, this);
   return this;
  };
  Raphael.el.$ = function(f) { 
    var $node = $(this.node);
    if (!f) return $node;
    f.call($node, $node, this);
    return this;
  }
  
  Raphael.el.each = function(f) {
    f.call(this, 0, this)
    return this;
  }

  var css2attr = {"background-color": "fill", 
                  "border-top-width": "stroke-width",
                  "border-top-color": "stroke"};
  var ClassCache = {};
  Raphael.el.setClass = function(clazz) {
    if (clazz instanceof jQuery) {
      var $tmp = clazz;
    } else {
      if (ClassCache[clazz]) return ClassCache[clazz];
      var $tmp = clazz instanceof jQuery ? clazz :
        $("<div style='display: none'></div>").addClass(clazz).appendTo(document.body);
    }
    var $ref = $("<div style='display: none'></div>").appendTo(document.body);
    var attr = {};
    $.each(css2attr, function(k, v) {
      var css = $tmp.css(k);
      if (css !== $ref.css(k))
        attr[v] = css;
    });
    if ($tmp != clazz) {
      $tmp.remove();
      ClassCache[clazz] = attr;
    }
    $ref.remove();
    return this.attr(attr);
  }

  var SetExtensions = {
    $: function(f) { 
      var $set = $($.map(this.items, function(item) { return item.node }));
      if (!f) return $set;
      f.call($set, $set, this);
      return this;
    }, 
    each: function(f) {
      $.each(this.items, f);
      return this;
    }
  };

  Raphael.fn.group = function() {
    var s = this.set.apply(this, arguments);
    $.extend(s, SetExtensions);
    return s;
  }

  Raphael.fn.outgrow = function(top, right, bottom, left) {
    switch (arguments.length) {
      case 0: return this;
      case 1: right = bottom = left = top; break;
      case 2: bottom = top; left = right; break;
      case 3: left = 0; break;
    }
    var w = this.dims.w+=right+left;
    var h = this.dims.h+=top+bottom;
    this.setSize(w, h);
    this.$().css({"margin-top": top, "margin-right": right, "margin-bottom": bottom, "margin-left": left});
    return this;
  }

  function Paper($this) {
    var paper = $this.data("-raphael-paper");
    if (paper) return paper;
    var W = $this.width();
    var H = $this.height();
    paper = Raphael($this[0], W, H);
    paper.dims = {w: W, h: H};
    $this.data({"-raphael-paper": paper,
                "-raphael-shapes": {byid: {}, colls: {}}});
    return paper;
  }

  // .draw(name, raphael-object) 
  // .draw(name, drawing-primitive, args)
  // .draw(name, drawing-primitive, args, attrs-or-f)
  // or a repetition of such args;
  // returns a raphael object
  $.fn.draw = function() {
    var $this = $(this).eq(0);
    var paper = Paper($this);
    var shapes = $this.data("-raphael-shapes");

    var addShape = function(name, shape) {
      var selectors = name.split(/\s+/);
      selectors.push("*");
      for(var i = 0; i < selectors.length; i++) {
        var selector = selectors[i];
        if (selector.charAt(0) === "#") {
          shape.id = selector.substring(1);
          shapes.byid[selector] = shape;
        } else 
          (shapes.colls[selector] || (shapes.colls[selector] = paper.group()))
            .push(shape);
      }
    };

    var i = 0;
    var group = [];
    var shape;
    while(i < arguments.length) {
      var name = arguments[i];
      if (arguments[i+2] instanceof Array) {
        var attrs = arguments[i+3];
        shape = paper[arguments[i+1]].apply(paper, arguments[i+2]);
        if ((typeof attrs === "object") && !(attrs instanceof String)) {
          shape.attr(attrs);
          i+=4;
        } else if (typeof attrs === "function") {
          attrs.call(shape, shape);
          i+=4;
        } else {
          i+=3;
        }
      } else {
        shape = arguments[i+1];
        i += 2;
      }
      addShape(name, shape);
      group.push(shape); 
    }
    return (group.length === 1) ? group[0] : paper.group.apply(paper, group);
  }

  $.fn.R = function(selector, f) {
    var $this = $(this).eq(0);
    if (arguments.length === 1 && typeof selector === "object" 
        && !(selector instanceof String)) {
      // copy paper from the src (selector)
      var $src = $(selector);
      Paper($src);
      var data = $src.data();
      $this.data({"-raphael-paper": data["-raphael-paper"],
                  "-raphael-shapes": {byid: data["-raphael-shapes"].byid,
                                      colls: data["-raphael-shapes"].colls}});
      return this;
    }
    var paper = Paper($this);
    if (arguments.length === 0) return paper;
    if (arguments.length === 1 && typeof selector === "function") {
      selector.call(paper, paper);
      return this;
    }

    var shapes = $this.data("-raphael-shapes");
    var byid = shapes.byid;
    var colls = shapes.colls;
    var selectors = selector.split(/\s+/);
    var selection;
    if (selectors.length === 1) {
      selection = ((selectors[0].charAt(0) === "#")
        ? byid[selectors[0]] : colls[selectors[0]])
        || paper.group();
    } else {
      selection = paper.group();
      for(var i = 0; i < selectors.length; i++) {
        var shape = (selectors[i].charAt(0) === "#")
          ? byid[selectors[i]] : colls[selectors[i]];
        if (shape) selection.push(shape);
      }
    }
    if (f) {
      f.apply(selection, selection, paper);
      return this;
    }
    return selection;
  }
  // end of the bridge
});

