import { Point } from '@svgdotjs/svg.js'
import { Matrix } from '@svgdotjs/svg.js'
import { on, off, Box } from '@svgdotjs/svg.js'

const getCoordsFromEvent = (ev) => {
  if (ev.changedTouches) {
    ev = ev.changedTouches[0]
  }
  return { x: ev.clientX, y: ev.clientY }
}

const maxBoxFromPoints = (points) => {
  let x = Infinity
  let y = Infinity
  let x2 = -Infinity
  let y2 = -Infinity

  for (let i = 0; i < points.length; i++) {
    const p = points[i]
    x = Math.min(x, p[0])
    y = Math.min(y, p[1])
    x2 = Math.max(x2, p[0])
    y2 = Math.max(y2, p[1])
  }

  return new Box(x, y, x2 - x, y2 - y)
}

function scaleBox(box, origin, scale) {
  const points = [
    [box.x, box.y],
    [box.x + box.width, box.y],
    [box.x + box.width, box.y + box.height],
    [box.x, box.y + box.height],
  ]

  const newPoints = points.map(([x, y]) => {
    // Translate to origin
    const translatedX = x - origin[0]
    const translatedY = y - origin[1]

    // Scale
    const scaledX = translatedX * scale
    const scaledY = translatedY * scale

    // Translate back
    return [scaledX + origin[0], scaledY + origin[1]]
  })

  return maxBoxFromPoints(newPoints)
}

export class ResizeHandler {
  constructor(el) {
    this.el = el
    el.remember('_ResizeHandler', this)
    this.lastCoordinates = null
    this.eventType = ''
    this.lastEvent = null
    this.handleResize = this.handleResize.bind(this)
    this.resize = this.resize.bind(this)
    this.endResize = this.endResize.bind(this)
    this.rotate = this.rotate.bind(this)
    this.movePoint = this.movePoint.bind(this)
  }

  active(value, options) {
    this.preserveAspectRatio = options.preserveAspectRatio ?? false
    this.aroundCenter = options.aroundCenter ?? false
    this.grid = options.grid ?? 0
    this.degree = options.degree ?? 0

    // remove all resize events
    this.el.off('.resize')

    if (!value) return

    this.el.on(
      [
        'lt.resize',
        'rt.resize',
        'rb.resize',
        'lb.resize',
        't.resize',
        'r.resize',
        'b.resize',
        'l.resize',
        'rot.resize',
        'point.resize',
      ],
      this.handleResize
    )

    // in case the options were changed mid-resize,
    // we have to replay the last event to see the immediate effect of the option change
    if (this.lastEvent) {
      if (this.eventType === 'rot') {
        this.rotate(this.lastEvent)
      } else if (this.eventType === 'point') {
        this.movePoint(this.lastEvent)
      } else {
        this.resize(this.lastEvent)
      }
    }
  }

  // This is called when a user clicks on one of the resize points
  handleResize(e) {
    this.eventType = e.type
    const { event, index, points } = e.detail
    const isMouse = !event.type.indexOf('mouse')

    // Check for left button
    if (isMouse && (event.which || event.buttons) !== 1) {
      return
    }

    // Fire beforedrag event
    if (this.el.dispatch('beforeresize', { event: e, handler: this }).defaultPrevented) {
      return
    }

    this.box = this.el.bbox()
    this.startPoint = this.el.point(getCoordsFromEvent(event))
    this.index = index
    this.points = points.slice()

    // We consider the resize done, when a touch is canceled, too
    const eventMove = (isMouse ? 'mousemove' : 'touchmove') + '.resize'
    const eventEnd = (isMouse ? 'mouseup' : 'touchcancel.resize touchend') + '.resize'

    if (e.type === 'point') {
      on(window, eventMove, this.movePoint)
    } else if (e.type === 'rot') {
      on(window, eventMove, this.rotate)
    } else {
      on(window, eventMove, this.resize)
    }
    on(window, eventEnd, this.endResize)
  }

