import { chrome, gecko, ie, mac, presto, safari, webkit } from "../util/browser"
import { e_preventDefault } from "../util/event"

import { startWorker } from "./highlight_worker"
import { alignHorizontally } from "./line_numbers"
import { updateDisplaySimple} from "./update_display"

// Sync the scrollable area and scrollbars, ensure the viewport
// covers the visible area.
export function setScrollTop(cm, val) {
  if (Math.abs(cm.doc.scrollTop - val) < 2) return
  cm.doc.scrollTop = val
  if (!gecko) updateDisplaySimple(cm, {top: val})
  if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val
  cm.display.scrollbars.setScrollTop(val)
  if (gecko) updateDisplaySimple(cm)
  startWorker(cm, 100)
}
// Sync scroller and scrollbar, ensure the gutter elements are
// aligned.
export function setScrollLeft(cm, val, isScroller) {
  if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return
  val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth)
  cm.doc.scrollLeft = val
  alignHorizontally(cm)
  if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val
  cm.display.scrollbars.setScrollLeft(val)
}

// Since the delta values reported on mouse wheel events are
// unstandardized between browsers and even browser versions, and
// generally horribly unpredictable, this code starts by measuring
// the scroll effect that the first few mouse wheel events have,
// and, from that, detects the way it can convert deltas to pixel
// offsets afterwards.
//
// The reason we want to know the amount a wheel event will scroll
// is that it gives us a chance to update the display before the
// actual scrolling happens, reducing flickering.

let wheelSamples = 0, wheelPixelsPerUnit = null
// Fill in a browser-detected starting value on browsers where we
// know one. These don't have to be accurate -- the result of them
// being wrong would just be a slight flicker on the first wheel
// scroll (if it is large enough).
if (ie) wheelPixelsPerUnit = -.53
else if (gecko) wheelPixelsPerUnit = 15
else if (chrome) wheelPixelsPerUnit = -.7
else if (safari) wheelPixelsPerUnit = -1/3

function wheelEventDelta(e) {
  let dx = e.wheelDeltaX, dy = e.wheelDeltaY
  if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail
  if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail
  else if (dy == null) dy = e.wheelDelta
  return {x: dx, y: dy}
}
export function wheelEventPixels(e) {
  let delta = wheelEventDelta(e)
  delta.x *= wheelPixelsPerUnit
  delta.y *= wheelPixelsPerUnit
  return delta
}

export function onScrollWheel(cm, e) {
  let delta = wheelEventDelta(e), dx = delta.x, dy = delta.y

  let display = cm.display, scroll = display.scroller
  // Quit if there's nothing to scroll here
  let canScrollX = scroll.scrollWidth > scroll.clientWidth
  let canScrollY = scroll.scrollHeight > scroll.clientHeight
  if (!(dx && canScrollX || dy && canScrollY)) return

  // Webkit browsers on OS X abort momentum scrolls when the target
  // of the scroll event is removed from the scrollable element.
  // This hack (see related code in patchDisplay) makes sure the
  // element is kept around.
  if (dy && mac && webkit) {
    outer: for (let cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) {
      for (let i = 0; i < view.length; i++) {
        if (view[i].node == cur) {
          cm.display.currentWheelTarget = cur
          break outer
        }
      }
    }
  }

  // On some browsers, horizontal scrolling will cause redraws to
  // happen before the gutter has been realigned, causing it to
  // wriggle around in a most unseemly way. When we have an
  // estimated pixels/delta value, we just handle horizontal
  // scrolling entirely here. It'll be slightly off from native, but
  // better than glitching out.
  if (dx && !gecko && !presto && wheelPixelsPerUnit != null) {
    if (dy && canScrollY)
      setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight)))
    setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth)))
    // Only prevent default scrolling if vertical scrolling is
    // actually possible. Otherwise, it causes vertical scroll
    // jitter on OSX trackpads when deltaX is small and deltaY
    // is large (issue #3579)
    if (!dy || (dy && canScrollY))
      e_preventDefault(e)
    display.wheelStartX = null // Abort measurement, if in progress
    return
  }

  // 'Project' the visible viewport to cover the area that is being
  // scrolled into view (if we know enough to estimate it).
  if (dy && wheelPixelsPerUnit != null) {
    let pixels = dy * wheelPixelsPerUnit
    let top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight
    if (pixels < 0) top = Math.max(0, top + pixels - 50)
    else bot = Math.min(cm.doc.height, bot + pixels + 50)
    updateDisplaySimple(cm, {top: top, bottom: bot})
  }

  if (wheelSamples < 20) {
    if (display.wheelStartX == null) {
      display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop
      display.wheelDX = dx; display.wheelDY = dy
      setTimeout(() => {
        if (display.wheelStartX == null) return
        let movedX = scroll.scrollLeft - display.wheelStartX
        let movedY = scroll.scrollTop - display.wheelStartY
        let sample = (movedY && display.wheelDY && movedY / display.wheelDY) ||
          (movedX && display.wheelDX && movedX / display.wheelDX)
        display.wheelStartX = display.wheelStartY = null
        if (!sample) return
        wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1)
        ++wheelSamples
      }, 200)
    } else {
      display.wheelDX += dx; display.wheelDY += dy
    }
  }
}
