import Plugin from 'src/plugin-system/plugin.class';
import DomAccess from 'src/helper/dom-access.helper';
import { Vector2 } from 'src/helper/vector.helper';
import ViewportDetection from 'src/helper/viewport-detection.helper';
import Iterator from 'src/helper/iterator.helper';

const PORTRAIT_ORIENTATION = 1;
const LANDSCAPE_ORIENTATION = 0;

/**
 * handles the magnifier lens functionality
 * on the detail page
 *
 * MagnifyLensPlugin class
 */
export default class MagnifierPlugin extends Plugin {

    static options = {

        /**
         * multiplier of how far the image should be zoomed in
         *
         * @type number
         */
        zoomFactor: 3,

        /**
         * container for the image
         *
         * @type string
         */
        imageContainerSelector: '.js-magnifier-container',

        /**
         * selector of the image in which the overlay should be created
         *
         * @type string
         */
        imageSelector: '.js-magnifier-image',

        /**
         * data attribute for the high resolution image
         *
         * @type string
         */
        fullImageDataAttribute: 'data-full-image',

        /**
         * class for the container in which the zoomed image should be created
         *
         * @type string
         */
        zoomImageContainerSelector: '.js-magnifier-zoom-image-container',

        /**
         * class for the image overlay
         *
         * @type string
         */
        overlayClass: 'js-magnifier-overlay',

        /**
         * class for the zoomed image element
         *
         * @type string
         */
        zoomImageClass: 'js-magnifier-zoom-image',

        /**
         * magnified image is over gallery
         *
         * @type boolean
         */
        magnifierOverGallery: false,

        /**
         * scale size for zoomed image element
         *
         * @type boolean
         */
        scaleZoomImage: false,

        /**
         * keep the aspect ratio of the image when zoomed
         *
         * @type boolean
         */
        keepAspectRatioOnZoom: true,

        /**
         * css cursor type when zoom is active
         *
         * @type string
         */
        cursorType: 'none',
    };

    init() {
        this._imageContainers = DomAccess.querySelectorAll(this.el, this.options.imageContainerSelector);

        if (this.options.magnifierOverGallery) {
            this._zoomImageContainer = DomAccess.querySelector(this.el, this.options.zoomImageContainerSelector);
        } else {
            this._zoomImageContainer = DomAccess.querySelector(document, this.options.zoomImageContainerSelector);
        }

        this._registerEvents();
    }

    /**
     * Registers all necessary event listeners.
     */
    _registerEvents() {
        Iterator.iterate(this._imageContainers, imageContainer => {
            const image = DomAccess.querySelector(imageContainer, this.options.imageSelector, false);
            if (image) {
                image.addEventListener('mousemove', (event) => this._onMouseMove(event, imageContainer, image), false);
                imageContainer.addEventListener('mouseout', (event) => this._stopMagnify(event), false);
            }
        });
    }

    /**
     * returns whether or not the lens should be active
     *
     * @return {boolean}
     * @private
     */
    _isActive() {
        const allowedViewports = [
            ViewportDetection.isLG(),
            ViewportDetection.isXL(),
            ViewportDetection.isXXL(),
        ];

        return allowedViewports.indexOf(true) !== -1;
    }

    /**
     * @param {HTMLElement} el
     * @param {string} type
     * @private
     */
    _setCursor(el, type) {
        if (el) el.style.cursor = type;
    }

    /**
     * Eventhandler for handling the
     * mouse movement on the image container.
     *
     * @param {Event} event
     * @param {HTMLElement} imageContainer
     * @param {HTMLElement} image
     */
    _onMouseMove(event, imageContainer, image) {

        if (this._isActive()) {
            this._setCursor(image, this.options.cursorType);
            this._createOverlay(imageContainer);
            this._createZoomImage();
            this._getImageUrl(image);

            if (this._imageUrl && this._zoomImage && this._overlay) {
                const containerPos = this._getContainerPos(imageContainer);
                const imagePos = this._getImagePos(image);
                const imageDimensions = this._getImageDimensions(image);
                const imageSize = this._getImageSize(image);
                const overlaySize = this._getOverlaySize(imageSize);
                const imageOffset = containerPos.subtract(imagePos);
                imageOffset.x = Math.abs(imageOffset.x);
                imageOffset.y = Math.abs(imageOffset.y);

                const mousePos = new Vector2(event.pageX, event.pageY).subtract(imagePos);

                const mousePosPercent = mousePos.divide(imageSize).clamp(0, 1);

                this._setOverlayPosition(imageOffset, overlaySize, imageSize, mousePosPercent);
                this._setZoomImage(mousePos, imageSize, overlaySize, imageDimensions);
            }
        }

        this.$emitter.publish('onMouseMove');
    }

