/*
 * Copyright Neil Cochrane 2006
 * @author neilcochrane
 *
 * Feel free to use the code without restriction
 * in open source projects.
 * There is no cost or warranty attached and the code is
 * provided as is.
 *
 * http://www.jroller.com/swinguistuff/entry/text_completion
 */
package com.jroller.completer;

import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;

import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JWindow;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;


public class CompleterFilterWithWindow extends CompleterFilter
{

	public CompleterFilterWithWindow(Object[] completerObjs, JTextComponent textField)
	{
		super(completerObjs, textField);
		_init();
	}

	@Override
	public void insertString(FilterBypass filterBypass, int offset, String string, AttributeSet attributeSet) throws BadLocationException
	{
		setFilterWindowVisible(false);
		super.insertString(filterBypass, offset, string, attributeSet);
	}

	@Override
	public void remove(FilterBypass filterBypass, int offset, int length) throws BadLocationException
	{
		setFilterWindowVisible(false);
		super.remove(filterBypass, offset, length);
	}

	@Override
	public void replace(FilterBypass filterBypass, int offset, int length, String string, AttributeSet attributeSet) throws BadLocationException
	{
		if(!isPerformCompletion()) {
			super.replace(filterBypass, offset, length, string, attributeSet);
			return;
		}

		if (_isAdjusting)
		{
			filterBypass.replace(offset, length, string, attributeSet);
			return;
		}

		super.replace(filterBypass, offset, length, string, attributeSet);

		if (getLeadingSelectedIndex() == -1)
		{
			if (isFilterWindowVisible())
				setFilterWindowVisible(false);

			return;
		}

		_lm.setFilter(_preText);

		if (!isFilterWindowVisible())
			setFilterWindowVisible(true);
		else
			_setWindowHeight();

		_list.setSelectedValue(_textField.getText(), true);
	}

	private void _init()
	{
		_fwl = new FilterWindowListener();
		_lm = new FilterListModel(_objectList);
		_tfkl = new TextFieldKeyListener();
		_textField.addKeyListener(_tfkl);

		EscapeAction escape = new EscapeAction();
		_textField.registerKeyboardAction(escape, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0),
				JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
	}

	public boolean isFilterWindowVisible()
	{
		return ((_win != null) && (_win.isVisible()));
	}

	public void setCaseSensitive(boolean caseSensitive)
	{
		super.setCaseSensitive(caseSensitive);
		_lm.setCaseSensitive(caseSensitive);
	}

	public void setFilterWindowVisible(boolean visible)
	{
		if (visible)
		{
			_initWindow();
			_list.setModel(_lm);
			_win.setVisible(true);
			_textField.requestFocus();
			_textField.addFocusListener(_fwl);
		}
		else
		{
			if (_win == null)
				return;

			_win.setVisible(false);
			_win.removeFocusListener(_fwl);
			Window ancestor = SwingUtilities.getWindowAncestor(_textField);
			ancestor.removeMouseListener(_fwl);
			_textField.removeFocusListener(_fwl);
			_textField.removeAncestorListener(_fwl);
			_list.removeMouseListener(_lml);
			_list.removeListSelectionListener(_lsl);
			_lsl = null;
			_lml = null;
			_win.dispose();
			_win = null;
			_list = null;
		}
	}

