import "../core/document";
import "../core/rebind";
import "../event/drag";
import "../event/event";
import "../event/mouse";
import "../event/touches";
import "../selection/selection";
import "../interpolate/zoom";
import "behavior";

d3.behavior.zoom = function() {
  var view = {x: 0, y: 0, k: 1},
      translate0, // translate when we started zooming (to avoid drift)
      center0, // implicit desired position of translate0 after zooming
      center, // explicit desired position of translate0 after zooming
      size = [960, 500], // viewport size; required for zoom interpolation
      scaleExtent = d3_behavior_zoomInfinity,
      duration = 250,
      zooming = 0,
      mousedown = "mousedown.zoom",
      mousemove = "mousemove.zoom",
      mouseup = "mouseup.zoom",
      mousewheelTimer,
      touchstart = "touchstart.zoom",
      touchtime, // time of last touchstart (to detect double-tap)
      event = d3_eventDispatch(zoom, "zoomstart", "zoom", "zoomend"),
      x0,
      x1,
      y0,
      y1;

  // Lazily determine the DOM’s support for Wheel events.
  // https://developer.mozilla.org/en-US/docs/Mozilla_event_reference/wheel
  if (!d3_behavior_zoomWheel) {
    d3_behavior_zoomWheel = "onwheel" in d3_document ? (d3_behavior_zoomDelta = function() { return -d3.event.deltaY * (d3.event.deltaMode ? 120 : 1); }, "wheel")
        : "onmousewheel" in d3_document ? (d3_behavior_zoomDelta = function() { return d3.event.wheelDelta; }, "mousewheel")
        : (d3_behavior_zoomDelta = function() { return -d3.event.detail; }, "MozMousePixelScroll");
  }

  function zoom(g) {
    g   .on(mousedown, mousedowned)
        .on(d3_behavior_zoomWheel + ".zoom", mousewheeled)
        .on("dblclick.zoom", dblclicked)
        .on(touchstart, touchstarted);
  }

  zoom.event = function(g) {
    g.each(function() {
      var dispatch = event.of(this, arguments),
          view1 = view;
      if (d3_transitionInheritId) {
        d3.select(this).transition()
            .each("start.zoom", function() {
              view = this.__chart__ || {x: 0, y: 0, k: 1}; // pre-transition state
              zoomstarted(dispatch);
            })
            .tween("zoom:zoom", function() {
              var dx = size[0],
                  dy = size[1],
                  cx = center0 ? center0[0] : dx / 2,
                  cy = center0 ? center0[1] : dy / 2,
                  i = d3.interpolateZoom(
                    [(cx - view.x) / view.k, (cy - view.y) / view.k, dx / view.k],
                    [(cx - view1.x) / view1.k, (cy - view1.y) / view1.k, dx / view1.k]
                  );
              return function(t) {
                var l = i(t), k = dx / l[2];
                this.__chart__ = view = {x: cx - l[0] * k, y: cy - l[1] * k, k: k};
                zoomed(dispatch);
              };
            })
            .each("interrupt.zoom", function() {
              zoomended(dispatch);
            })
            .each("end.zoom", function() {
              zoomended(dispatch);
            });
      } else {
        this.__chart__ = view;
        zoomstarted(dispatch);
        zoomed(dispatch);
        zoomended(dispatch);
      }
    });
  }

  zoom.translate = function(_) {
    if (!arguments.length) return [view.x, view.y];
    view = {x: +_[0], y: +_[1], k: view.k}; // copy-on-write
    rescale();
    return zoom;
  };

  zoom.scale = function(_) {
    if (!arguments.length) return view.k;
    view = {x: view.x, y: view.y, k: null}; // copy-on-write
    scaleTo(+_);
    rescale();
    return zoom;
  };

  zoom.scaleExtent = function(_) {
    if (!arguments.length) return scaleExtent;
    scaleExtent = _ == null ? d3_behavior_zoomInfinity : [+_[0], +_[1]];
    return zoom;
  };

  zoom.center = function(_) {
    if (!arguments.length) return center;
    center = _ && [+_[0], +_[1]];
    return zoom;
  };

  zoom.size = function(_) {
    if (!arguments.length) return size;
    size = _ && [+_[0], +_[1]];
    return zoom;
  };

  zoom.duration = function(_) {
    if (!arguments.length) return duration;
    duration = +_; // TODO function based on interpolateZoom distance?
    return zoom;
  };

  zoom.x = function(z) {
    if (!arguments.length) return x1;
    x1 = z;
    x0 = z.copy();
    view = {x: 0, y: 0, k: 1}; // copy-on-write
    return zoom;
  };

  zoom.y = function(z) {
    if (!arguments.length) return y1;
    y1 = z;
    y0 = z.copy();
    view = {x: 0, y: 0, k: 1}; // copy-on-write
    return zoom;
  };

  function location(p) {
    return [(p[0] - view.x) / view.k, (p[1] - view.y) / view.k];
  }

  function point(l) {
    return [l[0] * view.k + view.x, l[1] * view.k + view.y];
  }

  function scaleTo(s) {
    view.k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], s));
  }

  function translateTo(p, l) {
    l = point(l);
    view.x += p[0] - l[0];
    view.y += p[1] - l[1];
  }

  function zoomTo(that, p, l, k) {
    that.__chart__ = {x: view.x, y: view.y, k: view.k};

    scaleTo(Math.pow(2, k));
    translateTo(center0 = p, l);

    that = d3.select(that);
    if (duration > 0) that = that.transition().duration(duration);
    that.call(zoom.event);
  }

  function rescale() {
    if (x1) x1.domain(x0.range().map(function(x) { return (x - view.x) / view.k; }).map(x0.invert));
    if (y1) y1.domain(y0.range().map(function(y) { return (y - view.y) / view.k; }).map(y0.invert));
  }

  function zoomstarted(dispatch) {
    if (!zooming++) dispatch({type: "zoomstart"});
  }

  function zoomed(dispatch) {
    rescale();
    dispatch({type: "zoom", scale: view.k, translate: [view.x, view.y]});
  }

  function zoomended(dispatch) {
    if (!--zooming) dispatch({type: "zoomend"}), center0 = null;
  }

  function mousedowned() {
    var that = this,
        dispatch = event.of(that, arguments),
        dragged = 0,
        subject = d3.select(d3_window(that)).on(mousemove, moved).on(mouseup, ended),
        location0 = location(d3.mouse(that)),
        dragRestore = d3_event_dragSuppress(that);

    d3_selection_interrupt.call(that);
    zoomstarted(dispatch);

    function moved() {
      dragged = 1;
      translateTo(d3.mouse(that), location0);
      zoomed(dispatch);
    }

    function ended() {
      subject.on(mousemove, null).on(mouseup, null);
      dragRestore(dragged);
      zoomended(dispatch);
    }
  }

  // These closures persist for as long as at least one touch is active.
  function touchstarted() {
    var that = this,
        dispatch = event.of(that, arguments),
        locations0 = {}, // touchstart locations
        distance0 = 0, // distance² between initial touches
        scale0, // scale when we started touching
        zoomName = ".zoom-" + d3.event.changedTouches[0].identifier,
        touchmove = "touchmove" + zoomName,
        touchend = "touchend" + zoomName,
        targets = [],
        subject = d3.select(that),
        dragRestore = d3_event_dragSuppress(that);

    started();
    zoomstarted(dispatch);

    // Workaround for Chrome issue 412723: the touchstart listener must be set
    // after the touchmove listener.
    subject.on(mousedown, null).on(touchstart, started); // prevent duplicate events

    // Updates locations of any touches in locations0.
    function relocate() {
      var touches = d3.touches(that);
      scale0 = view.k;
      touches.forEach(function(t) {
        if (t.identifier in locations0) locations0[t.identifier] = location(t);
      });
      return touches;
    }

    // Temporarily override touchstart while gesture is active.
    function started() {

      // Listen for touchmove and touchend on the target of touchstart.
      var target = d3.event.target;
      d3.select(target).on(touchmove, moved).on(touchend, ended);
      targets.push(target);

      // Only track touches started on the same subject element.
      var changed = d3.event.changedTouches;
      for (var i = 0, n = changed.length; i < n; ++i) {
        locations0[changed[i].identifier] = null;
      }

      var touches = relocate(),
          now = Date.now();

      if (touches.length === 1) {
        if (now - touchtime < 500) { // dbltap
          var p = touches[0];
          zoomTo(that, p, locations0[p.identifier], Math.floor(Math.log(view.k) / Math.LN2) + 1);
          d3_eventPreventDefault();
        }
        touchtime = now;
      } else if (touches.length > 1) {
        var p = touches[0], q = touches[1],
            dx = p[0] - q[0], dy = p[1] - q[1];
        distance0 = dx * dx + dy * dy;
      }
    }

    function moved() {
      var touches = d3.touches(that),
          p0, l0,
          p1, l1;

      d3_selection_interrupt.call(that);

      for (var i = 0, n = touches.length; i < n; ++i, l1 = null) {
        p1 = touches[i];
        if (l1 = locations0[p1.identifier]) {
          if (l0) break;
          p0 = p1, l0 = l1;
        }
      }

      if (l1) {
        var distance1 = (distance1 = p1[0] - p0[0]) * distance1 + (distance1 = p1[1] - p0[1]) * distance1,
            scale1 = distance0 && Math.sqrt(distance1 / distance0);
        p0 = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2];
        l0 = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2];
        scaleTo(scale1 * scale0);
      }

      touchtime = null;
      translateTo(p0, l0);
      zoomed(dispatch);
    }

    function ended() {
      // If there are any globally-active touches remaining, remove the ended
      // touches from locations0.
      if (d3.event.touches.length) {
        var changed = d3.event.changedTouches;
        for (var i = 0, n = changed.length; i < n; ++i) {
          delete locations0[changed[i].identifier];
        }
        // If locations0 is not empty, then relocate and continue listening for
        // touchmove and touchend.
        for (var identifier in locations0) {
          return void relocate(); // locations may have detached due to rotation
        }
      }
      // Otherwise, remove touchmove and touchend listeners.
      d3.selectAll(targets).on(zoomName, null);
      subject.on(mousedown, mousedowned).on(touchstart, touchstarted);
      dragRestore();
      zoomended(dispatch);
    }
  }

  function mousewheeled() {
    var dispatch = event.of(this, arguments);
    if (mousewheelTimer) clearTimeout(mousewheelTimer);
    else d3_selection_interrupt.call(this), translate0 = location(center0 = center || d3.mouse(this)), zoomstarted(dispatch);
    mousewheelTimer = setTimeout(function() { mousewheelTimer = null; zoomended(dispatch); }, 50);
    d3_eventPreventDefault();
    scaleTo(Math.pow(2, d3_behavior_zoomDelta() * 0.002) * view.k);
    translateTo(center0, translate0);
    zoomed(dispatch);
  }

  function dblclicked() {
    var p = d3.mouse(this),
        k = Math.log(view.k) / Math.LN2;

    zoomTo(this, p, location(p), d3.event.shiftKey ? Math.ceil(k) - 1 : Math.floor(k) + 1);
  }

  return d3.rebind(zoom, event, "on");
};

var d3_behavior_zoomInfinity = [0, Infinity], // default scale extent
    d3_behavior_zoomDelta, // initialized lazily
    d3_behavior_zoomWheel;
