import Animate from '../mixin/animate';
import Class from '../mixin/class';
import {$$, addClass, append, assign, before, children, css, findIndex, getEventPos, getViewport, hasTouch, height, index, isEmpty, isInput, off, offset, on, parent, pointerDown, pointerMove, pointerUp, pointInRect, remove, removeClass, scrollParents, scrollTop, toggleClass, Transition, trigger, within} from 'uikit-util';

export default {

    mixins: [Class, Animate],

    props: {
        group: String,
        threshold: Number,
        clsItem: String,
        clsPlaceholder: String,
        clsDrag: String,
        clsDragState: String,
        clsBase: String,
        clsNoDrag: String,
        clsEmpty: String,
        clsCustom: String,
        handle: String
    },

    data: {
        group: false,
        threshold: 5,
        clsItem: 'uk-sortable-item',
        clsPlaceholder: 'uk-sortable-placeholder',
        clsDrag: 'uk-sortable-drag',
        clsDragState: 'uk-drag',
        clsBase: 'uk-sortable',
        clsNoDrag: 'uk-sortable-nodrag',
        clsEmpty: 'uk-sortable-empty',
        clsCustom: '',
        handle: false,
        pos: {}
    },

    created() {
        ['init', 'start', 'move', 'end'].forEach(key => {
            const fn = this[key];
            this[key] = e => {
                assign(this.pos, getEventPos(e));
                fn(e);
            };
        });
    },

    events: {

        name: pointerDown,
        passive: false,
        handler: 'init'

    },

    computed: {

        target() {
            return (this.$el.tBodies || [this.$el])[0];
        },

        items() {
            return children(this.target);
        },

        isEmpty: {

            get() {
                return isEmpty(this.items);
            },

            watch(empty) {
                toggleClass(this.target, this.clsEmpty, empty);
            },

            immediate: true

        },

        handles: {

            get({handle}, el) {
                return handle ? $$(handle, el) : this.items;
            },

            watch(handles, prev) {
                css(prev, {touchAction: '', userSelect: ''});
                css(handles, {touchAction: hasTouch ? 'none' : '', userSelect: 'none'}); // touchAction set to 'none' causes a performance drop in Chrome 80
            },

            immediate: true

        }

    },

    update: {

        write(data) {

            if (!this.drag || !parent(this.placeholder)) {
                return;
            }

            const {pos: {x, y}, origin: {offsetTop, offsetLeft}, placeholder} = this;

            css(this.drag, {
                top: y - offsetTop,
                left: x - offsetLeft
            });

            const sortable = this.getSortable(document.elementFromPoint(x, y));

            if (!sortable) {
                return;
            }

            const {items} = sortable;

            if (items.some(Transition.inProgress)) {
                return;
            }

            const target = findTarget(items, {x, y});

            if (items.length && (!target || target === placeholder)) {
                return;
            }

            const previous = this.getSortable(placeholder);
            const insertTarget = findInsertTarget(sortable.target, target, placeholder, x, y, sortable === previous && data.moved !== target);

            if (insertTarget === false) {
                return;
            }

            if (insertTarget && placeholder === insertTarget) {
                return;
            }

            if (sortable !== previous) {
                previous.remove(placeholder);
                data.moved = target;
            } else {
                delete data.moved;
            }

            sortable.insert(placeholder, insertTarget);

            this.touched.add(sortable);
        },

        events: ['move']

    },

    methods: {

        init(e) {

            const {target, button, defaultPrevented} = e;
            const [placeholder] = this.items.filter(el => within(target, el));

            if (!placeholder
                || defaultPrevented
                || button > 0
                || isInput(target)
                || within(target, `.${this.clsNoDrag}`)
                || this.handle && !within(target, this.handle)
            ) {
                return;
            }

            e.preventDefault();

            this.touched = new Set([this]);
            this.placeholder = placeholder;
            this.origin = assign({target, index: index(placeholder)}, this.pos);

            on(document, pointerMove, this.move);
            on(document, pointerUp, this.end);

            if (!this.threshold) {
                this.start(e);
            }

        },

        start(e) {

            this.drag = appendDrag(this.$container, this.placeholder);
            const {left, top} = this.placeholder.getBoundingClientRect();
            assign(this.origin, {offsetLeft: this.pos.x - left, offsetTop: this.pos.y - top});

            addClass(this.drag, this.clsDrag, this.clsCustom);
            addClass(this.placeholder, this.clsPlaceholder);
            addClass(this.items, this.clsItem);
            addClass(document.documentElement, this.clsDragState);

            trigger(this.$el, 'start', [this, this.placeholder]);

            trackScroll(this.pos);

            this.move(e);
        },

        move(e) {

            if (this.drag) {
                this.$emit('move');
            } else if (Math.abs(this.pos.x - this.origin.x) > this.threshold || Math.abs(this.pos.y - this.origin.y) > this.threshold) {
                this.start(e);
            }

        },

        end() {

            off(document, pointerMove, this.move);
            off(document, pointerUp, this.end);
            off(window, 'scroll', this.scroll);

            if (!this.drag) {
                return;
            }

            untrackScroll();

            const sortable = this.getSortable(this.placeholder);

            if (this === sortable) {
                if (this.origin.index !== index(this.placeholder)) {
                    trigger(this.$el, 'moved', [this, this.placeholder]);
                }
            } else {
                trigger(sortable.$el, 'added', [sortable, this.placeholder]);
                trigger(this.$el, 'removed', [this, this.placeholder]);
            }

            trigger(this.$el, 'stop', [this, this.placeholder]);

            remove(this.drag);
            this.drag = null;

            this.touched.forEach(({clsPlaceholder, clsItem}) =>
                this.touched.forEach(sortable =>
                    removeClass(sortable.items, clsPlaceholder, clsItem)
                )
            );
            this.touched = null;
            removeClass(document.documentElement, this.clsDragState);

        },

        insert(element, target) {

            addClass(this.items, this.clsItem);

            const insert = () => target
                ? before(target, element)
                : append(this.target, element);

            this.animate(insert);

        },

        remove(element) {

            if (!within(element, this.target)) {
                return;
            }

            this.animate(() => remove(element));

        },

        getSortable(element) {
            do {
                const sortable = this.$getComponent(element, 'sortable');

                if (sortable && (sortable === this || this.group !== false && sortable.group === this.group)) {
                    return sortable;
                }
            } while ((element = parent(element)));
        }

    }

};

