import Position from '../mixin/position';
import Togglable from '../mixin/togglable';
import {addClass, Animation, apply, attr, css, includes, isTouch, matches, MouseTracker, offset, on, once, pointerEnter, pointerLeave, query, removeClasses, toggleClass, trigger, within} from 'uikit-util';

let active;

export default {

    mixins: [Position, Togglable],

    args: 'pos',

    props: {
        mode: 'list',
        toggle: Boolean,
        boundary: Boolean,
        boundaryAlign: Boolean,
        delayShow: Number,
        delayHide: Number,
        clsDrop: String
    },

    data: {
        mode: ['click', 'hover'],
        toggle: '- *',
        boundary: window,
        boundaryAlign: false,
        delayShow: 0,
        delayHide: 800,
        clsDrop: false,
        animation: ['uk-animation-fade'],
        cls: 'uk-open'
    },

    computed: {

        boundary({boundary}, $el) {
            return query(boundary, $el);
        },

        clsDrop({clsDrop}) {
            return clsDrop || `uk-${this.$options.name}`;
        },

        clsPos() {
            return this.clsDrop;
        }

    },

    created() {
        this.tracker = new MouseTracker();
    },

    connected() {

        addClass(this.$el, this.clsDrop);

        const {toggle} = this.$props;
        this.toggle = toggle && this.$create('toggle', query(toggle, this.$el), {
            target: this.$el,
            mode: this.mode
        });

        !this.toggle && trigger(this.$el, 'updatearia');

    },

    disconnected() {
        if (this.isActive()) {
            active = null;
        }
    },

    events: [

        {

            name: 'click',

            delegate() {
                return `.${this.clsDrop}-close`;
            },

            handler(e) {
                e.preventDefault();
                this.hide(false);
            }

        },

        {

            name: 'click',

            delegate() {
                return 'a[href^="#"]';
            },

            handler({defaultPrevented, current: {hash}}) {
                if (!defaultPrevented && hash && !within(hash, this.$el)) {
                    this.hide(false);
                }
            }

        },

        {

            name: 'beforescroll',

            handler() {
                this.hide(false);
            }

        },

        {

            name: 'toggle',

            self: true,

            handler(e, toggle) {

                e.preventDefault();

                if (this.isToggled()) {
                    this.hide(false);
                } else {
                    this.show(toggle, false);
                }
            }

        },

        {

            name: 'toggleshow',

            self: true,

            handler(e, toggle) {
                e.preventDefault();
                this.show(toggle);
            }

        },

        {

            name: 'togglehide',

            self: true,

            handler(e) {
                e.preventDefault();
                this.hide();
            }

        },

        {

            name: pointerEnter,

            filter() {
                return includes(this.mode, 'hover');
            },

            handler(e) {
                if (!isTouch(e)) {
                    this.clearTimers();
                }
            }

        },

        {

            name: pointerLeave,

            filter() {
                return includes(this.mode, 'hover');
            },

            handler(e) {
                if (!isTouch(e) && !matches(this.$el, ':hover')) {
                    this.hide();
                }
            }

        },

        {

            name: 'beforeshow',

            self: true,

            handler() {
                this.clearTimers();
                Animation.cancel(this.$el);
                this.position();
            }

        },

        {

            name: 'show',

            self: true,

            handler() {

                active = this;

                this.tracker.init();
                trigger(this.$el, 'updatearia');

                // If triggered from an click event handler, delay adding the click handler
                const off = delayOn(document, 'click', ({defaultPrevented, target}) => {
                    if (!defaultPrevented && !within(target, this.$el) && !(this.toggle && within(target, this.toggle.$el))) {
                        this.hide(false);
                    }
                });

                once(this.$el, 'hide', off, {self: true});
            }

        },

        {

            name: 'beforehide',

            self: true,

            handler() {
                this.clearTimers();
            }

        },

        {

            name: 'hide',

            handler({target}) {

                if (this.$el !== target) {
                    active = active === null && within(target, this.$el) && this.isToggled() ? this : active;
                    return;
                }

                active = this.isActive() ? null : active;
                trigger(this.$el, 'updatearia');
                this.tracker.cancel();
            }

        },

        {

            name: 'updatearia',

            self: true,

            handler(e, toggle) {

                e.preventDefault();

                this.updateAria(this.$el);

                if (toggle || this.toggle) {
                    attr((toggle || this.toggle).$el, 'aria-expanded', this.isToggled());
                    toggleClass(this.toggle.$el, this.cls, this.isToggled());
                }
            }
        }

    ],

    update: {

        write() {

            if (this.isToggled() && !Animation.inProgress(this.$el)) {
                this.position();
            }

        },

        events: ['resize']

    },

    methods: {

        show(toggle = this.toggle, delay = true) {

            if (this.isToggled() && toggle && this.toggle && toggle.$el !== this.toggle.$el) {
                this.hide(false);
            }

            this.toggle = toggle;

            this.clearTimers();

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

            if (active) {

                if (delay && active.isDelaying) {
                    this.showTimer = setTimeout(this.show, 10);
                    return;
                }

                while (active && !within(this.$el, active.$el)) {
                    active.hide(false);
                }
            }

            this.showTimer = setTimeout(() => !this.isToggled() && this.toggleElement(this.$el, true), delay && this.delayShow || 0);

        },

        hide(delay = true) {

            const hide = () => this.toggleNow(this.$el, false);

            this.clearTimers();

            this.isDelaying = getPositionedElements(this.$el).some(el => this.tracker.movesTo(el));

            if (delay && this.isDelaying) {
                this.hideTimer = setTimeout(this.hide, 50);
            } else if (delay && this.delayHide) {
                this.hideTimer = setTimeout(hide, this.delayHide);
            } else {
                hide();
            }
        },

        clearTimers() {
            clearTimeout(this.showTimer);
            clearTimeout(this.hideTimer);
            this.showTimer = null;
            this.hideTimer = null;
            this.isDelaying = false;
        },

        isActive() {
            return active === this;
        },

        position() {

            removeClasses(this.$el, `${this.clsDrop}-(stack|boundary)`);
            css(this.$el, {top: '', left: '', display: 'block'});
            toggleClass(this.$el, `${this.clsDrop}-boundary`, this.boundaryAlign);

            const boundary = offset(this.boundary);
            const alignTo = this.boundaryAlign ? boundary : offset(this.toggle.$el);

            if (this.align === 'justify') {
                const prop = this.getAxis() === 'y' ? 'width' : 'height';
                css(this.$el, prop, alignTo[prop]);
            } else if (this.$el.offsetWidth > Math.max(boundary.right - alignTo.left, alignTo.right - boundary.left)) {
                addClass(this.$el, `${this.clsDrop}-stack`);
            }

            this.positionAt(this.$el, this.boundaryAlign ? this.boundary : this.toggle.$el, this.boundary);

            css(this.$el, 'display', '');

        }

    }

};

function getPositionedElements(el) {
    const result = [];
    apply(el, el => css(el, 'position') !== 'static' && result.push(el));
    return result;
}

export function delayOn(el, type, fn) {
    let off = once(el, type, () =>
        off = on(el, type, fn)
    , true);
    return () => off();
}
