/* eslint-disable no-console */
/* eslint-disable no-case-declarations */
/* eslint-disable no-unused-vars */
import { Elements } from './utils.js'
import Poppable from './poppable.js'

/** A Popup that shows text labels, images, or html
 */
export default class Popup extends Poppable {
    /**
     * Creates an instance of Popup.
     * @param {any} [{
     *     parent = null,     - The DOM parent element.
     *     content = null,    - A dict object with type strings (text, img, html) as keys
     *                       and corresponding values.
     *     context = window,    - A context object for poppable elements
     *     fontSize = "1em",  - Describes the font size as CSS value
     *     fontFamily = "Arial", - Describes the font family as CSS value
     *     padding = 16,      - {number || string} padding - Describes the padding as CSS value
     *     notchSize = 10,    - {number || string} notchSize - Describes the size of the notch (callout) as CSS value
     *     switchPos = false,
     *     minWidth = null,
     *     maxWidth = 800,
     *     backgroundColor = "#EEE",  - The color of the background as CSS value
     *     normalColor = "#444", - normalColor  - The color of textitems as CSS value
     *     notchPosition = "bottomLeft",
     *     zIndex = 0,
     *     keepWithin = null, - Ensure that popup is visible within the bounds of the given container
     *     autoClose = true,  - Autoclose the Popup on tap
     *     closeIcon = null,
     *     resizeIcon = null,
     *     closeCommand = null,
     *     draggable = false
     *      noStyle = false - When true, prevents the popup from doing any aesthetic manipulations to the DOM leaving the styling completely to the style sheets.
     * }={}]
     * @memberof Popup
     */
    constructor({
        parent = null,
        content = null,
        context = window,
        fontSize = '1em',
        fontFamily = 'Arial',
        padding = 16,
        notchSize = 10,
        switchPos = false,
        minWidth = null,
        maxWidth = 800,
        backgroundColor = '#EEE',
        normalColor = '#444',
        notchPosition = 'bottomCenter',
        zIndex = 0,
        keepWithin = null,
        autoClose = true,
        closeIcon = null,
        resizeIcon = null,
        closeCommand = null,
        draggable = false,
        posOffset = 0,
        targetBoundingBox = null,
        useEventPosWithBoundingBox = false,
        interactive = false,
        onResize = null,
        onMove = null,
        noStyle = false,
        hideOnUp = true,
    } = {}) {
        super()
        this.context = context
        this.noStyle = noStyle
        this.hideOnUp = hideOnUp
        this.padding = padding
        this.notchPosition = notchPosition
        this.notchSize = notchSize
        this.switchPos = switchPos
        this.fontSize = fontSize
        this.fontFamily = fontFamily
        this.minWidth = minWidth
        this.maxWidth = maxWidth
        this.normalColor = normalColor
        this.backgroundColor = backgroundColor
        this.keepWithin = keepWithin
        this.autoClose = autoClose
        this.resizeIcon = resizeIcon
        this.closeIcon = closeIcon
        this.closeCommand = closeCommand
        this.zIndex = zIndex
        this.parent = parent || document.body
        this.draggable = draggable
        this.posOffset = posOffset
        this.targetBoundingBox = targetBoundingBox
        this.useEventPosWithBoundingBox = useEventPosWithBoundingBox
        this.currentPos = null
        this.insertedNode = null
        this.loaded = false
        this.interactive = interactive
        this.onload = null
        this.onResize = onResize
        this.onMove = onMove
        if (content) {
            this.show(content)
        }
    }

