/**
 * Add event(s) to element(s).
 * @param elements DOM-Elements
 * @param events Event names
 * @param fn Callback
 * @param options Optional options
 * @return Array passed arguments
 */
export const on = eventListener.bind(null, 'addEventListener');

/**
 * Remove event(s) from element(s).
 * @param elements DOM-Elements
 * @param events Event names
 * @param fn Callback
 * @param options Optional options
 * @return Array passed arguments
 */
export const off = eventListener.bind(null, 'removeEventListener');

function eventListener(method, elements, events, fn, options = {}) {

    // Normalize array
    if (elements instanceof HTMLCollection || elements instanceof NodeList) {
        elements = Array.from(elements);
    } else if (!Array.isArray(elements)) {
        elements = [elements];
    }

    if (!Array.isArray(events)) {
        events = [events];
    }

    for (const el of elements) {
        for (const ev of events) {
            el[method](ev, fn, {capture: false, ...options});
        }
    }

    return Array.prototype.slice.call(arguments, 1);
}

/**
 * Creates an DOM-Element out of a string (Single element).
 * @param html HTML representing a single element
 * @returns {Element | null} The element.
 */
export function createElementFromString(html) {
    const div = document.createElement('div');
    div.innerHTML = html.trim();
    return div.firstElementChild;
}

/**
 * Removes an attribute from a HTMLElement and returns the value.
 * @param el
 * @param name
 * @return {string}
 */
export function removeAttribute(el, name) {
    const value = el.getAttribute(name);
    el.removeAttribute(name);
    return value;
}

/**
 * Creates a new html element, every element which has
 * a ':ref' attribute will be saved in a object (which will be returned)
 * where the value of ':ref' is the object-key and the value the HTMLElement.
 *
 * It's possible to create a hierarchy if you add a ':obj' attribute. Every
 * sibling will be added to the object which will get the name from the 'data-con' attribute.
 *
 * If you want to create an Array out of multiple elements, you can use the ':arr' attribute,
 * the value defines the key and all elements, which has the same parent and the same 'data-arr' attribute,
 * would be added to it.
 *
 * @param str - The HTML String.
 */
export function createFromTemplate(str) {

    // Recursive function to resolve template
    function resolve(element, base = {}) {

        // Check key and container attribute
        const con = removeAttribute(element, ':obj');
        const key = removeAttribute(element, ':ref');
        const subtree = con ? (base[con] = {}) : base;

        // Check and save element
        key && (base[key] = element);
        for (const child of Array.from(element.children)) {
            const arr = removeAttribute(child, ':arr');
            const sub = resolve(child, arr ? {} : subtree);

            if (arr) {

                // Check if there is already an array and add element
                (subtree[arr] || (subtree[arr] = []))
                    .push(Object.keys(sub).length ? sub : child);
            }
        }

        return base;
    }

    return resolve(createElementFromString(str));
}

/**
 * Polyfill for safari & firefox for the eventPath event property.
 * @param evt The event object.
 * @return [String] event path.
 */
export function eventPath(evt) {
    let path = evt.path || (evt.composedPath && evt.composedPath());
    if (path) return path;

    let el = evt.target.parentElement;
    path = [evt.target, el];
    while (el = el.parentElement) path.push(el);

    path.push(document, window);
    return path;
}

/**
 * Resolves a HTMLElement by query.
 * @param val
 * @returns {null|Document|Element}
 */
export function resolveElement(val) {
    if (val instanceof Element) {
        return val;
    } else if (typeof val === 'string') {
        return val.split(/>>/g).reduce((pv, cv, ci, a) => {
            pv = pv.querySelector(cv);
            return ci < a.length - 1 ? pv.shadowRoot : pv;
        }, document);
    }

    return null;
}

/**
 * Creates the ability to change numbers in an input field with the scroll-wheel.
 * @param el
 * @param mapper
 */
export function adjustableInputNumbers(el, mapper = v => v) {

    function handleScroll(e) {
        const inc = ([0.001, 0.01, 0.1])[Number(e.shiftKey || e.ctrlKey * 2)] * (e.deltaY < 0 ? 1 : -1);

        let index = 0;
        let off = el.selectionStart;
        el.value = el.value.replace(/[\d.]+/g, (v, i) => {

            // Check if number is in cursor range and increase it
            if (i <= off && i + v.length >= off) {
                off = i;
                return mapper(Number(v), inc, index);
            }

            index++;
            return v;
        });

        el.focus();
        el.setSelectionRange(off, off);

        // Prevent default and trigger input event
        e.preventDefault();
        el.dispatchEvent(new Event('input'));
    }

    // Bind events
    on(el, 'focus', () => on(window, 'wheel', handleScroll, {passive: false}));
    on(el, 'blur', () => off(window, 'wheel', handleScroll));
}