	private void _initWindow()
	{
		Window ancestor = SwingUtilities.getWindowAncestor(_textField);
		_win = new JWindow(ancestor);
		_win.addWindowFocusListener(_fwl);
		_textField.addAncestorListener(_fwl);
		ancestor.addMouseListener(_fwl);
		_lsl = new ListSelListener();
		_lml = new ListMouseListener();

		_list = new JList(_lm);
		_list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
		_list.setFocusable(false);
		_list.setPrototypeCellValue("Prototype");
		_list.addListSelectionListener(_lsl);
		_list.addMouseListener(_lml);

		_sp = new JScrollPane(_list,
				JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
				JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
		_sp.setFocusable(false);
		_sp.getVerticalScrollBar().setFocusable(false);

		_setWindowHeight();
		_win.setLocation(_textField.getLocationOnScreen().x, _textField.getLocationOnScreen().y + _textField.getHeight());
		_win.getContentPane().add(_sp);
	}

	private void _setWindowHeight()
	{
		int height = _list.getFixedCellHeight() * Math.min(MAX_VISIBLE_ROWS, _lm.getSize());
		height += _list.getInsets().top + _list.getInsets().bottom;
		height += _sp.getInsets().top + _sp.getInsets().bottom;

		_win.setSize(_textField.getWidth(), height);
		_sp.setSize(_textField.getWidth(), height); // bottom border fails to draw without this
	}

	public void setCompleterMatches(Object[] objectsToMatch)
	{
		boolean visible = isFilterWindowVisible();
		if (visible)
			setFilterWindowVisible(false);

		super.setCompleterMatches(objectsToMatch);
		_lm.setCompleterMatches(objectsToMatch);
		_lm.setFilter(_preText);
		if(visible)
			setFilterWindowVisible(true);

	}

	class EscapeAction extends AbstractAction
	{
		public void actionPerformed(ActionEvent e)
		{
			if (isFilterWindowVisible())
				setFilterWindowVisible(false);
		}
	}

	private class FilterWindowListener extends MouseAdapter
	implements AncestorListener, FocusListener, WindowFocusListener
	{
		public void ancestorMoved(AncestorEvent event)
		{
			setFilterWindowVisible(false);
		}
		public void ancestorAdded(AncestorEvent event)
		{
			setFilterWindowVisible(false);
		}
		public void ancestorRemoved(AncestorEvent event)
		{
			setFilterWindowVisible(false);
		}

		public void focusLost(FocusEvent e)
		{
			if (e.getOppositeComponent() != _win)
				setFilterWindowVisible(false);
		}

		public void focusGained(FocusEvent e){}

		public void windowLostFocus(WindowEvent e)
		{
			Window w = e.getOppositeWindow();

			if (w.getFocusOwner() != _textField)
				setFilterWindowVisible(false);
		}
		public void windowGainedFocus(WindowEvent e) {}

		@Override
		public void mousePressed(MouseEvent e)
		{
			setFilterWindowVisible(false);
		}
	}

	private class TextFieldKeyListener extends KeyAdapter
	{
		@Override
		public void keyPressed(KeyEvent e)
		{
			// return immediately if it's not one of the control keys we're interested in.
			if (!(
					e.getKeyCode() == KeyEvent.VK_UP || 
					e.getKeyCode() == KeyEvent.VK_PAGE_DOWN ||
					e.getKeyCode() == KeyEvent.VK_PAGE_UP ||
					e.getKeyCode() == KeyEvent.VK_ENTER ||
					e.getKeyCode() == KeyEvent.VK_DOWN))
				return;
			
			// pressing down opens the filter window immediately, even if the data is not fully loaded.
			if ((e.getKeyCode() == KeyEvent.VK_DOWN) && !isFilterWindowVisible())
			{
				_preText = _textField.getText();
				_lm.setFilter(_preText);

				if (_lm.getSize() > 0)
					setFilterWindowVisible(true);
				else
					return;
			}
			
			if (isFilterWindowVisible()) 
			{
				// pressing enter closes the filter window
				if (e.getKeyCode() == KeyEvent.VK_ENTER)
				{
					setFilterWindowVisible(false);
					return;
				}
	
				int index = -1;
	
				// navigation keys move the selection in the filter window
				if (e.getKeyCode() == KeyEvent.VK_DOWN)
					index = Math.min(_list.getSelectedIndex() + 1, _list.getModel().getSize()-1);
				else if (e.getKeyCode() == KeyEvent.VK_UP)
					index = Math.max(_list.getSelectedIndex() - 1, 0);
				else if (e.getKeyCode() == KeyEvent.VK_PAGE_UP)
					index = Math.max(_list.getSelectedIndex() - MAX_VISIBLE_ROWS, 0);
				else if (e.getKeyCode() == KeyEvent.VK_PAGE_DOWN)
					index = Math.min(_list.getSelectedIndex() + MAX_VISIBLE_ROWS, _list.getModel().getSize()-1);
	
				if (index == -1)
					return;
	
				_list.setSelectedIndex(index);
				_list.scrollRectToVisible(_list.getCellBounds(index, index));
			}
		}
	}

	private class ListSelListener implements ListSelectionListener
	{
		public void valueChanged(ListSelectionEvent e)
		{
			_isAdjusting = true;
			_textField.setText(_list.getSelectedValue().toString());
			_isAdjusting = false;
			_textField.select(_preText.length(), _textField.getText().length());
		}
	}

	private class ListMouseListener extends MouseAdapter
	{
		@Override
		public void mouseClicked(MouseEvent e)
		{
			if (e.getClickCount() == 2)
				setFilterWindowVisible(false);
		}
	}

	protected FilterWindowListener _fwl;
	protected JWindow _win;
	protected TextFieldKeyListener _tfkl;
	protected ListSelListener _lsl;
	protected ListMouseListener _lml;
	protected JList _list;
	protected JScrollPane _sp;
	protected FilterListModel _lm;

	protected boolean _isAdjusting = false;

	public static int MAX_VISIBLE_ROWS = 8;
}
