import React from 'react';
import debounce from 'lodash/debounce';
import isEqual from 'lodash/isEqual';
import isFunction from 'lodash/isFunction';
import isNumber from 'lodash/isNumber';
import Typography from '@material-ui/core/Typography';

import { T } from '../../i18n';
import PropTypes from 'prop-types';
import './Style.scss';

const whichTransitionEvent = function() {
    let t;
    const el = document.createElement('fakeelement');
    const transitions = {
        transition: 'transitionend',
        OTransition: 'oTransitionEnd',
        MozTransition: 'transitionend',
        WebkitTransition: 'webkitTransitionEnd',
    };

    for (t in transitions) {
        if (el.style[t] !== undefined) {
            return transitions[t];
        }
    }
};

const transitionEvent = whichTransitionEvent();

export default class Dialog extends React.Component {
    static propTypes = {
        relativePosition: PropTypes.array,
        animation: PropTypes.string,
        overlayClassName: PropTypes.string,
        onClose: PropTypes.func,
        fixToLeft: PropTypes.bool,
        relativeTo: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
        refValue: PropTypes.any,
        relativeOffset: PropTypes.object,
        movable: PropTypes.bool,
        parent: PropTypes.instanceOf(HTMLElement),
        show: PropTypes.bool,
        children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]),
        contentOverflow: PropTypes.string,
        title: PropTypes.string,
        overlay: PropTypes.bool,
        className: PropTypes.string,
    };

    static defaultProps = {
        relativePosition: [
            ['center', 'center'],
            ['center', 'center'],
        ],
        animation: 'all', // ('show', 'grow', 'all') -> css class with transition,
        overlayClassName: '',
        onClose: false,
        fixToLeft: false,
        relativeTo: undefined, // the element to which the dialog is relative (if not set it will be fixed)
        refValue: undefined, // if this value changes it will trigger the recalculation of the dialog height
        relativeOffset: undefined, // { top, left }
        movable: false,
    };

    static zIndex = 1000;

    constructor(props) {
        super(props);
        this.position = {};
        this.zIndex = Dialog.zIndex += 2;
        this.imageLoaded = false;
        this.lastRelativeElement = null;
        this.offset = {
            top: 0,
            left: 0,
        };
        this.dialogContainer = React.createRef();
        this.dialogClose = React.createRef();
        this.dialogTitleBar = React.createRef();
        this.dialog = React.createRef();
        this.dialogOverlay = React.createRef();
        this.dialogContent = React.createRef();
    }

    getParent = () => {
        return (
            this.props.parent ||
            (this.dialogContainer.current && this.dialogContainer.current.parentElement) ||
            document.body.firstElementChild
        );
    };

    componentDidUpdate() {
        if (this.shouldUpdate) {
            this.shouldUpdate = false;

            const position = this.calculatePosition();
            // check the position after rendering
            if (!isEqual(position, this.position)) {
                // rerender if not correct
                this.position = position;

                this.forceUpdate();
            } else {
                // if no change no update needed
                this.shouldUpdate = false;
            }
        }
    }

    componentWillReceiveProps(props) {
        let shouldRecalculate = this.props.show ^ props.show; // was or will be shown

        if (!isEqual(props, this.props) && props.show) {
            if (!this.props.show) {
                this.zIndex = Dialog.zIndex += 2;
            }

            const relativeElement = isFunction(props.relativeTo) ? props.relativeTo() : props.relativeTo;

            if (relativeElement && relativeElement !== this.lastRelativeElement) {
                this.lastRelativeElement = relativeElement;
                shouldRecalculate = true;
            }

            if (props.refValue !== this.props.refValue || props.children !== this.props.children) {
                shouldRecalculate = true;
            }
        }

        if (shouldRecalculate) {
            this.position = this.calculatePosition(props);
        }
    }

    onContentImageLoaded = event => {
        // recalculating position when images are loaded
        if (event.target.tagName === 'IMG') {
            this.imageLoaded = true;
            this.shouldUpdate = true;
            this.forceUpdate();
        }
    };

    onContentImageError = event => {
        // recalculating position when images are loaded
        if (event.target.tagName === 'IMG') {
            this.imageLoaded = false;
            this.shouldUpdate = true;
            this.forceUpdate();
        }
    };

    componentDidMount() {
        const { relativeTo, show } = this.props;
        const relativeElement = isFunction(relativeTo) ? relativeTo() : relativeTo;
        if (relativeElement && show) {
            this.lastRelativeElement = relativeElement;
        }

        this.position = this.calculatePosition();
        this.forceUpdate();

        this.dialog.current.addEventListener(transitionEvent, evt => {
            if (evt.target === this.dialog.current) {
                // check the position after rendering
                const position = this.calculatePosition();
                // check the position after rendering
                if (!isEqual(position, this.position)) {
                    // rerender if not correct
                    this.position = position;
                    this.forceUpdate();
                }
            }
        });

        if ('MutationObserver' in window) {
            const updateIfPositionChanged = () => {
                const position = this.calculatePosition();
                // check the position after rendering
                if (!isEqual(position, this.position)) {
                    // rerender if not correct
                    this.position = position;
                    this.forceUpdate();
                }
            };
            const updateDebounced = debounce(updateIfPositionChanged, 50);

            // create an observer instance
            this.observer = new MutationObserver(mutations => {
                mutations.forEach(mutation => {
                    switch (mutation.type) {
                        case 'childList':
                            Array.prototype.forEach.call(mutation.target.children, child => {
                                if (child.tagName === 'IMG') {
                                    child.removeEventListener('load', this.onContentImageLoaded, false);
                                    child.addEventListener('load', this.onContentImageLoaded, false);
                                    child.removeEventListener('error', this.onContentImageError, false);
                                    child.addEventListener('error', this.onContentImageError, false);
                                }
                            });
                            break;
                    }
                    updateDebounced();
                });
            });
            const config = { attributes: true, childList: true, characterData: true, subtree: true };
            this.observer.observe(this.dialog.current, config);
        }

        if (this.props.movable) {
            this.initMoving();
        }
    }

    componentWillUnmount() {
        this.dialog.current &&
            this.dialog.current.removeEventListener('load', this.onContentImageLoaded, true);
        this.observer && this.observer.disconnect();
    }

    calculateHeightAdds(element) {
        const compstyle =
            typeof window.getComputedStyle === 'undefined'
                ? element.currentStyle
                : window.getComputedStyle(element);
        const marginTop = parseInt(compstyle.marginTop);
        const marginBottom = parseInt(compstyle.marginBottom);
        const borderTopWidth = parseInt(compstyle.borderTopWidth);
        const borderBottomWidth = parseInt(compstyle.borderBottomWidth);
        return (
            (isNaN(marginTop) ? 0 : marginTop) +
            (isNaN(marginBottom) ? 0 : marginBottom) +
            (isNaN(borderTopWidth) ? 0 : borderTopWidth) +
            (isNaN(borderBottomWidth) ? 0 : borderBottomWidth)
        );
    }

    calculateVerticalPaddings(element) {
        const compstyle =
            typeof window.getComputedStyle === 'undefined'
                ? element.currentStyle
                : window.getComputedStyle(element);
        const paddingTop = parseInt(compstyle.paddingTop);
        const paddingBottom = parseInt(compstyle.paddingBottom);
        return (isNaN(paddingTop) ? 0 : paddingTop) + (isNaN(paddingBottom) ? 0 : paddingBottom);
    }

    calculateHeight(element) {
        return element.offsetHeight + this.calculateHeightAdds(element);
    }

    calculatePosition = props => {
        let { width, height, relativePosition, show, fixToLeft, relativeOffset } = props || this.props;

        let relativeTo = this.lastRelativeElement;

        const parent = this.getParent();

        if (relativeTo === 'parent') {
            relativeTo = parent;
        }
        const { dialogContent, dialogTitleBar, dialog } = this;

        const dialogStyle =
            typeof window.getComputedStyle === 'undefined'
                ? dialog.current.currentStyle
                : window.getComputedStyle(dialog.current);

        // read the inner content height
        let contentHeight;
        let maxContentHeight;
        if (!height) {
            if (dialogContent.current && dialogTitleBar.current) {
                contentHeight = dialogContent.current.scrollHeight;
                const contentHeightAdds = this.calculateHeightAdds(dialogContent.current);
                height = contentHeight + contentHeightAdds + dialogTitleBar.current.offsetHeight;

                // add horizontal padding and border of the dialog element
                const paddingAndBorder =
                    (parseInt(dialogStyle.paddingTop) || 0) +
                    (parseInt(dialogStyle.paddingBottom) || 0) +
                    (parseInt(dialogStyle.borderTop) || 0) +
                    (parseInt(dialogStyle.borderBottom) || 0);
                height += paddingAndBorder;
                maxContentHeight =
                    (/%$/.test(dialogStyle.maxHeight)
                        ? (parent.clientHeight * parseInt(dialogStyle.maxHeight)) / 100
                        : parseInt(dialogStyle.maxHeight)) - dialogTitleBar.current.offsetHeight;
                if (!isNumber(maxContentHeight)) {
                    // fallback to parents height
                    maxContentHeight = parent.clientHeight;
                }
            } else {
                height = dialog.current && Math.max(dialog.current.scrollHeight, dialog.current.clientHeight);
            }
        }
        if (width === 'adjust' && relativeTo) {
            width = relativeTo.offsetWidth;
        }
        if (!width) {
            width = dialog.current && Math.max(dialog.current.scrollWidth, dialog.current.offsetWidth);
        }
        let element = relativeTo,
            top = 0,
            left = 0,
            bottom,
            contentOverflow = this.position.contentOverflow || false;

        if (element) {
            do {
                top += element.offsetTop || 0;
                left += element.offsetLeft || 0;

                // fix scroll offset
                let scrollElement = element;
                do {
                    top -= scrollElement.scrollTop || 0;
                    left -= scrollElement.scrollLeft || 0;
                    scrollElement = scrollElement.parentElement;
                } while (scrollElement && scrollElement !== element.offsetParent);

                element = element.offsetParent;
            } while (element && element !== parent);
        }

        // set max width
        if (width > parent.clientWidth) {
            width = parent.clientWidth;
        }
        // set max height
        if (height > parent.clientHeight) {
            height = parent.clientHeight;
        }
        // set content overflow
        if (dialogContent.current) {
            contentOverflow = contentHeight > maxContentHeight;
        }

        if (!!relativeTo && relativePosition[0]) {
            const x = relativePosition[0]; // x = [0:<dialogPositionX>,1:<basePositionX>]
            // cx: horizontal base value, relative position to the element
            let cx = 0;
            switch (x[1]) {
                case 'left':
                    cx = left;
                    break;
                case 'right':
                    cx = left + relativeTo.offsetWidth;
                    break;
                case 'center':
                    cx = left + relativeTo.offsetWidth / 2;
                    break;
            }
            // dialog positioning to cx
            switch (x[0]) {
                case 'left':
                    left = cx;
                    break;
                case 'right':
                    left = cx - width;
                    break;
                case 'center':
                    left = cx - Math.round(width / 2);
                    break;
            }
            const y = relativePosition[1]; // y = [0:<dialogPositionY>,1:<basePositionY>]
            // cy: vertical base value, relative position to the element
            let cy = 0;
            switch (y[1]) {
                case 'top':
                    cy = top;
                    break;
                case 'bottom':
                    cy = top + relativeTo.offsetHeight;
                    break;
                case 'center':
                    cy = top + relativeTo.offsetHeight / 2;
                    break;
            }
            // dialog positioning to cy
            switch (y[0]) {
                case 'top':
                    top = cy;
                    break;
                case 'bottom':
                    top = cy - height;
                    bottom = parent.clientHeight - cy;
                    if (bottom + height > parent.clientHeight) {
                        const margins = parseInt(dialogStyle.marginBottom) + parseInt(dialogStyle.marginTop);
                        height = parent.clientHeight - bottom - margins;
                    }
                    break;
                case 'center':
                    top = cy - Math.round(height / 2);
                    break;
            }
            if (relativeOffset) {
                top += +relativeOffset.top;
                left += +relativeOffset.left;
            }
        }

        left += this.offset.left;
        top += this.offset.top;

        // correct left
        if (left + width > parent.clientWidth) {
            left = parent.clientWidth - width;
        }
        if (left < 0) {
            left = 0;
        }
        // correct top
        if (top + height > parent.clientHeight) {
            top = parent.clientHeight - height;
        }
        if (top < 0) {
            top = 0;
        }
        // set height and overflow if needed
        if (bottom) {
            // set max height
            if (bottom + height > parent.clientHeight) {
                height = parent.clientHeight - bottom;
                contentOverflow = true;
            }
        }

        const position = relativeTo
            ? {
                  top: bottom ? 'auto' : top,
                  left: fixToLeft ? undefined : left,
                  bottom: bottom || 'auto',
              }
            : {};

        if (!show) {
            return {
                ...position,
                width,
                height,
                opacity: 0,
            };
        }

        return {
            ...position,
            width,
            height,
            opacity: !!width && !!height ? 1 : 0,
            contentOverflow: 'contentOverflow' in this.props ? this.props.contentOverflow : contentOverflow,
            contentHeight: contentHeight,
        };
    };

    onCloseClick = e => {
        e.stopPropagation();
        this.props.onClose && this.props.onClose();
    };

    stopEvent = e => e.stopPropagation();

    getPointerPosition(e) {
        let x = 0;
        let y = 0;
        if (e.touches !== undefined) {
            x = e.touches[0].clientX;
            y = e.touches[0].clientY;
        } else {
            x = e.clientX;
            y = e.clientY;
        }
        return {
            x,
            y,
        };
    }

    initMoving() {
        if ('ontouchstart' in window) {
            this.dialogTitleBar.current.addEventListener('touchstart', this.moveStart);
            this.dialogTitleBar.current.addEventListener('touchend', this.moveEnd);
        }
        this.dialogTitleBar.current.addEventListener('mousedown', this.moveStart);
        this.dialogTitleBar.current.addEventListener('mouseup', this.moveEnd);

        //window.addEventListener('resize', this.detectOrientation);
    }

    /*detectOrientation = () => {
        let orientation = 'landscape';
        if (window.matchMedia("(orientation: portrait)").matches) {
            orientation = 'portrait';
        }
        if (this.orientation !== orientation) {
            this.orientation = orientation;
        }
    };*/

    moveStart = e => {
        if (e.target === this.dialogClose.current) {
            return;
        }
        e.preventDefault();
        const pp = this.getPointerPosition(e);
        this.startPosition = { x: pp.x - this.offset.left, y: pp.y - this.offset.top };
        this.dialog.current.style.transition = 'none';

        if ('ontouchstart' in window) {
            window.addEventListener('touchmove', this.moveHandler);
        }
        window.addEventListener('mousemove', this.moveHandler);
    };

    moveHandler = e => {
        const pp = this.getPointerPosition(e);

        const lastLeft = this.offset.left;
        const lastTop = this.offset.top;

        this.offset.left = pp.x - this.startPosition.x;
        this.offset.top = pp.y - this.startPosition.y;
        const position = this.calculatePosition();
        if (position.top === this.position.top) {
            this.offset.top = lastTop;
        }
        if (position.left === this.position.left) {
            this.offset.left = lastLeft;
        }
        this.dialog.current.style.left = position.left + 'px';
        this.dialog.current.style.top = position.top + 'px';
        this.position = position;
    };

    moveEnd = e => {
        if (e.target === this.dialogClose.current) {
            return;
        }
        e.preventDefault();
        if ('ontouchstart' in window) {
            window.removeEventListener('touchmove', this.moveHandler);
        }
        window.removeEventListener('mousemove', this.moveHandler);
        this.dialog.current.style.transition = '';
    };

    render() {
        const {
            show,
            title,
            children,
            overlay,
            overlayClassName,
            className,
            animation,
            onClose,
        } = this.props;
        const {
            zIndex,
            onCloseClick,
            dialog,
            dialogContainer,
            dialogClose,
            dialogTitleBar,
            dialogContent,
            dialogOverlay,
        } = this;
        const { contentOverflow, ...position } = { ...this.position };
        const display = show ? 'block' : 'none';
        const imageClass = this.imageLoaded ? 'with-image' : '';
        const containerClassName = ['dialog-container', animation, imageClass, className || ''].join(' ');
        return (
            <div
                className={containerClassName}
                style={{ display }}
                ref={dialogContainer}
                onClick={onCloseClick}
            >
                {overlay && (
                    <div
                        ref={dialogOverlay}
                        className={'dialog-overlay ' + (overlayClassName || '')}
                        style={{ zIndex }}
                    />
                )}

                <div
                    ref={dialog}
                    className="dialog"
                    style={{ ...position, zIndex: zIndex + 1 }}
                    onClick={this.stopEvent}
                >
                    <Typography variant={'subtitle1'}>
                        <div className="dialog-title-bar" ref={dialogTitleBar}>
                            {onClose && (
                                <span ref={dialogClose} className="dialog-close" onClick={onCloseClick}>
                                    X
                                </span>
                            )}
                            {title && <h3 className="dialog-title">{T(title)}</h3>}
                        </div>
                    </Typography>
                    <div
                        className={'dialog-content' + (contentOverflow ? ' overflow' : '')}
                        ref={dialogContent}
                    >
                        <Typography>{children}</Typography>
                    </div>
                </div>
            </div>
        );
    }
}
