import type { ModifierKey } from '../common'
import { Dom, disposable, isModifierKeyMatch } from '../common'
import { Base } from './base'

type EventType =
  | 'leftMouseDown'
  | 'rightMouseDown'
  | 'mouseWheel'
  | 'mouseWheelDown'
export interface PanningOptions {
  enabled?: boolean
  modifiers?: string | ModifierKey[] | null
  eventTypes?: EventType[]
}

export class PanningManager extends Base {
  private panning: boolean
  private clientX: number
  private clientY: number
  private mousewheelHandle: Dom.MouseWheelHandle
  private isSpaceKeyPressed: boolean

  protected get widgetOptions() {
    return this.options.panning
  }

  get pannable() {
    return this.widgetOptions && this.widgetOptions.enabled === true
  }

  protected init() {
    this.onRightMouseDown = this.onRightMouseDown.bind(this)
    this.onKeyDown = this.onKeyDown.bind(this)
    this.onKeyUp = this.onKeyUp.bind(this)
    this.startListening()
    this.updateClassName()
  }

  protected startListening() {
    this.graph.on('blank:mousedown', this.onMouseDown, this)
    this.graph.on('node:unhandled:mousedown', this.onMouseDown, this)
    this.graph.on('edge:unhandled:mousedown', this.onMouseDown, this)
    Dom.Event.on(this.graph.container, 'mousedown', this.onRightMouseDown)
    Dom.Event.on(document.body, {
      keydown: this.onKeyDown,
      keyup: this.onKeyUp,
    })
    this.mousewheelHandle = new Dom.MouseWheelHandle(
      this.graph.container,
      this.onMouseWheel.bind(this),
      this.allowMouseWheel.bind(this),
    )
    this.mousewheelHandle.enable()
  }

  protected stopListening() {
    this.graph.off('blank:mousedown', this.onMouseDown, this)
    this.graph.off('node:unhandled:mousedown', this.onMouseDown, this)
    this.graph.off('edge:unhandled:mousedown', this.onMouseDown, this)
    Dom.Event.off(this.graph.container, 'mousedown', this.onRightMouseDown)
    Dom.Event.off(document.body, {
      keydown: this.onKeyDown,
      keyup: this.onKeyUp,
    })
    if (this.mousewheelHandle) {
      this.mousewheelHandle.disable()
    }
  }

  allowPanning(e: Dom.EventObject, strict?: boolean) {
    ;(e as any).spaceKey = this.isSpaceKeyPressed
    return (
      this.pannable &&
      isModifierKeyMatch(e, this.widgetOptions.modifiers, strict)
    )
  }

  protected startPanning(evt: Dom.MouseDownEvent) {
    const e = this.view.normalizeEvent(evt)
    this.clientX = e.clientX
    this.clientY = e.clientY
    this.panning = true
    this.updateClassName(evt)
    Dom.Event.on(document.body, {
      'mousemove.panning touchmove.panning': this.pan.bind(this),
      'mouseup.panning touchend.panning': this.stopPanning.bind(this),
      'mouseleave.panning': this.stopPanning.bind(this),
    })
    Dom.Event.on(window as any, 'mouseup.panning', this.stopPanning.bind(this))
  }

  protected pan(evt: Dom.MouseMoveEvent) {
    const e = this.view.normalizeEvent(evt)
    const dx = e.clientX - this.clientX
    const dy = e.clientY - this.clientY
    this.clientX = e.clientX
    this.clientY = e.clientY
    this.graph.translateBy(dx, dy)
  }

  // eslint-disable-next-line
  protected stopPanning(e: Dom.MouseUpEvent) {
    this.panning = false
    this.updateClassName(e)
    Dom.Event.off(document.body, '.panning')
    Dom.Event.off(window as any, '.panning')
  }

