(function($) {
  $.fn.svgload = function(f) {
    this[0].svgweb.addOnLoad(function() { f.call(this, $) });
    return this;
  }

  function log(x) {
    console && console.log && console.log(x);
  }

  function interpolator(c0, c1) {
    if (c0 == c1) return function() { return c0; };
    var i0 = parseInt(c0, 16), i1 = parseInt(c1, 16);
    var delta = i1 - i0;
    return function(x) {
      var i = Math.round(i0 + x * delta);
      return (i < 16 ? "0" : "") + i.toString(16);
    }
  }

  function color_interpolator(color0, color1, gamma) {
    var re = /#?(..)(..)(..)/;
    var c0 = re.exec(color0);
    var c1 = re.exec(color1);
    var r = interpolator(c0[1], c1[1]);
    var g = interpolator(c0[2], c1[2]);
    var b = interpolator(c0[3], c1[3]);
    gamma = gamma | 1;
    return function(x) {
      var x = Math.pow(x, gamma);
      return "#" + r(x) + g(x) + b(x);
    }
  }

  $.svgbind = function(elem, name, f) {
    f && elem.addEventListener(name, f, false);
  };

  $.fn.svgbind = function(name, f) {
    return this.each(function() {
      $.svgbind(this, name, f);
    });
  }

  function toNumber(s) {
    var r = /^\s*[+-]?(\d+.?\d*([eE][+-]?\d+)?)\s*$/.exec(s);
    return r && Number(r[1]);
  }
  
  function minMaxDelta () {
    var min = Math.min.apply(Math, arguments);
    var max = Math.max.apply(Math, arguments);
    var delta = max - min;
    return {min: min, max: max, delta: delta};
  }

  function intensity(x, range) {
    if (range.delta == 0) return 1; // why not?
    return (x - range.min)/range.delta;
  }

  function toDataFn(fnOrData) {
    if (fnOrData === undefined) return;
    return $.isFunction(fnOrData) ? fnOrData 
      : function() { return fnOrData };
  }

  function toDimFn(stringOrHashOrFn) {
    return $.isFunction(stringOrHashOrFn) ? stringOrHashOrFn
      : (typeof stringOrHashOrFn) == "string" 
        ? function() { return stringOrHashOrFn }
      : function(dim) { return stringOrHashOrFn[dim] };
  }

  function memoize1(fn) {
    if (fn === undefined) return;
    var m = {};
    return function(k) {
      var _k = "_" + k;
      return (_k in m) ? m[_k] : m[_k] = fn(k);
    }
  }

  function curry(fn) {
    if (fn === undefined) return;
    return memoize1(function(k) {
      return function() {
        var args = [k];
        args.push.apply(args, arguments);
        return fn.apply(this, args);
      }
    });
  }

  var selectDimEvent = "-svg-map-dim-select";
  /* table: an html table whose rows ids match ids of path elements in the map
            (selector or jQuery mapelemect or dom node)
     mapelem: the mapelemect tag for the svg 
              (selector or jQuery mapelemect or dom node)
     Options:
      DATA
       data: hash, indexed by id, of homogen records (hashs)
             Default: {}

       dataFn: callback to build/override "data", called once per row 
               in the html table.
               Signature: function(numval_col1, ..., numval_colN)
               Default: -

       summaryFn: callback to compute summary info (eg min/max) for each column 
                  in "data".
                  Signature: function(x_1, ..., x_N)
                  Default: compute a hash with min/max/delta


      GRADIENT
       min: color to use when intensity = 0
       max: color to use when intensity = 1
       gamma: control intensity mapping
              Default: 1 (linear)

       intensityFn: callback to compute intensity for a value
                    Signature: function(value, summary)
                    Default: (value - summary.min) / summary.delta

       colorFn: callback to compute a color for a value
                Signature: function(value, summary)
                Default: a gradient parametrized by min, max, delta 
                         and intensityFn


      COLOR
       highlight: color when hovering
       na: color when no data for this dim & id
       
 
      DIM selection:
       selectDimGroup: links (<a>) used to toggle dims, can be a selector, 
                       a jQuery mapelemect, an array of dom nodes
                       Default: "#color-select a"
       selectedDimClass: class to add to link for the active dim 
                         Default: "selected-dim"
   */
  function svgmap(table, mapelem, options, etc) {
    if (etc) {
      var mapcontainer = $(mapelem)[0];
      var url = options;
      options = etc;
      mapelem = document.createElement('object', true);
      mapelem.setAttribute('type', 'image/svg+xml');
      mapelem.setAttribute('data', url);
      mapelem.setAttribute('width', '480');
      mapelem.setAttribute('height', '500');
      mapelem.addEventListener('load', function() {
        var that = this; // this is the handler, needed for flash
        // timeout to prevent a refresh bug in FF: the map appears grey 
        // BUT if you force a repaint (eg by switching to another app)
        // colors appears when hovering...
        setTimeout(function() {
          svgmap(table, that, options);
        }, 1);
      }, false);
      $(mapcontainer).empty();
      svgweb.appendChild(mapelem, mapcontainer);
      return;
    }
    options = $.extend({data: {},
                        highlight: "yellow", // color to display when hovering
                        na: "#b3b3b3", // N/A, color to use when no data
                        min: "#dadaff",
                        max: "#3030ff",
                        gamma: 1,
                        summaryFn: minMaxDelta,
                        intensityFn: intensity,
                        selectDimGroup: "#color-select a",
                        selectedDimClass: "selected-dim",
                        itemsSelector: "> tbody > tr",
                        valuesSelector: "> td",
                        toNumber: toNumber
                        }, options);
    $.each(["na", "min", "max", "gamma"], function(_, opt) {
      options[opt] = toDimFn(options[opt]);
    });

    var $trs = $(table).find(options.itemsSelector).filter("[id]");
    if ($trs.length == 0) return;

    var colorFn = curry(options.colorFn) || 
                    memoize1(function(dim) {
                      return (function(gradient, intensityFn) {
                        return function(datum, summary) {
                          return gradient(intensityFn(datum, summary));
                        }
                      })(color_interpolator(options.min(dim), 
                          options.max(dim), options.gamma(dim)), 
                         options.intensityFn);
                    });
    var data = options.data;
    var dataFn = toDataFn(options.dataFn);
    var summaryFn = options.summaryFn;
    var num = options.toNumber;

    mapelem = $(mapelem)[0];
    var svgDoc = mapelem.contentDocument;
    var countriesPaths = [];
    var $trsById = {};

    var ids = []
    $trs.each(function(i) {
      var id = this.id.toLowerCase();
      $trsById[id] = $(this);
      ids.push(id);
      var path = svgDoc.getElementById(id);
      if (!path) {
        log("no path for id: " + id);
        return;
      }
      countriesPaths.push(path);
      countriesPaths[id] = path;
      if (dataFn) {
        var nums = $.map($(this).find(options.valuesSelector), function(td) {
          return [num($(td).text())];
        });
        data[id] = dataFn.apply(this, nums);
      }
    });
    
    var dims = [];
    for(var dim in data[ids[0]]) dims.push(dim);

    var summaries = {};
    $.each(dims, function(_, dim) {
      var vals = [];
      for(var k in data) {
        var v = data[k][dim];
        if (!v) continue;
        vals.push(v);
      }
      summaries[dim] = summaryFn.apply(window, vals);
    })

    var colors = {};
    $.each(ids, function(_, id) {
      var color = colors[id] = {};
      var datum = data[id] || {};
      $.each(dims, function(_, dim) {
        var d = datum[dim]
        if (d == undefined) {
          color[dim] = options.na;
        } else {
          color[dim] = colorFn(dim)(d, summaries[dim]);
        }
      });
    });
   
    var selectedDim = document.location.hash.substring(1);
    if (!summaries[selectedDim]) selectedDim = dims[0]; 

    function adapter(handler) {
      return handler && function(e) {
        var target = e.target;
        return handler.call(this, e, target, $trsById[target.id]); 
      }
    }

    $(countriesPaths).svgbind("mouseover", 
      adapter(function(_, path) {
        path.style.fill = options.highlight;
        return false;
      }));
    $(countriesPaths).svgbind("mouseout",
      adapter(function(_, path) {
        path.style.fill = colors[path.id] && colors[path.id][selectedDim] || options.na(selectedDim);
        return false;
      }));

    $(countriesPaths).svgbind("click", adapter(options.click));
    $(countriesPaths).svgbind("mouseover", adapter(options.mouseover));
    $(countriesPaths).svgbind("mouseout", adapter(options.mouseout));

    var $selectDimGroup = $(options.selectDimGroup);
    function colorize(dim) {
      $.each(countriesPaths, function(_, path) {
        path.style.fill = colors[path.id][selectedDim];
      });
      $selectDimGroup
        .removeClass(options.selectedDimClass)
        .addClass(options.selectedDimClass);
    }
    var $mapelem = $(mapelem);
    $mapelem.bind(selectDimEvent, function(e, dim) {
      dim && colorize(dim);
    }) 
    $mapelem.selectDim(selectedDim);
    $selectDimGroup.click(function() {
    //$selectDimGroup.hover(function() {
      var href = String(this.href);
      selectedDim = href.substring(href.lastIndexOf('#') + 1);
      $mapelem.selectDim(selectedDim);
    });
  }

  $.fn.svgmap = function(map, options, etc) {
    return this.each(function() {
      svgmap(this, map, options, etc);
    });
  }

  $.fn.selectDim = function(dimOrHandler) {
    if ($.isFunction(dimOrHandler)) {
      return $(this).bind(selectDimEvent, dimOrHandler);
    } else {
      return $(this).trigger(selectDimEvent, dimOrHandler);
    }
  }
})(jQuery);