    /** Setup popup with a dictionary of content types and contents.
     * @param {Object} content - A dict object with type strings (text, img, html) as keys
     *                            and corresponding values.
     * @return {Popup} this
     */
    setup(content) {
        //console.log("Popup.setup", this.draggable)
        this.content = content
        this.items = {}
        this.element = document.createElement('div')
        this.element.classList.add('popup')
        this.setAlpha(this.element, 0)
        // this.element.style.opacity = 0
        Elements.addClass(this.element, 'unselectable')
        this.notch = document.createElement('div')
        Elements.setStyle(this.notch, this.notchStyle())
        this.notch.className = 'notch'
        this.setupDraggable()
        if (this.closeIcon) {
            let img = document.createElement('img')
            img.setAttribute('draggable', false)
            img.src = this.closeIcon
            img.style.position = 'absolute'
            img.style.right = '0px'
            img.style.top = '0px'
            img.style.width = '16px'
            img.style.height = '16px'
            img.onclick = (e) => {
                this.close()
            }
            this.element.appendChild(img)
        }
        if (this.resizeIcon) {
            let img = document.createElement('img')
            img.style.position = 'absolute'
            img.style.right = '0px'
            img.style.bottom = '0px'
            img.style.width = '16px'
            img.style.height = '16px'
            img.src = this.resizeIcon
            img.setAttribute('draggable', true)
            img.ondragstart = (e) => {
                this.currentPos = { x: e.clientX, y: e.clientY }
                return true
            }
            img.ondrag = (e) => {
                e.preventDefault()

                let target = this.element.querySelector('iframe') || this.element
                let delta = {
                    x: e.clientX - this.currentPos.x,
                    y: e.clientY - this.currentPos.y,
                }

                this.currentPos = { x: e.clientX, y: e.clientY }
                if (delta.x == 0 && delta.y == 0) return

                let rect = target.getBoundingClientRect()
                let width = rect.width + delta.x
                let height = rect.height + delta.y
                target.style.width = width + 'px'
                target.style.height = height + 'px'

                switch (this.notchPosition) {
                    case 'bottomLeft':
                    case 'bottomCenter':
                        let bottom = parseFloat(this.element.style.bottom)
                        this.element.style.bottom = bottom - delta.y + 'px'
                        break
                    default:
                        break
                }
                //console.log("onResize", this.onResize)
                if (this.onResize) {
                    this.onResize({ target, delta, width, height })
                }
            }
            img.ondragend = (e) => {}
            this.element.appendChild(img)
        }

        for (let key in content) {
            console.log('using', key, this.loaded)
            switch (key) {
                case 'selector':
                    break
                case 'text':
                    let text = document.createElement('span')
                    this.element.appendChild(text)
                    text.innerHTML = content[key]
                    Elements.setStyle(text, { color: this.normalColor })
                    Elements.addClass(text, 'unselectable')
                    Elements.addClass(text, 'PopupContent')
                    this.insertedNode = text
                    this.loaded = true
                    break
                case 'img':
                    alert('img to be implemented')
                    break
                case 'iframe':
                    let iframe = document.createElement('iframe')
                    iframe.setAttribute('frameBorder', 0)
                    iframe.src = content[key]
                    iframe.onload = (e) => {
                        let body = iframe.contentWindow.document.body
                        let observer = new MutationObserver(() => {
                            this.iframeChanged(iframe)
                        })
                        observer.observe(iframe.contentWindow.document, {
                            attributes: true,
                            subtree: true,
                            childList: true,
                            characterData: true,
                        })
                        let w = Math.max(body.scrollWidth, body.offsetWidth)
                        let h = Math.max(body.scrollHeight, body.offsetHeight)
                        iframe.style.width = w + 'px'
                        iframe.style.height = h + 'px'
                        this.layoutAfterInsert()
                        if (this.onload != null) {
                            this.onload()
                        }
                        this.loaded = true
                    }
                    this.element.appendChild(iframe)
                    Elements.addClass(iframe, 'PopupContent')
                    this.insertIntoDOM()
                    return
                case 'html':
                    this.loaded = false
                    let div = document.createElement('div')
                    Elements.addClass(div, 'PopupContent')
                    this.element.appendChild(div)
                    div.innerHTML = content.html
                    //console.log("insert", content)
                    let selector = content.selector
                    if (selector) {
                        this.insertedNode = div.querySelector(selector)
                        if (this.insertedNode == null) {
                            div.innerHTML = `<p style="color:red;">Popup content not found. Missing ${selector}</p>`
                            this.insertedNode = div.firstElementChild
                        }
                    } else {
                        this.insertedNode = div.firstElementChild || div
                    }
                    this.setAlpha(this.insertedNode, 0)
                    let images = this.element.querySelectorAll('img')
                    let total = 0
                    if (images.length > 0) {
                        let count = 0
                        for (let image of images) {
                            if (!image.complete && !image.src.startsWith('data:')) {
                                total += 1
                                console.log('image not complete', image.src)
                                image.onload = (e) => {
                                    count += 1
                                    if (count == total) {
                                        this.loaded = true
                                        if (this.onload != null) {
                                            this.onload()
                                        }
                                    }
                                }
                            }
                        }
                    }
                    if (total == 0) {
                        this.loaded = true
                    }
                    break
                case 'node':
                    this.loaded = true
                    Elements.addClass(content.node, 'PopupContent')
                    this.element.appendChild(content.node)
                    this.insertedNode = content.node
                    this.setAlpha(this.insertedNode, 0)
                    break
                default:
                    alert('Unexpected content type: ' + key)
                    break
            }
        }
        this.insertIntoDOM()
        this.layoutAfterInsert()
        this.setupEventHandler()
        return this
    }