    /**
     * sets the position of the overlay
     *
     * @param {Vector2} imageOffset
     * @param {Vector2} overlaySize
     * @param {Vector2} imageSize
     * @param {Vector2} mousePosPercent
     * @return {VectorBase|*}
     * @private
     */
    _setOverlayPosition(imageOffset, overlaySize, imageSize, mousePosPercent) {
        let overlayPos = imageOffset.subtract(overlaySize.divide(2)); // offset the lens so that the cursor is in the middle
        overlayPos = overlayPos.add(imageSize.multiply(mousePosPercent)); // add the mouse offset
        overlayPos = overlayPos.clamp(imageOffset, imageOffset.add(imageSize).subtract(overlaySize)); // clamp the position to image min max
        this._overlay.style.left = `${overlayPos.x}px`;
        this._overlay.style.top = `${overlayPos.y}px`;

        return overlayPos;
    }

    /**
     *  sets the background position of the zoomed image
     *
     * @param {Vector2} mousePos
     * @param {Vector2} imageSize
     * @param {Vector2} overlaySize
     * @param {Vector2} imageDimensions
     *
     * @private
     */
    _setZoomImage(mousePos, imageSize, overlaySize, imageDimensions) {
        this._setZoomImageSize(imageSize);

        // set background image
        this._zoomImage.style.backgroundImage = `url('${this._imageUrl}')`;

        // set background image size
        const zoomImageBackgroundSize = this.calculateZoomBackgroundImageSize(imageDimensions, imageSize);
        this._zoomImage.style.backgroundSize = `${zoomImageBackgroundSize.x}px ${zoomImageBackgroundSize.y}px`;

        // set background image position
        const zoomImagePosPercent = this.calculateZoomImageBackgroundPosition(mousePos, imageSize, overlaySize, imageDimensions, zoomImageBackgroundSize);
        this._zoomImage.style.backgroundPosition = `-${zoomImagePosPercent.x}px -${zoomImagePosPercent.y}px`;

        this.$emitter.publish('setZoomImagePosition');
    }

    /**
     * @param {Vector2} imageSize
     * @private
     */
    _setZoomImageSize(imageSize) {
        const factor = imageSize.y / imageSize.x;
        const zoomImageSize = this._getZoomImageSize();
        const height = this.options.keepAspectRatioOnZoom
            ? this.options.scaleZoomImage ? zoomImageSize.x * factor : zoomImageSize.y
            : zoomImageSize.x;
        this._zoomImage.style.height = `${height}px`;
        this._zoomImage.style.minHeight = `${height}px`;
    }

    /**
     * calculate the percentage position
     * when the image factors mismatch
     *
     * @param {Vector2} mousePos
     * @param {Vector2} imageSize
     * @param {Vector2} overlaySize
     * @param {Vector2} imageDimensions
     * @param {Vector2} zoomImageBackgroundSize
     *
     * @returns {Vector2}
     */
    calculateZoomImageBackgroundPosition(mousePos, imageSize, overlaySize, imageDimensions, zoomImageBackgroundSize) {
        const maxOverlayRange = imageSize.subtract(imageSize.divide(this.options.zoomFactor)).subtract(new Vector2(1, 1));
        let position = mousePos.subtract(overlaySize.divide(2)).clamp(0, imageSize.subtract(overlaySize)).divide(maxOverlayRange);
        const orientation = this.getImageOrientation(imageDimensions, imageSize);
        const percentWidthWithoutLens = 1 - 1 / this.options.zoomFactor;

        if (this.options.keepAspectRatioOnZoom) {
            position = position.clamp(0, 1);
        }

        if (orientation === LANDSCAPE_ORIENTATION) {
            position = position.multiply(new Vector2(percentWidthWithoutLens, 1));
            position = this.calculateImagePosition(position, imageSize, imageDimensions, 'y', 'x');
            position = position.multiply(new Vector2(1, percentWidthWithoutLens));
        } else if (orientation === PORTRAIT_ORIENTATION) {
            position = position.multiply(new Vector2(1, percentWidthWithoutLens));
            position = this.calculateImagePosition(position, imageSize, imageDimensions, 'x', 'y');
            position = position.multiply(new Vector2(percentWidthWithoutLens, 1));

        }

        return zoomImageBackgroundSize.multiply(position);
    }

