/**
 * This file is part of pwt.
 *
 * pwt is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
 * General Public License as published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * pwt is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
 * General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with pwt. If not,
 * see <http://www.gnu.org/licenses/>.
 */
package fr.putnami.pwt.core.widget.client;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.text.shared.AbstractRenderer;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.IsWidget;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import fr.putnami.pwt.core.theme.client.CssStyle;
import fr.putnami.pwt.core.widget.client.base.AbstractInputSelect;
import fr.putnami.pwt.core.widget.client.base.SimpleStyle;
import fr.putnami.pwt.core.widget.client.util.StyleUtils;

public class InputMultiSelect<T> extends AbstractInputSelect<T, List<T>> {

	private static final CssStyle STYLE_SELECTED = new SimpleStyle("input-select-selected");
	private static final CssStyle STYLE_HIGHLIGHTED = new SimpleStyle("input-select-highlighted");

	private Comparator<T> simpleComparator = new Comparator<T>() {
		@Override
		public int compare(T o1, T o2) {
			return Integer.compare(InputMultiSelect.this.getOrderedItems().indexOf(o1), InputMultiSelect.this
				.getOrderedItems().indexOf(o2));
		}
	};

	private class MultiSelectionRenderer extends AbstractRenderer<List<T>> {

		@Override
		public String render(List<T> values) {
			String result = "";
			if (values != null) {
				Collections.sort(values, InputMultiSelect.this.simpleComparator);
				result = Joiner.on(", ").join(Iterables.transform(values, new Function<T, String>() {
					@Override
					public String apply(T input) {
						return InputMultiSelect.this.getChoiceRenderer().renderItem(input);
					}
				}));
			}
			return result;
		}
	}

	private class MultiSelectionHandler implements SelectItemSelectionHandler<T, List<T>> {
		private int currentHighlightedIndex = -1;
		private List<T> selectedItems;

		@Override
		public void onItemClick(T item) {
			if (this.selectedItems == null) {
				this.selectedItems = new ArrayList<T>();
			}
			boolean selectItem = !this.selectedItems.contains(item);
			this.selectItem(item, selectItem);
			if (selectItem) {
				this.selectedItems.add(item);
			} else {
				this.selectedItems.remove(item);
				if (this.selectedItems.isEmpty()) {
					this.selectedItems = null;
				}
			}
			this.highlightByIndex(InputMultiSelect.this.getOrderedItems().indexOf(item));
			ValueChangeEvent.fire(InputMultiSelect.this, this.selectedItems);
		}

		@Override
		public void onHomeKeyDown() {
			this.highlightByIndex(0);
		}

		@Override
		public void onEndKeyDown() {
			this.highlightByIndex(InputMultiSelect.this.getOrderedItems().size() - 1);
		}

		@Override
		public void onUpKeyDown() {
			this.highlightByIndex(this.currentHighlightedIndex - 1);
		}

		@Override
		public void onDownKeyDown() {
			this.highlightByIndex(this.currentHighlightedIndex + 1);
		}

		private void highlightByIndex(int index) {
			int newIndex = Math.min(InputMultiSelect.this.getOrderedItems().size() - 1, Math.max(0, index));
			this.highlightItem(this.currentHighlightedIndex, false);
			this.highlightItem(newIndex, true);
			this.currentHighlightedIndex = newIndex;
			this.scrollToHighlighted();
		}

		private void highlightItem(int itemIndex, boolean highlighted) {
			if (itemIndex != -1) {
				T item = InputMultiSelect.this.getOrderedItems().get(itemIndex);
				NavLink currentSelection = InputMultiSelect.this.getItemsLinks().get(item);
				if (currentSelection != null) {
					StyleUtils.toggleStyle(currentSelection, InputMultiSelect.STYLE_HIGHLIGHTED, highlighted);
				}
			}
		}

		public void onEnterKeyDown() {
			if (this.currentHighlightedIndex != -1) {
				this.onItemClick(InputMultiSelect.this.getOrderedItems().get(this.currentHighlightedIndex));
			}
		}

		@Override
		public void onItemSearch(T searchResult) {
			this.highlightByIndex(InputMultiSelect.this.getOrderedItems().indexOf(searchResult));
		}

		@Override
		public void setSelection(List<T> selection, boolean fireEvents) {
			List<T> oldSelection = this.selectedItems;
			if (oldSelection != null) {
				for (T item : oldSelection) {
					this.selectItem(item, false);
				}
			}

			this.selectedItems = selection;
			if (selection != null) {
				for (T item : selection) {
					this.selectItem(item, true);
				}
			}
			this.highlightItem(this.currentHighlightedIndex, false);
			this.currentHighlightedIndex = -1;
			if (fireEvents) {
				ValueChangeEvent.fireIfNotEqual(InputMultiSelect.this, oldSelection, selection);
			}
		}

		private void scrollToHighlighted() {
			if (InputMultiSelect.this.getDropdown().isOpen() && this.currentHighlightedIndex != -1) {
				T item = InputMultiSelect.this.getOrderedItems().get(this.currentHighlightedIndex);
				NavLink currentSelection = InputMultiSelect.this.getItemsLinks().get(item);
				if (currentSelection != null) {
					currentSelection.getElement().scrollIntoView();
				}
			}
		}

		private void selectItem(T item, boolean selected) {
			NavLink currentSelection = InputMultiSelect.this.getItemsLinks().get(item);
			if (currentSelection != null) {
				currentSelection.setActive(selected);
				StyleUtils.toggleStyle(currentSelection, InputMultiSelect.STYLE_SELECTED, selected);
			}
		}

		@Override
		public List<T> getSelection() {
			return this.selectedItems;
		}
	}

	public InputMultiSelect() {
		super();
		this.setMultiple(true);
		this.setSelectionHandler(new MultiSelectionHandler());
		this.setSelectionRenderer(new MultiSelectionRenderer());
	}

	protected InputMultiSelect(InputMultiSelect<T> source) {
		super(source);
		this.setSelectionHandler(new MultiSelectionHandler());
		this.setSelectionRenderer(new MultiSelectionRenderer());
	}

	@Override
	public IsWidget cloneWidget() {
		return new InputMultiSelect<T>(this);
	}

	@Override
	public void addItem(T item) {
		super.addItem(item);
		this.getItemsLinks().get(item).setPreventClickWhenActive(false);
	}

	@Override
	public void onBrowserEvent(Event event) {
		super.onBrowserEvent(event);
		boolean mustKillEvent = false;
		if (DOM.eventGetType(event) == Event.ONKEYDOWN) {
			switch (event.getKeyCode()) {
				case KeyCodes.KEY_ENTER:
					if (this.getDropdown().isOpen()) {
						((MultiSelectionHandler) this.getSelectionHandler()).onEnterKeyDown();
						mustKillEvent = true;
					}
					break;
				case KeyCodes.KEY_TAB:
				case KeyCodes.KEY_ESCAPE:
					this.getDropdown().close();
					break;
				default:
					break;
			}
		}
		if (mustKillEvent) {
			event.preventDefault();
			event.stopPropagation();
		}
	}

}