    handleClose(e) {
        let closing = this.closingEvent(e)
        if (closing) {
            this.close(e)
        } else {
            this.setupCloseHandler()
        }
    }

    setupCloseHandler() {
        let close = this.handleClose
        if (this.hideOnUp) {
            if (window.PointerEvent)
                this.parent.addEventListener('pointerup', close.bind(this), {
                    capture: true,
                    once: true,
                })
            else if (window.TouchEvent)
                this.parent.addEventListener('touchend', close.bind(this), {
                    capture: true,
                    once: true,
                })
            else
                this.parent.addEventListener('mouseup', close.bind(this), {
                    capture: true,
                    once: true,
                })
        } else {
            if (window.PointerEvent)
                this.parent.addEventListener('pointerdown', close.bind(this), {
                    capture: true,
                    once: true,
                })
            else if (window.TouchEvent)
                this.parent.addEventListener('touchstart', close.bind(this), {
                    capture: true,
                    once: true,
                })
            else
                this.parent.addEventListener('mousedown', close.bind(this), {
                    capture: true,
                    once: true,
                })
        }
    }

    setupEventHandler() {
        if (this.autoClose) {
            this.setupCloseHandler()
        }
    }

    closingEvent(e) {
        if (this.interactive) {
            let node = e.target.closest('.PopupContent')
            return node == null
        }
        return true
    }

    iframeChanged(iframe) {
        let body = iframe.contentWindow.document.body
        let w = Math.max(body.scrollWidth, body.offsetWidth)
        let h = Math.max(body.scrollHeight, body.offsetHeight)
        iframe.style.width = w + 'px'
        iframe.style.height = h + 'px'
    }

    setupDraggable() {
        if (this.draggable) {
            let target = this.element
            target.setAttribute('draggable', true)
            target.ondragstart = (e) => {
                this.currentPos = { x: e.clientX, y: e.clientY }
                var img = document.createElement('img')
                img.src = 'data:image/gifbase64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
                e.dataTransfer.setDragImage(img, 0, 0)
            }
            target.ondrag = (e) => {
                e.preventDefault()
                let delta = {
                    x: e.clientX - this.currentPos.x,
                    y: e.clientY - this.currentPos.y,
                }
                this.currentPos = { x: e.clientX, y: e.clientY }
                let left = parseFloat(target.style.left)
                let top = parseFloat(target.style.top)

                target.style.left = left + delta.x + 'px'
                target.style.top = top + delta.y + 'px'

                //console.log("Popup.ondrag", target, event.target)
                if (this.onMove) {
                    this.onMove({ target, delta })
                }

                this.lastDrag = { left, top }
            }
            target.ondragend = (e) => {
                target.style.left = this.lastDrag.left + 'px'
                target.style.top = this.lastDrag.top + 'px'
                this.currentPos = null
            }
        }
    }