    /**
     * @param position
     * @param imageSize
     * @param imageDimensions
     * @param coordOne
     * @param coordTwo
     *
     * @returns {*}
     */
    calculateImagePosition(position, imageSize, imageDimensions, coordOne, coordTwo) {
        const compressedImageSize = (imageDimensions[coordOne] * (imageSize[coordTwo] / imageSize[coordOne]));
        const offsetPercent = (1 - (compressedImageSize / (imageDimensions[coordTwo] / 1))) / 2;
        position[coordTwo] = this.calculateOffsetPercent(offsetPercent, position[coordTwo]);

        return position;
    }

    calculateOffsetPercent(offset, percent) {
        return offset + ((1 - (offset * 2)) * percent);
    }

    /**
     * @param {Vector2} imageDimensions
     * @param {Vector2} imageSize
     * @returns {Vector2}
     */
    calculateZoomBackgroundImageSize(imageDimensions, imageSize) {
        const orientation = this.getImageOrientation(imageDimensions, imageSize);
        const zoomImageSize = this._getZoomImageSize();
        let size = new Vector2(0, 0);

        if (orientation === PORTRAIT_ORIENTATION) {
            size = new Vector2(zoomImageSize.x, zoomImageSize.x * imageDimensions.y / imageDimensions.x);
        } else if (orientation === LANDSCAPE_ORIENTATION) {
            size = new Vector2(zoomImageSize.y * imageDimensions.x / imageDimensions.y, zoomImageSize.y);
        }

        return size.multiply(this.options.zoomFactor);
    }

    /**
     * returns the orientation of the detail image
     * landscape or portrait
     *
     * @param imageDimensions
     * @param imageSize
     * @returns {LANDSCAPE_ORIENTATION|PORTRAIT_ORIENTATION}
     */
    getImageOrientation(imageDimensions, imageSize) {
        if (this._assertEqualFactors(imageDimensions, imageSize)) {
            return (imageSize.x > imageSize.y) ? LANDSCAPE_ORIENTATION : PORTRAIT_ORIENTATION;
        }

        return (imageSize.x / imageSize.y > imageDimensions.x / imageDimensions.y) ? PORTRAIT_ORIENTATION : LANDSCAPE_ORIENTATION;
    }

    /**
     * @param imageDimensions
     * @param imageSize
     * @returns {boolean}
     *
     * @private
     */
    _assertEqualFactors(imageDimensions, imageSize) {
        const imageDimensionFactor = this._roundToTwoDigits(imageDimensions.x / imageDimensions.y);
        const imageSizeFactor = this._roundToTwoDigits(imageSize.x / imageSize.y);

        return imageSizeFactor === imageDimensionFactor;
    }

    /**
     * @param {HTMLElement} imageContainer
     * @return {Vector2}
     *
     * @private
     */
    _getContainerPos(imageContainer) {
        const containerBoundingRect = imageContainer.getBoundingClientRect();
        return new Vector2(containerBoundingRect.left + window.pageXOffset, containerBoundingRect.top + window.pageYOffset);
    }

    /**
     * @param {HTMLElement} image
     * @return {Vector2}
     * @private
     */
    _getImagePos(image) {
        const imageBoundingRect = image.getBoundingClientRect();
        return new Vector2(imageBoundingRect.left + window.pageXOffset, imageBoundingRect.top + window.pageYOffset);
    }

