window.fSelect = (() => {

    var build = {};

    class fSelect {

        constructor(selector, options) {
            let that = this;

            var defaults = {
                placeholder: 'Select some options',
                numDisplayed: 3,
                overflowText: '{n} selected',
                searchText: 'Search',
                noResultsText: 'No results found',
                showSearch: true,
                optionFormatter: false
            };

            that.settings = Object.assign({}, defaults, options);
            build = {output: '', optgroup: 0, idx: 0};

            if ('string' === typeof selector) {
                var nodes = Array.from(document.querySelectorAll(selector));
            }
            else if (selector instanceof Node) {
                var nodes = [selector];
            }
            else if (Array.isArray(selector)) {
                var nodes = selector;
            }
            else {
                var nodes = [];
            }

            if ('undefined' === typeof window.fSelectInit) {
                window.fSelectInit = {
                    searchCache: '',
                    lastChoice: null,
                    lastFocus: null,
                    activeEl: null
                };
                that.bindEvents();
            }

            nodes.forEach((input) => {
                if (typeof input.fselect === 'object') {
                    input.fselect.destroy();
                }

                that.settings.multiple = input.matches('[multiple]');
                input.fselect = that;
                that.input = input;
                that.create();
            });
        }

        create() {
            var that = this;
            var options = that.buildOptions();
            var label = that.getDropdownLabel();
            var mode = (that.settings.multiple) ? 'multiple' : 'single';
            var searchClass = (that.settings.showSearch) ? '' : ' fs-hidden';
            var noResultsClass = (build.idx < 2) ? '' : ' fs-hidden';

            var html = `
            <div class="fs-wrap ${mode}" tabindex="0">
                <div class="fs-label-wrap">
                    <div class="fs-label">${label}</div>
                    <span class="fs-arrow"></span>
                </div>
                <div class="fs-dropdown fs-hidden">
                    <div class="fs-search${searchClass}">
                        <input type="text" placeholder="${that.settings.searchText}" />
                    </div>
                    <div class="fs-no-results${noResultsClass}">${that.settings.noResultsText}</div>
                    <div class="fs-options">${options}</div>
                </div>
            </div>
            `;

            var tpl = document.createElement('template');
            tpl.innerHTML = html;
            var wrap = tpl.content.querySelector('.fs-wrap');
            that.input.parentNode.insertBefore(wrap, that.input.nextSibling);
            that.input.classList.add('fs-hidden');

            // add a relationship link
            that.input._rel = wrap;
            wrap._rel = that.input;
        }

        destroy() {
            this.input._rel.remove();
            this.input.classList.remove('fs-hidden');
            delete this.input._rel;
        }

        reload() {
            this.destroy();
            this.create();
        }

        open() {
            var wrap = this.input._rel;
            wrap.classList.add('fs-open');
            wrap.querySelector('.fs-dropdown').classList.remove('fs-hidden');

            // don't auto-focus for touch devices
            if (! window.matchMedia("(pointer: coarse)").matches) {
                wrap.querySelector('.fs-search input').focus();
            }

            // restore searchCache if there is still an input in search
            window.fSelectInit.searchCache = wrap.querySelector('.fs-search input')

            window.fSelectInit.lastChoice = this.getSelectedOptions('value');
            window.fSelectInit.activeEl = wrap;

            this.trigger('fs:opened', wrap);
        }

        close() {
            this.input._rel.classList.remove('fs-open');
            this.input._rel.querySelector('.fs-dropdown').classList.add('fs-hidden');

            window.fSelectInit.searchCache = '';
            window.fSelectInit.lastChoice = null;
            window.fSelectInit.lastFocus = null;
            window.fSelectInit.activeEl = null;

            this.trigger('fs:closed', this.input._rel);
        }

        buildOptions(parent) {
            var that = this;
            var parent = parent || that.input;

            Array.from(parent.children).forEach((node) => {
                if ('optgroup' === node.nodeName.toLowerCase()) {
                    var opt = `<div class="fs-optgroup-label" data-group="${build.optgroup}">${node.label}</div>`;
                    build.output += opt;
                    that.buildOptions(node);
                    build.optgroup++;
                }
                else {
                    var val = node.value;

                    // skip the first choice in multi-select mode
                    if (0 === build.idx && '' === val && that.settings.multiple) {
                        build.idx++;
                        return;
                    }

                    var classes = ['fs-option', 'g' + build.optgroup];

                    // append existing classes
                    node.className.split(' ').forEach((className) => {
                        if ('' !== className) {
                            classes.push(className);
                        }
                    });

                    if (node.matches('[disabled]')) classes.push('disabled');
                    if (node.matches('[selected]')) classes.push('selected');
                    classes = classes.join(' ');

                    if ('function' === typeof that.settings.optionFormatter) {
                        node.label = that.settings.optionFormatter(node.label, node);
                    }

                    var opt = `<div class="${classes}" data-value="${val}" data-idx="${build.idx}" tabindex="-1"><span class="fs-checkbox"><i></i></span><div class="fs-option-label">${node.label}</div></div>`;

                    build.output += opt;
                    build.idx++;
                }
            });

            return build.output;
        }

        getSelectedOptions(field, context) {
            var context = context || this.input;
            return Array.from(context.selectedOptions).map((opt) => {
                return (field) ? opt[field] : opt;
            });
        }

        getAdjacentSibling(which) {
            var that = this;
            var which = which || 'next';
            var sibling = window.fSelectInit.lastFocus;
            var selector = '.fs-option:not(.fs-hidden):not(.disabled)';

            if (sibling) {
                sibling = sibling[which + 'ElementSibling'];

                while (sibling) {
                    if (sibling.matches(selector)) break;
                    sibling = sibling[which + 'ElementSibling'];
                }

                return sibling;
            }
            else if ('next' == which) {
                sibling = that.input._rel.querySelector(selector);
            }

            return sibling;
        }

        getDropdownLabel() {
            var that = this;
            var labelText = that.getSelectedOptions('text');

            if (labelText.length < 1) {
                labelText = that.settings.placeholder;
            }
            else if (labelText.length > that.settings.numDisplayed) {
                labelText = that.settings.overflowText.replace('{n}', labelText.length);
            }
            else {
                labelText = labelText.join(', ');
            }

            return labelText;
        }

        debounce(func, wait) {
            var timeout;
            return (...args) => {
                var boundFunc = func.bind(this, ...args);
                clearTimeout(timeout);
                timeout = setTimeout(boundFunc, wait);
            }
        }

        trigger(eventName, ...args) {
            document.dispatchEvent(new CustomEvent(eventName, {detail: [...args]}));
        }

        on(eventName, elementSelector, handler) {
            document.addEventListener(eventName, function(e) {
                // loop parent nodes from the target to the delegation node
                for (var target = e.target; target && target != this; target = target.parentNode) {
                    if (target.matches(elementSelector)) {
                        handler.call(target, e);
                        break;
                    }
                }
            }, false);
        }

        bindEvents() {
            var that = this;
            var optionSelector = '.fs-option:not(.fs-hidden):not(.disabled)';
            var unaccented = (str) => str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');

            // debounce search for better performance
            that.on('keyup', '.fs-search input', that.debounce(function(e) {
                var wrap = e.target.closest('.fs-wrap');
                var options = wrap._rel.options;

                var matchOperators = /[|\\{}()[\]^$+*?.]/g;
                var keywords = e.target.value.replace(matchOperators, '\\$&');
                keywords = unaccented(keywords);

                // if the searchCache already has a prefixed version of this search
                // then don't un-hide the existing exclusions
                if (0 !== keywords.indexOf(window.fSelectInit.searchCache)) {
                    wrap.querySelectorAll('.fs-option, .fs-optgroup-label').forEach((node) => node.classList.remove('fs-hidden'));
                }

                window.fSelectInit.searchCache = keywords;

                for (var i = 0; i < options.length; i++) {
                    if ('' === options[i].value) continue;

                    var needle = new RegExp(keywords, 'gi');
                    var haystack = unaccented(options[i].text);

                    if (null === haystack.match(needle)) {
                        wrap.querySelector('.fs-option[data-idx="' + i + '"]').classList.add('fs-hidden');
                    }
                }

                // hide optgroups if no choices
                wrap.querySelectorAll('.fs-optgroup-label').forEach((node) => {
                    var group = node.getAttribute('data-group');
                    var container = node.closest('.fs-options');
                    var count = container.querySelectorAll('.fs-option.g' + group + ':not(.fs-hidden)').length;

                    if (count < 1) {
                        node.classList.add('fs-hidden');
                    }
                });

                // toggle the noResultsText div
                if (wrap.querySelectorAll('.fs-option:not(.fs-hidden)').length) {
                    wrap.querySelector('.fs-no-results').classList.add('fs-hidden');
                }
                else {
                    wrap.querySelector('.fs-no-results').classList.remove('fs-hidden');
                }

            }, 100));

            that.on('click', optionSelector, function(e) {
                var wrap = this.closest('.fs-wrap');
                var value = this.getAttribute('data-value');
                var input = wrap._rel;
                var isMultiple = wrap.classList.contains('multiple');

                if (!isMultiple) {
                    input.value = value;
                    wrap.querySelectorAll('.fs-option.selected').forEach((node) => node.classList.remove('selected'));
                }
                else {
                    var idx = parseInt(this.getAttribute('data-idx'));
                    input.options[idx].selected = !this.classList.contains('selected');
                }

                this.classList.toggle('selected');

                var label = input.fselect.getDropdownLabel();
                wrap.querySelector('.fs-label').innerHTML = label;

                // fire a change event
                input.dispatchEvent(new Event('change', { bubbles: true }));
                input.fselect.trigger('fs:changed', wrap);

                if (!isMultiple) {
                    input.fselect.close();
                }

                e.stopImmediatePropagation();
            });

            that.on('keydown', '*', function(e) {
                var wrap = this.closest('.fs-wrap');

                if (!wrap) return;

                if (-1 < [38, 40, 27].indexOf(e.which)) {
                    e.preventDefault();
                }

                if (32 == e.which || 13 == e.which) { // space, enter
                    if (e.target.closest('.fs-search')) {
                        // preserve spaces for search
                    }
                    else if (e.target.matches(optionSelector)) {
                        e.target.click();
                        e.preventDefault();
                    }
                    else {
                        wrap.querySelector('.fs-label').click();
                        e.preventDefault();
                    }
                }
                else if (38 == e.which) { // up
                    var sibling = wrap._rel.fselect.getAdjacentSibling('previous');
                    window.fSelectInit.lastFocus = sibling; // stop at the search box

                    if (sibling) {
                        sibling.focus();
                    }
                    else {
                        wrap.querySelector('.fs-search input').focus();
                    }
                }
                else if (40 == e.which) { // down
                    var sibling = wrap._rel.fselect.getAdjacentSibling('next');

                    if (sibling) {
                        sibling.focus();
                        window.fSelectInit.lastFocus = sibling; // stop at the bottom
                    }
                }
                else if (9 == e.which || 27 == e.which) { // tab, esc
                    wrap._rel.fselect.close();
                }
            });

            that.on('click', '*', function(e) {
                var wrap = this.closest('.fs-wrap');
                var lastActive = window.fSelectInit.activeEl;

                if (wrap) {
                    var labelWrap = this.closest('.fs-label-wrap');

                    if (labelWrap) {
                        if (lastActive) {
                            lastActive._rel.fselect.close();
                        }
                        if (wrap !== lastActive) {
                            wrap._rel.fselect.open();
                        }
                    }
                }
                else {
                    if (lastActive) {
                        lastActive._rel.fselect.close();
                    }
                }
            });
        }
    }

    var $ = (selector, options) => new fSelect(selector, options);

    return $;

})();

if ('undefined' !== typeof fUtil) {
    fUtil.fn.fSelect = function(opts) {
        this.each(function() { // no arrow function to preserve "this"
            fSelect(this, opts);
        });
        return this;
    };
}
