/*
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 *
 * The Apereo Foundation licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
*/
package org.unitime.timetable.gwt.client.widgets;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import org.unitime.timetable.gwt.client.aria.AriaStatus;
import org.unitime.timetable.gwt.client.aria.AriaSuggestBox;
import org.unitime.timetable.gwt.client.aria.AriaTextBox;
import org.unitime.timetable.gwt.client.aria.HasAriaLabel;
import org.unitime.timetable.gwt.resources.GwtAriaMessages;
import org.unitime.timetable.gwt.resources.GwtMessages;
import org.unitime.timetable.gwt.resources.GwtResources;

import com.google.gwt.aria.client.AutocompleteValue;
import com.google.gwt.aria.client.Id;
import com.google.gwt.aria.client.Roles;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.core.shared.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.HasAllFocusHandlers;
import com.google.gwt.event.dom.client.HasAllKeyHandlers;
import com.google.gwt.event.dom.client.HasBlurHandlers;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.event.dom.client.HasFocusHandlers;
import com.google.gwt.event.dom.client.HasKeyDownHandlers;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.logical.shared.HasSelectionHandlers;
import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.TakesValue;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.IsSerializable;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Focusable;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasEnabled;
import com.google.gwt.user.client.ui.HasText;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.MenuBar;
import com.google.gwt.user.client.ui.MenuItem;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.SuggestOracle;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.ValueBoxBase;
import com.google.gwt.user.client.ui.Widget;

/**
 * @author Tomas Muller
 */
public class FilterBox extends AbsolutePanel implements HasValue<String>, HasValueChangeHandlers<String>, HasText, Focusable, HasAllKeyHandlers, HasAllFocusHandlers, HasAriaLabel, HasEnabled, HasSelectionHandlers<FilterBox.Suggestion> {
	protected static GwtAriaMessages ARIA = GWT.create(GwtAriaMessages.class);
	protected static GwtResources RESOURCES = GWT.create(GwtResources.class);
	protected static GwtMessages MESSAGES = GWT.create(GwtMessages.class);
	protected static String[] sColors = new String[] {
		"blue", "green", "orange", "yellow", "pink",
		"purple", "teal", "darkpurple", "steelblue", "lightblue",
		"lightgreen", "yellowgreen", "redorange", "lightbrown", "lightpurple",
		"grey", "bluegrey", "lightteal", "yellowgrey", "brown"
	};
	
	protected AriaTextBox iFilter;
	protected PopupPanelKeepFocus iFilterPopup, iSuggestionsPopup;
	protected boolean iFocus = false;
	protected BlurHandler iBlurHandler;
	protected FocusHandler iFocusHandler;
	protected SuggestionsProvider iSuggestionsProvider = new DefaultSuggestionsProvider(null);
	protected SuggestionMenu iSuggestionMenu;
	
	protected Parser iParser = new DefaultParser();
	protected Chip2Color iChip2Color = new DefaultChip2Color();
	protected List<Filter> iFilters = new ArrayList<Filter>();
	protected Focusable iLastFocusedWidget = null;
	protected Image iFilterOpen, iFilterClose, iFilterClear;
	protected HandlerRegistration iResizeHandler;
	
	protected TakesValue<String> iDefaultValueProvider = null;
	
	protected boolean iShowSuggestionsOnFocus = false;
	
