// (C) Copyright 2020 Hewlett-Packard Enterprise Company, L.P.

define(['hp/core/Localizer', 'jquery', 'hp/lib/jquery.hpScrollParent'],
function(localizer) {
    (function($) {
        // jQuery plugin definition
        
        /**
         * Provides a searchable combo box control.
         *
         * Initialize with $(...).hpSearchCombo(args);
         *
         * @param {args} Object with properties as follows:
         *
         *  REQUIRED
         *    getResults: function that will be called when the drop down
         *      is made visible and when the user searches.
         *      function (string, handlers, count, start) {
         *          // generate results, can be asynchronous
         *          handlers.success({count: N, total: M, start: Q,
         *              members: [{id: X, name: Y}, ...]});
         *      }
         *      You can also add "help" and "error" properties to the returned
         *      results to provide additional annotations to the choices.
         *    OR
         *    results: same object signature as returned by getResults() above.
         *      Use this for smaller, fixed data sets.
         *  OPTIONAL
         *    fixedResults: same object signature as returned by getResults() above.
         *      Specifies particular results that should be made more prominent.
         *      It is up to the caller to filter these out of the results or getResults.
         *    nameProperty: results property name for the visible label
         *    valueProperty: results property name for the invisible value
         *    size: how wide to make the text input, similar to <input size=""/>
         *    width: how wide to make the text input, similar to <input style="width:"/>
         *
         * Subsequently set value with $(...).hpSearchCombo('set', {...});
         *    @param {action} 'set'
         *    @param {args} can be:
         *      an object mimicing a single result returned by getResults,
         *      a string value,
         *      omitted to clear
         */
        $.fn.hpSearchCombo = function(action, args) {
          
            var SELECT = '.hp-search-combo-select';
            var SELECT_OPTION = SELECT + ' option';
            var INPUT = '.hp-search-combo-input';
            var MENU = '.hp-search-combo-menu';
            var OPTIONS = MENU + ' .hp-options';
            var OPTION = OPTIONS + ' li';
            var FIXED_OPTIONS = MENU + ' .hp-fixed-options';
            var FIXED_OPTION = FIXED_OPTIONS + ' li';
            var HEADER = MENU + ' .hp-header';
            var FOOTER = MENU + ' .hp-footer';
            var CLEAR = '.hp-close';
            var CONTROL = '.hp-search-combo-control';
            var BODY = '#hp-body-div';
            var ACTIVE = 'hp-active';
            var SELECTED = 'hp-selected';
            var DISABLED = 'hp-disabled';
            var SPINNER_HTML = '<li class="hp-more"><div class="hp-spinner-small">' +
              '<div class="hp-spinner-image"></div></div></li>';
            var ENTER = 13;
            var ESCAPE = 27;
            var TAB = 9;
            var UP_ARROW = 38;
            var DOWN_ARROW = 40;
            var LEFT_ARROW = 37;
            var RIGHT_ARROW = 39;
            var PAGE_SIZE = 35;
            var instance; // keep Sonar happy
            
            function hpSearchCombo(elem) {
                
                var container;
                var valueProperty = 'id';
                var nameProperty = 'name';
                var getResults = null;      // function to get results
                var allResults = null;    // array or object of results
                var fixedResults = null;// array or object of results
                var doSearching = false;    // whether searching is performed internally
                var maxResults;             // how many results to put in the drop menu
                var moreResultsAvailable = false;
                var preOptionText = '';     // input text before selecting options
                var selectedOptionIndex = -1;
                var searchText;             // current search text
                var manualInput = false;   // track user input
                var controlClicked = false; // track control button click
                var spinner = $(SPINNER_HTML);
                
                function alignWithResultsFormat(results) {
                    if (results && $.isArray(results)) {
                        return {
                            count: results.length,
                            total: results.length,
                            start: 0,
                            members: results
                        };
                    } else {
                        return results;
                    }
                }
                
                function getSelection() {
                    var selectOption = $(SELECT_OPTION, container);
                    var result = null;
                    if (selectOption.length > 0) {
                        result = {name: selectOption.text()};
                        if (selectOption.attr('value')) {
                            result.value = selectOption.attr('value');
                        }
                    }
                    return result;
                }
                
                function setSelection(option, eventName) {
                    var prior = getSelection();
                    var optionElement;
                    eventName = eventName || 'change';
                    // reset manualInput if this method is not triggered by
                    // user's typing into the input field. 
                    manualInput = (eventName === 'input');
                    
                    $(INPUT, container).val(option.name);
                    if (option.name && option.name.length > 0) {
                        $(CLEAR, container).show();
                    } else {
                        $(CLEAR, container).hide();
                    }
                    
                    $(SELECT, container).empty();
                    
                    if (option.value) {
                        optionElement = $('<option></option>').
                            attr('value', option.value).
                            text(option.name);
                        $(SELECT, container).append(optionElement);
                        $(SELECT, container).val(option.value);
                        if (! prior || prior.value !== option.value) {
                            $(SELECT, container).trigger(eventName, option.value);
                        }
                    } else {
                        optionElement = $('<option></option>').
                            text(option.name);
                        $(SELECT, container).append(optionElement);
                        $(SELECT, container).val(option.name);
                        if (! prior || prior.name !== option.name) {
                            $(SELECT, container).trigger(eventName, option.name);
                        }
                    }
                }
                
                function onOptionClick(ev) {
                    var option = $(this);
                    setSelection({name: $('.hp-name', option).text(),
                        value: option.attr('data-id')});
                    hideMenu();
                    ev.preventDefault();
                }
                
                function hideMenu(ev) {
                    if (! ev || ev.target !== $(INPUT, container)[0]) {
                        $(BODY).off('click.hpSearchCombo', hideMenu);
                        $(container).removeClass(ACTIVE);
                        $(MENU, container).css('min-width', '');
                    }
                }
                
                function showMenu() {
                    if (! $(MENU, container).hasClass(ACTIVE)) {
                        $(container).addClass(ACTIVE);
                        $(MENU, container).css('min-width', $(INPUT, container).outerWidth());
                        // avoid bouncing
                        setTimeout(function () {
                            $(BODY).on('click.hpSearchCombo', hideMenu);
                        }, 50);
                        container.hpScrollParent($(MENU, container));
                    }
                }
                
                function showHeader(localizationId, args) {
                    var text = localizer.getString(localizationId, args);
                    $(HEADER, container).text(text).show();
                    showMenu();
                }
                
                function showFooter() {
                    if (controlClicked) {
                        var text = localizer.getString('search.searchCombo.searchReset');
                        $(FOOTER + ' a', container).text(text);
                        $(FOOTER, container).show();
                    }
                }
                
                function createOption(result) {
                    var option = $('<li></li>');
                    var content = $('<span></span>').addClass('hp-name');
                    var annotation;
                    option.append(content);
                    if (result.substring) {
                        option.attr('data-id', result);
                        content.text(result);
                    } else {
                        option.attr('data-id', result[valueProperty]);
                        content.text(result[nameProperty]);
                    }
                    if (result.help) {
                        annotation = $('<span></span>').addClass('hp-help').
                            text(result.help);
                        option.append(annotation);
                    }
                    if (result.error) {
                        annotation = $('<span></span>').addClass('hp-error').
                            text(result.error);
                        option.append(annotation);
                    }
                    return option;
                }
                
                function optionForSelectedIndex() {
                    var option;
                    if (selectedOptionIndex >= 0) {
                        if (fixedResults) {
                            if (selectedOptionIndex < fixedResults.count) {
                                option = $(FIXED_OPTION + ':eq('
                                    + selectedOptionIndex + ')', container);
                            } else {
                                option = $(OPTION + ':eq(' +
                                    (selectedOptionIndex - fixedResults.count) + ')', container);
                            }
                        } else {
                            option = $(OPTION + ':eq('
                                + selectedOptionIndex + ')', container);
                        }
                    }
                    return option;
                }
                
                function scrollToOption(option) {
                    var options = $(OPTIONS, container);
                    var top = option.position().top;
                    var bottom = top + option.outerHeight();
                    var scrollTop = options.scrollTop();
                    var height = options.outerHeight();
                    
                    if (top < 0) {
                        options.scrollTop(scrollTop + top);
                    } else if (bottom > height) {
                        options.scrollTop(scrollTop + (bottom - height));
                    }
                }
                
                function selectOptionForSelectedIndex() {
                    $(OPTION, container).removeClass(SELECTED);
                    $(FIXED_OPTION, container).removeClass(SELECTED);
                    var option = optionForSelectedIndex();
                    if (option) {
                        $(INPUT, container).val($('.hp-name', option).text());
                        option.addClass(SELECTED);
                        if (option.parent()[0] == $(OPTIONS, container)[0]) {
                            scrollToOption(option);
                        }
                    }
                }
                
                function searchResults(string, results) {
                    var searchedResults = results;
                    var regexp;
                    if (string && string.length > 0) {
                        regexp = new RegExp(string, 'i');
                        var newResults = 
                            $.grep(results.members, function(result, index) {
                                var name;
                                if (result.substring) {
                                    name = result;
                                } else {
                                    name = result[nameProperty];
                                }
                                return (name.match(regexp));
                            });
                        searchedResults = alignWithResultsFormat(newResults);
                    }
                    return searchedResults;
                }
                
                function messageForSearchedResults(results, count) {
                    if (count > 0) {
                        if (! results.total || count < results.total) {
                            showHeader('search.searchCombo.partialMatch',
                                [count, (results.total ? results.total : '?')]);
                        } else if (count > 1) {
                            showHeader('search.searchCombo.allMatches', [count]);
                        } else {
                            showHeader('search.searchCombo.singleMatch', [count]);
                            showFooter();
                        }
                    } else {
                        showHeader('search.searchCombo.noMatches');
                        showFooter();
                    }
                }
                
                function messageForAllResults(results, count) {
                    if (count > 0) {
                        if (! results.total || count < results.total) {
                            showHeader('search.searchCombo.partial',
                                [count, (results.total ? results.total : '?')]);
                        } else if (maxResults > PAGE_SIZE) {
                            // more than a page's worth, show all message
                            showHeader('search.searchCombo.all', [count]);
                        } else {
                            // no search text and showing all, no message needed
                            $(HEADER, container).empty().hide();
                        }
                    } else {
                        showHeader('search.searchCombo.none');
                    }
                }
                
                function messageForResults(results) {
                    var count = Math.min(results.start + results.count, maxResults);
                    $(FOOTER, container).hide();
                    if (searchText) {
                        messageForSearchedResults(results, count);
                    } else {
                        messageForAllResults(results, count);
                    }
                }
                
                function onResults(string, results) {
                    var option, length;
                    // are these are the latest results?
                    if (string === searchText) {
                        // convert array results to REST collection style
                        results = alignWithResultsFormat(results);
                        
                        if (doSearching) {
                            results = searchResults(string, results);
                        }
                        
                        spinner.remove();
                        messageForResults(results);
                        
                        if (! results.hasOwnProperty('start') || results.start === 0) {
                            // only empty if we're starting from the beginning,
                            // otherwise, we will append
                            $(OPTIONS, container).empty();
                        }
                        if (results.count > 0) {
                            length = Math.min(results.count, (maxResults - results.start));
                            for (var i=0; i<length; i++) {
                                option = createOption(results.members[i]);
                                $(OPTIONS, container).append(option);
                            }
                        }
                        
                        // preserve selection, if any
                        selectOptionForSelectedIndex();
                        
                        if ((results.start + results.count) > maxResults ||
                            results.total > maxResults) {
                            moreResultsAvailable = true;
                            $(OPTIONS, container).append(spinner);
                        } else {
                            moreResultsAvailable = false;
                        }
                        
                        container.hpScrollParent($(MENU, container));
                    }
                }
                
                function showOptions(string) {
                    if (! $(container).hasClass(ACTIVE) ||
                        string !== searchText) {
                        
                        $(FOOTER, container).hide();
                        $(HEADER, container).empty().hide();
                        $(OPTIONS, container).empty();
                        $(OPTIONS, container).append(spinner);
                        showMenu();
                        $(INPUT, container).focus();
                    
                        selectedOptionIndex = -1;
                        searchText = string;
                        maxResults = PAGE_SIZE;
                        moreResultsAvailable = false;
                    
                        if (getResults) {
                            getResults(string, {
                                success: function(results) {
                                    onResults(string, results);
                                }
                            }, maxResults, 0);
                        } else if (allResults) {
                            onResults(string, allResults);
                        }
                    }
                }
                
                function onScroll() {
                    var elem = $(OPTIONS, container);
                    // if we scroll to the bottom and there are more results, load them
                    if (elem.outerHeight() + elem.scrollTop() >= elem[0].scrollHeight &&
                        moreResultsAvailable) {
                        maxResults += PAGE_SIZE;
                        if (getResults) {
                            getResults(searchText, {
                                success: function(results) {
                                    onResults(searchText, results);
                                }
                            }, PAGE_SIZE, (maxResults - PAGE_SIZE));
                        } else if (allResults) {
                            onResults(searchText, allResults);
                        }
                    }
                }
                
                function onControlClick() {
                    if (!container.hasClass(DISABLED)) {
                        controlClicked = true;
                        $(INPUT, container).focus();
                    
                        if ($(container).hasClass(ACTIVE)) {
                            hideMenu();
                        } else {
                            showOptions($(INPUT, container).val());
                        }
                    }
                }
                
                function onClearClick() {
                    if (!container.hasClass(DISABLED)) {
                        // make sure the menu is hidden so showOptions will open it
                        hideMenu();
                        setSelection({name: ''});
                        if (!container.hasClass(DISABLED)) {
                            showOptions('');
                        }
                        $(INPUT, container).focus();
                    }
                }
                
                function onKeyDown(ev) {
                    var keyCode = (ev.which ? ev.which : ev.keyCode);
                    if (keyCode == ESCAPE || keyCode == TAB) {
                        hideMenu();
                    } else if (keyCode == ENTER) {
                        if (selectedOptionIndex >= 0) {
                            var li = $('ol li:eq(' + selectedOptionIndex + ')', container);
                            selectedOptionIndex = -1;
                            $(li).trigger('click');
                            ev.stopPropagation();
                        }
                        hideMenu();
                        ev.preventDefault();
                    } else if (keyCode == RIGHT_ARROW ||  keyCode == LEFT_ARROW) {
                        if (selectedOptionIndex >= 0) { 
                            $('ol li:eq(' + selectedOptionIndex + ')', container).
                            trigger('click');
                        }
                    } else if (keyCode == DOWN_ARROW || keyCode == UP_ARROW) {
                        ev.preventDefault();
                    }
                }
                
                function onDownArrow() {
                    manualInput = true;
                    controlClicked = false;
                    if (-1 === selectedOptionIndex) {
                        preOptionText = $(INPUT, container).val();
                    }
                    if (! $(container).hasClass(ACTIVE)) {
                        showOptions($(INPUT, container).val());
                    } else {
                        selectedOptionIndex += 1;
                        selectedOptionIndex = Math.min(selectedOptionIndex,
                            ($(OPTION, container).length +
                                $(FIXED_OPTION, container).length) - 1);
                        selectOptionForSelectedIndex();
                    }
                }
                
                function onUpArrow() {
                    manualInput = true;
                    controlClicked = false;
                    selectedOptionIndex -= 1;
                    if (selectedOptionIndex < 0) {
                        selectedOptionIndex = -1;
                        // go back to editing
                        $(OPTION, container).removeClass(SELECTED);
                        $(FIXED_OPTION, container).removeClass(SELECTED);
                        $(INPUT, container).val(preOptionText).focus();
                    } else {
                        selectOptionForSelectedIndex();
                    }
                }
                
                function onKeyUp(ev) {
                    var keyCode = (ev.which ? ev.which : ev.keyCode);
                    if (keyCode == ENTER || keyCode == ESCAPE || keyCode == TAB) {
                        // handled by keydown
                    } else if (keyCode == DOWN_ARROW) {
                        onDownArrow();
                    } else if (keyCode == UP_ARROW) {
                        onUpArrow();
                    } else {
                        if ($(INPUT, container).val().length > 0) {
                            $(CLEAR, container).show();
                        } else {
                            //empty the select option
                            if ($(SELECT, container).val() && $(SELECT, container).val().length > 0) {
                                $(SELECT, container).empty();
                            }
                            $(CLEAR, container).hide();
                        }
                        showOptions($(INPUT, container).val());
                    }
                }
                
                function updateSelectionValue(selection) {
                    // see if we have a value we can use from an option
                    $(OPTION + ' .hp-name', container).
                        each(function (index, elem) {
                            if (selection.name === $(elem).text()) {
                                selection.value = $(elem).parent().attr('data-id');
                                return false;
                            }
                        });
                }
                
                function onInput(ev) {
                    var name = $(INPUT, container).val();
                    manualInput = true;
                    controlClicked = false;
                    if (name.length > 0) {
                        $(CLEAR, container).show();
                        var selection = {name: name};
                        updateSelectionValue(selection);
                        setSelection(selection, 'input');
                    } else {
                        $(CLEAR, container).hide();
                    }
                    showOptions(name);
                }
                
                /*
                 * Evaluate if the value in the input field should be processed
                 * when the input lost focus.
                 * 
                 * The value should be processed if the input field is manually changed by
                 * the user (by typing or pasting), AND focus is not lost due to the user 
                 * clicking the control.
                 */
                function onBlur(ev) {
                    var name = $(INPUT, container).val();
                    var selection;
                    if (manualInput) {
                        if (controlClicked) {
                            controlClicked = false;
                        } else {
                            // if the user had manually typed or pasted into
                            // the input box, process the data 
                            selection = {name: name};
                            updateSelectionValue(selection);    
                            setSelection(selection, 'input');
                            if (selection.value) {
                                $(SELECT, container).trigger('change', selection.value);
                            } else {
                                $(SELECT, container).trigger('change', selection.name);
                            }
                        }
                    }
                }
                
                function destroy() {
                    hideMenu();
                    $(OPTIONS, container).off('.hpSearchCombo');
                    container.off('.hpSearchCombo');
                }
                
                function build() {
                    var selector = $(elem);
                    
                    if (selector.hasClass('hp-search-combo-select')) {
                        container = selector.parent();
                    } else {
                        selector.addClass('hp-search-combo-select');
                        selector.wrap('<div class="hp-search-combo"/>');
                        container = selector.parent();
                        selector.hide();

                        container.append('<input id="' + selector.attr('id') +
                            '-input" class="hp-search-combo-input"/>');
                        container.append('<div class="hp-close"></div>');
                        container.append('<div class="hp-search-combo-control"/>');
                        
                        if (selector.prop('disabled')) {
                            container.addClass(DISABLED);
                            $('input', container).attr('disabled', 'disabled');
                        }
                        
                        // IE workaround for not handling input padding
                        if ($('html').hasClass('ie8') ||
                            $('html').hasClass('ie9')) {
                            container.append('<div class="hp-search-combo-input-mask"></div>');
                        }
                        
                        // Turn off Firefox autocomplete
                        $('input', container).attr('autocomplete', 'off');
                        
                        container.append(
                            '<div class="hp-search-combo-menu">' +
                              '<ol class="hp-fixed-options"/>' +
                              '<div class="hp-header"/>' +
                              '<ol class="hp-options"/>' +
                              '<div class="hp-footer">' +
                                '<a></a>' +
                              '</div>' +
                            '</div>');
                    }
                }
                
                function setDimensions() {
                    if (args.size) {
                        $(INPUT, container).attr('size', args.size);
                    }
                    if (args.width) {
                        $(INPUT, container).css('width', args.width);
                    }
                }
                
                function initializeFromArgs() {
                    getResults = alignWithResultsFormat(args.getResults);
                    allResults = alignWithResultsFormat(args.results);
                    fixedResults = alignWithResultsFormat(args.fixedResults);
                    
                    if (! getResults && ! allResults && ! fixedResults) {
                        console.warn('no results provided');
                    }
                    
                    if (allResults && ! getResults) {
                        doSearching = true;
                    }
                
                    if (args.valueProperty) {
                        valueProperty = args.valueProperty;
                    }
                    if (args.nameProperty) {
                        nameProperty = args.nameProperty;
                    }
                    if (args.maxResults) {
                        maxResults = args.maxResults;
                    }
                    if (args.searchingMessage) {
                        searchingMessage = args.searchingMessage;
                    }
                    if (args.noMatchesMessage) {
                        noMatchesMessage = args.noMatchesMessage;
                    }
                    if (args.matchesMessage) {
                        matchesMessage = args.matchesMessage;
                    }
                    setDimensions();
                }
                
                function initializeAction() {
                    var length, option;
                    
                    build();
                    
                    if (! args) {
                        console.warn('no arguments specified');
                    } else {
                        initializeFromArgs();
                    }
                    
                    if (fixedResults) {
                        length = fixedResults.count;
                        for (var i=0; i<length; i++) {
                            option = createOption(fixedResults.members[i]);
                            $(FIXED_OPTIONS, container).append(option);
                        }
                    }
                    
                    $(CLEAR, container).hide();
                    $(HEADER, container).hide();
                
                    $(container).on('keyup.hpSearchCombo', INPUT, onKeyUp);
                    $(container).on('keydown.hpSearchCombo', INPUT, onKeyDown);
                    $(container).on('input.hpSearchCombo', INPUT, onInput);
                    // Modernizr can't detect oninput
                    if ($('html').hasClass('ie8')) {
                        $(container).on('paste.hpSearchCombo', INPUT, function () {
                            setTimeout(onInput, 10);
                        });
                    }
                    
                    // give onControlClick() a chance to process first before onBlur.
                    $(container).on('blur.hpSearchCombo', INPUT, function() {
                        setTimeout(onBlur, 200);
                    });
                    $(container).on('click.hpSearchCombo', CONTROL, onControlClick);
                    $(container).on('click.hpSearchCombo', CLEAR + ', ' + FOOTER, onClearClick);
                    $(container).on('click.hpSearchCombo', OPTION, onOptionClick);
                    $(container).on('click.hpSearchCombo', FIXED_OPTION, onOptionClick);
                    $(OPTIONS, container).on('scroll.hpSearchCombo', onScroll);
                }
                
                function setAction() {
                    var option = {name: ''};
                    container = $(elem).parent();
                    if (args) {
                        if ('string' === typeof(args)) {
                            option.name = args;
                        } else {
                            option.name = args[nameProperty];
                            option.value = args[valueProperty];
                        }
                    }
                    setSelection(option);
                    // close menu
                    if ($(container).hasClass(ACTIVE)) {
                        $(CONTROL, container).trigger('click');
                    }
                }

                function initialize() {
                  
                    if (! action || 'object' === typeof(action)) {
                        args = action;
                        action = 'initialize';
                    }
                    
                    if ('initialize' === action) {
                        initializeAction();
                    } else if ('destroy' === action) {
                        container = $(elem).parent();
                        destroy();
                    } else if ('disable' === action) {
                        container = $(elem).parent();
                        container.addClass(DISABLED);
                        $('input', container).attr('disabled', 'disabled');
                    } else if ('enable' === action) {
                        container = $(elem).parent();
                        container.removeClass(DISABLED);
                        $('input', container).removeAttr('disabled');
                    } else if ('set' === action) {
                        setAction();
                    }
                }
                
                initialize();
            }
            
            // pluginify
            var ret;
            this.each(function() {
                var $elem = $(this);
                instance = new hpSearchCombo($elem[0]);
                ret = ret ? ret.add($elem) : $elem;
            });
            return ret;
        };
    }(jQuery));
});
