package org.geogebra.desktop.gui.autocompletion;

import static java.awt.event.KeyEvent.VK_DOWN;
import static java.awt.event.KeyEvent.VK_ENTER;
import static java.awt.event.KeyEvent.VK_ESCAPE;
import static java.awt.event.KeyEvent.VK_PAGE_DOWN;
import static java.awt.event.KeyEvent.VK_PAGE_UP;
import static java.awt.event.KeyEvent.VK_TAB;
import static java.awt.event.KeyEvent.VK_UP;

import java.awt.Font;
import java.awt.Rectangle;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.HierarchyBoundsAdapter;
import java.awt.event.HierarchyEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.JList;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.text.BadLocationException;

import org.geogebra.desktop.gui.inputfield.AutoCompleteTextFieldD;

/**
 * Provides completion popup for {@link AutoCompleteTextFieldD}. Derived from
 * OptionsPopup.
 * 
 * @author Arnaud Delobelle
 */
public class CompletionsPopup {
	private final AutoCompleteTextFieldD textField;
	private final int maxPopupRowCount;

	private final JPopupMenu popup;
	private final DelegatingListModel listModel;
	private final JList list;

	private DocumentListener textFieldDocListener;
	private KeyListener keyListener;
	private KeyListener[] textFieldKeyListeners;
	private int current_length;

	/**
	 * Initializes components and registers event listeners.
	 * 
	 * @param textField
	 *            The text field
	 * @param listCellRenderer
	 *            A list cell renderer which visualizes the options returned by
	 *            the provided {@link CompletionProvider}
	 * @param maxPopupRowCount
	 *            The maximal number of rows for the options popup
	 */
	public CompletionsPopup(AutoCompleteTextFieldD textField,
			ListCellRenderer listCellRenderer, int maxPopupRowCount) {
		this.textField = textField;
		this.maxPopupRowCount = maxPopupRowCount;

		// Initialize components
		listModel = new DelegatingListModel();
		list = new JList(listModel);
		list.setCellRenderer(listCellRenderer);
		list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
		list.setFocusable(false);
		popup = new JPopupMenu();
		popup.add(new JScrollPane(list));
		popup.setBorder(BorderFactory.createEmptyBorder());
		popup.setFocusable(false);
		current_length = -1; // current length of sentence
		registerListeners();
	}

	/**
	 * Set the font to display the completions
	 * 
	 * @param font
	 *            the new font
	 */
	public void setFont(Font font) {
		list.setFont(font);
	}

	private class PopupListener implements PopupMenuListener {

		@Override
		public void popupMenuCanceled(PopupMenuEvent e) {
			// ignore
		}

		@Override
		public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
			textField.removeKeyListener(keyListener);
			for (KeyListener listener : textFieldKeyListeners) {
				textField.addKeyListener(listener);
			}
		}

