import { delimiter } from '../modules/core/regex.js'
import { globals } from '../utils/window.js'
import { register } from '../utils/adopter.js'
import { registerMethods } from '../utils/methods.js'
import Matrix from './Matrix.js'
import Point from './Point.js'
import parser from '../modules/core/parser.js'

export function isNulledBox (box) {
  return !box.width && !box.height && !box.x && !box.y
}

export function domContains (node) {
  return node === globals.document
    || (globals.document.documentElement.contains || function (node) {
      // This is IE - it does not support contains() for top-level SVGs
      while (node.parentNode) {
        node = node.parentNode
      }
      return node === globals.document
    }).call(globals.document.documentElement, node)
}

export default class Box {
  constructor (...args) {
    this.init(...args)
  }

  addOffset () {
    // offset by window scroll position, because getBoundingClientRect changes when window is scrolled
    this.x += globals.window.pageXOffset
    this.y += globals.window.pageYOffset
    return new Box(this)
  }

  init (source) {
    const base = [ 0, 0, 0, 0 ]
    source = typeof source === 'string'
      ? source.split(delimiter).map(parseFloat)
      : Array.isArray(source)
        ? source
        : typeof source === 'object'
          ? [ source.left != null
            ? source.left
            : source.x, source.top != null ? source.top : source.y, source.width, source.height ]
          : arguments.length === 4
            ? [].slice.call(arguments)
            : base

    this.x = source[0] || 0
    this.y = source[1] || 0
    this.width = this.w = source[2] || 0
    this.height = this.h = source[3] || 0

    // Add more bounding box properties
    this.x2 = this.x + this.w
    this.y2 = this.y + this.h
    this.cx = this.x + this.w / 2
    this.cy = this.y + this.h / 2

    return this
  }

  isNulled () {
    return isNulledBox(this)
  }

  // Merge rect box with another, return a new instance
  merge (box) {
    const x = Math.min(this.x, box.x)
    const y = Math.min(this.y, box.y)
    const width = Math.max(this.x + this.width, box.x + box.width) - x
    const height = Math.max(this.y + this.height, box.y + box.height) - y

    return new Box(x, y, width, height)
  }

  toArray () {
    return [ this.x, this.y, this.width, this.height ]
  }

  toString () {
    return this.x + ' ' + this.y + ' ' + this.width + ' ' + this.height
  }

  transform (m) {
    if (!(m instanceof Matrix)) {
      m = new Matrix(m)
    }

    let xMin = Infinity
    let xMax = -Infinity
    let yMin = Infinity
    let yMax = -Infinity

    const pts = [
      new Point(this.x, this.y),
      new Point(this.x2, this.y),
      new Point(this.x, this.y2),
      new Point(this.x2, this.y2)
    ]

    pts.forEach(function (p) {
      p = p.transform(m)
      xMin = Math.min(xMin, p.x)
      xMax = Math.max(xMax, p.x)
      yMin = Math.min(yMin, p.y)
      yMax = Math.max(yMax, p.y)
    })

    return new Box(
      xMin, yMin,
      xMax - xMin,
      yMax - yMin
    )
  }

}

function getBox (el, getBBoxFn, retry) {
  let box

  try {
    // Try to get the box with the provided function
    box = getBBoxFn(el.node)

    // If the box is worthless and not even in the dom, retry
    // by throwing an error here...
    if (isNulledBox(box) && !domContains(el.node)) {
      throw new Error('Element not in the dom')
    }
  } catch (e) {
    // ... and calling the retry handler here
    box = retry(el)
  }

  return box
}

export function bbox () {
  // Function to get bbox is getBBox()
  const getBBox = (node) => node.getBBox()

  // Take all measures so that a stupid browser renders the element
  // so we can get the bbox from it when we try again
  const retry = (el) => {
    try {
      const clone = el.clone().addTo(parser().svg).show()
      const box = clone.node.getBBox()
      clone.remove()
      return box
    } catch (e) {
      // We give up...
      throw new Error(`Getting bbox of element "${el.node.nodeName}" is not possible: ${e.toString()}`)
    }
  }

  const box = getBox(this, getBBox, retry)
  const bbox = new Box(box)

  return bbox
}

export function rbox (el) {
  const getRBox = (node) => node.getBoundingClientRect()
  const retry = (el) => {
    // There is no point in trying tricks here because if we insert the element into the dom ourselves
    // it obviously will be at the wrong position
    throw new Error(`Getting rbox of element "${el.node.nodeName}" is not possible`)
  }

  const box = getBox(this, getRBox, retry)
  const rbox = new Box(box)

  // If an element was passed, we want the bbox in the coordinate system of that element
  if (el) {
    return rbox.transform(el.screenCTM().inverseO())
  }

  // Else we want it in absolute screen coordinates
  // Therefore we need to add the scrollOffset
  return rbox.addOffset()
}

// Checks whether the given point is inside the bounding box
export function inside (x, y) {
  const box = this.bbox()

  return x > box.x
    && y > box.y
    && x < box.x + box.width
    && y < box.y + box.height
}

registerMethods({
  viewbox: {
    viewbox (x, y, width, height) {
      // act as getter
      if (x == null) return new Box(this.attr('viewBox'))

      // act as setter
      return this.attr('viewBox', new Box(x, y, width, height))
    },

    zoom (level, point) {
      // Its best to rely on the attributes here and here is why:
      // clientXYZ: Doesn't work on non-root svgs because they dont have a CSSBox (silly!)
      // getBoundingClientRect: Doesn't work because Chrome just ignores width and height of nested svgs completely
      //                        that means, their clientRect is always as big as the content.
      //                        Furthermore this size is incorrect if the element is further transformed by its parents
      // computedStyle: Only returns meaningful values if css was used with px. We dont go this route here!
      // getBBox: returns the bounding box of its content - that doesnt help!
      let { width, height } = this.attr([ 'width', 'height' ])

      // Width and height is a string when a number with a unit is present which we can't use
      // So we try clientXYZ
      if ((!width && !height) || (typeof width === 'string' || typeof height === 'string')) {
        width = this.node.clientWidth
        height = this.node.clientHeight
      }

      // Giving up...
      if (!width || !height) {
        throw new Error('Impossible to get absolute width and height. Please provide an absolute width and height attribute on the zooming element')
      }

      const v = this.viewbox()

      const zoomX = width / v.width
      const zoomY = height / v.height
      const zoom = Math.min(zoomX, zoomY)

      if (level == null) {
        return zoom
      }

      let zoomAmount = zoom / level

      // Set the zoomAmount to the highest value which is safe to process and recover from
      // The * 100 is a bit of wiggle room for the matrix transformation
      if (zoomAmount === Infinity) zoomAmount = Number.MAX_SAFE_INTEGER / 100

      point = point || new Point(width / 2 / zoomX + v.x, height / 2 / zoomY + v.y)

      const box = new Box(v).transform(
        new Matrix({ scale: zoomAmount, origin: point })
      )

      return this.viewbox(box)
    }
  }
})

register(Box, 'Box')