// @ts-check
import Hammer from "hammerjs";

/**
 * @typedef {object} SmoothZoomable
 * @property {HTMLElement} $container
 * @property {HTMLElement} $visibleWorld
 * @property {number} scale
 * @property {function(): void} [attachScrollListeners]
 * @property {function(): void} [detachScrollListeners]
 * @property {function({ clientX: number, clientY: number}): void} updateScaleCenter
 * @property {{ x: number, y: number }} scaleCenter
 */

/** Manages pinch-zoom, ctrl-wheel, and trackpad pinch smooth zooming. */
export class ModeSmoothZoom {
  /** @param {SmoothZoomable} mode */
  constructor(mode) {
    /** @type {SmoothZoomable} */
    this.mode = mode;

    this.pinchMoveFrame = null;
    this.pinchMoveFramePromise = Promise.resolve();
    this.oldScale = 1;
    this.lastEvent = null;

    /** @type {HammerManager} */
    this.hammer = null;
  }

  attach() {
    this.attachCtrlZoom();

    // Hammer.js by default set userSelect to None; we don't want that!
    // TODO: Is there any way to do this not globally on Hammer?
    delete Hammer.defaults.cssProps.userSelect;
    this.hammer = new Hammer.Manager(this.mode.$container, {
      touchAction: "pan-x pan-y",
    });

    this.hammer.add(new Hammer.Pinch());

    this.hammer.on("pinchstart", this._pinchStart);
    this.hammer.on("pinchmove", this._pinchMove);
    this.hammer.on("pinchend", this._pinchEnd);

    // iOS fires pinchcancel ~randomly; it looks like it sometimes
    // things the pinch becomes a pan, at which point it cancels?
    // More work needed here.
    this.hammer.on("pinchcancel", this._pinchEnd);
  }

  _pinchStart = () => {
    // Do this in case the pinchend hasn't fired yet.
    this.oldScale = 1;
    this.mode.$visibleWorld.style.willChange = "transform";
    this.detachCtrlZoom();
    this.mode.detachScrollListeners?.();
  }

  /** @param {HammerInput} e */
  _pinchMove = async (e) => {
    this.lastEvent = e;
    if (!this.pinchMoveFrame) {
      let pinchMoveFramePromiseRes = null;
      this.pinchMoveFramePromise = new Promise(
        (res) => (pinchMoveFramePromiseRes = res)
      );

      // Buffer these events; only update the scale when request animation fires
      this.pinchMoveFrame = requestAnimationFrame(() => {
        this.mode.updateScaleCenter({
          clientX: this.lastEvent.center.x,
          clientY: this.lastEvent.center.y,
        });
        this.mode.scale *= this.lastEvent.scale / this.oldScale;
        this.oldScale = this.lastEvent.scale;
        this.pinchMoveFrame = null;
        pinchMoveFramePromiseRes();
      });
    }
  }

  _pinchEnd = async () => {
    // Want this to happen after the pinchMoveFrame,
    // if one is in progress; otherwise setting oldScale
    // messes up the transform.
    await this.pinchMoveFramePromise;
    this.mode.scaleCenter = { x: 0.5, y: 0.5 };
    this.oldScale = 1;
    this.mode.$visibleWorld.style.willChange = "auto";
    this.attachCtrlZoom();
    this.mode.attachScrollListeners?.();
  }

  /** @private */
  attachCtrlZoom() {
    window.addEventListener("wheel", this.handleCtrlWheel, { passive: false });
  }

  /** @private */
  detachCtrlZoom() {
    window.removeEventListener("wheel", this.handleCtrlWheel);
  }

  /**
   * @private
   * @param {WheelEvent} ev
   **/
  handleCtrlWheel = (ev) => {
    if (!ev.ctrlKey) return;
    ev.preventDefault();
    const zoomMultiplier =
        // Zooming on macs was painfully slow; likely due to their better
        // trackpads. Give them a higher zoom rate.
        /Mac/i.test(navigator.platform)
          ? 0.045
          : // This worked well for me on Windows
          0.03;

    // Zoom around the cursor
    this.mode.updateScaleCenter(ev);
    this.mode.scale *= 1 - Math.sign(ev.deltaY) * zoomMultiplier;
  }
}

@@ -798,83 +798,381 @@
 ll;%0A
-%0A    /** @type %7BHammerManager%7D */%0A    this.hammer = null;%0A  %7D%0A%0A  attach() %7B
+  %7D%0A%0A  attach() %7B%0A    this.attachCtrlZoom();%0A%0A    // GestureEvents work only on Safari; they interfere with Hammer,%0A    // so block them.%0A    this.mode.$container.addEventListener('gesturestart', this._preventEvent);%0A    this.mode.$container.addEventListener('gesturechange', this._preventEvent);%0A    this.mode.$container.addEventListener('gestureend', this._preventEvent);
 %0A   
@@ -1177,34 +1177,57 @@
    this.
+_
 attach
-CtrlZoom();%0A
+Hammer();%0A  %7D%0A%0A  _attachHammer() %7B
 %0A    // 
@@ -1404,29 +1404,30 @@
 Select;%0A    
-this.
+const 
 hammer = new
@@ -1509,29 +1509,24 @@
    %7D);%0A%0A    
-this.
 hammer.add(n
@@ -1546,29 +1546,24 @@
 ch());%0A%0A    
-this.
 hammer.on(%22p
@@ -1593,29 +1593,24 @@
 Start);%0A    
-this.
 hammer.on(%22p
@@ -1638,29 +1638,24 @@
 hMove);%0A    
-this.
 hammer.on(%22p
@@ -1685,221 +1685,158 @@
 d);%0A
-%0A
     
-// iOS fires pinchcancel ~randomly; it looks like it sometimes%0A    // things the pinch becomes a pan, at which point it cancels?%0A    // More work needed here.%0A    this.hammer.on(%22pinchcancel%22, this._pinchEnd)
+hammer.on(%22pinchcancel%22, this._pinchCancel);%0A  %7D%0A%0A  /** @param %7BEvent%7D ev */%0A  _preventEvent = (ev) =%3E %7B%0A    ev.preventDefault();%0A    return false
 ;%0A  
@@ -3221,32 +3221,229 @@
 eners?.();%0A  %7D%0A%0A
+  _pinchCancel = async () =%3E %7B%0A    // iOS fires pinchcancel ~randomly; it looks like it sometimes%0A    // thinks the pinch becomes a pan, at which point it cancels?%0A    await this._pinchEnd();%0A  %7D%0A%0A
   /** @private *

Fix pinch zoom on iOS! \o/