let trackTimer;
function trackScroll(pos) {

    let last = Date.now();
    trackTimer = setInterval(() => {

        let {x, y} = pos;
        y += window.pageYOffset;

        const dist = (Date.now() - last) * .3;
        last = Date.now();

        scrollParents(document.elementFromPoint(x, pos.y), /auto|scroll/).reverse().some(scrollEl => {

            let {scrollTop: scroll, scrollHeight} = scrollEl;

            const {top, bottom, height} = offset(getViewport(scrollEl));

            if (top < y && top + 35 > y) {
                scroll -= dist;
            } else if (bottom > y && bottom - 35 < y) {
                scroll += dist;
            } else {
                return;
            }

            if (scroll > 0 && scroll < scrollHeight - height) {
                scrollTop(scrollEl, scroll);
                return true;
            }

        });

    }, 15);

}

function untrackScroll() {
    clearInterval(trackTimer);
}

function appendDrag(container, element) {
    const clone = append(container, element.outerHTML.replace(/(^<)(?:li|tr)|(?:li|tr)(\/>$)/g, '$1div$2'));

    css(clone, 'margin', '0', 'important');
    css(clone, assign({
        boxSizing: 'border-box',
        width: element.offsetWidth,
        height: element.offsetHeight
    }, css(element, ['paddingLeft', 'paddingRight', 'paddingTop', 'paddingBottom'])));

    height(clone.firstElementChild, height(element.firstElementChild));

    return clone;
}

function findTarget(items, point) {
    return items[findIndex(items, item => pointInRect(point, item.getBoundingClientRect()))];
}

function findInsertTarget(list, target, placeholder, x, y, sameList) {

    if (!children(list).length) {
        return;
    }

    const rect = target.getBoundingClientRect();
    if (!sameList) {

        if (!isHorizontal(list, placeholder)) {
            return y < rect.top + rect.height / 2
                ? target
                : target.nextElementSibling;
        }

        return target;
    }

    const placeholderRect = placeholder.getBoundingClientRect();
    const sameRow = linesIntersect(
        [rect.top, rect.bottom],
        [placeholderRect.top, placeholderRect.bottom]
    );

    const pointerPos = sameRow ? x : y;
    const lengthProp = sameRow ? 'width' : 'height';
    const startProp = sameRow ? 'left' : 'top';
    const endProp = sameRow ? 'right' : 'bottom';

    const diff = placeholderRect[lengthProp] < rect[lengthProp] ? rect[lengthProp] - placeholderRect[lengthProp] : 0;

    if (placeholderRect[startProp] < rect[startProp]) {

        if (diff && pointerPos < rect[startProp] + diff) {
            return false;
        }

        return target.nextElementSibling;
    }

    if (diff && pointerPos > rect[endProp] - diff) {
        return false;
    }

    return target;
}

function isHorizontal(list, placeholder) {

    const single = children(list).length === 1;

    if (single) {
        append(list, placeholder);
    }

    const items = children(list);
    const isHorizontal = items.some((el, i) => {
        const rectA = el.getBoundingClientRect();
        return items.slice(i + 1).some(el => {
            const rectB = el.getBoundingClientRect();
            return !linesIntersect([rectA.left, rectA.right], [rectB.left, rectB.right]);
        });
    });

    if (single) {
        remove(placeholder);
    }

    return isHorizontal;
}

function linesIntersect(lineA, lineB) {
    return lineA[1] > lineB[0] && lineB[1] > lineA[0];
}