	public FilterBox() {
		setStyleName("unitime-FilterBox");
		
		final Timer blur = new Timer() {
			@Override
			public void run() {
				if (!iFocus) {
					removeStyleName("unitime-FilterBoxFocus");
					if (isFilterPopupShowing()) hideFilterPopup();
				}
			}
		};

		iFocusHandler = new FocusHandler() {
			@Override
			public void onFocus(FocusEvent event) {
				if (event.getSource() != null && event.getSource() instanceof Focusable)
					iLastFocusedWidget = (Focusable)event.getSource();
				iFocus = true;
				addStyleName("unitime-FilterBoxFocus");
				if (iShowSuggestionsOnFocus) refreshSuggestions();
			}
		};
		
		iBlurHandler = new BlurHandler() {
			@Override
			public void onBlur(BlurEvent event) {
				iFocus = false;
				iLastFocusedWidget = null;
				blur.schedule(100);
			}
		};
		
		iFilter = new AriaTextBox();
		iFilter.setStyleName("filter");

		
        iFilter.addKeyDownHandler(new KeyDownHandler() {
			@Override
			public void onKeyDown(KeyDownEvent event) {
				if (isFilterPopupShowing()) {
					hideFilterPopup();
				}
				if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_BACKSPACE && iFilter.getText().isEmpty()) {
					ChipPanel last = getLastChipPanel();
					if (last != null) {
						remove(last);
						resizeFilterIfNeeded();
						setAriaLabel(toAriaString());
						ValueChangeEvent.fire(FilterBox.this, getValue());
						setStatus(getAriaLabel());
					}
				}
				if (isSuggestionsShowing()) {
					switch (event.getNativeEvent().getKeyCode()) {
					case KeyCodes.KEY_DOWN:
						iSuggestionMenu.selectItem(iSuggestionMenu.getSelectedItemIndex() + 1);
						setStatus(ARIA.onSuggestion(iSuggestionMenu.getSelectedItemIndex() + 1, iSuggestionMenu.getNumItems(), iSuggestionMenu.getSelectedSuggestion().toAriaString(FilterBox.this)));
						break;
					case KeyCodes.KEY_UP:
						if (iSuggestionMenu.getSelectedItemIndex() == -1) {
							iSuggestionMenu.selectItem(iSuggestionMenu.getNumItems() - 1);
						} else {
							iSuggestionMenu.selectItem(iSuggestionMenu.getSelectedItemIndex() - 1);
						}
						setStatus(ARIA.onSuggestion(iSuggestionMenu.getSelectedItemIndex() + 1, iSuggestionMenu.getNumItems(), iSuggestionMenu.getSelectedSuggestion().toAriaString(FilterBox.this)));
						break;
					case KeyCodes.KEY_ENTER:
						iSuggestionMenu.executeSelected();
						hideSuggestions();
						break;
					case KeyCodes.KEY_TAB:
						hideSuggestions();
						break;
					case KeyCodes.KEY_ESCAPE:
						hideSuggestions();
						break;
					}
					switch (event.getNativeEvent().getKeyCode()) {
					case KeyCodes.KEY_DOWN:
					case KeyCodes.KEY_UP:
					case KeyCodes.KEY_ENTER:
					case KeyCodes.KEY_ESCAPE:
						event.preventDefault();
						event.stopPropagation();
					}
				} else {
					if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_DOWN && (event.getNativeEvent().getAltKey() || iFilter.getCursorPos() == iFilter.getText().length())) {
						showSuggestions();
						event.preventDefault();
						event.stopPropagation();
					}
				}
			}
		});
        iFilter.addKeyUpHandler(new KeyUpHandler() {
			@Override
			public void onKeyUp(KeyUpEvent event) {
				refreshSuggestions();
			}
		});
		iFilter.addFocusHandler(iFocusHandler);
		iFilter.addBlurHandler(iBlurHandler);
		add(iFilter);
		iFilter.addChangeHandler(new ChangeHandler() {
			@Override
			public void onChange(ChangeEvent event) {
				setAriaLabel(toAriaString());
				ValueChangeEvent.fire(FilterBox.this, getValue());
			}
		});
		
		iFilterClear = new Image(RESOURCES.filter_clear());
		iFilterClear.setAltText(MESSAGES.altClearFilter());
		iFilterClear.setTitle(MESSAGES.altClearFilter());
		iFilterClear.setStyleName("button-image");
        add(iFilterClear);
        iFilterClear.setVisible(false);
        Roles.getDocumentRole().setAriaHiddenState(iFilterClear.getElement(), true);
        iFilter.addChangeHandler(new ChangeHandler() {
			@Override
			public void onChange(ChangeEvent event) {
				if (iFilterClear.isVisible() && getValue().isEmpty()) {
					resizeFilterIfNeeded();
				} else if (!iFilterClear.isVisible() && !getValue().isEmpty()) {
					resizeFilterIfNeeded();
				}
			}
		});
		
        iFilterOpen = new Image(RESOURCES.filter_open());
        iFilterOpen.setAltText(MESSAGES.altOpenFilter());
        iFilterOpen.setTitle(MESSAGES.altOpenFilter());
        iFilterOpen.addStyleName("button-image");
        add(iFilterOpen);
        Roles.getDocumentRole().setAriaHiddenState(iFilterOpen.getElement(), true);
        
        iFilterClose = new Image(RESOURCES.filter_close());
        iFilterClose.setAltText(MESSAGES.altCloseFilter());
        iFilterClose.setTitle(MESSAGES.altCloseFilter());
        iFilterClose.addStyleName("button-image");
        add(iFilterClose);
        iFilterClose.setVisible(false);
        Roles.getDocumentRole().setAriaHiddenState(iFilterClose.getElement(), true);
        
        iFilterPopup = new PopupPanelKeepFocus();
        iFilterPopup.setStyleName("unitime-FilterBoxPopup");
        iFilterPopup.setAutoHideEnabled(false);
        iSuggestionMenu = new SuggestionMenu();
        iSuggestionsPopup = new PopupPanelKeepFocus();
        iSuggestionsPopup.setWidget(iSuggestionMenu);
        iSuggestionsPopup.setStyleName("unitime-FilterBoxPopup");
        
        sinkEvents(Event.ONMOUSEDOWN);
        
		iSuggestionsPopup.getElement().setAttribute("id", DOM.createUniqueId());
		Roles.getTextboxRole().setAriaOwnsProperty(iFilter.getElement(), Id.of(iSuggestionsPopup.getElement()));
		
		Roles.getTextboxRole().setAriaAutocompleteProperty(iFilter.getElement(), AutocompleteValue.NONE);
	}
	
	public void setSuggestionsProvider(SuggestionsProvider suggestionsProvider) { iSuggestionsProvider = new DefaultSuggestionsProvider(suggestionsProvider); }
	public SuggestionsProvider getSuggestionsProvider() { return iSuggestionsProvider; }
	
	protected void applySuggestion(Suggestion suggestion) {
		iFilter.setText(suggestion.getReplacementString());
		if (suggestion.getChipToAdd() != null) {
			if (hasChip(suggestion.getChipToAdd()))
				removeChip(suggestion.getChipToAdd(), false);
			else
				addChip(suggestion.getChipToAdd(), false);
		}
		if (suggestion.getChipToRemove() != null)
			removeChip(suggestion.getChipToRemove(), false);
	}
	
	@Override
	public void setWidth(String width) {
		super.setWidth(width);
		iSuggestionsPopup.setWidth(width);
		iFilterPopup.setWidth(width);
	}
	
	public void setParser(Parser parser) { iParser = parser; }
	
	public boolean isSuggestionsShowing() {
		return iSuggestionsPopup.isShowing();
	}
	
	public boolean isFilterPopupShowing() {
		return iFilterPopup.isShowing();
	}
	
	public void hideSuggestions() {
		iSuggestionsPopup.hide();
	}
	
	public void hideFilterPopup() {
		iFilterPopup.hide();
		iFilterOpen.setVisible(true);
		iFilterClose.setVisible(false);
		if (iLastFocusedWidget != null && !iLastFocusedWidget.equals(iFilter)) iFilter.setFocus(true);
	}
	
	public void addFilter(Filter filter) {
		iFilters.add(filter);
	}
	
	public List<Filter> getFilters() { return iFilters; }
	
	public Filter getFilter(String command) {
		for (Filter filter: getFilters())
			if (filter.getCommand().equals(command)) return filter;
		return null;
	}
	
	public void showFilterPopup() {
		if (!isEnabled()) return;
		iFilterPopup.setWidget(createFilterPopup());
		if (iFilterPopup.isShowing()) {
			iFilterPopup.moveRelativeTo(this);
			if (iLastFocusedWidget != null) iLastFocusedWidget.setFocus(true);
		} else {
			iFilterPopup.setWidth(getElement().getClientWidth() + "px");
			iFilterPopup.showRelativeTo(this);
			iFilterOpen.setVisible(false);
			iFilterClose.setVisible(true);
		}
	}
	
	protected Widget createFilterPopup() {
		final AbsolutePanel popupPanel = new AbsolutePanel();
		popupPanel.addStyleName("panel");
		
		for (final Filter filter: iFilters) {
			final AbsolutePanel filterPanel = new AbsolutePanel();
			filterPanel.addStyleName("filter");
			filterPanel.setVisible(false);
			popupPanel.add(filterPanel);
			filter.getPopupWidget(this, new AsyncCallback<Widget>() {
				@Override
				public void onFailure(Throwable caught) {
					if (filter.getLabel() != null && !filter.getLabel().isEmpty()) {
						Label label = new Label(filter.getLabel(), false);
						label.addStyleName("command");
						filterPanel.add(label);
					} else if (filter.getCommand().length() > 0) {
						Label label = new Label(filter.getCommand().replace('_', ' '), false);
						label.addStyleName("command");
						filterPanel.add(label);
					}
					Label error = new Label(caught.getMessage(), false);
					error.addStyleName("error");
					filterPanel.add(error);
					filterPanel.setVisible(true);
				}
				@Override
				public void onSuccess(Widget widget) {
					if (widget == null) return;
					filterPanel.add(widget);
					filterPanel.setVisible(true);
				}
			});
		}
		
		if (iDefaultValueProvider != null) {
			boolean selected = iDefaultValueProvider.getValue().equals(getValue().trim());
			final Image star = new Image(selected ? RESOURCES.starSelected() : RESOURCES.star());
			star.setAltText(selected ? MESSAGES.altStarFilterSelected() : MESSAGES.altStarFilter());
			star.setTitle(selected ? MESSAGES.altStarFilterSelected() : MESSAGES.altStarFilter());
			star.addStyleName("button-star");
	        star.addMouseDownHandler(new MouseDownHandler() {
				@Override
				public void onMouseDown(MouseDownEvent event) {
					iDefaultValueProvider.setValue(getValue().trim());
					star.setResource(RESOURCES.starSelected());
					star.setAltText(MESSAGES.altStarFilterSelected());
					star.setTitle(MESSAGES.altStarFilterSelected());
					event.getNativeEvent().stopPropagation();
					event.getNativeEvent().preventDefault();
				}
			});
	        popupPanel.add(star);
		}
		
		return popupPanel;
	}
	
	private String iLastValue = null;
	public void showSuggestions() {
		iLastValue = null;
		refreshSuggestions();
	}
	
	public void refreshSuggestions() {
		if (getSuggestionsProvider() == null) return;
		if (isFilterPopupShowing()) return;
		if (!isEnabled()) return;
		String value = getValue();
		if (value.equals(iLastValue)) return;
		iLastValue = value;
		final String query = iFilter.getText();
		getSuggestionsProvider().getSuggestions(getChips(null), query, new AsyncCallback<Collection<Suggestion>>() {
			@Override
			public void onFailure(Throwable caught) {
				if (iSuggestionsPopup.isShowing()) iSuggestionsPopup.hide();
			}

			@Override
			public void onSuccess(Collection<Suggestion> result) {
				if (!query.equals(iFilter.getText())) return; // old request
				if (result != null && !result.isEmpty()) {
					updateSuggestions(result);
					iSuggestionsPopup.setWidth(getElement().getClientWidth() + "px");
					iSuggestionsPopup.showRelativeTo(FilterBox.this);
				} else {
					if (iSuggestionsPopup.isShowing()) iSuggestionsPopup.hide();
				}
			}
		});
	}
	
	protected void updateSuggestions(Collection<Suggestion> suggestions) {
		iSuggestionMenu.clearItems();
		int selected = -1;
		for (final Suggestion suggestion: suggestions) {
			SuggestionMenuItem item = new SuggestionMenuItem(suggestion);
			if (selected < 0 && suggestion.isSelected())
				selected = iSuggestionMenu.getNumItems();
			iSuggestionMenu.addItem(item);
			if (iSuggestionMenu.getNumItems() == 20) break;
		}
		if (selected >= 0)
			iSuggestionMenu.selectItem(selected);
		else
			iSuggestionMenu.selectItem(0);
		if (iSuggestionMenu.getNumItems() == 1) {
			setStatus(ARIA.showingOneSuggestion(iSuggestionMenu.getSelectedSuggestion().toAriaString(this)));
		} else if (iSuggestionMenu.getSelectedItemIndex() == 0) {
			setStatus(ARIA.showingMultipleSuggestions(iSuggestionMenu.getNumItems(), toAriaString(), iSuggestionMenu.getSelectedSuggestion().toAriaString(this)));
		} else {
			setStatus(ARIA.onSuggestion(iSuggestionMenu.getSelectedItemIndex() + 1, iSuggestionMenu.getNumItems(), iSuggestionMenu.getSelectedSuggestion().toAriaString(this)));
		}
	}
	
	@Override
	public void onBrowserEvent(Event event) {
    	Element target = DOM.eventGetTarget(event);

	    switch (DOM.eventGetType(event)) {
	    case Event.ONMOUSEDOWN:
	    	boolean open = iFilterOpen.getElement().equals(target);
	    	boolean close = iFilterClose.getElement().equals(target);
	    	boolean clear = iFilterClear.getElement().equals(target);
	    	boolean filter = iFilter.getElement().equals(target);
	    	if (isFilterPopupShowing() || close) {
	    		hideFilterPopup();
	    	} else if (open) {
	    		hideSuggestions();
	    		showFilterPopup();
	    	}
	    	if (clear) {
				iFilter.setText("");
				removeAllChips();
				setAriaLabel(toAriaString());
				ValueChangeEvent.fire(FilterBox.this, getValue());
	    	}
	    	if (!filter) {
				event.stopPropagation();
				event.preventDefault();
		    	Scheduler.get().scheduleDeferred(new ScheduledCommand() {
					@Override
					public void execute() {
						iFilter.setFocus(true);
					}
				});
	    	}
	    	break;
	    }
	}
	
	@Override
	protected void onAttach() {
		super.onAttach();
		resizeFilterIfNeeded();
		iResizeHandler = Window.addResizeHandler(new ResizeHandler() {
			@Override
			public void onResize(ResizeEvent event) {
				resizeFilterIfNeeded();
			}
		});
	}
	
	@Override
	protected void onDetach() {
		super.onDetach();
		if (iResizeHandler != null) {
			iResizeHandler.removeHandler();
			iResizeHandler = null;
		}
	}
	
	protected void resizeFilterIfNeeded() {
		if (!isAttached()) return;
		ChipPanel last = getLastChipPanel();
		iFilterOpen.setVisible(isEnabled() && !isFilterPopupShowing());
		iFilterClear.setVisible(isEnabled() && (!iFilter.getText().isEmpty() || last != null));
		int buttonWidth = (isFilterPopupShowing() ? iFilterClose : iFilterOpen).getElement().getOffsetWidth() + iFilterClear.getElement().getOffsetWidth() + 8;
		if (last != null) {
			int width = getAbsoluteLeft() + getOffsetWidth() - last.getAbsoluteLeft() - last.getOffsetWidth() - buttonWidth;
			if (width < 100)
				width = getElement().getClientWidth() - buttonWidth;
			iFilter.getElement().getStyle().setWidth(width, Unit.PX);
		} else {
			iFilter.getElement().getStyle().setWidth(getElement().getClientWidth() - buttonWidth, Unit.PX);
		}
		if (isSuggestionsShowing())
			iSuggestionsPopup.moveRelativeTo(this);
		if (isFilterPopupShowing())
			iFilterPopup.moveRelativeTo(this);
	}
	
	protected String getChipColor(Chip chip) {
		return iChip2Color.getColor(chip.getCommand());
	}
	
	public void addChip(Chip chip, boolean fireEvents) {
		final ChipPanel panel = new ChipPanel(chip, getChipColor(chip));
		panel.addClickHandler(new ClickHandler() {
			@Override
			public void onClick(ClickEvent event) {
				remove(panel);
				resizeFilterIfNeeded();
				setAriaLabel(toAriaString());
				ValueChangeEvent.fire(FilterBox.this, getValue());
			}
		});
		insert(panel, getWidgetIndex(iFilter));
		resizeFilterIfNeeded();
		setAriaLabel(toAriaString());
		if (fireEvents)
			ValueChangeEvent.fire(this, getValue());
	}
	
	public boolean removeChip(Chip chip, boolean fireEvents) {
		for (int i = 0; i < getWidgetCount(); i++) {
			Widget w = getWidget(i);
			if (w instanceof ChipPanel && ((ChipPanel)w).getChip().equals(chip)) {
				remove(i);
				resizeFilterIfNeeded();
				setAriaLabel(toAriaString());
				if (fireEvents)
					ValueChangeEvent.fire(FilterBox.this, getValue());
				return true;
			}
		}
		return false;
	}
	
	protected ChipPanel getFirstChipPanel() {
		for (int i = 0; i < getWidgetCount(); i++) {
			Widget w = getWidget(i);
			if (w instanceof ChipPanel) return (ChipPanel)w;
		}
		return null;
	}
	
	protected ChipPanel getLastChipPanel() {
		ChipPanel last = null;
		for (int i = 0; i < getWidgetCount(); i++) {
			Widget w = getWidget(i);
			if (w instanceof ChipPanel) last = (ChipPanel)w;
		}
		return last;
	}
	
	public boolean hasChip(Chip chip) {
		for (int i = 0; i < getWidgetCount(); i++) {
			Widget w = getWidget(i);
			if (w instanceof ChipPanel && ((ChipPanel)w).getChip().equals(chip)) return true;
		}
		return false;
	}
	
	public Chip getChip(String command) {
		for (int i = 0; i < getWidgetCount(); i++) {
			Widget w = getWidget(i);
			if (w instanceof ChipPanel && ((ChipPanel)w).getChip().getCommand().equalsIgnoreCase(command))
				return ((ChipPanel)w).getChip();
		}
		return null;
	}
	
	public List<Chip> getChips(String command) {
		List<Chip> chips = new ArrayList<Chip>();
		for (int i = 0; i < getWidgetCount(); i++) {
			Widget w = getWidget(i);
			if (w instanceof ChipPanel && (command == null || ((ChipPanel)w).getChip().getCommand().equalsIgnoreCase(command))) {
				chips.add(((ChipPanel)w).getChip());
			}
		}
		return chips;
	}
	
	public void fixLabel(Chip chip) {
		for (int i = 0; i < getWidgetCount(); i++) {
			Widget w = getWidget(i);
			if (w instanceof ChipPanel && ((ChipPanel)w).getChip().equals(chip)) {
				((ChipPanel)w).setText(chip.getTranslatedValue());
				resizeFilterIfNeeded();
			}
		}
	}
	
	public void removeAllChips() {
		for (int i = getWidgetCount() - 1; i >= 0; i--) {
			Widget w = getWidget(i);
			if (w instanceof ChipPanel) remove(i);
		}
		resizeFilterIfNeeded();
	}
	
	public static class ChipPanel extends AbsolutePanel implements HasClickHandlers, HasText, HasEnabled {
		private Chip iChip;
		private Label iLabel;
		private HTML iButton;
		
		public ChipPanel(Chip chip, String color) {
			iChip = chip;
			setStyleName("chip");
			addStyleName(color);
			iLabel = new Label(chip.getTranslatedValue());
			iLabel.setStyleName("text");
			add(iLabel);
			iButton = new HTML("&times;");
			iButton.setStyleName("button");
			add(iButton);
			if (chip.hasToolTip())
				setTitle(toString() + "\n" + chip.getToolTip());
			else
				setTitle(toString());
			Roles.getDocumentRole().setAriaHiddenState(getElement(), true);
		}
		
		@Override
		public HandlerRegistration addClickHandler(ClickHandler handler) {
			return iButton.addClickHandler(handler);
		}

		@Override
		public String getText() {
			return iLabel.getText();
		}

		@Override
		public void setText(String text) {
			iLabel.setText(text);
		}
		
		public Chip getChip() {
			return iChip;
		}
		
		public String toString() {
			return getChip().toString();
		}
		
		public String toAriaString() {
			return getChip().toAriaString();
		}

		@Override
		public boolean isEnabled() {
			return iButton.isVisible();
		}

		@Override
		public void setEnabled(boolean enabled) {
			iButton.setVisible(enabled);
		}
	}

	@Override
	public String getValue() {
		String ret = "";
		for (Chip chip: getChips(null))
			ret += chip.toString() + " ";
		return ret + iFilter.getText();
	}
	
	public String toAriaString() {
		String ret = "";
		for (Chip chip: getChips(null)) {
			if (!ret.isEmpty()) ret += ", ";
			ret += chip.toAriaString();
		}
		if (!iFilter.getText().isEmpty()) {
			if (!ret.isEmpty()) ret += ", ";
			ret += iFilter.getText();
		}
		return ret.isEmpty() ? ARIA.emptyFilter() : ret;
	}
	
	@Override
	public void setValue(String text) {
		setValue(text, false);
	}
	
	@Override
	public void setValue(String text, final boolean fireEvents) {
		removeAllChips();
		iParser.parse(text, getFilters(), new AsyncCallback<Parser.Results>() {
			@Override
			public void onFailure(Throwable caught) {
			}
			@Override
			public void onSuccess(Parser.Results result) {
				for (Chip chip: result.getChips())
					addChip(chip, false);
				iFilter.setText(result.getFilter());
				resizeFilterIfNeeded();
				setAriaLabel(toAriaString());
				if (fireEvents)
					ValueChangeEvent.fire(FilterBox.this, getValue());
			}
		});
	}
	
	@Override
	public String getText() {
		return iFilter.getText();
	}
	
	@Override
	public void setText(String text) {
		iFilter.setText(text);
	}

	@Override
	public HandlerRegistration addValueChangeHandler(ValueChangeHandler<String> handler) {
		return addHandler(handler, ValueChangeEvent.getType());
	}
	
	public static class Chip implements IsSerializable {
		private String iCommand, iValue, iLabel, iHint;
		private String iTranslatedCommand, iTranslatedValue;
		private Integer iCount;
		public Chip() {}
		
		public Chip(String command, String value) {
			iCommand = command; iValue = value;
		}
		
		public Chip withCount(Integer count) {
			iCount = count;
			if (iCount != null && iCount <= 0) iCount = null;
			return this;
		}
		
		public Chip withTranslatedCommand(String translation) {
			iTranslatedCommand = translation; return this;
		}
		
		public Chip withTranslatedValue(String translation) {
			iTranslatedValue = translation; return this;
		}
		
		public Chip withToolTip(String hint) {
			iHint = hint; return this;
		}
		
		public Chip withLabel(String label) {
			iLabel = label; return this;
		}

		public String getCommand() { return iCommand; }
		public String getTranslatedCommand() { return iTranslatedCommand == null || iTranslatedCommand.isEmpty() ? iCommand.replace('_', ' '): iTranslatedCommand; }
		
		public String getValue() { return iValue; }
		public String getTranslatedValue() { return iTranslatedValue == null || iTranslatedValue.isEmpty() ? iValue : iTranslatedValue; }
		public String getLabel() { return iLabel == null || iLabel.isEmpty() ? iTranslatedValue == null || iTranslatedValue.isEmpty() ? iValue : iTranslatedValue : iLabel; }
		
		public boolean hasToolTip() { return iHint != null && !iHint.isEmpty(); }
		public String getToolTip() { return iHint; }

		public boolean hasCount() { return iCount != null && iCount > 0; }
		public Integer getCount() { return iCount; }
		
		@Override
		public boolean equals(Object other) {
			if (other == null || !(other instanceof Chip)) return false;
			Chip chip = (Chip)other;
			return (chip.getCommand() == null || chip.getCommand().equalsIgnoreCase(getCommand())) && (chip.getValue() == null || chip.getValue().equalsIgnoreCase(getValue()));
		}
		
		@Override
		public String toString() {
			return getCommand() + ":" + (getValue().contains(" ") ? "\"" + getValue() + "\"" : getValue());
		}
		
		public String toAriaString() {
			return getTranslatedCommand() + " " + getTranslatedValue();
		}
		
		public boolean startsWith(String text) {
			String t = text.toLowerCase();
			if (iValue != null && iValue.toLowerCase().startsWith(t)) return true;
			if (iLabel != null && iLabel.toLowerCase().startsWith(t)) return true;
			if (iTranslatedValue != null && iTranslatedValue.toLowerCase().startsWith(t)) return true;
			if (iCommand != null && t.startsWith(iCommand.toLowerCase() + " ")) {
				if (iValue != null && (iCommand + " " + iValue).toLowerCase().startsWith(t)) return true;
				if (iLabel != null && (iCommand + " " + iLabel).toLowerCase().startsWith(t)) return true;
				if (iTranslatedValue != null && (iCommand + " " + iTranslatedValue).toLowerCase().startsWith(t)) return true;
			}
			if (iCommand != null && t.startsWith(iCommand.toLowerCase() + ":")) {
				if (iValue != null && (iCommand + ":" + iValue).toLowerCase().startsWith(t)) return true;
				if (iLabel != null && (iCommand + ":" + iLabel).toLowerCase().startsWith(t)) return true;
				if (iTranslatedValue != null && (iCommand + ":" + iTranslatedValue).toLowerCase().startsWith(t)) return true;
			}
			if (iTranslatedCommand != null && t.startsWith(iTranslatedCommand.toLowerCase() + " ")) {
				if (iValue != null && (iTranslatedCommand + " " + iValue).toLowerCase().startsWith(t)) return true;
				if (iLabel != null && (iTranslatedCommand + " " + iLabel).toLowerCase().startsWith(t)) return true;
				if (iTranslatedValue != null && (iTranslatedCommand + " " + iTranslatedValue).toLowerCase().startsWith(t)) return true;
			}
			if (iTranslatedCommand != null && t.startsWith(iTranslatedCommand.toLowerCase() + ":")) {
				if (iValue != null && (iTranslatedCommand + ":" + iValue).toLowerCase().startsWith(t)) return true;
				if (iLabel != null && (iTranslatedCommand + ":" + iLabel).toLowerCase().startsWith(t)) return true;
				if (iTranslatedValue != null && (iTranslatedCommand + ":" + iTranslatedValue).toLowerCase().startsWith(t)) return true;
			}
			return false;
		}
	}
	
	public interface Filter {
		public String getCommand();
		public String getLabel();
		public void validate(String text, AsyncCallback<Chip> callback);
		public void getSuggestions(List<Chip> chips, String text, AsyncCallback<Collection<Suggestion>> callback);
		public void getPopupWidget(FilterBox box, AsyncCallback<Widget> callback);
	}
	
	public static interface Parser {
		public void parse(String text, Collection<Filter> filters, AsyncCallback<Parser.Results> callback);
		
		public static class Results implements IsSerializable {
			private Collection<Chip> iChips = new ArrayList<Chip>();
			private String iFilter = null;
			
			public Results(String filter, Collection<Chip> chips) {
				iChips = chips; iFilter = filter;
			}
			
			public Collection<Chip> getChips() { return iChips; }

			public String getFilter() { return iFilter; }
			
			@Override
			public String toString() {
				return "ParserResults{chips=" + getChips() + ", filter=" + getFilter() + "}";
			}
		}
	}
	
	public static class DefaultParser implements Parser {
		private static RegExp[] sRegExps = new RegExp[] {
				RegExp.compile("^(\\w+):\"([^\"]*)\"(.*)$", "i"),
				RegExp.compile("^(\\w+):([^ ]*) (.*)$", "i"),
				RegExp.compile("^(\\w+):([^ ]*)$", "i")};
		
		@Override
		public void parse(String text, Collection<Filter> filters, AsyncCallback<Parser.Results> callback) {
			parse(new ArrayList<Chip>(), text, filters, callback);
		}
		
		private void parse(final List<Chip> chips, final String text, final Collection<Filter> filters, final AsyncCallback<Parser.Results> callback) {
			if (text.isEmpty()) {
				callback.onSuccess(new Parser.Results(text, chips));
			} else {
				for (RegExp regExp: sRegExps) {
					final MatchResult r = regExp.exec(text);
					if (r == null) continue;
					for (Filter filter: filters) {
						if (filter.getCommand().equals(r.getGroup(1))) {
							filter.validate(r.getGroup(2), new AsyncCallback<Chip>() {
								@Override
								public void onFailure(Throwable caught) {
									callback.onSuccess(new Parser.Results(text, chips));
								}

								@Override
								public void onSuccess(Chip result) {
									if (result == null) {
										callback.onSuccess(new Parser.Results(text, chips));
									} else {
										chips.add(result);
										if (r.getGroupCount() > 3) {
											parse(chips, r.getGroup(3).trim(), filters, callback);
										} else {
											callback.onSuccess(new Parser.Results("", chips));
										}
									}
								}
							});
							return;
						}
					}
				}
				callback.onSuccess(new Parser.Results(text, chips));
			}
		}
	}
	
	public static abstract class SimpleFilter implements Filter {
		private String iCommand;
		private String iLabel;
		private boolean iMultiple = true;
		
		public SimpleFilter(String command, String label) {
			iCommand = command;
			iLabel = label;
		}
		
		public boolean isMultipleSelection() { return iMultiple; }
		public Filter setMultipleSelection(boolean multiple) { iMultiple = multiple; return this; }
		
		@Override
		public String getCommand() {
			return iCommand;
		}
		
		@Override
		public String getLabel() {
			return iLabel == null || iLabel.isEmpty() ? iCommand.replace('_', ' ') : iLabel;
		}
		
		public abstract void getValues(List<Chip> chips, String text, AsyncCallback<Collection<Chip>> callback);
		
		@Override
		public void getSuggestions(final List<Chip> chips, final String text, final AsyncCallback<Collection<Suggestion>> callback) {
			if (text.isEmpty()) {
				callback.onSuccess(null);
			} else {
				getValues(chips, text, new AsyncCallback<Collection<Chip>>() {

					@Override
					public void onFailure(Throwable caught) {
						callback.onFailure(caught);
					}

					@Override
					public void onSuccess(Collection<Chip> result) {
						List<Suggestion> ret = new ArrayList<FilterBox.Suggestion>();
						if (getCommand().toLowerCase().startsWith(text) || getLabel().toLowerCase().startsWith(text)) {
							for (Chip chip: result)
								if (chips.contains(chip)) { // already in there -- remove
									ret.add(new Suggestion(chip));
								} else {
									Chip old = null;
									for (Chip c: chips) { if (c.getCommand().equals(getCommand())) { old = c; break; } }
									ret.add(new Suggestion(chip, isMultipleSelection() ? null : old));
								}
						} else {
							for (Chip chip: result)
								if (chip.startsWith(text)) {
									if (chips.contains(chip)) { // already in there -- remove
										ret.add(new Suggestion(chip));
									} else {
										Chip old = null;
										for (Chip c: chips) { if (c.getCommand().equals(getCommand())) { old = c; break; } }
										ret.add(new Suggestion(chip, isMultipleSelection() ? null : old));
									}
								}
						}
						callback.onSuccess(ret);
					}
					
				});
			}
		}

		@Override
		public void getPopupWidget(final FilterBox box, final AsyncCallback<Widget> callback) {
			getValues(box.getChips(null), box.getText(), new AsyncCallback<Collection<Chip>>() {
				@Override
				public void onFailure(Throwable caught) {
					callback.onFailure(caught);
				}

				@Override
				public void onSuccess(Collection<Chip> values) {
					if (values == null || values.isEmpty()) {
						callback.onSuccess(null);
						return;
					}
					AbsolutePanel popup = new AbsolutePanel();
					if (getLabel() != null && !getLabel().isEmpty()) {
						Label label = new Label(getLabel(), false);
						label.addStyleName("command");
						popup.add(label);
					} else {
						Label label = new Label(getCommand().replace('_', ' '), false);
						label.addStyleName("command");
						popup.add(label);
					}
					for (final Chip value: values) {
						String html = SafeHtmlUtils.htmlEscape(value.getLabel());
						if (value.hasCount())
							html += "<span class='item-hint'>(" + value.getCount() + ")</span>";
						else if (value.hasToolTip())
							html += "<span class='item-hint'>" + value.getToolTip() + "</span>";
						HTML item = new HTML(html, false);
						if (value.hasToolTip())
							item.setTitle(value.getToolTip());
						item.addStyleName("value");
						item.addMouseDownHandler(new MouseDownHandler() {
							@Override
							public void onMouseDown(MouseDownEvent event) {
								if (isMultipleSelection()) {
									if (!box.removeChip(value, true))
										box.addChip(value, true);
								} else {
									Chip old = box.getChip(value.getCommand());
									if (old == null) {
										box.addChip(value, true);
									} else if (!old.equals(value)) {
										box.removeChip(old, false);
										box.addChip(value, true);
									}
								}
								event.getNativeEvent().stopPropagation();
								event.getNativeEvent().preventDefault();
							}
						});
						popup.add(item);
					}
					callback.onSuccess(popup);
				}
			});
		}
		
	}
	
	public static class StaticSimpleFilter extends SimpleFilter {
		private List<Chip> iValues = new ArrayList<Chip>();
		private boolean iValidate;
		
		public StaticSimpleFilter(String command, String label, boolean validate, String... values) {
			super(command, label);
			iValidate = validate;
			for (String value: values)
				iValues.add(new Chip(command, value).withLabel(label));
		}
		
		public StaticSimpleFilter(String command, String label, String... values) {
			this(command, label, values.length > 0, values);
		}
		
		public StaticSimpleFilter(String command, String label, boolean validate, Collection<Chip> chips) {
			super(command, label);
			iValidate = validate;
			if (chips != null)
				iValues.addAll(chips);
		}
		
		public StaticSimpleFilter(String command, String label, Collection<Chip> chips) {
			this(command, label, chips != null && !chips.isEmpty(), chips);
		}

		@Override
		public void getValues(List<Chip> chips, String text, AsyncCallback<Collection<Chip>> callback) {
			callback.onSuccess(iValues);
		}
		
		public void setValues(List<Chip> values) { iValues = values; }

		@Override
		public void validate(String text, AsyncCallback<Chip> callback) {
			if (iValidate) {
				for (Chip chip: iValues)
					if (chip.getValue().equals(text)) {
						callback.onSuccess(chip);
						return;
					}
				callback.onFailure(new Exception("Unknown value " + text + "."));
			} else {
				callback.onSuccess(new Chip(getCommand(), text).withTranslatedCommand(getLabel()));
			}
		}
	}
	
	public static class CustomFilter implements Filter {
		private String iCommand;
		private String iLabel;
		private AbsolutePanel iPanel = null;
		private Widget[] iWidgets;
		private boolean iVisible = true;
		
		public CustomFilter(String command, String label, Widget... popupWidgets) {
			iCommand = command;
			iLabel = label;
			iWidgets = popupWidgets;
		}

		@Override
		public String getCommand() {
			return iCommand;
		}
		
		@Override
		public String getLabel() {
			return iLabel == null || iLabel.isEmpty() ? iCommand.replace('_', ' ') : iLabel;
		}
		
		@Override
		public void validate(String value, AsyncCallback<Chip> callback) {
			callback.onSuccess(new Chip(getCommand(), value).withTranslatedCommand(getLabel()));
		}
		
		public boolean isVisible() { return iVisible; }
		public void setVisible(boolean visible) { iVisible = visible; }
		
		private void fixHandlers(final FilterBox box, Widget w) {
			if (w instanceof HasBlurHandlers)
				((HasBlurHandlers)w).addBlurHandler(box.iBlurHandler);
			if (w instanceof HasFocusHandlers)
				((HasFocusHandlers)w).addFocusHandler(box.iFocusHandler);
			if (w instanceof HasKeyDownHandlers)
				((HasKeyDownHandlers)w).addKeyDownHandler(new KeyDownHandler() {
					@Override
					public void onKeyDown(KeyDownEvent event) {
						if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ESCAPE)
							if (box.isFilterPopupShowing()) box.hideFilterPopup();
					}
				});
		}

		@Override
		public void getPopupWidget(final FilterBox box, AsyncCallback<Widget> callback) {
			if (!isVisible()) {
				callback.onSuccess(null);
				return;
			}
			if (iPanel == null) {
				iPanel = new AbsolutePanel();
				iPanel.addStyleName("filter");
				if (getLabel() != null && !getLabel().isEmpty()) {
					Label label = new Label(getLabel(), false);
					label.addStyleName("command");
					iPanel.add(label);
				} else if (getCommand() != null && !getCommand().isEmpty()) {
					Label label = new Label(getCommand().replace('_', ' '), false);
					label.addStyleName("command");
					iPanel.add(label);
				}
				AbsolutePanel other = new AbsolutePanel();
				other.addStyleName("other");
				for (final Widget w: iWidgets) {
					w.addStyleName("inline");
					if (w instanceof AriaSuggestBox) {
						fixHandlers(box, ((AriaSuggestBox)w).getValueBox());
						fixHandlers(box, ((AriaSuggestBox)w).getSuggestionMenu());
						((AriaSuggestBox)w).addSelectionHandler(new SelectionHandler<SuggestOracle.Suggestion>() {
							@Override
							public void onSelection(SelectionEvent<SuggestOracle.Suggestion> event) {
								((AriaSuggestBox) w).setFocus(true);
							}
						});
					} else
						fixHandlers(box, w);
					if (w instanceof TimeSelector) {
						fixHandlers(box, ((TimeSelector)w).getTimeMenu());
						((TimeSelector)w).addSelectionHandler(new SelectionHandler<Integer>() {
							@Override
							public void onSelection(SelectionEvent<Integer> event) {
								box.setFocus(true);
							}
						});
					}
					other.add(w);
				}
				iPanel.add(other);
			}
			callback.onSuccess(iPanel);
		}

		@Override
		public void getSuggestions(List<Chip> chips, String text, AsyncCallback<Collection<Suggestion>> callback) {
			callback.onSuccess(null);
		}
	}
	
	public interface Chip2Color {
		public String getColor(String command);
	}
	
	public class DefaultChip2Color implements Chip2Color {
		
		@Override
		public String getColor(String command) {
			for (int i = 0; i < iFilters.size(); i++)
				if (iFilters.get(i).getCommand().equals(command)) return sColors[i];
			return "red";
		}
		
		
	}
	
	public static interface SuggestionsProvider {
		public void getSuggestions(List<Chip> chips, String text, AsyncCallback<Collection<Suggestion>> callback);
	}
	
	public class DefaultSuggestionsProvider implements SuggestionsProvider {
		SuggestionsProvider iNext;
		
		public DefaultSuggestionsProvider(SuggestionsProvider next) {
			iNext = next;
		}

		@Override
		public void getSuggestions(List<Chip> chips, String text, AsyncCallback<Collection<Suggestion>> callback) {
			List<Suggestion> suggestions = new ArrayList<Suggestion>();
			if (!text.isEmpty())
				iterateFilters(chips, text, suggestions, getFilters().iterator(), callback);
			else
				returnSuggestions(chips, text, suggestions, callback);
		}
		
		public void iterateFilters(final List<Chip> chips, final String text, final List<Suggestion> suggestions, final Iterator<Filter> filters, final AsyncCallback<Collection<Suggestion>> callback) {
			if (filters.hasNext()) {
				Filter filter = filters.next();
				filter.getSuggestions(chips, text, new AsyncCallback<Collection<Suggestion>>() {
					@Override
					public void onFailure(Throwable caught) {
						iterateFilters(chips, text, suggestions, filters, callback);
					}
					@Override
					public void onSuccess(Collection<Suggestion> result) {
						if (result != null) suggestions.addAll(result);
						iterateFilters(chips, text, suggestions, filters, callback);
					}
				});
			} else {
				returnSuggestions(chips, text, suggestions, callback);
			}
		}
		
		public void returnSuggestions(final List<Chip> chips, final String text, final List<Suggestion> suggestions, final AsyncCallback<Collection<Suggestion>> callback) {
			if (iNext == null)
				callback.onSuccess(suggestions);
			else
				iNext.getSuggestions(chips, text, new AsyncCallback<Collection<Suggestion>>() {

					@Override
					public void onFailure(Throwable caught) {
						callback.onSuccess(suggestions);
					}

					@Override
					public void onSuccess(Collection<Suggestion> result) {
						if (result != null) suggestions.addAll(result);
						callback.onSuccess(suggestions);
					}
				});
		}
	}
	
	public static class Suggestion implements IsSerializable {
		private String iDisplay, iReplacement, iHint;
		private Chip iAdd, iRemove;
		private boolean iSelected;
		
		public Suggestion() {}
		public Suggestion(String displayString, String replacementString) {
			iDisplay = displayString;
			iReplacement = replacementString;
		}
		
		public Suggestion(String displayString, String replacementString, String hint) {
			iDisplay = displayString;
			iReplacement = replacementString;
			iHint = "<span class='item-hint'>" + hint + "</span>";
		}

		public Suggestion(Chip chip) {
			iAdd = chip; iReplacement = ""; 
			if (chip.hasToolTip()) {
				iDisplay = chip.getLabel();
				iHint = " <span class='item-hint'>" + chip.getToolTip() + "</span>";
			}
		}
		
		public Suggestion(Chip add, Chip remove) {
			iAdd = add; iRemove = remove; iReplacement = ""; 
			if ((add != null ? add : remove).hasToolTip()) {
				iDisplay = (add != null ? add : remove).getLabel();
				iHint = " <span class='item-hint'>" + (add != null ? add : remove).getToolTip() + "</span>";
			}
		}
		
		public Suggestion(String displayString, Chip add) {
			iDisplay = displayString; iReplacement = ""; iAdd = add;
			if (add.hasToolTip()) {
				iHint = " <span class='item-hint'>" + add.getToolTip() + "</span>";
			} else {
				iHint = "<span class='item-command'>" + add.getTranslatedCommand() + "</span>";
			}
		}
		
		public Suggestion(String displayString, Chip add, Chip remove) {
			iDisplay = displayString; iReplacement = ""; iAdd = add; iRemove = remove;
			if ((add != null ? add : remove).hasToolTip()) {
				iHint = " <span class='item-hint'>" + (add != null ? add : remove).getToolTip() + "</span>";
			} else {
				iHint = "<span class='item-command'>" + (add != null ? add : remove).getTranslatedCommand() + "</span>";
			}
		}
		
		public void setDisplayString(String display) { iDisplay = display; }
		public String getDisplayString() { return iDisplay; }
		
		public void setReplacementString(String replacement) { iReplacement = replacement; }
		public String getReplacementString() { return iReplacement; }
		
		public void setHint(String hint) { iHint = hint; }
		public String getHint() { return iHint; }
		
		public void setChipToAdd(Chip chip) { iAdd = chip; }
		public Chip getChipToAdd() { return iAdd; }
		
		public void setChipToRemove(Chip chip) { iRemove = chip; }
		public Chip getChipToRemove() { return iRemove; }
		
		public Chip getChip() { return getChipToAdd() == null ? getChipToRemove() : getChipToAdd(); }
		
		public boolean isSelected() { return iSelected; }
		public void setSelected(boolean selected) { iSelected = selected; }
		
		@Override
		public String toString() {
			return ((getDisplayString() == null ? "" : getDisplayString()) +
				(getChipToAdd() == null ? "" : " +" + getChipToAdd()) +
				(getChipToRemove() == null ? "" : " -" + getChipToRemove())).trim();
		}

		public String toAriaString(FilterBox box) {
			if (getChipToAdd() != null) {
				if (getChipToRemove() != null)
					return ARIA.chipReplace(getChipToAdd().getTranslatedCommand(), getChipToAdd().getLabel());
				else {
					if (box.hasChip(getChipToAdd()))
						return ARIA.chipDelete(getChipToAdd().getTranslatedCommand(), getChipToAdd().getLabel());
					else
						return ARIA.chipAdd(getChipToAdd().getTranslatedCommand(), getChipToAdd().getLabel());
				}
			} else if (getChipToRemove() != null) {
				return ARIA.chipDelete(getChipToRemove().getTranslatedCommand(), getChipToRemove().getLabel());
			}
			return SafeHtmlUtils.htmlEscape(getDisplayString()) + (getHint() == null ? "" : " " + getHint());
		}
	}
	
	public class PopupPanelKeepFocus extends PopupPanel {
		public PopupPanelKeepFocus() {
			super(true, false);
			setPreviewingAllNativeEvents(true);
			sinkEvents(Event.ONMOUSEDOWN);
		}
		
		@Override
		public void onBrowserEvent(Event event) {
			switch (DOM.eventGetType(event)) {
		    case Event.ONMOUSEDOWN:
		    	iFocus = true;
		    	break;
			}
		}
		
		public final void moveRelativeTo(final UIObject target) {
			position(target, getOffsetWidth(), getOffsetHeight());
		}
		
		private void position(final UIObject relativeObject, int offsetWidth, int offsetHeight) {
			int textBoxOffsetWidth = relativeObject.getOffsetWidth();
			int offsetWidthDiff = offsetWidth - textBoxOffsetWidth;
			int left = relativeObject.getAbsoluteLeft();
			if (offsetWidthDiff > 0) {
				int windowRight = Window.getClientWidth() + Window.getScrollLeft();
				int windowLeft = Window.getScrollLeft();
				int distanceToWindowRight = windowRight - left;
				int distanceFromWindowLeft = left - windowLeft;
				if (distanceToWindowRight < offsetWidth && distanceFromWindowLeft >= offsetWidthDiff) {
					left -= offsetWidthDiff;
				}
			}
			int top = relativeObject.getAbsoluteTop();
			int windowTop = Window.getScrollTop();
			int windowBottom = Window.getScrollTop() + Window.getClientHeight();
			int distanceFromWindowTop = top - windowTop;
			int distanceToWindowBottom = windowBottom - (top + relativeObject.getOffsetHeight());
			if (distanceToWindowBottom < offsetHeight && distanceFromWindowTop >= offsetHeight) {
				top -= offsetHeight;
			} else {
				top += relativeObject.getOffsetHeight();
			}
			setPopupPosition(left, top);
		}
	}
	
	private class SuggestionMenu extends MenuBar {
		SuggestionMenu() {
			super(true);
			setStyleName("");
			setFocusOnHoverEnabled(false);
		}
		
		public int getNumItems() {
			return getItems().size();
		}
		
		public int getSelectedItemIndex() {
			MenuItem selectedItem = getSelectedItem();
			if (selectedItem != null)
				return getItems().indexOf(selectedItem);
			return -1;
		}
		
		public void selectItem(int index) {
			List<MenuItem> items = getItems();
			if (index > -1 && index < items.size()) {
				selectItem(items.get(index));
			}
		}
		
		public void executeSelected() {
			MenuItem selected = getSelectedItem();
			if (selected != null)
				selected.getScheduledCommand().execute();
		}
		
		public Suggestion getSelectedSuggestion() {
			MenuItem selectedItem = getSelectedItem();
			return selectedItem == null ? null : ((SuggestionMenuItem)selectedItem).getSuggestion();
		}
	}
	
	private class SuggestionMenuItem extends MenuItem {
		private Suggestion iSuggestion = null;
		
		private SuggestionMenuItem(final Suggestion suggestion) {
			super(suggestion.getDisplayString() == null
					? suggestion.getChip().getLabel() + " <span class='item-command'>" + suggestion.getChip().getTranslatedCommand() + "</span>"
					: SafeHtmlUtils.htmlEscape(suggestion.getDisplayString()) + (suggestion.getHint() == null ? "" : " " + suggestion.getHint()),
				true,
				new Command() {
					@Override
					public void execute() {
						hideSuggestions();
						setStatus(ARIA.suggestionSelected(suggestion.toAriaString(FilterBox.this)));
						applySuggestion(suggestion);
						iLastValue = getValue();
						setAriaLabel(toAriaString());
						fireSelectionEvent(suggestion);
						ValueChangeEvent.fire(FilterBox.this, getValue());
					}
				});
			setStyleName("item");
			getElement().setAttribute("whiteSpace", "nowrap");
			iSuggestion = suggestion;
		}
		
		public Suggestion getSuggestion() {
			return iSuggestion;
		}
	}

	@Override
	public int getTabIndex() {
		return iFilter.getTabIndex();
	}


	@Override
	public void setTabIndex(int index) {
		iFilter.setTabIndex(index);
	}

	@Override
	public void setAccessKey(char key) {
		iFilter.setAccessKey(key);
	}

	@Override
	public void setFocus(boolean focused) {
		iFilter.setFocus(focused);
	}

	@Override
	public HandlerRegistration addKeyUpHandler(KeyUpHandler handler) {
		return iFilter.addKeyUpHandler(handler);
	}

	@Override
	public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
		return iFilter.addKeyDownHandler(handler);
	}

	@Override
	public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
		return iFilter.addKeyPressHandler(handler);
	}
	
	@Override
	public HandlerRegistration addFocusHandler(FocusHandler handler) {
		return iFilter.addFocusHandler(handler);
	}
	
	@Override
	public HandlerRegistration addBlurHandler(BlurHandler handler) {
		return iFilter.addBlurHandler(handler);
	}
	
	public boolean isShowSuggestionsOnFocus() { return iShowSuggestionsOnFocus; }
	public void setShowSuggestionsOnFocus(boolean showSuggestionsOnFocus) { iShowSuggestionsOnFocus = showSuggestionsOnFocus; }

	@Override
	public String getAriaLabel() {
		return iFilter.getAriaLabel();
	}

	@Override
	public void setAriaLabel(String text) {
		iFilter.setAriaLabel(text);
	}
	
	public void setStatus(String text) {
		AriaStatus.getInstance().setHTML(text);
	}
	
	public void setDefaultValueProvider(TakesValue<String> defaultValue) {
		iDefaultValueProvider = defaultValue;
	}
	
	public TakesValue<String> getDefaultValueProvider() {
		return iDefaultValueProvider;
	}

	@Override
	public boolean isEnabled() {
		return iFilter.isEnabled();
	}

	@Override
	public void setEnabled(boolean enabled) {
		iFilter.setEnabled(enabled);
		for (int i = 0; i < getWidgetCount(); i++) {
			Widget w = getWidget(i);
			if (w instanceof ChipPanel)
				((ChipPanel)w).setEnabled(enabled);
		}
		if (isFilterPopupShowing()) hideFilterPopup();
		if (isSuggestionsShowing()) hideSuggestions();
		resizeFilterIfNeeded();
	}

	@Override
	public HandlerRegistration addSelectionHandler(SelectionHandler<Suggestion> handler) {
		return addHandler(handler, SelectionEvent.getType());
	}
	
	private void fireSelectionEvent(Suggestion suggestion) {
		SelectionEvent.fire(this, suggestion);
	}
	
	public ValueBoxBase<String> getValueBox() {
		return iFilter;
	}
}