  resize(e) {
    this.lastEvent = e

    const endPoint = this.snapToGrid(this.el.point(getCoordsFromEvent(e)))

    let dx = endPoint.x - this.startPoint.x
    let dy = endPoint.y - this.startPoint.y

    if (this.preserveAspectRatio && this.aroundCenter) {
      dx *= 2
      dy *= 2
    }

    const x = this.box.x + dx
    const y = this.box.y + dy
    const x2 = this.box.x2 + dx
    const y2 = this.box.y2 + dy

    let box = new Box(this.box)

    if (this.eventType.includes('l')) {
      box.x = Math.min(x, this.box.x2)
      box.x2 = Math.max(x, this.box.x2)
    }

    if (this.eventType.includes('r')) {
      box.x = Math.min(x2, this.box.x)
      box.x2 = Math.max(x2, this.box.x)
    }

    if (this.eventType.includes('t')) {
      box.y = Math.min(y, this.box.y2)
      box.y2 = Math.max(y, this.box.y2)
    }

    if (this.eventType.includes('b')) {
      box.y = Math.min(y2, this.box.y)
      box.y2 = Math.max(y2, this.box.y)
    }

    box.width = box.x2 - box.x
    box.height = box.y2 - box.y

    // after figuring out the resulting box,
    // we have to check if the aspect ratio should be preserved
    // if so, we have to find the correct scaling factor and scale the box around a fixed point (usually the opposite of the handle)
    // in case aroundCenter is active, the fixed point is the center of the box
    if (this.preserveAspectRatio) {
      const scaleX = box.width / this.box.width
      const scaleY = box.height / this.box.height

      const order = ['lt', 't', 'rt', 'r', 'rb', 'b', 'lb', 'l']

      const origin = (order.indexOf(this.eventType) + 4) % order.length
      const constantPoint = this.aroundCenter ? [this.box.cx, this.box.cy] : this.points[origin]

      let scale = this.eventType.includes('t') || this.eventType.includes('b') ? scaleY : scaleX
      scale = this.eventType.length === 2 ? Math.max(scaleX, scaleY) : scale

      box = scaleBox(this.box, constantPoint, scale)
    }

    if (
      this.el.dispatch('resize', {
        box: new Box(box),
        angle: 0,
        eventType: this.eventType,
        event: e,
        handler: this,
      }).defaultPrevented
    ) {
      return
    }

    this.el.size(box.width, box.height).move(box.x, box.y)
  }

  movePoint(e) {
    this.lastEvent = e
    const { x, y } = this.snapToGrid(this.el.point(getCoordsFromEvent(e)))
    const pointArr = this.el.array().slice()
    pointArr[this.index] = [x, y]

    if (
      this.el.dispatch('resize', {
        box: maxBoxFromPoints(pointArr),
        angle: 0,
        eventType: this.eventType,
        event: e,
        handler: this,
      }).defaultPrevented
    ) {
      return
    }

    this.el.plot(pointArr)
  }

  rotate(e) {
    this.lastEvent = e

    const startPoint = this.startPoint
    const endPoint = this.el.point(getCoordsFromEvent(e))

    const { cx, cy } = this.box

    const dx1 = startPoint.x - cx
    const dy1 = startPoint.y - cy

    const dx2 = endPoint.x - cx
    const dy2 = endPoint.y - cy

    const c = Math.sqrt(dx1 * dx1 + dy1 * dy1) * Math.sqrt(dx2 * dx2 + dy2 * dy2)

    if (c === 0) {
      return
    }
    let angle = (Math.acos((dx1 * dx2 + dy1 * dy2) / c) / Math.PI) * 180

    // catches 0 angle and NaN angle that are zero as well (but numerically instable)
    if (!angle) return

    if (endPoint.x < startPoint.x) {
      angle = -angle
    }

    const matrix = new Matrix(this.el)
    const { x: ox, y: oy } = new Point(cx, cy).transformO(matrix)

    const { rotate } = matrix.decompose()
    const resultAngle = this.snapToAngle(rotate + angle) - rotate

    if (
      this.el.dispatch('resize', {
        box: this.box,
        angle: resultAngle,
        eventType: this.eventType,
        event: e,
        handler: this,
      }).defaultPrevented
    ) {
      return
    }

    this.el.transform(matrix.rotateO(resultAngle, ox, oy))
  }

  endResize(ev) {
    // Unbind resize and end events to window
    if (this.eventType !== 'rot' && this.eventType !== 'point') {
      this.resize(ev)
    }

    this.lastEvent = null

    this.eventType = ''
    off(window, 'mousemove.resize touchmove.resize')
    off(window, 'mouseup.resize touchend.resize')
  }

  snapToGrid(point) {
    if (this.grid) {
      point.x = Math.round(point.x / this.grid) * this.grid
      point.y = Math.round(point.y / this.grid) * this.grid
    }

    return point
  }

  snapToAngle(angle) {
    if (this.degree) {
      angle = Math.round(angle / this.degree) * this.degree
    }

    return angle
  }
}
