import { css, LitElement, PropertyValues } from 'lit'
import { customElement, property, state } from 'lit/decorators.js'

function getPoint(e: Event): { x: number; y: number } | undefined {
  if ((e as MouseEvent).button == 0) {
    return {
      x: (e as MouseEvent).clientX,
      y: (e as MouseEvent).clientY
    }
  } else if ((e as TouchEvent).touches?.length == 1) {
    const touch = (e as TouchEvent).touches[0]
    return {
      x: touch.clientX,
      y: touch.clientY
    }
  } else {
    return
  }
}

@customElement('zoomable-image')
export class ZoomableImage extends LitElement {
  static styles = css`
    :host {
      display: block;
      position: relative;
      width: 100%;
      height: 100%;
      overflow: hidden;
    }

    :host::before {
      content: '';
      display: block;

      position: absolute;
      top: 0;
      right: 0;
      width: 100%;
      height: 100%;

      background-image: var(
        --background-image,
        linear-gradient(var(--primary-color, lightgray), var(--primary-color, lightgray))
      );
      transform: var(--image-transform, '');
      background-repeat: no-repeat;
      background-position: center;
      background-size: contain;
    }
  `

  @property({ type: String }) src?: string

  @state() zoom: number = 1
  @state() left: number = 0
  @state() top: number = 0

  private dragStart?: { x: number; y: number }

  private dragEndHandler = this.onDragEnd.bind(this) as EventListener
  private dragMoveHandler = this.onDragMove.bind(this) as EventListener
  private dragStartHandler = this.onDragStart.bind(this) as EventListener
  private wheelEventHandler = this.onWheelEvent.bind(this) as EventListener
  private resetHandler = this.onReset.bind(this) as EventListener

  connectedCallback(): void {
    super.connectedCallback()

    this.addEventListener('mousewheel', this.wheelEventHandler, false)

    this.addEventListener('mousedown', this.dragStartHandler)
    this.addEventListener('touchstart', this.dragStartHandler)
    this.addEventListener('dblclick', this.resetHandler)

    document.addEventListener('mouseup', this.dragEndHandler)
    document.addEventListener('touchend', this.dragEndHandler)
    document.addEventListener('touchcancel', this.dragEndHandler)
    document.addEventListener('mousemove', this.dragMoveHandler)
    document.addEventListener('touchmove', this.dragMoveHandler)
  }

  disconnectedCallback() {
    this.removeEventListener('mousewheel', this.wheelEventHandler, false)

    this.removeEventListener('mousedown', this.dragStartHandler!)
    this.removeEventListener('touchstart', this.dragStartHandler!)
    this.removeEventListener('dblclick', this.resetHandler!)

    document.removeEventListener('mouseup', this.dragEndHandler!)
    document.removeEventListener('touchend', this.dragEndHandler!)
    document.removeEventListener('touchcancel', this.dragEndHandler!)
    document.removeEventListener('mousemove', this.dragMoveHandler!)
    document.removeEventListener('touchmove', this.dragMoveHandler!)

    super.disconnectedCallback()
  }

  updated(changes: PropertyValues<this>) {
    const style = this.style

    if (changes.has('src') && this.src) {
      style.setProperty('--background-image', `url('${this.src}')`)
    }

    style.setProperty('--image-transform', `translate(${this.left}%, ${this.top}%) scale(${this.zoom}, ${this.zoom})`)
  }

  onWheelEvent(e: MouseEvent) {
    e.preventDefault()

    var delta = Math.max(-1, Math.min(1, (e as WheelEvent).deltaY || -(e as WheelEvent).detail))

    this.zoom *= 1 - delta / 20
  }

  onDragStart(e: Event) {
    const point = getPoint(e)

    if (point) {
      this.dragStart = point
      e.stopPropagation()
      return false
    }
  }

  onDragMove(e: Event) {
    if (!this.dragStart) {
      return false
    }

    const point = getPoint(e)

    if (!point) {
      return false
    }

    e.stopPropagation()
    e.preventDefault()

    const dragStart = point
    var { x, y } = point

    x -= this.dragStart.x
    y -= this.dragStart.y

    this.dragStart = dragStart

    const { offsetHeight, offsetWidth } = this

    this.top += (y / offsetHeight) * 100
    this.left += (x / offsetWidth) * 100

    return false
  }

  onDragEnd(e: Event) {
    if (this.dragStart) {
      e.stopPropagation()
      e.preventDefault()

      delete this.dragStart
    }
  }

  onReset(e: Event) {
    this.zoom = 1
    this.left = 0
    this.top = 0
  }
}
