/*******************************************************************************
 * Copyright (c) 2000, 2003 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package net.sourceforge.phpdt.internal.ui.text;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.jface.text.Assert;
import org.eclipse.jface.text.rules.ICharacterScanner;
import org.eclipse.jface.text.rules.IRule;
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.jface.text.rules.IWordDetector;
import org.eclipse.jface.text.rules.Token;

/**
 * An implementation of <code>IRule</code> capable of detecting words.
 * <p>
 * Word rules also allow for the association of tokens with specific words. That
 * is, not only can the rule be used to provide tokens for exact matches, but
 * also for the generalized notion of a word in the context in which it is used.
 * A word rules uses a word detector to determine what a word is.
 * </p>
 * <p>
 * This word rule allows a word detector to be shared among different word
 * matchers. Its up to the word matchers to decide if a word matches and, in
 * this a case, which token is associated with that word.
 * </p>
 * 
 * @see IWordDetector
 * @since 3.0
 */
public class CombinedWordRule implements IRule {

	/**
	 * Word matcher, that associates matched words with tokens.
	 */
	public static class WordMatcher {

		/** The table of predefined words and token for this matcher */
		private Map fWords = new HashMap();

		/**
		 * Adds a word and the token to be returned if it is detected.
		 * 
		 * @param word
		 *            the word this rule will search for, may not be
		 *            <code>null</code>
		 * @param token
		 *            the token to be returned if the word has been found, may
		 *            not be <code>null</code>
		 */
		public void addWord(String word, IToken token) {
			Assert.isNotNull(word);
			Assert.isNotNull(token);

			fWords.put(new CharacterBuffer(word), token);
		}

		/**
		 * Returns the token associated to the given word and the scanner state.
		 * 
		 * @param scanner
		 *            the scanner
		 * @param word
		 *            the word
		 * @return the token or <code>null</code> if none is associated by
		 *         this matcher
		 */
		public IToken evaluate(ICharacterScanner scanner, CharacterBuffer word) {
			IToken token = (IToken) fWords.get(word);
			if (token != null)
				return token;
			return Token.UNDEFINED;
		}

		/**
		 * Removes all words.
		 */
		public void clearWords() {
			fWords.clear();
		}
	}

	/**
	 * Character buffer, mutable <b>or</b> suitable for use as key in hash
	 * maps.
	 */
	public static class CharacterBuffer {

		/** Buffer content */
		private char[] fContent;

		/** Buffer content size */
		private int fLength = 0;

		/** Is hash code cached? */
		private boolean fIsHashCached = false;

		/** The hash code */
		private int fHashCode;

		/**
		 * Initialize with the given capacity.
		 * 
		 * @param capacity
		 *            the initial capacity
		 */
		public CharacterBuffer(int capacity) {
			fContent = new char[capacity];
		}

		/**
		 * Initialize with the given content.
		 * 
		 * @param string
		 *            the initial content
		 */
		public CharacterBuffer(String content) {
			fContent = content.toCharArray();
			fLength = content.length();
		}

		/**
		 * Empties this buffer.
		 */
		public void clear() {
			fIsHashCached = false;
			fLength = 0;
		}

		/**
		 * Appends the given character to the buffer.
		 * 
		 * @param c
		 *            the character
		 */
		public void append(char c) {
			fIsHashCached = false;
			if (fLength == fContent.length) {
				char[] old = fContent;
				fContent = new char[old.length << 1];
				System.arraycopy(old, 0, fContent, 0, old.length);
			}
			fContent[fLength++] = c;
		}

		/**
		 * Returns the length of the content.
		 * 
		 * @return the length
		 */
		public int length() {
			return fLength;
		}

		/**
		 * Returns the content as string.
		 * 
		 * @return the content
		 */
		public String toString() {
			return new String(fContent, 0, fLength);
		}

		/**
		 * Returns the character at the given position.
		 * 
		 * @param i
		 *            the position
		 * @return the character at position <code>i</code>
		 */
		public char charAt(int i) {
			return fContent[i];
		}

		/*
		 * @see java.lang.Object#hashCode()
		 */
		public int hashCode() {
			if (fIsHashCached)
				return fHashCode;

			int hash = 0;
			for (int i = 0, n = fLength; i < n; i++)
				hash = 29 * hash + fContent[i];
			fHashCode = hash;
			fIsHashCached = true;
			return hash;
		}

		/*
		 * @see java.lang.Object#equals(java.lang.Object)
		 */
		public boolean equals(Object obj) {
			if (obj == this)
				return true;
			if (!(obj instanceof CharacterBuffer))
				return false;
			CharacterBuffer buffer = (CharacterBuffer) obj;
			int length = buffer.length();
			if (length != fLength)
				return false;
			for (int i = 0; i < length; i++)
				if (buffer.charAt(i) != fContent[i])
					return false;
			return true;
		}

		/**
		 * Is the content equal to the given string?
		 * 
		 * @param string
		 *            the string
		 * @return <code>true</code> iff the content is the same character
		 *         sequence as in the string
		 */
		public boolean equals(String string) {
			int length = string.length();
			if (length != fLength)
				return false;
			for (int i = 0; i < length; i++)
				if (string.charAt(i) != fContent[i])
					return false;
			return true;
		}
	}

