/*
 * 01/06/2009
 *
 * MarkOccurrencesSupport.java - Handles marking all occurrences of the
 * currently selected identifier in a text area.
 * 
 * This library is distributed under a modified BSD license.  See the included
 * RSyntaxTextArea.License.txt file for details.
 */
package com.fr.design.gui.syntax.ui.rsyntaxtextarea;

import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.Timer;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;

import com.fr.design.gui.syntax.ui.rtextarea.SmartHighlightPainter;



/**
 * Marks all occurrences of the token at the current caret position, if it is
 * an identifier.
 *
 * @author Robert Futrell
 * @version 1.0
 * @see OccurrenceMarker
 */
class MarkOccurrencesSupport implements CaretListener, ActionListener {

	private RSyntaxTextArea textArea;
	private Timer timer;
	private SmartHighlightPainter p;

	/**
	 * The default color used to mark occurrences.
	 */
	public static final Color DEFAULT_COLOR = new Color(224, 224, 224);

	/**
	 * The default delay.
	 */
	private static final int DEFAULT_DELAY_MS = 1000;


	/**
	 * Constructor.  Creates a listener with a 1 second delay.
	 */
	public MarkOccurrencesSupport() {
		this(DEFAULT_DELAY_MS);
	}


	/**
	 * Constructor.
	 *
	 * @param delay The delay between when the caret last moves and when the
	 *        text should be scanned for matching occurrences.  This should
	 *        be in milliseconds.
	 */
	public MarkOccurrencesSupport(int delay) {
		this(delay, DEFAULT_COLOR);
	}


	/**
	 * Constructor.
	 *
	 * @param delay The delay between when the caret last moves and when the
	 *        text should be scanned for matching occurrences.  This should
	 *        be in milliseconds.
	 * @param color The color to use to mark the occurrences.  This cannot be
	 *        <code>null</code>.
	 */
	public MarkOccurrencesSupport(int delay, Color color) {
		timer = new Timer(delay, this);
		timer.setRepeats(false);
		p = new SmartHighlightPainter();
		setColor(color);
	}


	/**
	 * Called after the caret has been moved and a fixed time delay has
	 * elapsed.  This locates and highlights all occurrences of the identifier
	 * at the caret position, if any.
	 *
	 * @param e The event.
	 */
	public void actionPerformed(ActionEvent e) {

		// Don't do anything if they are selecting text.
		Caret c = textArea.getCaret();
		if (c.getDot()!=c.getMark()) {
			return;
		}

		RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
		OccurrenceMarker occurrenceMarker = doc.getOccurrenceMarker();
		boolean occurrencesChanged = false;

		if (occurrenceMarker!=null) {

			doc.readLock();
			try {

				// Get the token at the caret position.
				int line = textArea.getCaretLineNumber();
				Token tokenList = textArea.getTokenListForLine(line);
				int dot = c.getDot();
				Token t = RSyntaxUtilities.getTokenAtOffset(tokenList, dot);
				if (t==null /* EOL */ || !isValidType(t) || isNonWordChar(t)) {
					// Try to the "left" of the caret.
					dot--;
					try {
						if (dot>=textArea.getLineStartOffset(line)) {
							t = RSyntaxUtilities.getTokenAtOffset(tokenList, dot);
						}
					} catch (BadLocationException ble) {
						ble.printStackTrace(); // Never happens
					}
				}

				if (t!=null && isValidType(t) && !isNonWordChar(t)) {
					removeHighlights();
					RSyntaxTextAreaHighlighter h = (RSyntaxTextAreaHighlighter)
							textArea.getHighlighter();
					occurrenceMarker.markOccurrences(doc, t, h, p);
					//textArea.repaint();
					// TODO: Do a textArea.repaint() instead of repainting each
					// marker as it's added if count is huge
					occurrencesChanged = true;
				}

			} finally {
				doc.readUnlock();
				//time = System.currentTimeMillis() - time;
				//System.out.println("MarkOccurrencesSupport took: " + time + " ms");
			}

		}

		if (occurrencesChanged) {
			textArea.fireMarkedOccurrencesChanged();
		}

	}