  protected updateClassName(e?: Dom.EventObject) {
    const eventTypes = this.widgetOptions.eventTypes
    if (eventTypes?.length === 1 && eventTypes.includes('mouseWheel')) {
      return
    }
    const container = this.view.container
    const panning = this.view.prefixClassName('graph-panning')
    const pannable = this.view.prefixClassName('graph-pannable')
    const selection = this.graph.getPlugin<any>('selection')
    const allowRubberband = selection && selection.allowRubberband(e, true)
    const allowRightMouseRubberband =
      eventTypes?.includes('leftMouseDown') && !allowRubberband
    if (
      this.allowPanning(e ?? ({} as Dom.EventObject), true) ||
      (this.allowPanning(e ?? ({} as Dom.EventObject)) &&
        allowRightMouseRubberband)
    ) {
      if (this.panning) {
        Dom.addClass(container, panning)
        Dom.removeClass(container, pannable)
      } else {
        Dom.removeClass(container, panning)
        Dom.addClass(container, pannable)
      }
    } else if (!this.panning) {
      Dom.removeClass(container, panning)
      Dom.removeClass(container, pannable)
    }
  }

  protected onMouseDown({ e }: { e: Dom.MouseDownEvent }) {
    if (!this.allowBlankMouseDown(e)) {
      return
    }

    const selection = this.graph.getPlugin<any>('selection')
    const allowRubberband = selection && selection.allowRubberband(e, true)
    if (
      this.allowPanning(e, true) ||
      (this.allowPanning(e) && !allowRubberband)
    ) {
      this.startPanning(e)
    }
  }

  protected onRightMouseDown(e: Dom.MouseDownEvent) {
    const eventTypes = this.widgetOptions.eventTypes
    if (!(eventTypes?.includes('rightMouseDown') && e.button === 2)) {
      return
    }
    if (this.allowPanning(e, true)) {
      this.startPanning(e)
    }
  }

  protected onMouseWheel(e: WheelEvent, deltaX: number, deltaY: number) {
    this.graph.translateBy(-deltaX, -deltaY)
  }

  protected onKeyDown(e: Dom.KeyDownEvent) {
    if (e.which === 32) {
      this.isSpaceKeyPressed = true
    }
    this.updateClassName(e)
  }

  protected onKeyUp(e: Dom.KeyUpEvent) {
    if (e.which === 32) {
      this.isSpaceKeyPressed = false
    }
    this.updateClassName(e)
  }

  protected allowBlankMouseDown(e: Dom.MouseDownEvent) {
    const eventTypes = this.widgetOptions.eventTypes
 
    const isTouchEvent = (typeof e.type === 'string' && e.type.startsWith('touch')) || e.pointerType === 'touch'
    if (isTouchEvent) return eventTypes?.includes('leftMouseDown')

    return (
      (eventTypes?.includes('leftMouseDown') && e.button === 0) ||
      (eventTypes?.includes('mouseWheelDown') && e.button === 1)
    )
  }

  protected allowMouseWheel(e: WheelEvent) {
    return (
      this.pannable &&
      !e.ctrlKey &&
      this.widgetOptions.eventTypes?.includes('mouseWheel')
    )
  }

  autoPanning(x: number, y: number) {
    const buffer = 10
    const graphArea = this.graph.getGraphArea()

    let dx = 0
    let dy = 0
    if (x <= graphArea.left + buffer) {
      dx = -buffer
    }

    if (y <= graphArea.top + buffer) {
      dy = -buffer
    }

    if (x >= graphArea.right - buffer) {
      dx = buffer
    }

    if (y >= graphArea.bottom - buffer) {
      dy = buffer
    }

    if (dx !== 0 || dy !== 0) {
      this.graph.translateBy(-dx, -dy)
    }
  }

  enablePanning() {
    if (!this.pannable) {
      this.widgetOptions.enabled = true
      this.updateClassName()
    }
  }

  disablePanning() {
    if (this.pannable) {
      this.widgetOptions.enabled = false
      this.updateClassName()
    }
  }

  @disposable()
  dispose() {
    this.stopListening()
  }
}