	/** Internal setting for the uninitialized column constraint */
	private static final int UNDEFINED = -1;

	/** The word detector used by this rule */
	private IWordDetector fDetector;

	/**
	 * The default token to be returned on success and if nothing else has been
	 * specified.
	 */
	private IToken fDefaultToken;

	/** The column constraint */
	private int fColumn = UNDEFINED;

	/** Buffer used for pattern detection */
	private CharacterBuffer fBuffer = new CharacterBuffer(16);

	/** List of word matchers */
	private List fMatchers = new ArrayList();

	/**
	 * Creates a rule which, with the help of an word detector, will return the
	 * token associated with the detected word. If no token has been associated,
	 * the scanner will be rolled back and an undefined token will be returned
	 * in order to allow any subsequent rules to analyze the characters.
	 * 
	 * @param detector
	 *            the word detector to be used by this rule, may not be
	 *            <code>null</code>
	 * 
	 * @see #addWord(String, IToken)
	 */
	public CombinedWordRule(IWordDetector detector) {
		this(detector, null, Token.UNDEFINED);
	}

	/**
	 * Creates a rule which, with the help of an word detector, will return the
	 * token associated with the detected word. If no token has been associated,
	 * the specified default token will be returned.
	 * 
	 * @param detector
	 *            the word detector to be used by this rule, may not be
	 *            <code>null</code>
	 * @param defaultToken
	 *            the default token to be returned on success if nothing else is
	 *            specified, may not be <code>null</code>
	 * 
	 * @see #addWord(String, IToken)
	 */
	public CombinedWordRule(IWordDetector detector, IToken defaultToken) {
		this(detector, null, defaultToken);
	}

	/**
	 * Creates a rule which, with the help of an word detector, will return the
	 * token associated with the detected word. If no token has been associated,
	 * the scanner will be rolled back and an undefined token will be returned
	 * in order to allow any subsequent rules to analyze the characters.
	 * 
	 * @param detector
	 *            the word detector to be used by this rule, may not be
	 *            <code>null</code>
	 * @param matcher
	 *            the initial word matcher
	 * 
	 * @see #addWord(String, IToken)
	 */
	public CombinedWordRule(IWordDetector detector, WordMatcher matcher) {
		this(detector, matcher, Token.UNDEFINED);
	}

	/**
	 * Creates a rule which, with the help of an word detector, will return the
	 * token associated with the detected word. If no token has been associated,
	 * the specified default token will be returned.
	 * 
	 * @param detector
	 *            the word detector to be used by this rule, may not be
	 *            <code>null</code>
	 * @param matcher
	 *            the initial word matcher
	 * @param defaultToken
	 *            the default token to be returned on success if nothing else is
	 *            specified, may not be <code>null</code>
	 * 
	 * @see #addWord(String, IToken)
	 */
	public CombinedWordRule(IWordDetector detector, WordMatcher matcher,
			IToken defaultToken) {

		Assert.isNotNull(detector);
		Assert.isNotNull(defaultToken);

		fDetector = detector;
		fDefaultToken = defaultToken;
		if (matcher != null)
			addWordMatcher(matcher);
	}

	/**
	 * Adds the given matcher.
	 * 
	 * @param matcher
	 *            the matcher
	 */
	public void addWordMatcher(WordMatcher matcher) {
		fMatchers.add(matcher);
	}

	/**
	 * Sets a column constraint for this rule. If set, the rule's token will
	 * only be returned if the pattern is detected starting at the specified
	 * column. If the column is smaller then 0, the column constraint is
	 * considered removed.
	 * 
	 * @param column
	 *            the column in which the pattern starts
	 */
	public void setColumnConstraint(int column) {
		if (column < 0)
			column = UNDEFINED;
		fColumn = column;
	}

	/*
	 * @see IRule#evaluate(ICharacterScanner)
	 */
	public IToken evaluate(ICharacterScanner scanner) {
		int c = scanner.read();
		if (fDetector.isWordStart((char) c)) {
			if (fColumn == UNDEFINED || (fColumn == scanner.getColumn() - 1)) {

				fBuffer.clear();
				do {
					fBuffer.append((char) c);
					c = scanner.read();
				} while (c != ICharacterScanner.EOF
						&& fDetector.isWordPart((char) c));
				scanner.unread();

				for (int i = 0, n = fMatchers.size(); i < n; i++) {
					IToken token = ((WordMatcher) fMatchers.get(i)).evaluate(
							scanner, fBuffer);
					if (!token.isUndefined())
						return token;
				}

				if (fDefaultToken.isUndefined())
					unreadBuffer(scanner);

				return fDefaultToken;
			}
		}

		scanner.unread();
		return Token.UNDEFINED;
	}

	/**
	 * Returns the characters in the buffer to the scanner.
	 * 
	 * @param scanner
	 *            the scanner to be used
	 */
	private void unreadBuffer(ICharacterScanner scanner) {
		for (int i = fBuffer.length() - 1; i >= 0; i--)
			scanner.unread();
	}
}