    moveDragged(target) {}

    insertIntoDOM(layout = true) {
        this.setAlpha(this.insertedNode, 0)
        this.element.appendChild(this.notch)
        this.parent.appendChild(this.element)
    }

    layoutAfterInsert() {
        Elements.setStyle(this.element, this.defaultStyle())
        this.layout()
        //this.element.style.opacity = 1
    }

    /** Layout the menu items. Needed only in the subclass.
     */
    layout() {}

    remove() {
        if (this.parent.contains(this.element)) this.parent.removeChild(this.element)
        this.unregister(this.context)
    }

    /** Close and remove the Popup from the DOM tree.
     */
    close(event) {
        //console.log("Popup.close", this.closeCommand)
        this.unregister(this.context)
        if (this.closeCommand) {
            this.closeCommand(this, () => this.remove(), event)
        } else {
            this.remove()
        }
    }

    /**
     * Set the alpha value to show or hide the popup. Uses CSS transitions.
     * (A former dependency on TweenLite has beeen removed.)
     *
     * @param {*} targets
     * @param {*} value
     * @memberof Popup
     */
    setAlpha(targets, value) {
        let objs = targets instanceof Array ? targets : [targets]
        for (let obj of objs) {
            if (value) {
                obj.style.transition = 'opacity 0.2s ease-in'
            }
            obj.style.opacity = value
        }
        // if (value) {
        //     TweenLite.to(targets, 0.2,  { autoAlpha: value })
        // }
        // else {
        //     TweenLite.set(targets, { autoAlpha: 0 })
        // }
    }

    /**
     * Starts a fade in animation.
     *
     * @memberof Popup
     */
    fadeIn() {
        this.setAlpha([this.element, this.insertedNode], 1)
    }

    /** Shows the Popup with the given commands at the specified point.
     * @param {Object} content - A dict object with type strings (text, img, html) as keys
     *                            and corresponding values.
     * @param {Point} point - The position as x, y coordinates {px}.
     * @return {Popup} this
     */
    showAt(content, point) {
        this.setup(content)
        console.log('showAt', this.loaded)
        if (this.loaded) {
            this.placeAt(point)
            this.fadeIn()
        } else {
            this.setAlpha([this.element, this.insertedNode], 0)
            this.onload = () => {
                this.layoutAfterInsert()
                this.placeAt(point)
                this.fadeIn()
            }
        }
        return this
    }

    /**
     *  Place the origin, i.e. the upper left corner at the given position using CSS styles.
     *
     * @param {any} x
     * @param {any} y
     * @memberof Popup
     */
    placeOrigin(x, y) {
        Elements.setStyle(this.element, { left: x + 'px', top: y + 'px' })
    }

    /**
     * Calculate the local coordinates within the keepWithin container.
     *
     * @param {any} x
     * @param {any} y
     * @returns
     * @memberof Popup
     */
    localPointWithin(x, y, width, height) {
        let pt = { x, y }
        return pt
    }

    withinDimensions() {
        return {
            width: this.keepWithin.offsetWidth,
            height: this.keepWithin.offsetHeight,
        }
    }

    localDimensions() {
        return {
            width: this.element.offsetWidth,
            height: this.element.offsetHeight,
        }
    }

    /**
     * Returns the notch position depending on the local coordinates within the keepWithin container
     * Divides the space vertically into top, center, bottom and horizontally into left, center, right
     *
     * @param {any} x
     * @param {any} y
     * @returns
     * @memberof Popup
     */
    notchPositionWithin(x, y) {
        let horizontal = 'Center'
        let vertical = 'center'
        let { width, height } = this.withinDimensions()
        let local = this.localPointWithin(x, y, width, height)
        if (local.y < height * 0.33) {
            vertical = 'top'
        }
        if (local.y > height * 0.66) {
            vertical = 'bottom'
        }
        if (local.x < width * 0.33) {
            horizontal = 'Left'
        }
        if (local.x > width * 0.66) {
            horizontal = 'Right'
        }
        let result = vertical + horizontal
        if (result == 'centerCenter') return this.notchPosition
        return result
    }