    /**
     * @param {HTMLElement} image
     * @return {Vector2}
     *
     * @private
     */
    _getImageDimensions(image) {
        const { naturalWidth: width, naturalHeight: height } = image;
        return new Vector2(width, height);
    }

    /**
     * @param {HTMLElement} image
     * @return {Vector2}
     *
     * @private
     */
    _getImageSize(image) {
        const imageBoundingRect = image.getBoundingClientRect();
        return new Vector2(imageBoundingRect.width, imageBoundingRect.height);
    }

    /**
     * @return {Vector2}
     * @private
     */
    _getZoomImageSize() {
        const imageBoundingRect = this._zoomImage.getBoundingClientRect();
        return new Vector2(imageBoundingRect.width, imageBoundingRect.height);
    }

    /**
     * @param {Vector2} zoomImageSize
     *
     * @return {VectorBase|*}
     * @private
     */
    _getOverlaySize(zoomImageSize) {
        const overlaySize = zoomImageSize.divide(this.options.zoomFactor);

        if (!this.options.keepAspectRatioOnZoom) {
            const min = Math.min(overlaySize.x, overlaySize.y);

            overlaySize.x = min;
            overlaySize.y = min;
        }

        this._overlay.style.width = `${Math.ceil(overlaySize.x)}px`;
        this._overlay.style.height = `${Math.ceil(overlaySize.y)}px`;

        return overlaySize;
    }

    /**
     * creates the image overlay element
     *
     * @param container
     * @return {HTMLElement|any}
     * @private
     */
    _createOverlay(container) {
        this._overlay = container.querySelector(`.${this.options.overlayClass}`);
        if (this._overlay) {
            return this._overlay;
        }

        const html = `<div class="magnifier-overlay  ${this.options.overlayClass}">&nbsp;</div>`;
        this._overlay = container.insertAdjacentHTML('beforeend', html);

        this.$emitter.publish('createOverlay');

        return this._overlay;
    }

    /**
     * removes the image overlay element
     *
     * @return {HTMLElement|any}
     * @private
     */
    _removeOverlay() {
        const overlays = document.querySelectorAll(`.${this.options.overlayClass}`);
        Iterator.iterate(overlays, overlay => overlay.remove());

        this.$emitter.publish('removeOverlay');
    }

    /**
     * creates the zoom image element
     *
     * @return {HTMLElement}
     * @private
     */
    _createZoomImage() {
        this._zoomImage = this._zoomImageContainer.querySelector(`.${this.options.zoomImageClass}`);

        if (this._zoomImage) {
            return this._zoomImage;
        }

        this._zoomImageContainer.style.position = 'relative';
        const html = `<div class="magnifier-zoom-image  ${this.options.zoomImageClass}">&nbsp;</div>`;
        this._zoomImage = this._zoomImageContainer.insertAdjacentHTML('beforeend', html);

        this.$emitter.publish('createZoomImage');

        return this._zoomImage;
    }

    /**
     * removes the zoom image element
     *
     * @private
     */
    _removeZoomImage() {
        const zoomImages = document.querySelectorAll(`.${this.options.zoomImageClass}`);
        Iterator.iterate(zoomImages, zoomImage => zoomImage.remove());

        this.$emitter.publish('removeZoomImage');
    }

    /**
     * sets the image url
     *
     * @param {HTMLElement} image
     * @private
     */
    _getImageUrl(image) {
        this._imageUrl = DomAccess.getDataAttribute(image, this.options.fullImageDataAttribute);

        this.$emitter.publish('getImageUrl');
    }

    /**
     * stops the magnify effect
     *
     * @private
     */
    _stopMagnify() {
        this._removeZoomImage();
        this._removeOverlay();

        const images = DomAccess.querySelectorAll(document, this.options.imageSelector);
        Iterator.iterate(images, image => this._setCursor(image, 'default'));

        this.$emitter.publish('stopMagnify');
    }

    /**
     * rounds value to two decimal places
     *
     * @param value
     * @returns {*}
     *
     * @private
     */
    _roundToTwoDigits(value) {
        return Math.round(value * 1000) / 1000;
    }
}
