/**
 * 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.base;

import com.google.common.base.Predicate;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.text.shared.Renderer;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;

import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;

import fr.putnami.pwt.core.model.client.base.HasPlaceholder;
import fr.putnami.pwt.core.theme.client.CssStyle;
import fr.putnami.pwt.core.widget.client.ButtonDropdown;
import fr.putnami.pwt.core.widget.client.DropdownHeader;
import fr.putnami.pwt.core.widget.client.NavLink;
import fr.putnami.pwt.core.widget.client.helper.CompositeFocusHelper;
import fr.putnami.pwt.core.widget.client.util.StyleUtils;

public abstract class AbstractInputSelect<T, U> extends AbstractInputChoice<T, U> implements HasPlaceholder {

	private static final CssStyle STYLE_INPUT_SELECT = new SimpleStyle("input-select");

	private class ItemClickCommand implements ScheduledCommand {
		private T item;

		ItemClickCommand(T item) {
			this.item = item;
		}

		@Override
		public void execute() {
			AbstractInputSelect.this.getSelectionHandler().onItemClick(this.item);
			AbstractInputSelect.this.setFocus(true);
		}
	}

	public interface SelectItemSelectionHandler<T, U> extends ChoiceSelectionHandler<T, U> {

		void onHomeKeyDown();

		void onEndKeyDown();

		void onUpKeyDown();

		void onDownKeyDown();

		void onItemSearch(T searchResult);
	}

	public interface SelectRenderer<T, U> extends ChoiceRenderer<T> {

		Renderer<U> getSelectionRenderer();

		void setSelectionRenderer(Renderer<U> renderer);

		String renderSelection(U selection);

		String getPlaceholder();

		void setPlaceholder(String placeholder);
	}

	private class SelectRendererImpl extends ChoiceRendererImpl implements SelectRenderer<T, U> {

		private Renderer<U> selectionRenderer;
		private String placeholder;

		@Override
		public Renderer<U> getSelectionRenderer() {
			return this.selectionRenderer;
		}

		@Override
		public void setSelectionRenderer(Renderer<U> renderer) {
			this.selectionRenderer = renderer;
		}

		@Override
		public String renderSelection(U selection) {
			return this.render(selection, this.selectionRenderer);
		}

		@Override
		public String getPlaceholder() {
			return this.placeholder;
		}

		@Override
		public void setPlaceholder(String placeholder) {
			this.placeholder = placeholder;
		}

		@Override
		protected String renderNull() {
			if (!AbstractInputSelect.this.isNullValueAllowed()) {
				return this.placeholder != null ? this.placeholder : ChoiceRendererImpl.DEFAULT_NULL_RENDER;
			}
			return super.renderNull();
		}
	}

	private class KeyPressHandler implements Predicate<NavLink> {

		private static final long KEY_INTERVAL = 1200L;

		private String currentString = "";
		private long lastSearchTime = 0L;

		public void handleKeyPress(int charCode) {
			long currentTime = System.currentTimeMillis();
			if (currentTime - this.lastSearchTime > KeyPressHandler.KEY_INTERVAL) {
				this.currentString = String.valueOf((char) charCode).toLowerCase();
			} else {
				this.currentString += String.valueOf((char) charCode).toLowerCase();
			}
			this.lastSearchTime = currentTime;
			NavLink matchingLink = Iterables.find(AbstractInputSelect.this.itemsLinks.values(), this, null);
			if (matchingLink != null) {
				AbstractInputSelect.this.selectionHandler.onItemSearch(AbstractInputSelect.this.itemsLinks.inverse().get(
					matchingLink));
			}
		}

		@Override
		public boolean apply(NavLink input) {
			return input.getLabel() != null && input.getLabel().toLowerCase().startsWith(this.currentString);
		}
	}

	private SelectRenderer<T, U> selectRenderer = new SelectRendererImpl();
	private SelectItemSelectionHandler<T, U> selectionHandler;

	private final KeyPressHandler keyPressHandler = new KeyPressHandler();
	private CompositeFocusHelper compositeFocusHelper;

	private final Map<String, Collection<T>> itemsMap;
	private BiMap<T, NavLink> itemsLinks = HashBiMap.create();

	private final SimpleDropdown dropdown;

	public AbstractInputSelect() {
		super(new ButtonDropdown());
		this.itemsMap = Maps.newLinkedHashMap();
		this.dropdown = (SimpleDropdown) this.getWidget();

		this.endConstruct();
	}

	protected AbstractInputSelect(AbstractInputSelect<T, U> source) {
		super(new ButtonDropdown(), source);
		this.dropdown = (SimpleDropdown) this.getWidget();
		this.itemsMap = source.itemsMap;
		this.selectRenderer = source.selectRenderer;

		this.endConstruct();
	}

	@Override
	protected void endConstruct() {
		StyleUtils.removeStyle(this, ButtonDropdown.STYLE_BUTTON_GROUP);
		StyleUtils.addStyle(this, AbstractInputSelect.STYLE_INPUT_SELECT);

		this.compositeFocusHelper = CompositeFocusHelper.createFocusHelper(this, this.dropdown);
		this.dropdown.addAnchorStyle(AbstractInput.STYLE_CONTROL);

		this.sinkEvents(Event.ONKEYDOWN | Event.ONKEYPRESS);

		super.endConstruct();
	}

	@Override
	public void setItems(Collection<T> items) {
		Map<String, Collection<T>> map = Maps.newHashMapWithExpectedSize(1);
		map.put(null, items);
		this.setItemsMap(map);
	}

	public void setItemsMap(Map<String, Collection<T>> itemsMap) {
		U currentValue = this.getValue();
		this.itemsMap.clear();
		this.itemsMap.putAll(itemsMap);
		this.edit(currentValue);
	}

	@Override
	protected void redrawInternal() {
		this.dropdown.clear();
		this.itemsLinks.clear();
		this.getOrderedItems().clear();

		if (this.isNullValueAllowed() && !this.isMultiple()) {
			this.addItem(null);
		}

		if (this.itemsMap != null) {
			for (Entry<String, Collection<T>> entry : this.itemsMap.entrySet()) {
				if (entry.getKey() != null) {
					this.dropdown.addMenuContent(new DropdownHeader(entry.getKey()));
				}
				for (T item : entry.getValue()) {
					this.addItem(item);
				}
			}
		}
	}

	public void addItem(T item) {
		NavLink link = new NavLink(this.getChoiceRenderer().renderItem(item), new ItemClickCommand(item));
		this.dropdown.addMenuContent(link);
		this.itemsLinks.put(item, link);
		this.getOrderedItems().add(item);
	}

	@Override
	public void setNullRender(String nullRender) {
		super.setNullRender(nullRender);
		this.renderLabel();
	}

	public void setSelectionRenderer(Renderer<U> renderer) {
		this.selectRenderer.setSelectionRenderer(renderer);
	}

	@Override
	public void setHtmlId(String htmlId) {
		super.setHtmlId(htmlId);
		this.dropdown.getAnchor().getElement().setId(htmlId);
	}

	@Override
	public String getPlaceholder() {
		return this.selectRenderer.getPlaceholder();
	}

	@Override
	public void setPlaceholder(String placeholder) {
		this.selectRenderer.setPlaceholder(placeholder);
		this.renderLabel();
	}

	@Override
	public void edit(U value) {
		super.edit(value);
		this.renderLabel();
	}

	private void renderLabel() {
		this.dropdown.setLabel(this.selectRenderer.renderSelection(this.getValue()));
	}

	@Override
	public void onValueChange(ValueChangeEvent<U> event) {
		if (event != null && event.getSource() == this) {
			this.dropdown.setLabel(this.selectRenderer.renderSelection(event.getValue()));
		}
	}

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

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

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

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

	@Override
	public com.google.gwt.event.shared.HandlerRegistration addBlurHandler(BlurHandler handler) {
		return this.compositeFocusHelper.addBlurHandler(handler);
	}

	@Override
	public com.google.gwt.event.shared.HandlerRegistration addFocusHandler(FocusHandler handler) {
		return this.compositeFocusHelper.addFocusHandler(handler);
	}

	@Override
	public void onBrowserEvent(Event event) {
		super.onBrowserEvent(event);
		boolean mustKillEvent = false;
		switch (DOM.eventGetType(event)) {
			case Event.ONKEYDOWN:
				switch (event.getKeyCode()) {
					case KeyCodes.KEY_HOME:
						this.selectionHandler.onHomeKeyDown();
						mustKillEvent = true;
						break;
					case KeyCodes.KEY_END:
						this.selectionHandler.onEndKeyDown();
						mustKillEvent = true;
						break;
					case KeyCodes.KEY_UP:
						this.selectionHandler.onUpKeyDown();
						mustKillEvent = true;
						break;
					case KeyCodes.KEY_DOWN:
						this.selectionHandler.onDownKeyDown();
						mustKillEvent = true;
						break;
					default:
						break;
				}
				break;
			case Event.ONKEYPRESS:
				this.keyPressHandler.handleKeyPress(event.getCharCode());
				break;
			default:
				break;
		}

		if (mustKillEvent) {
			event.preventDefault();
			event.stopPropagation();
		}
	}

	@Override
	protected void setChoiceRenderer(ChoiceRenderer<T> choiceRenderer) {
		if (choiceRenderer instanceof SelectRenderer) {
			this.selectRenderer = (SelectRenderer<T, U>) choiceRenderer;
		}
	}

	@Override
	protected ChoiceRenderer<T> getChoiceRenderer() {
		return this.selectRenderer;
	}

	@Override
	protected ChoiceSelectionHandler<T, U> getSelectionHandler() {
		assert this.selectionHandler != null : "SelectionHandler is not set !";
		return this.selectionHandler;
	}

	@Override
	protected void setSelectionHandler(ChoiceSelectionHandler<T, U> selectionHandler) {
		if (selectionHandler instanceof SelectItemSelectionHandler) {
			this.selectionHandler = (SelectItemSelectionHandler<T, U>) selectionHandler;
		}
	}

	protected Map<T, NavLink> getItemsLinks() {
		return this.itemsLinks;
	}

	protected SimpleDropdown getDropdown() {
		return this.dropdown;
	}

}