// TODO unbind zoom behavior?
d3.behavior.zoom = function() {
  var xyz = [0, 0, 0],
      event = d3.dispatch("zoom"),
      extent = d3_behavior_zoomInfiniteExtent;

  function zoom() {
    this
        .on("mousedown.zoom", mousedown)
        .on("mousewheel.zoom", mousewheel)
        .on("DOMMouseScroll.zoom", mousewheel)
        .on("dblclick.zoom", dblclick)
        .on("touchstart.zoom", touchstart);

    d3.select(window)
        .on("mousemove.zoom", d3_behavior_zoomMousemove)
        .on("mouseup.zoom", d3_behavior_zoomMouseup)
        .on("touchmove.zoom", d3_behavior_zoomTouchmove)
        .on("touchend.zoom", d3_behavior_zoomTouchup)
        .on("click.zoom", d3_behavior_zoomClick, true);
  }

  // snapshot the local context for subsequent dispatch
  function start() {
    d3_behavior_zoomXyz = xyz;
    d3_behavior_zoomExtent = extent;
    d3_behavior_zoomDispatch = event.zoom;
    d3_behavior_zoomEventTarget = d3.event.target;
    d3_behavior_zoomTarget = this;
    d3_behavior_zoomArguments = arguments;
  }

  function mousedown() {
    start.apply(this, arguments);
    d3_behavior_zoomPanning = d3_behavior_zoomLocation(d3.svg.mouse(d3_behavior_zoomTarget));
    d3_behavior_zoomMoved = false;
    d3.event.preventDefault();
    window.focus();
  }

  // store starting mouse location
  function mousewheel() {
    start.apply(this, arguments);
    if (!d3_behavior_zoomZooming) d3_behavior_zoomZooming = d3_behavior_zoomLocation(d3.svg.mouse(d3_behavior_zoomTarget));
    d3_behavior_zoomTo(d3_behavior_zoomDelta() + xyz[2], d3.svg.mouse(d3_behavior_zoomTarget), d3_behavior_zoomZooming);
  }

  function dblclick() {
    start.apply(this, arguments);
    var mouse = d3.svg.mouse(d3_behavior_zoomTarget);
    d3_behavior_zoomTo(d3.event.shiftKey ? Math.ceil(xyz[2] - 1) : Math.floor(xyz[2] + 1), mouse, d3_behavior_zoomLocation(mouse));
  }

  // doubletap detection
  function touchstart() {
    start.apply(this, arguments);
    var touches = d3_behavior_zoomTouchup(),
        touch,
        now = Date.now();
    if ((touches.length === 1) && (now - d3_behavior_zoomLast < 300)) {
      d3_behavior_zoomTo(1 + Math.floor(xyz[2]), touch = touches[0], d3_behavior_zoomLocations[touch.identifier]);
    }
    d3_behavior_zoomLast = now;
  }

  zoom.extent = function(x) {
    if (!arguments.length) return extent;
    extent = x == null ? d3_behavior_zoomInfiniteExtent : x;
    return zoom;
  };

  zoom.on = function(type, listener) {
    event.on(type, listener);
    return zoom;
  };

  return zoom;
};

var d3_behavior_zoomDiv,
    d3_behavior_zoomPanning,
    d3_behavior_zoomZooming,
    d3_behavior_zoomLocations = {}, // identifier -> location
    d3_behavior_zoomLast = 0,
    d3_behavior_zoomXyz,
    d3_behavior_zoomExtent,
    d3_behavior_zoomDispatch,
    d3_behavior_zoomEventTarget,
    d3_behavior_zoomTarget,
    d3_behavior_zoomArguments,
    d3_behavior_zoomMoved,
    d3_behavior_zoomStopClick;

function d3_behavior_zoomLocation(point) {
  return [
    point[0] - d3_behavior_zoomXyz[0],
    point[1] - d3_behavior_zoomXyz[1],
    d3_behavior_zoomXyz[2]
  ];
}

// detect the pixels that would be scrolled by this wheel event
function d3_behavior_zoomDelta() {

  // mousewheel events are totally broken!
  // https://bugs.webkit.org/show_bug.cgi?id=40441
  // not only that, but Chrome and Safari differ in re. to acceleration!
  if (!d3_behavior_zoomDiv) {
    d3_behavior_zoomDiv = d3.select("body").append("div")
        .style("visibility", "hidden")
        .style("top", 0)
        .style("height", 0)
        .style("width", 0)
        .style("overflow-y", "scroll")
      .append("div")
        .style("height", "2000px")
      .node().parentNode;
  }

  var e = d3.event, delta;
  try {
    d3_behavior_zoomDiv.scrollTop = 1000;
    d3_behavior_zoomDiv.dispatchEvent(e);
    delta = 1000 - d3_behavior_zoomDiv.scrollTop;
  } catch (error) {
    delta = e.wheelDelta || (-e.detail * 5);
  }

  return delta * .005;
}