    placeAt(point) {
        let x = point.x
        let y = point.y
        let notchPosition = this.notchPosition
        if (this.keepWithin != null) {
            notchPosition = this.notchPositionWithin(x, y)
        }
        Elements.setStyle(this.notch, this.notchStyle(notchPosition))
        this.notch.className = 'notch ' + notchPosition
        let { width, height } = this.localDimensions()

        //if targetBoundingBox is set, popup is placed next to the rectangle
        if (this.targetBoundingBox) {
            let bbTop = this.targetBoundingBox.y
            let bbBottom = this.targetBoundingBox.y + this.targetBoundingBox.height
            let bbLeft = this.targetBoundingBox.x
            let bbRight = this.targetBoundingBox.x + this.targetBoundingBox.width
            //console.log("place popup with bb set:", x, y, bbTop, bbBottom, bbLeft, bbRight)
            switch (notchPosition) {
                case 'bottomLeft':
                case 'bottomRight':
                case 'bottomCenter':
                    y = bbTop
                    if (!this.useEventPosWithBoundingBox) x = (bbLeft + bbRight) / 2
                    break
                case 'topLeft':
                case 'topRight':
                case 'topCenter':
                    y = bbBottom
                    if (!this.useEventPosWithBoundingBox) x = (bbLeft + bbRight) / 2
                    break
                case 'centerRight':
                    x = bbLeft
                    if (!this.useEventPosWithBoundingBox) y = (bbTop + bbBottom) / 2
                    break
                case 'centerLeft':
                    x = bbRight
                    if (!this.useEventPosWithBoundingBox) y = (bbTop + bbBottom) / 2
                    break
                default:
                    break
            }
        }

        //calculate position depending on several (optional) parameters
        switch (notchPosition) {
            case 'bottomLeft':
                x -= this.padding
                x -= this.notchSize
                y -= height
                y -= this.notchSize * 2
                y -= this.posOffset
                break
            case 'bottomRight':
                x -= width
                x += this.padding
                x += this.notchSize
                y -= height
                y -= this.notchSize * 2
                y -= this.posOffset
                break
            case 'bottomCenter':
                x -= width / 2
                y -= height
                y -= this.notchSize * 2
                y -= this.posOffset
                break
            case 'topLeft':
                x -= this.padding
                x -= this.notchSize
                y += this.notchSize * 2
                y += this.posOffset
                break
            case 'topRight':
                x -= width
                x += this.padding
                x += this.notchSize
                y += this.notchSize * 2
                y += this.posOffset
                break
            case 'topCenter':
                x -= width / 2
                y += this.notchSize * 2
                y += this.posOffset
                break
            case 'centerRight':
                x -= width + this.notchSize * 2
                x -= this.posOffset
                y -= height / 2
                break
            case 'centerLeft':
                //console.log("height", height)
                y -= height / 2
                x += this.notchSize * 2
                x += this.posOffset
                break
            default:
                break
        }
        this.placeOrigin(x, y)
    }

    /** Shows the Popup with the given commands at the current position.
     * @param {Object} content - A dict object with type strings (text, img, html) as keys
     *                            and corresponding values.
     * @return {Popup} this
     */
    show(content) {
        this.setup(content)
        this.fadeIn()
        return this
    }

