/*
 * The MIT License
 *
 * Copyright (c) 2009-2023 PrimeTek Informatics
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.primefaces.component.autocomplete;

import java.io.IOException;
import java.util.*;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;
import javax.faces.event.PhaseId;

import org.primefaces.component.column.Column;
import org.primefaces.event.AutoCompleteEvent;
import org.primefaces.expression.SearchExpressionUtils;
import org.primefaces.renderkit.InputRenderer;
import org.primefaces.util.ComponentUtils;
import org.primefaces.util.HTML;
import org.primefaces.util.LangUtils;
import org.primefaces.util.WidgetBuilder;

public class AutoCompleteRenderer extends InputRenderer {

    @Override
    public void decode(FacesContext context, UIComponent component) {
        AutoComplete ac = (AutoComplete) component;
        String clientId = ac.getClientId(context);
        Map<String, String> params = context.getExternalContext().getRequestParameterMap();

        if (!shouldDecode(ac)) {
            return;
        }

        if (ac.isMultiple()) {
            decodeMultiple(context, ac);
        }
        else {
            decodeSingle(context, ac);
        }

        decodeBehaviors(context, ac);

        // AutoComplete event
        String query = params.get(clientId + "_query");
        if (query != null || ac.isClientCacheRequest(context)) {
            AutoCompleteEvent autoCompleteEvent = new AutoCompleteEvent(ac, query);
            autoCompleteEvent.setPhaseId(PhaseId.APPLY_REQUEST_VALUES);
            ac.queueEvent(autoCompleteEvent);
        }
    }

    protected void decodeSingle(FacesContext context, AutoComplete ac) {
        Map<String, String> params = context.getExternalContext().getRequestParameterMap();
        String clientId = ac.getClientId(context);
        String valueParam = (ac.getVar() != null) ? clientId + "_hinput" : clientId + "_input";
        String submittedValue = params.get(valueParam);

        if (submittedValue != null) {
            ac.setSubmittedValue(submittedValue);
        }
    }

    protected void decodeMultiple(FacesContext context, AutoComplete ac) {
        Map<String, String[]> paramValues = context.getExternalContext().getRequestParameterValuesMap();
        Map<String, String> params = context.getExternalContext().getRequestParameterMap();
        String clientId = ac.getClientId(context);
        String[] hinputValues = paramValues.get(clientId + "_hinput");
        String[] submittedValues = (hinputValues != null) ? hinputValues : new String[]{};
        String inputValue = params.get(clientId + "_input");

        if (!isValueBlank(inputValue)) {
            submittedValues = LangUtils.concat(submittedValues, new String[]{inputValue});
        }

        if (submittedValues.length > 0) {
            ac.setSubmittedValue(submittedValues);
        }
        else {
            ac.setSubmittedValue("");
        }
    }

    @Override
    public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
        AutoComplete ac = (AutoComplete) component;
        Map<String, String> params = context.getExternalContext().getRequestParameterMap();
        String query = params.get(ac.getClientId(context) + "_query");

        if ((ac.isClientQueryMode() || ac.isHybridQueryMode()) && !ac.isCache()) {
            ac.setCache(true);
        }

        if (query != null) {
            if (ac.isDynamicLoadRequest(context)) {
                encodePanel(context, ac);
            }
            else {
                encodeResults(context, component);
            }
        }
        else if (ac.isClientCacheRequest(context)) {
            encodeResults(context, ac);
        }
        else {
            encodeMarkup(context, ac);
            encodeScript(context, ac);
        }
    }

    @SuppressWarnings("unchecked")
    public void encodeResults(FacesContext context, UIComponent component) throws IOException {
        AutoComplete ac = (AutoComplete) component;
        Object results = ac.getSuggestions();
        int maxResults = ac.getMaxResults();

        if (ac.isServerQueryMode() && maxResults != Integer.MAX_VALUE && results != null && ((List) results).size() > maxResults) {
            results = ((List) results).subList(0, ac.getMaxResults());
        }

        encodeSuggestions(context, ac, results);
    }

    protected void encodeMarkup(FacesContext context, AutoComplete ac) throws IOException {
        if (ac.isMultiple()) {
            encodeMultipleMarkup(context, ac);
        }
        else {
            encodeSingleMarkup(context, ac);
        }
    }

    protected void encodeSingleMarkup(FacesContext context, AutoComplete ac) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        String clientId = ac.getClientId(context);
        boolean isDropdown = ac.isDropdown();
        boolean disabled = ac.isDisabled();

        String styleClass = getStyleClassBuilder(context)
                .add(AutoComplete.STYLE_CLASS)
                .add(ac.getStyleClass())
                .add(isDropdown, AutoComplete.DROPDOWN_SYLE_CLASS)
                .add(disabled, "ui-state-disabled")
                .build();

        writer.startElement("span", null);
        writer.writeAttribute("id", clientId, null);
        writer.writeAttribute("class", styleClass, null);

        if (ac.getStyle() != null) {
            writer.writeAttribute("style", ac.getStyle(), null);
        }

        encodeInput(context, ac, clientId);

        if (ac.getVar() != null) {
            encodeHiddenInput(context, ac, clientId);
        }

        if (isDropdown) {
            encodeDropDown(context, ac, clientId);
        }

        if (!ac.isDynamic()) {
            encodePanel(context, ac);
        }

        writer.endElement("span");
    }

    protected void encodeInput(FacesContext context, AutoComplete ac, String clientId) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        String var = ac.getVar();
        String itemLabel;
        String inputStyle = ac.getInputStyle();
        String defaultStyleClass = ac.isDropdown() ? AutoComplete.INPUT_WITH_DROPDOWN_CLASS : AutoComplete.INPUT_CLASS;
        String inputStyleClass = createStyleClass(ac, AutoComplete.PropertyKeys.inputStyleClass.name(), defaultStyleClass);
        String autocompleteProp = (ac.getAutocomplete() != null) ? ac.getAutocomplete() : "off";

        writer.startElement("input", null);
        writer.writeAttribute("id", clientId + "_input", null);
        writer.writeAttribute("name", clientId + "_input", null);
        writer.writeAttribute("type", ac.getType(), null);
        writer.writeAttribute("class", inputStyleClass, null);
        writer.writeAttribute("autocomplete", autocompleteProp, null);
        writer.writeAttribute(HTML.ARIA_ROLE, HTML.ARIA_ROLE_COMBOBOX, null);
        writer.writeAttribute(HTML.ARIA_CONTROLS, clientId + "_panel", null);
        writer.writeAttribute(HTML.ARIA_EXPANDED, "false", null);
        writer.writeAttribute(HTML.ARIA_HASPOPUP, "listbox", null);

        if (inputStyle != null) {
            writer.writeAttribute("style", inputStyle, null);
        }

        renderAccessibilityAttributes(context, ac);
        renderPassThruAttributes(context, ac, HTML.INPUT_TEXT_ATTRS_WITHOUT_EVENTS);
        renderDomEvents(context, ac, HTML.INPUT_TEXT_EVENTS);

        if (var == null) {
            itemLabel = ComponentUtils.getValueToRender(context, ac);

            if (itemLabel != null) {
                writer.writeAttribute("value", itemLabel, null);
            }
        }
        else {
            Map<String, Object> requestMap = context.getExternalContext().getRequestMap();

            if (ac.isValid()) {
                requestMap.put(var, ac.getValue());
                itemLabel = ac.getItemLabel();
            }
            else {
                Object submittedValue = ac.getSubmittedValue();

                Object value = ac.getValue();

                if (submittedValue == null && value != null) {
                    requestMap.put(var, value);
                    itemLabel = ac.getItemLabel();
                }
                else if (submittedValue != null) {
                    // retrieve the actual item (pojo) from the converter
                    try {
                        Object item = getConvertedValue(context, ac, String.valueOf(submittedValue));
                        requestMap.put(var, item);
                        itemLabel = ac.getItemLabel();
                    }
                    catch (ConverterException ce) {
                        itemLabel = String.valueOf(submittedValue);
                    }

                }
                else {
                    itemLabel = null;
                }

            }

            if (itemLabel != null) {
                writer.writeAttribute("value", itemLabel, null);
            }

            requestMap.remove(var);
        }

        renderValidationMetadata(context, ac);

        writer.endElement("input");
    }

    protected void encodeHiddenInput(FacesContext context, AutoComplete ac, String clientId) throws IOException {
        String valueToRender = ComponentUtils.getValueToRender(context, ac);
        renderHiddenInput(context, clientId + "_hinput", valueToRender, ac.isDisabled());
    }

    protected void encodeHiddenSelect(FacesContext context, AutoComplete ac, String clientId, List<String> values) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        String id = clientId + "_hinput";

        writer.startElement("select", null);
        writer.writeAttribute("id", id, null);
        writer.writeAttribute("name", id, null);
        writer.writeAttribute("multiple", "multiple", null);
        writer.writeAttribute("class", "ui-helper-hidden-accessible", null);
        writer.writeAttribute("tabindex", "-1", null);
        writer.writeAttribute(HTML.ARIA_HIDDEN, "true", null);

        if (ac.isDisabled()) {
            writer.writeAttribute("disabled", "disabled", "disabled");
        }

        renderValidationMetadata(context, ac);

        for (int i = 0; i < values.size(); i++) {
            String value = values.get(i);
            writer.startElement("option", null);
            writer.writeAttribute("value", value, null);
            writer.writeAttribute("selected", "selected", null);
            writer.endElement("option");
        }

        writer.endElement("select");
    }

    protected void encodeDropDown(FacesContext context, AutoComplete ac, String clientId) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        String dropdownClass = AutoComplete.DROPDOWN_CLASS;
        boolean disabled = ac.isDisabled() || ac.isReadonly();
        if (disabled) {
            dropdownClass += " ui-state-disabled";
        }

        writer.startElement("button", ac);
        writer.writeAttribute("id", clientId + "_button", null);
        writer.writeAttribute("class", dropdownClass, null);
        writer.writeAttribute("type", "button", null);
        if (disabled) {
            writer.writeAttribute("disabled", "disabled", null);
        }
        if (ac.getDropdownTabindex() != null) {
            writer.writeAttribute("tabindex", ac.getDropdownTabindex(), null);
        }
        else if (ac.getTabindex() != null) {
            writer.writeAttribute("tabindex", ac.getTabindex(), null);
        }

        writer.startElement("span", null);
        writer.writeAttribute("class", "ui-button-icon-primary ui-icon ui-icon-triangle-1-s", null);
        writer.endElement("span");

        writer.startElement("span", null);
        writer.writeAttribute("class", "ui-button-text", null);
        writer.write("&nbsp;");
        writer.endElement("span");

        writer.endElement("button");
    }

    protected void encodePanel(FacesContext context, AutoComplete ac) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        String styleClass = ac.getPanelStyleClass();
        styleClass = styleClass == null ? AutoComplete.PANEL_CLASS : AutoComplete.PANEL_CLASS + " " + styleClass;

        writer.startElement("span", null);
        writer.writeAttribute("id", ac.getClientId(context) + "_panel", null);
        writer.writeAttribute("class", styleClass, null);
        writer.writeAttribute("tabindex", "-1", null);

        if (ac.getPanelStyle() != null) {
            writer.writeAttribute("style", ac.getPanelStyle(), null);
        }

        if (ac.isDynamic() && ac.isDynamicLoadRequest(context)) {
            encodeResults(context, ac);
        }

        writer.endElement("span");
    }

    protected void encodeMultipleMarkup(FacesContext context, AutoComplete ac) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        String clientId = ac.getClientId(context);
        String inputId = clientId + "_input";

        List values;
        if (ac.isValid()) {
            values = (List) ac.getValue();
        }
        else {
            Object submittedValue = ac.getSubmittedValue();
            try {
                values = (List) getConvertedValue(context, ac, submittedValue);
            }
            catch (ConverterException ce) {
                values = Arrays.asList((String[]) submittedValue);
            }
        }

        List<String> stringValues = new ArrayList<>();
        boolean disabled = ac.isDisabled();
        boolean isDropdown = ac.isDropdown();
        String title = ac.getTitle();

        String style = ac.getStyle();

        String styleClass = getStyleClassBuilder(context)
                .add(AutoComplete.MULTIPLE_STYLE_CLASS)
                .add(ac.getStyleClass())
                .add(isDropdown, AutoComplete.DROPDOWN_SYLE_CLASS)
                .add(disabled, "ui-state-disabled")
                .build();

        String listClass = isDropdown ? AutoComplete.MULTIPLE_CONTAINER_WITH_DROPDOWN_CLASS : AutoComplete.MULTIPLE_CONTAINER_CLASS;
        listClass = createStyleClass(ac, null, listClass);
        String autocompleteProp = (ac.getAutocomplete() != null) ? ac.getAutocomplete() : "off";

        writer.startElement("div", null);
        writer.writeAttribute("id", clientId, null);
        writer.writeAttribute("class", styleClass, null);
        if (style != null) {
            writer.writeAttribute("style", style, null);
        }
        if (title != null) {
            writer.writeAttribute("title", title, null);
        }

        writer.startElement("ul", null);
        writer.writeAttribute("class", listClass, null);
        writer.writeAttribute("tabindex", -1, null);
        writer.writeAttribute(HTML.ARIA_ROLE, HTML.ARIA_ROLE_LISTBOX, null);
        writer.writeAttribute(HTML.ARIA_ORIENTATION, HTML.ARIA_ORIENTATION_HORIZONTAL, null);

        if (values != null && !values.isEmpty()) {
            Converter converter = ComponentUtils.getConverter(context, ac);
            String var = ac.getVar();
            boolean pojo = var != null;

            Collection<Object> items = ac.isUnique() ? new LinkedHashSet<>(values) : values;
            for (Object value : items) {
                Object itemValue = null;
                String itemLabel = null;

                if (pojo) {
                    context.getExternalContext().getRequestMap().put(var, value);
                    itemValue = ac.getItemValue();
                    itemLabel = ac.getItemLabel();
                }
                else {
                    itemValue = value;
                    itemLabel = String.valueOf(value);
                }

                String tokenValue = converter != null ? converter.getAsString(context, ac, itemValue) : String.valueOf(itemValue);
                String itemStyleClass = AutoComplete.TOKEN_DISPLAY_CLASS;
                if (ac.getItemStyleClass() != null) {
                    itemStyleClass += " " + ac.getItemStyleClass();
                }

                writer.startElement("li", null);
                writer.writeAttribute("data-token-value", tokenValue, null);
                writer.writeAttribute("class", itemStyleClass, null);
                writer.writeAttribute(HTML.ARIA_ROLE, HTML.ARIA_ROLE_OPTION, null);
                writer.writeAttribute(HTML.ARIA_LABEL, itemLabel, null);
                writer.writeAttribute(HTML.ARIA_SELECTED, "true", null);

                String labelClass = disabled ? AutoComplete.TOKEN_LABEL_DISABLED_CLASS : AutoComplete.TOKEN_LABEL_CLASS;
                writer.startElement("span", null);
                writer.writeAttribute("class", labelClass, null);
                writer.writeText(itemLabel, null);
                writer.endElement("span");

                if (!disabled) {
                    writer.startElement("span", null);
                    writer.writeAttribute("class", AutoComplete.TOKEN_ICON_CLASS, null);
                    writer.writeAttribute(HTML.ARIA_HIDDEN, "true", null);
                    writer.endElement("span");
                }

                writer.endElement("li");

                stringValues.add(tokenValue);
            }
        }

        writer.startElement("li", null);
        writer.writeAttribute("class", AutoComplete.TOKEN_INPUT_CLASS, null);
        writer.startElement("input", null);
        writer.writeAttribute("type", "text", null);
        writer.writeAttribute("id", inputId, null);
        writer.writeAttribute("name", inputId, null);
        writer.writeAttribute("autocomplete", autocompleteProp, null);

        renderAccessibilityAttributes(context, ac);
        writer.writeAttribute(HTML.ARIA_ROLE, HTML.ARIA_ROLE_COMBOBOX, null);
        writer.writeAttribute(HTML.ARIA_CONTROLS, clientId + "_panel", null);
        writer.writeAttribute(HTML.ARIA_EXPANDED, "false", null);
        writer.writeAttribute(HTML.ARIA_HASPOPUP, "listbox", null);

        renderPassThruAttributes(context, ac, HTML.INPUT_TEXT_ATTRS_WITHOUT_EVENTS);
        renderDomEvents(context, ac, HTML.INPUT_TEXT_EVENTS);

        writer.endElement("input");
        writer.endElement("li");

        writer.endElement("ul");

        if (ac.isDropdown()) {
            encodeDropDown(context, ac, clientId);
        }

        if (!ac.isDynamic()) {
            encodePanel(context, ac);
        }

        encodeHiddenSelect(context, ac, clientId, stringValues);

        writer.endElement("div");
    }

    protected void encodeSuggestions(FacesContext context, AutoComplete ac, Object items) throws IOException {
        boolean customContent = !ac.getColums().isEmpty();
        Converter converter = ComponentUtils.getConverter(context, ac);

        if (customContent) {
            encodeSuggestionsAsTable(context, ac, items, converter);
        }
        else {
            encodeSuggestionsAsList(context, ac, items, converter);
        }

        encodeFooter(context, ac);
    }

    protected void encodeFooter(FacesContext context, AutoComplete ac) throws IOException {
        UIComponent footer = ac.getFacet("footer");
        if (ComponentUtils.shouldRenderFacet(footer)) {
            ResponseWriter writer = context.getResponseWriter();
            writer.startElement("div", null);
            writer.writeAttribute("class", "ui-autocomplete-footer", null);
            footer.encodeAll(context);
            writer.endElement("div");
        }
    }

    protected void encodeSuggestionsAsTable(FacesContext context, AutoComplete ac, Object items, Converter converter) throws IOException {
        // do not render table if empty message and there are no records
        if (items == null || ((Collection) items).isEmpty()) {
            return;
        }
        ResponseWriter writer = context.getResponseWriter();
        String var = ac.getVar();
        boolean pojo = var != null;
        boolean hasHeader = false;

        for (int i = 0; i < ac.getColums().size(); i++) {
            Column column = ac.getColums().get(i);
            if (column.isRendered() && (column.getHeaderText() != null || ComponentUtils.shouldRenderFacet(column.getFacet("header")))) {
                hasHeader = true;
                break;
            }
        }

        writer.startElement("table", ac);
        writer.writeAttribute("class", AutoComplete.TABLE_CLASS, null);

        if (hasHeader) {
            writer.startElement("thead", ac);
            for (int i = 0; i < ac.getColums().size(); i++) {
                Column column = ac.getColums().get(i);
                if (!column.isRendered()) {
                    continue;
                }

                String headerText = column.getHeaderText();
                UIComponent headerFacet = column.getFacet("header");
                String styleClass = column.getStyleClass() == null ? "ui-state-default" : "ui-state-default " + column.getStyleClass();

                writer.startElement("th", null);
                writer.writeAttribute("class", styleClass, null);

                if (column.getStyle() != null) {
                    writer.writeAttribute("style", column.getStyle(), null);
                }

                if (ComponentUtils.shouldRenderFacet(headerFacet)) {
                    headerFacet.encodeAll(context);
                }
                else if (headerText != null) {
                    writer.writeText(headerText, null);
                }

                writer.endElement("th");
            }
            writer.endElement("thead");
        }

        writer.startElement("tbody", ac);

        if (items != null) {
            int index = 0;
            if (ac.isClientQueryMode() || items instanceof Map) {
                for (Map.Entry<String, List<String>> entry : ((Map<String, List<String>>) items).entrySet()) {
                    String key = entry.getKey();
                    List<String> list = entry.getValue();

                    for (Object item : list) {
                        encodeSuggestionItemsAsTable(context, ac, item, converter, pojo, var, key, index++);
                    }
                }
            }
            else {
                for (Object item : (List) items) {
                    encodeSuggestionItemsAsTable(context, ac, item, converter, pojo, var, null, index++);
                }

                if (ac.hasMoreSuggestions()) {
                    encodeMoreText(context, ac);
                }
            }
        }

        writer.endElement("tbody");
        writer.endElement("table");
    }

    protected void encodeSuggestionsAsList(FacesContext context, AutoComplete ac, Object items, Converter converter) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        String var = ac.getVar();
        Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
        boolean pojo = var != null;

        writer.startElement("ul", ac);
        writer.writeAttribute("class", AutoComplete.LIST_CLASS, null);
        writer.writeAttribute(HTML.ARIA_ROLE, HTML.ARIA_ROLE_LISTBOX, null);

        if (items != null) {
            int index = 0;
            if (ac.isClientQueryMode() || items instanceof Map) {
                for (Map.Entry<String, List<String>> entry : ((Map<String, List<String>>) items).entrySet()) {
                    String key = entry.getKey();
                    List<String> list = entry.getValue();

                    for (Object item : list) {
                        encodeSuggestionItemsAsList(context, ac, item, converter, pojo, var, key, index++);
                    }
                }
            }
            else {
                for (Object item : (List) items) {
                    encodeSuggestionItemsAsList(context, ac, item, converter, pojo, var, null, index++);
                }

                if (ac.hasMoreSuggestions()) {
                    encodeMoreText(context, ac);
                }
            }
        }

        writer.endElement("ul");

        if (pojo) {
            requestMap.remove(var);
        }
    }

    protected void encodeSuggestionItemsAsList(FacesContext context, AutoComplete ac, Object item, Converter converter,
            boolean pojo, String var, String key, int rowNumber) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
        UIComponent itemtip = ac.getFacet("itemtip");
        boolean hasGroupByTooltip = (ac.getValueExpression(AutoComplete.PropertyKeys.groupByTooltip.toString()) != null);

        writer.startElement("li", null);
        writer.writeAttribute("id", ac.getClientId(context) + "_item_" + rowNumber, null);
        writer.writeAttribute("class", AutoComplete.ITEM_CLASS, null);

        if (pojo) {
            requestMap.put(var, item);
            String value = converter == null ? String.valueOf(ac.getItemValue()) : converter.getAsString(context, ac, ac.getItemValue());
            String itemLabel = ac.getItemLabel();
            writer.writeAttribute("data-item-value", value, null);
            writer.writeAttribute("data-item-label", itemLabel, null);
            writer.writeAttribute("data-item-class", ac.getItemStyleClass(), null);
            writer.writeAttribute("data-item-group", ac.getGroupBy(), null);

            if (key != null) {
                writer.writeAttribute("data-item-key", key, null);
            }

            if (hasGroupByTooltip) {
                writer.writeAttribute("data-item-group-tooltip", ac.getGroupByTooltip(), null);
            }

            if (ac.isEscape()) {
                writer.writeText(itemLabel, null);
            }
            else {
                writer.write(itemLabel);
            }
        }
        else {
            String itemAsString = item.toString();
            writer.writeAttribute("data-item-label", itemAsString, null);
            writer.writeAttribute("data-item-value", itemAsString, null);
            writer.writeAttribute("data-item-class", ac.getItemStyleClass(), null);

            if (key != null) {
                writer.writeAttribute("data-item-key", key, null);
            }

            if (ac.isEscape()) {
                writer.writeText(item, null);
            }
            else {
                writer.write(itemAsString);
            }
        }

        writer.endElement("li");

        if (ComponentUtils.shouldRenderFacet(itemtip)) {
            writer.startElement("li", null);
            writer.writeAttribute("class", AutoComplete.ITEMTIP_CONTENT_CLASS, null);
            itemtip.encodeAll(context);
            writer.endElement("li");
        }
    }

    protected void encodeSuggestionItemsAsTable(FacesContext context, AutoComplete ac, Object item, Converter converter,
            boolean pojo, String var, String key, int index) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
        UIComponent itemtip = ac.getFacet("itemtip");
        boolean hasGroupByTooltip = (ac.getValueExpression(AutoComplete.PropertyKeys.groupByTooltip.toString()) != null);

        writer.startElement("tr", null);
        writer.writeAttribute("id", ac.getClientId(context) + "_item_" + index, null);
        writer.writeAttribute("class", AutoComplete.ROW_CLASS, null);

        if (pojo) {
            requestMap.put(var, item);
            String value = converter == null ? String.valueOf(ac.getItemValue()) : converter.getAsString(context, ac, ac.getItemValue());
            writer.writeAttribute("data-item-value", value, null);
            writer.writeAttribute("data-item-label", ac.getItemLabel(), null);
            writer.writeAttribute("data-item-class", ac.getItemStyleClass(), null);
            writer.writeAttribute("data-item-group", ac.getGroupBy(), null);

            if (key != null) {
                writer.writeAttribute("data-item-key", key, null);
            }

            if (hasGroupByTooltip) {
                writer.writeAttribute("data-item-group-tooltip", ac.getGroupByTooltip(), null);
            }
        }

        for (int i = 0; i < ac.getColums().size(); i++) {
            Column column = ac.getColums().get(i);
            if (column.isRendered()) {
                writer.startElement("td", null);
                if (key != null) {
                    writer.writeAttribute("data-item-key", key, null);
                }
                if (column.getStyle() != null) {
                    writer.writeAttribute("style", column.getStyle(), null);
                }
                if (column.getStyleClass() != null) {
                    writer.writeAttribute("class", column.getStyleClass(), null);
                }

                encodeIndexedId(context, column, index);

                writer.endElement("td");
            }
        }

        if (ComponentUtils.shouldRenderFacet(itemtip)) {
            writer.startElement("td", null);
            writer.writeAttribute("class", AutoComplete.ITEMTIP_CONTENT_CLASS, null);
            encodeIndexedId(context, itemtip, index);
            writer.endElement("td");
        }

        writer.endElement("tr");
    }

    protected void encodeScript(FacesContext context, AutoComplete ac) throws IOException {
        WidgetBuilder wb = getWidgetBuilder(context);
        wb.init("AutoComplete", ac);

        wb.attr("minLength", ac.getMinQueryLength(), 1)
                .attr("delay", ac.getQueryDelay())
                .attr("forceSelection", ac.isForceSelection(), false)
                .attr("scrollHeight", ac.getScrollHeight(), Integer.MAX_VALUE)
                .attr("multiple", ac.isMultiple(), false)
                .attr("appendTo", SearchExpressionUtils.resolveOptionalClientIdForClientSide(context, ac, ac.getAppendTo()))
                .attr("grouping", ac.getValueExpression(AutoComplete.PropertyKeys.groupBy.toString()) != null, false)
                .attr("queryEvent", ac.getQueryEvent(), null)
                .attr("dropdownMode", ac.getDropdownMode(), null)
                .attr("autoHighlight", ac.isAutoHighlight(), true)
                .attr("myPos", ac.getMy(), null)
                .attr("atPos", ac.getAt(), null)
                .attr("active", ac.isActive(), true)
                .attr("unique", ac.isUnique(), false)
                .attr("dynamic", ac.isDynamic(), false)
                .attr("autoSelection", ac.isAutoSelection(), true)
                .attr("escape", ac.isEscape(), true)
                .attr("queryMode", ac.getQueryMode())
                .attr("completeEndpoint", ac.getCompleteEndpoint())
                .attr("moreText", ac.getMoreText())
                .attr("hasFooter", ComponentUtils.shouldRenderFacet(ac.getFacet("footer")));

        if (ac.isCache()) {
            wb.attr("cache", true).attr("cacheTimeout", ac.getCacheTimeout());
        }

        if (ComponentUtils.shouldRenderFacet(ac.getFacet("itemtip"))) {
            wb.attr("itemtip", true, false)
                    .attr("itemtipMyPosition", ac.getItemtipMyPosition(), null)
                    .attr("itemtipAtPosition", ac.getItemtipAtPosition(), null);
        }

        if (ac.isMultiple()) {
            wb.attr("selectLimit", ac.getSelectLimit(), Integer.MAX_VALUE);
        }

        encodeClientBehaviors(context, ac);

        wb.finish();
    }

    @Override
    public Object getConvertedValue(FacesContext context, UIComponent component, Object submittedValue) throws ConverterException {
        AutoComplete ac = (AutoComplete) component;
        boolean isMultiple = ac.isMultiple();

        if (submittedValue == null || submittedValue.equals("") || ac.isMoreTextRequest(context)) {
            return isMultiple ? new ArrayList() : null;
        }

        Converter converter = ComponentUtils.getConverter(context, component);

        if (isMultiple) {
            String[] values = (String[]) submittedValue;
            List list = new ArrayList();

            for (String value : values) {
                if (isValueBlank(value)) {
                    continue;
                }

                Object convertedValue = converter != null ? converter.getAsObject(context, ac, value) : value;

                if (convertedValue != null) {
                    list.add(convertedValue);
                }
            }

            return list;
        }
        else {
            if (converter != null) {
                return converter.getAsObject(context, component, (String) submittedValue);
            }
            else {
                return submittedValue;
            }
        }
    }

    @Override
    public void encodeChildren(FacesContext context, UIComponent component) throws IOException {
        // Rendering happens on encodeEnd
    }

    @Override
    public boolean getRendersChildren() {
        return true;
    }

    public void encodeMoreText(FacesContext context, AutoComplete ac) throws IOException {
        int colSize = ac.getColums().size();
        String moreText = ac.getMoreText();
        ResponseWriter writer = context.getResponseWriter();

        if (colSize > 0) {
            writer.startElement("tr", null);
            writer.writeAttribute("class", AutoComplete.MORE_TEXT_TABLE_CLASS, null);

            writer.startElement("td", null);
            writer.writeAttribute("colspan", colSize, null);
            writer.writeText(moreText, "moreText");
            writer.endElement("td");

            writer.endElement("tr");
        }
        else {
            writer.startElement("li", null);
            writer.writeAttribute("class", AutoComplete.MORE_TEXT_LIST_CLASS, null);
            writer.writeText(moreText, "moreText");
            writer.endElement("li");
        }
    }
}
