/**
 * Enables a specified input field and hidden input field to function as a dropdown picklist with autocomplete search.
 * @param {string} getResultsFunction The name of the VisualForce remote function used to get autocomplete results.
 * @param {string} hiddenFieldId - The Id of the hidden input field.
 *                      This field should be mapped to the record being searched for.
 * @param {string} displayFieldId - The Id of the input field to use as a display.
 *                       Will contain human-readable content regarding the record being searched for.
 * @param {string} configName - The name of the autocomplete configuration record.
 * @param {number} minimumSearchLength - The minimum number of characters a user must type before a search is performed.
 * @param {string} noneFoundLabel - The text to display to the user if no results are found for their search
 *                       when they try to submit the form.
 * @param {string} tooManyLabel - The text to display to the user if too many (i.e. more than one) results are found
 *                     for their search when they try to submit the form.
 **/
function autoCompletePicklistSearch(getResultsFunction,
                hiddenFieldId,
                displayFieldId,
                configName,
                minimumSearchLength,
                noneFoundLabel,
                tooManyLabel) {
    if (!configName) {
        return;
    }

    var FIELD_NOT_VALID_CSS_CLASS = 'fieldNotValid';

    var queryTerm;
    var valueElement = $(document.getElementById(hiddenFieldId));
    var displayElement = $(document.getElementById(displayFieldId));

    /**
     * Returns a global, case-insensitive regex that will match the specified string.
     *
     * @param {string} str - The string that the regex should match.
     * @return {RegExp} A global, case-insensitive regex that matches the specified string.
     */
    function getRegex(str) {
        return new RegExp(str.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&"), 'gi');
    }

    /**
     * Bolds all matches of the specified regex in the specified string.
     * @param {RegExp} regex - The regex for which matches should be bolded.
     * @param {string} str - The string in which matches should be bolded.
     * @return {Node[]} An array of nodes that are either text nodes (for text that was not matched), or
     *  <strong> elements containing text nodes (for text that was matched).
     */
    function boldMatches(regex, str) {
        var index = 0;
        var result = $("<span>"); // temporary container node
        var match;
        while ((match = regex.exec(str)) !== null) {
            // add text between the end of the last match and the start of this one
            if (match.index > index) {
                result.append(document.createTextNode(str.substring(index, match.index)));
            }
            // bold text for the match
            result.append($('<strong>').text(match));
            index = regex.lastIndex;
        }
        // add text after the end of the last match
        result.append(document.createTextNode(str.substring(index)));
        return result.contents();
    }

    /**
     * Converts an autocomplete result from Salesforce to a format usable by jQuery UI Autocomplete.
     *
     * @param {Object} result - The result returned from Salesforce.
     * @return {Object} The converted result suitable for use with jQuery UI Autocomplete.
     */
    function mapResult(result) {
        return {
            label: result.Label,
            value: result.Label,
            id: result.Id,
            detail: result.Detail
        };
    }

    /**
     * Makes the VisualForce remote call to find the results of a provided search term.
     *
     * @param {string} searchTerm - The string to search against all specified fields.
     * @param {resultHandlerCallBack} resultHandler - A method to asynchronously handle the resulting array of records with.
     * @return {Object[]} The resulting array of matching records.
     */
    function findRecords(searchTerm, resultHandler) {
        Visualforce.remoting.Manager.invokeAction(
            getResultsFunction,
            configName,
            searchTerm,
            resultHandler,
            { escape: false }
        );
    }

    /**
     * A resultHandler for findRecords. Sets the display and/or hidden value field based on the results.
     *  This means both setting the value and adding/removing formatting to indicate the field value is invalid.
     *  Additionally this method will auto-complete the display element when only one record is retrieved.
     *
     * @callback findRecords~resultHandler
     * @param {Object[]} result - The array of result records to handle.
     * @param {Object} event - An object containing details of the remoting call including success, failure etc.
     */
    function setDisplaysBasedOnRecordsFound(result, event) {
        if (event.status) {
            if (result.length === 0) {
                markDisplayFieldAsInvalid(noneFoundLabel);
            } else if (result.length > 1) {
                markDisplayFieldAsInvalid(tooManyLabel);
            } else {
                setFields(mapResult(result[0]));
            }
        } else {
            markDisplayFieldAsInvalid(noneFoundLabel);
        }
    }

    /**
     * Marks the display field as invalid. Applies a red border and a provided label to be displayed on submission.
     *  Also zeroes the hidden element to ensure an invalid/outdated value is not submitted.
     *
     * @param {string} label - The label text to display on submission.
     */
    function markDisplayFieldAsInvalid(label) {
        displayElement.each(function() {
            this.classList.add(FIELD_NOT_VALID_CSS_CLASS);
            this.setCustomValidity(label);
        });

        valueElement.val('');
    }

    /**
     * Removes the invalid formatting and any validation labels from the display field.
     */
    function markDisplayFieldAsValid() {
        displayElement.each(function() {
            this.classList.remove(FIELD_NOT_VALID_CSS_CLASS);
            this.setCustomValidity('');
        });
    }

    /**
     * Sets the display and hidden elements based on a provided record.
     *  Also removes any field-not-valid formatting from the display field.
     *
     * @param {Object} item - The record to set our fields based on.
     */
    function setFields(item) {
        markDisplayFieldAsValid();

        valueElement.val(item.id);
        displayElement.val(item.value);
    }

    /**
     * Reacts to updates in the display element by clearing the hidden field if the display element is empty
     *  or looking up the current query string and updating the hidden field
     *  (as well as imposing validation) as needed.
     * @param {Object} ui - The ui object from the jQuery UI Autocomplete change event.
     */
    function reactToFieldUpdates(ui) {
        if (!displayElement.val()) {
            valueElement.val('');
            markDisplayFieldAsValid();
        } else if (!ui.item) {
            findRecords(displayElement.val(), setDisplaysBasedOnRecordsFound);
        } else {
            markDisplayFieldAsValid();
        }
    }

    displayElement.autocomplete({
        minLength : minimumSearchLength,
        source : function(request, response) {
            queryTerm = request.term;
            findRecords(queryTerm, function(result, event) {
                if (event.status) {
                    response(result.map(mapResult));
                } else {
                    response([]);
                }
            });
        },
        select : function(event, ui) {
            valueElement.val(ui.item.id);
            markDisplayFieldAsValid();
        },
        change : function(event, ui) {
            reactToFieldUpdates(ui);
        }
    }).keydown(function(event) {
        if (event.keyCode === 13) {
            event.preventDefault();
            displayElement.blur();
        }
    }).focus(function() {
        $(this).autocomplete('search', $(this).val());
    }).data('uiAutocomplete')._renderItem = function(ul, item) {
        var regex = getRegex(queryTerm);
        var entry = $('<a>').append(boldMatches(regex, item.label));

        if (item.detail) {
            $.each(item.detail, function(key, value) {
                if (value !== item.label) {
                    entry.append('<br>', $('<small>').append(boldMatches(regex, value)));
                }
            });
        }

        return $('<li></li>')
            .data('item.autocomplete', item)
            .append(entry)
            .appendTo(ul);
    };
}
