export var CardPlugin = CardPlugin || {}

export class CardPluginBase {
    apply(context) {
        this.context = context
        if (this.verify(context)) {
            this.append(context)
            console.log('Plugin ' + this.name + ' was verified successfully.')
            return true
        } else console.error('Could not verify module ' + this.name + '.')
        return false
    }

    get name() {
        return this.constructor.name
    }

    verify(context) {
        let funcs = this._getVerificationFunctions(context)
        for (let func of funcs) {
            if (!func()) return false
        }
        return true
    }

    _verifyElementsExist(context, ...selectors) {
        let missing = []

        for (let selector of selectors) {
            let requiredElement = context.querySelector(selector)
            if (requiredElement == null) {
                missing.push(selector)
            }
        }
        const valid = missing.length == 0
        if (!valid) console.error('Elements were missing: ', missing.join(', '))
        return valid
    }

    /**
     *  Appends the Plugin to the context.
     *
     * @memberof CardPlugin
     */
    append(context) {
        console.error(
            'Call of abstract method CardPlugin.prototype.append(context). Plugins need to overwrite the append method!'
        )
    }

    _getVerificationFunctions(context) {
        return [this._verifyContext.bind(this, context), this._verifyRequirements.bind(this, context)]
    }

    _verifyContext(context) {
        if (!(context instanceof HTMLElement)) {
            console.error('Context is not of type HTML Element.', context)
            return false
        } else return true
    }

    _verifyRequirements(context) {
        let requirements = this._collectAllRequirements()
        let missing = []

        requirements.forEach((module) => {
            if (context.modules.indexOf(module.name) == -1) {
                missing.push(module.name)
            }
        })

        const valid = missing.length == 0
        if (!valid)
            console.error(
                "Could not apply module '" +
                    this.name +
                    "'. Following modules are required but were missing: " +
                    missing.join(',')
            )
        else console.log('All requirements were met! Well done!')
        return valid
    }

    _collectAllRequirements() {
        let requirements = []
        let klass = this.__proto__
        while (klass) {
            if (klass.require != null) {
                requirements = requirements.concat(klass.require)
            }
            klass = klass.__proto__
        }
        return requirements
    }

    /**
     * Called when the card is removed.
     * Can be used to cleanup the plugin.
     *
     * @memberof CardPluginBase
     */
    remove() {}
}

CardPlugin.LightBox = class LightBox extends CardPluginBase {
    constructor(className, style = {}) {
        super()
        this.className = className
        this.style = style
    }

    append(context) {
        let wrapper = document.createElement('div')
        wrapper.className = this.className

        Object.assign(
            wrapper.style,
            {
                zIndex: 1000,
                // backgroundColor: "black",
                top: 0,
                left: 0,
                width: '100%',
                height: '100%',
            },
            this.style,
            {
                display: 'none',
                position: 'absolute',
            }
        )

        context.appendChild(wrapper)
    }
}

/**
 * The Enlargeable Overlay module allows the user to click on the thumbnail image,
 * and the images gets enlarged inside the card.
 *
 * @class EnlargeableThumbnail
 * @extends {CardPlugin}
 */