	/**
	 * Called when the caret moves in the text area.
	 *
	 * @param e The event.
	 */
	public void caretUpdate(CaretEvent e) {
		timer.restart();
	}


	/**
	 * Returns the color being used to mark occurrences.
	 *
	 * @return The color being used.
	 * @see #setColor(Color)
	 */
	public Color getColor() {
		return (Color)p.getPaint();
	}


	/**
	 * Returns the delay, in milliseconds.
	 *
	 * @return The delay.
	 * @see #setDelay(int)
	 */
	public int getDelay() {
		return timer.getDelay();
	}


	/**
	 * Returns whether a border is painted around marked occurrences.
	 *
	 * @return Whether a border is painted.
	 * @see #setPaintBorder(boolean)
	 * @see #getColor()
	 */
	public boolean getPaintBorder() {
		return p.getPaintBorder();
	}


	/**
	 * Installs this listener on a text area.  If it is already installed on
	 * another text area, it is uninstalled first.
	 *
	 * @param textArea The text area to install on.
	 */
	public void install(RSyntaxTextArea textArea) {
		if (this.textArea!=null) {
			uninstall();
		}
		this.textArea = textArea;
		textArea.addCaretListener(this);
		if (textArea.getMarkOccurrencesColor()!=null) {
			setColor(textArea.getMarkOccurrencesColor());
		}
	}


	/**
	 * Returns whether the specified token is a single non-word char (e.g. not
	 * in <tt>[A-Za-z]</tt>.  This is a HACK to work around the fact that many
	 * standard token makers return things like semicolons and periods as
	 * {@link Token#IDENTIFIER}s just to make the syntax highlighting coloring
	 * look a little better.
	 * 
	 * @param t The token to check.  This cannot be <tt>null</tt>.
	 * @return Whether the token is a single non-word char.
	 */
	private static final boolean isNonWordChar(Token t) {
		return t.length()==1 &&
				!RSyntaxUtilities.isLetter(t.charAt(0));
	}


	/**
	 * Returns whether the specified token is a type that we can do a
	 * "mark occurrences" on.
	 *
	 * @param t The token.
	 * @return Whether we should mark all occurrences of this token.
	 */
	private boolean isValidType(Token t) {
		return textArea.getMarkOccurrencesOfTokenType(t.getType());
	}


	/**
	 * Removes all highlights added to the text area by this listener.
	 */
	private void removeHighlights() {
		if (textArea!=null) {
			RSyntaxTextAreaHighlighter h = (RSyntaxTextAreaHighlighter)
													textArea.getHighlighter();
			h.clearMarkOccurrencesHighlights();
		}
	}


	/**
	 * Sets the color to use when marking occurrences.
	 *
	 * @param color The color to use.
	 * @see #getColor()
	 * @see #setPaintBorder(boolean)
	 */
	public void setColor(Color color) {
		p.setPaint(color);
		if (textArea!=null) {
			removeHighlights();
			caretUpdate(null); // Force a highlight repaint.
		}
	}


	/**
	 * Sets the delay between the last caret position change and when the
	 * text is scanned for matching identifiers.  A delay is needed to prevent
	 * repeated scanning while the user is typing.
	 *
	 * @param delay The new delay.
	 * @see #getDelay()
	 */
	public void setDelay(int delay) {
		timer.setDelay(delay);
	}


	/**
	 * Toggles whether a border is painted around marked highlights.
	 *
	 * @param paint Whether to paint a border.
	 * @see #getPaintBorder()
	 * @see #setColor(Color)
	 */
	public void setPaintBorder(boolean paint) {
		if (paint!=p.getPaintBorder()) {
			p.setPaintBorder(paint);
			if (textArea!=null) {
				textArea.repaint();
			}
		}
	}


	/**
	 * Uninstalls this listener from the current text area.  Does nothing if
	 * it not currently installed on any text area.
	 *
	 * @see #install(RSyntaxTextArea)
	 */
	public void uninstall() {
		if (textArea!=null) {
			removeHighlights();
			textArea.removeCaretListener(this);
		}
	}


}