    /** Configuration object. Return default styles as CSS values.
     */
    defaultStyle() {
        let padding = this.padding
        let style = {
            maxWidth: this.maxWidth + 'px',
            zIndex: this.zIndex,
            position: 'absolute',
        }
        if (this.minWidth) {
            style.minWidth = this.minWidth + 'px'
        }
        if (!this.noStyle) {
            Object.assign(style, {
                borderRadius: Math.round(this.padding / 2) + 'px',
                backgroundColor: this.backgroundColor,
                padding: this.padding + 'px',
                boxShadow: '0 10px 15px rgba(0, 0, 0, 0.3)',
                fontFamily: this.fontFamily,
                fontSize: this.fontSize,
                stroke: 'black',
                fill: 'white',
            })
        }

        return style
    }

    /** Configuration object. Return notch styles as CSS values.
     */
    notchStyle(notchPosition = null) {
        if (notchPosition == null) {
            notchPosition = this.notchPosition
        }
        let width = 0
        let height = 0
        let left = this.padding
        let size = this.localDimensions()
        if (notchPosition.endsWith('Right')) {
            left = size.width - this.padding - this.notchSize * 2
        }
        if (notchPosition.endsWith('Center')) {
            left = size.width / 2 - this.notchSize
        }
        left = Math.round(left) + 'px'
        let borderBottom = 0
        let borderTop = 0

        if (notchPosition.startsWith('bottom')) {
            if (this.noStyle) {
                return {
                    width,
                    height,
                    left,
                    bottom: -this.notchSize + 'px',
                    position: 'absolute',
                    borderStyle: 'solid',
                    borderTopWidth: this.notchSize + 'px',
                    borderRight: this.notchSize + 'px solid transparent',
                    borderLeft: this.notchSize + 'px solid transparent',
                    borderBottom: 0,
                }
            } else {
                return {
                    width,
                    height,
                    left,
                    boxShadow: '0 12px 15px rgba(0, 0, 0, 0.1)',
                    bottom: -this.notchSize + 'px',
                    position: 'absolute',
                    borderTop: this.notchSize + 'px solid ' + this.backgroundColor,
                    borderRight: this.notchSize + 'px solid transparent',
                    borderLeft: this.notchSize + 'px solid transparent',
                    borderBottom: 0,
                }
            }
        }
        if (notchPosition.startsWith('top')) {
            if (this.noStyle) {
                return {
                    width,
                    height,
                    left,
                    top: -this.notchSize + 'px',
                    position: 'absolute',
                    borderStyle: 'solid',
                    borderBottomWidth: this.notchSize + 'px',
                    borderRight: this.notchSize + 'px solid transparent',
                    borderLeft: this.notchSize + 'px solid transparent',
                    borderTop: 0,
                }
            } else {
                return {
                    width,
                    height,
                    left,
                    top: -this.notchSize + 'px',
                    position: 'absolute',
                    borderBottom: this.notchSize + 'px solid ' + this.backgroundColor,
                    borderRight: this.notchSize + 'px solid transparent',
                    borderLeft: this.notchSize + 'px solid transparent',
                    borderTop: 0,
                }
            }
        }

        if (this.noStyle) {
            if (notchPosition.endsWith('Left')) {
                left = -this.notchSize * 2 + 'px'
            }

            if (notchPosition.endsWith('Right')) {
                left = size.width + 'px'
            }

            let top = size.height / 2 - this.notchSize
            top = Math.round(top) + 'px'

            return {
                width,
                height,
                left,
                top,
                borderRightWidth: this.notchSize,
                borderLeftWidth: this.notchSize,
                position: 'absolute',
                borderTop: this.notchSize + 'px solid transparent',
                borderBottom: this.notchSize + 'px solid transparent',
            }
        } else {
            let borderRight = this.notchSize + 'px solid transparent'
            let borderLeft = this.notchSize + 'px solid transparent'
            let top = size.height / 2 - this.notchSize
            if (notchPosition.endsWith('Left')) {
                left = -this.notchSize * 2 + 'px'
                borderRight = this.notchSize + 'px solid ' + this.backgroundColor
                this.element.style.boxShadow = '15px 10px 15px  rgba(0, 0, 0, 0.3)'
            }
            if (notchPosition.endsWith('Right')) {
                left = size.width + 'px'
                borderLeft = this.notchSize + 'px solid ' + this.backgroundColor
                this.element.style.boxShadow = '15px 5px 15px rgba(0, 0, 0, 0.3)'
            }

            top = Math.round(top) + 'px'

            return {
                width,
                height,
                left,
                top,
                borderRight,
                borderLeft,
                //  boxShadow,
                position: 'absolute',
                borderTop: this.notchSize + 'px solid transparent',
                borderBottom: this.notchSize + 'px solid transparent',
            }
        }
    }