CardPlugin.EnlargeableThumbnail = class EnlargeableThumbnail extends CardPluginBase {
    constructor(
        wrapperSelector,
        overlaySelector = null,
        { zoomAnimationDuration = 0.4, fadeAnimationDuration = 0.4, interactionType = 'tap' } = {}
    ) {
        super()
        this.wrapperSelector = wrapperSelector
        this.overlaySelector = overlaySelector

        this.zoomAnimationDuration = zoomAnimationDuration
        this.fadeAnimationDuration = fadeAnimationDuration
        this.interactionType = interactionType
    }
    get require() {
        return [CardPlugin.LightBox]
    }

    _getVerificationFunctions(context) {
        let arr = super._getVerificationFunctions(context)
        let funcs = [this._verifyElementsExist.bind(this, context, this.wrapperSelector, this.overlaySelector)]
        return arr.concat(funcs)
    }

    append(context) {
        let source = this._retrieveSource(context)
        this.setupEnlargeableThumbnail(context, source)
    }

    /**
     * Get the preview image.
     *
     * It depends on the fact, that the thumbnail image is in the same directory
     *
     *
     * @param {*} context
     * @returns
     * @memberof EnlargeableThumbnail
     */
    _retrieveSource(context) {
        let img = context.querySelector(this.wrapperSelector + ' img')
        let src = img.getAttribute('src')
        let parts = src.split('/')
        parts.pop()
        parts.push(parts[parts.length - 1])
        let imagePath = parts.join('/') + '.jpg'
        return imagePath
    }

    setupEnlargeableThumbnail(context, src) {
        let wrapper = context.querySelector(this.wrapperSelector)
        let overlay = context.querySelector(this.overlaySelector)

        let icon = document.createElement('div')
        icon.className = 'button corner-button bottom-right icon zoom'
        wrapper.appendChild(icon)

        Object.assign(wrapper.style, {
            cursor: 'pointer',
        })

        InteractionMapper.on(this.interactionType, wrapper, () => {
            this.openThumbnailDetail(context, src)
        })

        InteractionMapper.on(this.interactionType, overlay, () => {
            this.closeThumnailDetail(context)
        })
    }

    openThumbnailDetail(context, src) {
        let overlay = context.querySelector('.img-overlay')
        overlay.innerHTML = ''
        let source = context.querySelector(this.wrapperSelector)
        let sourceStyle = window.getComputedStyle(source)
        let imageWrapper = source.cloneNode(true)
        let image = imageWrapper.querySelector('img')

        Object.assign(imageWrapper.style, {
            maxWidth: 'none',
            maxHeight: 'none',
        })

        Object.assign(image.style, {
            width: '100%',
            height: '100%',
            objectFit: 'cover',
        })

        this._replaceIcon(imageWrapper)

        image.onload = () => {
            let header = context.querySelector('header')
            let headerStlye = window.getComputedStyle(header)

            /**
             * First the maxFillRatio is considered.
             * It describes how much the image is allowed to exceed the context element.
             */
            const maxFillRatio = 1.5

            /**
             * The minor side should not exceed the height of the context window.
             */
            const maxMinorSize =
                context.offsetHeight - 2 * parseInt(headerStlye.paddingTop) - 2 * parseInt(headerStlye.marginTop)

            const max = {
                width: context.offsetWidth * maxFillRatio,
                height: context.offsetHeight * maxFillRatio,
            }

            let majorSide
            let minorSide
            const _width = { name: 'width', axis: 'x' }
            const _height = { name: 'height', axis: 'y' }
            if (image.naturalHeight > image.naturalWidth) {
                majorSide = _height
                minorSide = _width
            } else {
                majorSide = _width
                minorSide = _height
            }

            function capitalize(string) {
                return string.charAt(0).toUpperCase() + string.slice(1)
            }
            function getImageSize(side) {
                return image['natural' + capitalize(side.name)]
            }

            const majorImageSize = getImageSize(majorSide)
            // const minorImageSize = getImageSize(minorSide)

            let ratio = getImageSize(minorSide) / getImageSize(majorSide)
            let size = majorImageSize > max[majorSide.name] ? max[majorSide.name] : majorImageSize

            if (size * ratio > maxMinorSize) {
                size = maxMinorSize / ratio
            }

            let targetDimensions = {
                width: 0,
                height: 0,
            }

            let position = Points.fromPageToNode(context, Points.fromNodeToPage(source, { x: 0, y: 0 }))

            let targetOffset = {
                x: 0,
                y: 0,
            }

            targetDimensions[majorSide.name] = size
            targetDimensions[minorSide.name] = size * ratio

            targetOffset[majorSide.axis] =
                (context['offset' + capitalize(majorSide.name)] - targetDimensions[majorSide.name]) / 2
            targetOffset[minorSide.axis] =
                (context['offset' + capitalize(minorSide.name)] - targetDimensions[minorSide.name]) / 2

            overlay.appendChild(imageWrapper)

            TweenMax.set(imageWrapper, {
                left: 0,
                top: 0,
                x: position.x,
                y: position.y,
                position: 'absolute',
                width: parseInt(sourceStyle.width),
                height: parseInt(sourceStyle.height),
            })

            TweenMax.set(overlay, {
                display: 'flex',
                autoAlpha: 0,
            })

            TweenMax.to(imageWrapper, this.zoomAnimationDuration, {
                x: targetOffset.x,
                y: targetOffset.y,
                width: targetDimensions.width,
                height: targetDimensions.height,
            })
            TweenMax.to(overlay, this.fadeAnimationTime, {
                autoAlpha: 1,
            })
        }

        image.src = src
    }

    _replaceIcon(clone) {
        let zoomIcon = clone.querySelector('.icon.zoom')
        zoomIcon.classList.remove('zoom')
        zoomIcon.classList.add('close')
    }

    getBorderHeight(style) {
        const borderWidth = parseInt(style.borderTopWidth) + parseInt(style.borderBottomWidth)
        const padding = parseInt(style.paddingTop) + parseInt(style.paddingBottom)
        return parseInt(style.width) + borderWidth + padding
    }

    getBorderWidth(style) {
        const borderWidth = parseInt(style.borderLeftWidth) + parseInt(style.borderRightWidth)
        const padding = parseInt(style.paddingLeft) + parseInt(style.paddingRight)
        return parseInt(style.width) + borderWidth + padding
    }

    closeThumnailDetail(context) {
        let overlay = context.querySelector('.img-overlay')

        let timeline = new TimelineLite()

        timeline
            .to(overlay, this.fadeAnimationDuration, {
                autoAlpha: 0,
            })
            .set(overlay, {
                display: 'none',
            })
    }
}