		@Override
		public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
			// Remove key listeners and replace with own;
			textFieldKeyListeners = textField.getKeyListeners();
			for (KeyListener listener : textFieldKeyListeners) {
				textField.removeKeyListener(listener);
			}
			textField.addKeyListener(keyListener);
		}
	}

	private void registerListeners() {
		// Suggest completions on text changes, store reference to listener
		// object
		textFieldDocListener = new DocumentListener() {
			@Override
			public void removeUpdate(DocumentEvent e) {
				// only handle insert
			}

			@Override
			public void insertUpdate(DocumentEvent e) { /* showCompletions(); */
				if (current_length != e.getOffset()) {
					hidePopup();
					current_length = e.getOffset();
				}
			}

			@Override
			public void changedUpdate(DocumentEvent e) {
				// only handle insert
			}
		};

		textField.getDocument().addDocumentListener(textFieldDocListener);
		// Handle special keys (e.g. navigation)
		keyListener = new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent e) {
				handleSpecialKeys(e);
			}
		};
		// Hide popup when text field loses focus
		textField.addFocusListener(new FocusAdapter() {
			@Override
			public void focusLost(FocusEvent e) {
				hidePopup();
				current_length = -1;
			}
		});
		// Allow the user click on an option for completion
		list.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {
				handleMouseClick(e);
			}
		});
		// This doesn't work very well :(
		textField.addHierarchyBoundsListener(new HierarchyBoundsAdapter() {
			@Override
			public void ancestorMoved(HierarchyEvent e) {
				if (isPopupVisible()) {
					placePopup();
				}
			}
		});
		popup.addPopupMenuListener(new PopupListener());
	}

	protected void placePopup() {
		Rectangle startRect;
		try {
			startRect = textField.modelToView(textField.getCurrentWordStart());
		} catch (BadLocationException e) {
			// This won't happen of course :)
			startRect = new Rectangle(0, 0, 0, 0);
		}
		// Try to show popup just beneath the word to be completed
		popup.show(textField, startRect.x, startRect.y + startRect.height);
		// If it overlaps the word, then show the popup above the word
		if (popup.getLocationOnScreen().y
				- textField.getLocationOnScreen().y < startRect.y
						+ startRect.height) {
			popup.show(textField, startRect.x, startRect.y - popup.getHeight());
		}
	}

	/**
	 * Show completions
	 */
	public void showCompletions() {
		if (!textField.getAutoComplete()) {
			return;
		}
		List<String> completions = textField.getCompletions();
		if (completions == null) {
			hidePopup();
			return;
		}
		if (completions.size() > 0) {
			listModel.setDataList(completions);
			list.setSelectedIndex(0);
			list.ensureIndexIsVisible(0);
			showPopup();
		} else {
			hidePopup();
		}
	}

	private void showPopup() {
		// Adjust size of the popup if necessary
		int newPopupRowCount = Math.min(listModel.getSize(), maxPopupRowCount);
		list.setVisibleRowCount(newPopupRowCount);
		// Let the UI calculate the preferred size
		popup.setPreferredSize(null);
		popup.pack();
		placePopup();
	}

	private boolean isPopupVisible() {
		return popup.isVisible();
	}

	private void hidePopup() {
		if (!isPopupVisible()) {
			return;
		}
		popup.setVisible(false);
		list.clearSelection();
		// Reinstate textField's key listeners
	}

	protected void handleSpecialKeys(KeyEvent keyEvent) {
		if (!isPopupVisible()) {
			return;
		}

		switch (keyEvent.getKeyCode()) {
		case VK_ESCAPE: // [ESC] cancels the popup
			textField.cancelAutoCompletion();
			hidePopup();
			keyEvent.consume();
			break;
		case VK_ENTER: // [ENTER] validates the completions
			textField.validateAutoCompletion(list.getSelectedIndex(),
					textField.getCompletions());
			hidePopup();
			keyEvent.consume();
			break;
		case VK_DOWN: // [DOWN] next completion
		case VK_TAB: // [TAB]
			navigateRelative(+1);
			keyEvent.consume();
			break;
		case VK_UP: // [UP] prev. completion
			navigateRelative(-1);
			keyEvent.consume();
			break;
		case VK_PAGE_DOWN: // [PAGE_DOWN]
			navigateRelative(+maxPopupRowCount - 1);
			keyEvent.consume();
			break;
		case VK_PAGE_UP: // [PAGE_UP]
			navigateRelative(-maxPopupRowCount + 1);
			keyEvent.consume();
			break;
		default:
			hidePopup();
			current_length = -1;
			textField.processKeyEvent(keyEvent);
		}
	}

	private void navigateRelative(int offset) {
		boolean up = offset < 0;
		int end = listModel.getSize() - 1;
		int index = list.getSelectedIndex();

		// Wrap around
		if (-1 == index) {
			index = up ? end : 0;
		} else if (0 == index && up || end == index && !up) {
			index = -1;
		} else {
			index += offset;
			index = Math.max(0, Math.min(end, index));
		}

		if (-1 == index) {
			list.clearSelection();
		} else {
			list.setSelectedIndex(index);
			list.ensureIndexIsVisible(index);
		}
	}

	private void handleMouseClick(MouseEvent e) {
		if (SwingUtilities.isLeftMouseButton(e)) {
			textField.validateAutoCompletion(list.getSelectedIndex(),
					textField.getCompletions());
			hidePopup();
			current_length = -1;
		}
	}
}