    /** Convenient static methods to show and reuse a Popup implemented
     * as a class variable.
     * @param {Object} content - A dict object with type strings (text, img, html) as keys
     *                            and corresponding values.
     * @param {Point} point - The position as x, y coordinates {px}.
     * @param {boolean} autoClose - Autoclose the menu after selecting an item.
     */
    static open(
        content,
        point,
        {
            parent = null,
            context = window,
            fontSize = '1em',
            fontFamily = 'Arial',
            padding = 16,
            notchSize = 10,
            switchPos = false,
            minWidth = null,
            maxWidth = 800,
            backgroundColor = '#EEE',
            zIndex = 0,
            normalColor = '#444',
            closeIcon = null,
            resizeIcon = null,
            closeCommand = null,
            autoClose = true,
            keepWithin = null,
            draggable = false,
            posOffset = 0,
            targetBoundingBox = null,
            useEventPosWithBoundingBox = false,
            interactive = false,
            onResize = null,
            onMove = null,
        } = {}
    ) {
        let notchPosition = switchPos && point.y < 50 ? 'topCenter' : 'bottomCenter'
        let popup = new Popup({
            parent,
            context,
            fontFamily,
            fontSize,
            padding,
            notchSize,
            switchPos,
            minWidth,
            maxWidth,
            backgroundColor,
            normalColor,
            notchPosition,
            zIndex,
            autoClose,
            keepWithin,
            closeCommand,
            closeIcon,
            resizeIcon,
            draggable,
            posOffset,
            targetBoundingBox,
            useEventPosWithBoundingBox,
            interactive,
            onResize,
            onMove,
        })
        popup.register(context)
        popup.showAt(content, point)
        return popup
    }

    /** Convenient static method to close the Popup implemented
     * as a class variable. Calls the close command.
     */
    static closePopup(context = window) {
        let popup = Poppable.registrations.get(context)
        if (popup != null) {
            popup.close()
        }
    }

    /** Convenient static methods to remove the Popup implemented
     * as a class variable. Removes the popup without performing the close command.
     */
    static remove(context = window) {
        let popup = Poppable.registrations.get(context)
        if (popup != null) {
            popup.remove()
        }
    }

    /**
     * Convenient static method to compute the clicked rect of objects that have multiple clients rects.
     * Needed to position popups correctly above objects with line breaks, e.g. spans
     *
     * @static
     * @param {*} event
     * @returns {*} DOMRect
     * @memberof Popup
     */
    static targetRect(event) {
        let target = event.target
        let x = event.pageX
        let y = event.pageY
        for (let rect of target.getClientRects()) {
            let withinX = x >= rect.left && x <= rect.left + rect.width
            let withinY = y >= rect.top && y <= rect.top + rect.height
            if (withinX && withinY) {
                return rect
            }
        }
        return null
    }

    /**
     * Convenient static method to compute the center of objects that have multiple clients rects.
     * Needed to position popups correctly above objects with line breaks, e.g. spans
     *
     * @static
     * @param {*} event
     * @returns {*} Point
     * @memberof Popup
     */
    static targetCenter(event) {
        let target = event.target
        let x = event.pageX
        let y = event.pageY
        let rect = Popup.targetRect(event)
        if (rect != null) {
            x = rect.left + rect.width / 2
            y = rect.top + rect.height / 2
        }
        return { x, y }
    }
}