CardPlugin.Ui = class UiPlugin extends CardPluginBase {
    constructor(className, parent = null) {
        super()
        this.parent = parent
        this.className = className
    }

    _getVerificationFunctions(context) {
        let arr = super._getVerificationFunctions(context)
        let func = [this._doesParentExist.bind(this, context, this.parent)]
        return arr.concat(func)
    }

    _doesParentExist(context, parent) {
        if (parent == null) return true
        let valid = context.querySelector(parent) != null
        if (!valid) console.error('Could not find parent on context.', context, parent)
        return valid
    }

    append(context) {
        parent = this.parent == null ? context : context.querySelector(this.parent).appendChild(container)
        let container = document.createElement('div')
        container.className = this.className
        parent.appendChild(container)
    }
}

CardPlugin.Speech = class SpeechPlugin extends CardPluginBase {
    constructor(parentSelector, className, interactionType = 'tap') {
        super()
        this.className = className
        this.parentSelector = parentSelector
        this.interactionType = interactionType

        // We directly overwriting the function with a version that has a binded
        // reference to itself. Doing so provides an easy and reliable way to remove
        // the event listener using this function. -  SO
        this._domWasChanged = this._domWasChanged.bind(this)

        /* 
        Speech doesn't stop when page is navigated.
        Therefore we do it manually here.
        */
        window.addEventListener('beforeunload', () => {
            window.speechSynthesis.cancel()
        })

        // Binding the function beforehand ensures, that the end function is always the same.
        this._end = this._end.bind(this)

        this._setupUtterance()
        this.utterance.addEventListener('end', (event) => {
            this._end()
        })
    }

    get require() {
        return [CardPlugin.Ui]
    }

    subcardChanged(closed) {
        if (this.cardActive) {
            this._updateText(closed)
        }
    }

    get cardActive() {
        return this.activeUtterance == this.utterance
    }

    _updateText(ignoreSubcard = false) {
        let node = this.context
        let subcard = node.querySelector('.mainview > .subcard')

        if (ignoreSubcard) {
            if (subcard != null) {
                let clone = node.cloneNode(true)
                let clonedSubcard = clone.querySelector('.mainview > .subcard')
                clonedSubcard.parentNode.removeChild(clonedSubcard)
                node = clone
            }
        } else {
            if (subcard) {
                let clone = subcard.cloneNode(true)
                clone.querySelectorAll('figure').forEach((figure) => {
                    figure.parentNode.removeChild(figure)
                })

                node = clone
            }
        }

        let id = this.context.getAttribute('data-id')
        let src = this.context.getAttribute('data-source')
        let subcardSource = null
        if (subcard != null) {
            subcardSource = subcard.getAttribute('data-source')
        }

        if (!window.speechSynthesis.speaking) {
            this._start(node)
            Logging.log(`Started speech on card: id:${id} - source: ${src} - subcard: ${subcardSource}`)
        } else if (this.cardActive && this._sameText(node)) {
            Logging.log(`Stopped speech on card: id:${id} - source: ${src} - subcard: ${subcardSource}`)
            this._stop()
        } else {
            Logging.log(`Updated Text on card: id:${id} - source: ${src} - subcard: ${subcardSource}`)
            this._stop()
                .then(() => {
                    this._start(node)
                })
                .catch(console.error)
        }
    }

    _sameText(node) {
        return this.utterance.text == this._cleanupText(node)
    }

    _setupUtterance() {
        this.utterance = new SpeechSynthesisUtterance()
        this.utterance.lang = 'de-DE'
    }

    get require() {
        return [CardPlugin.Ui]
    }

    remove() {
        this.button = null
        this._stopThisSpeechIfPlaying()
        this.context.removeEventListener('DOMNodeRemoved', this._domWasChanged)
        super.remove()
    }

    append(context) {
        let container = context.querySelector(this.parentSelector)
        this.button = document.createElement('div')
        this.button.className = 'icon button ' + this.className
        container.appendChild(this.button)

        InteractionMapper.on(this.interactionType, this.button, () => {
            this.speak()
        })

        context.addEventListener('DOMNodeRemoved', this._domWasChanged)
    }

    /**
     * Don't remember why this was required - SO 20-11-2019
     */
    _domWasChanged(event) {
        if (event.target == this.context) this._stopThisSpeechIfPlaying()
    }

    /**
     * Stops the module if it is set in the context.
     */
    _stopThisSpeechIfPlaying() {
        if (this.context == null || this.context['lastSpeechNode'] == window.speechSynthesis['speechPluginNode']) {
            this._stop()
        }
    }

    _isSameNode(node) {
        return this.currentText == node.textContent
    }

    speak() {
        /**
     * This is a little bit ugly, but imho the most elegant of all dirty solutions.
     *
5ht         * Within the plugins we have no knowledge of other cards and such. But must differentiate the
     * clicks by their corresponding owner. The SpeechUtterance just takes a text and has no knowledge
     * about the node that is currently read to the user.
     *
     * This means, that we can identify same text, but not differentiate same text on different nodes.
     * To account for that, we add the node to the speechSynthesis object (#benefitsOfJavaScript) and
     * have access to the node, by - let's say - expanding the functionality of the SpeechSynthesis object.
     *
     * SO -17.07.19
     */

        this._updateText()
    }

    async _stop() {
        return new Promise((resolve) => {
            if (this.activeUtterance) {
                this.activeUtterance.addEventListener('end', resolve, {
                    once: true,
                })
            }

            window.speechSynthesis.cancel()
        })
    }

    get activeUtterance() {
        return window.speechSynthesis['speechPluginUtterance']
    }

    _end() {
        window.speechSynthesis['speechPluginNode'] = null
        window.speechSynthesis['speechPluginUtterance'] = null
        this._deactivateButton()
        this.context.classList.remove('speech-plugin-is-reading')
    }

    _start(node) {
        window.speechSynthesis.cancel()

        window.speechSynthesis['speechPluginUtterance'] = this.utterance
        window.speechSynthesis['speechPluginNode'] = node
        this.context['lastSpeechNode'] = node

        let cleanText = this._cleanupText(node)
        this.utterance.text = cleanText
        window.speechSynthesis.speak(this.utterance)
        this._activateButton()

        this.context.classList.add('speech-plugin-is-reading')
    }

    closed() {}

    _cleanupText(node) {
        let text = node.textContent
        text = this._removeShy(text)
        return text
    }

    _removeShy(text) {
        return text.replace(/\u00AD/g, '')
    }

    _activateButton() {
        if (this.button) this.button.classList.add('active')
    }
    _deactivateButton() {
        if (this.button) this.button.classList.remove('active')
    }
}