// Note: Since we don't rotate, it's possible for the touches to become
// slightly detached from their original positions. Thus, we recompute the
// touch points on touchend as well as touchstart!
function d3_behavior_zoomTouchup() {
  var touches = d3.svg.touches(d3_behavior_zoomTarget),
      i = -1,
      n = touches.length,
      touch;
  while (++i < n) d3_behavior_zoomLocations[(touch = touches[i]).identifier] = d3_behavior_zoomLocation(touch);
  return touches;
}

function d3_behavior_zoomTouchmove() {
  var touches = d3.svg.touches(d3_behavior_zoomTarget);
  switch (touches.length) {

    // single-touch pan
    case 1: {
      var touch = touches[0];
      d3_behavior_zoomTo(d3_behavior_zoomXyz[2], touch, d3_behavior_zoomLocations[touch.identifier]);
      break;
    }

    // double-touch pan + zoom
    case 2: {
      var p0 = touches[0],
          p1 = touches[1],
          p2 = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2],
          l0 = d3_behavior_zoomLocations[p0.identifier],
          l1 = d3_behavior_zoomLocations[p1.identifier],
          l2 = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2, l0[2]];
      d3_behavior_zoomTo(Math.log(d3.event.scale) / Math.LN2 + l0[2], p2, l2);
      break;
    }
  }
}

function d3_behavior_zoomMousemove() {
  d3_behavior_zoomZooming = null;
  if (d3_behavior_zoomPanning) {
    d3_behavior_zoomMoved = true;
    d3_behavior_zoomTo(d3_behavior_zoomXyz[2], d3.svg.mouse(d3_behavior_zoomTarget), d3_behavior_zoomPanning);
  }
}

function d3_behavior_zoomMouseup() {
  if (d3_behavior_zoomPanning) {
    if (d3_behavior_zoomMoved && d3_behavior_zoomEventTarget === d3.event.target) {
      d3_behavior_zoomStopClick = true;
    }
    d3_behavior_zoomMousemove();
    d3_behavior_zoomPanning = null;
  }
}

function d3_behavior_zoomClick() {
  if (d3_behavior_zoomStopClick && d3_behavior_zoomEventTarget === d3.event.target) {
    d3.event.stopPropagation();
    d3.event.preventDefault();
    d3_behavior_zoomStopClick = false;
    d3_behavior_zoomEventTarget = null;
  }
}

function d3_behavior_zoomTo(z, x0, x1) {
  z = d3_behavior_zoomExtentClamp(z, 2);
  var j = Math.pow(2, d3_behavior_zoomXyz[2]),
      k = Math.pow(2, z),
      K = Math.pow(2, (d3_behavior_zoomXyz[2] = z) - x1[2]),
      x_ = d3_behavior_zoomXyz[0],
      y_ = d3_behavior_zoomXyz[1],
      x = d3_behavior_zoomXyz[0] = d3_behavior_zoomExtentClamp((x0[0] - x1[0] * K), 0, k),
      y = d3_behavior_zoomXyz[1] = d3_behavior_zoomExtentClamp((x0[1] - x1[1] * K), 1, k),
      o = d3.event; // Events can be reentrant (e.g., focus).

  d3.event = {
    scale: k,
    translate: [x, y],
    transform: function(sx, sy) {
      if (sx) transform(sx, x_, x);
      if (sy) transform(sy, y_, y);
    }
  };

  function transform(scale, a, b) {
    scale.domain(scale.range().map(function(v) { return scale.invert(((v - b) * j) / k + a); }));
  }

  try {
    d3_behavior_zoomDispatch.apply(d3_behavior_zoomTarget, d3_behavior_zoomArguments);
  } finally {
    d3.event = o;
  }

  o.preventDefault();
}

var d3_behavior_zoomInfiniteExtent = [
  [-Infinity, Infinity],
  [-Infinity, Infinity],
  [-Infinity, Infinity]
];

function d3_behavior_zoomExtentClamp(x, i, k) {
  var range = d3_behavior_zoomExtent[i],
      r0 = range[0],
      r1 = range[1];
  return arguments.length === 3
      ? Math.max(r1 * (r1 === Infinity ? -Infinity : 1 / k - 1),
        Math.min(r0 === -Infinity ? Infinity : r0, x / k)) * k
      : Math.max(r0, Math.min(r1, x));
}
