/**
 * @typedef {object} ChildFilter~options
 * @property {string|HTMLElement|HTMLElement[]} query - The searchfield or a selector to select the
 *      searchfield in the wrapper element. Defaults to `input[type=text]` if not set.
 * @property {string} selector - The selector of the elements that should be filtered.
 * @property {Function} onApply
 */

/**
 * Simple child filter that allows for an input that controls whether or not
 * elements are being displayed.
 *
 * When the user types this class will automatically set `display: none` on the
 * elements that do not contain the typed string in their `.innerText`.
 *
 * If the search field has `display: none` then it will be revealed upon initialization.
 * This is useful to not show the searchfield to users without JS.
 *
 * @example
 * <div>
 *     <input style="display: none" />
 *     <ul>
 *         <li>Foo</li>
 *         <li>Bar</li>
 *         <li>Baz</li>
 *     </ul>
 * </div>
 *
 * new ChildFilter(div, { selector: 'div', query: 'input' });
 */
export class ChildFilter {


    /**
     * @param {HTMLElement} wrapper
     * @param {ChildFilter~options} options
     */
    constructor (wrapper, options = {}) {

        /** @private {HTMLElement} */
        this.wrapper = wrapper;

        /** @private {ChildFilter~options} */
        this.options = this.prepareOptions(options);

        /** @private {number} */
        this.limit = Infinity;

        this.initialize();
    }

    /**
     * Applies the given search query to the children.
     * If `null` is passed as the query then the query will be read from the input field (default).
     *
     * @param {?string} query
     * @returns {{ items: HTMLElement[], displayed: HTMLElement[], matching: HTMLElement[] }}
     * @public
     */
    apply(query = null) {
        query = query !== null ? query : this.getQueryFromInputElement();
        const searchString = query.toLowerCase();

        const items = this.items();
        const displayed = [];
        const matching = [];

        let visible = 0;
        for (const element of items) {
            const matches = element.innerText.toLowerCase().indexOf(searchString) !== -1;
            const limitReached = visible >= this.limit;
            const show = !limitReached && matches;

            if (show) {
                displayed.push(element);
            }
            if (matches) {
                matching.push(element);
            }

            element.style.display = show ? '' : 'none';
            visible += show ? 1 : 0;
        }

        this.updateSearchQueryInDom(query);
        this.callOnApplyHandlerIfExists(items, displayed, matching);

        return { items, displayed, matching };
    }

    /**
     * @private
     * @param {string} query
     * @returns {void}
     */
    updateSearchQueryInDom(query) {
        for (const input of this.options.query) {
            input.value = query;
        }
    }

    /**
     * @private
     * @param {HTMLElement[]} items
     * @param {HTMLElement[]} displayed
     * @param {HTMLElement[]} matching
     */
    callOnApplyHandlerIfExists(items, displayed, matching) {
        if (this.options.onApply) {
            this.options.onApply({ items, displayed, matching });
        }
    }

    /**
     * @private
     * @returns {string}
     */
    getQueryFromInputElement() {
        return this.options.query.length > 0 ? this.options.query[0].value : '';
    }

    /**
     * @public
     * @returns {HTMLElement[]}
     */
    items() {
        return [ ...this.wrapper.querySelectorAll(this.options.selector) ];
    }

    /** @private */
    initialize() {
        for (const query of this.options.query) {
            query.style.display = '';
            query.addEventListener('input', () => this.apply(query.value));
        }
    }

    /**
     * @param {ChildFilter~options} options
     * @returns {ChildFilter~options}
     * @private
     */
    prepareOptions(options) {
        if (!options.selector) {
            throw new Error('selector option must be passed.');
        }

        options.query = options.query || 'input[type=text]';
        if (typeof options.query === 'string') {
            options.query = this.wrapper.querySelectorAll(options.query);
        } else if (options.query instanceof Node) {
            options.query = [ options.query ];
        }

        return options;
    }

}
