/*
 * FindBugs - Find bugs in Java programs
 * Copyright (C) 2003,2004 University of Maryland
 * 
 * This library 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 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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 this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs;

import java.io.IOException;
import java.io.PushbackReader;
import java.io.Reader;
import java.util.BitSet;

/**
 * A simple tokenizer for Java source text.
 * This is not intended to be a compliant lexer;
 * instead, it is for quick and dirty scanning.
 *
 * @author David Hovemeyer
 * @see Token
 */
public class Tokenizer {
	private static final BitSet whiteSpace = new BitSet();

	static {
		whiteSpace.set(' ');
		whiteSpace.set('\t');
		whiteSpace.set('\r');
		whiteSpace.set('\f');
	}

	private static final BitSet single = new BitSet();

	static {
		single.set('!');
		single.set('%');
		single.set('^');
		single.set('&');
		single.set('*');
		single.set('(');
		single.set(')');
		single.set('-');
		single.set('+');
		single.set('=');
		single.set('[');
		single.set(']');
		single.set('{');
		single.set('}');
		single.set('|');
		single.set(':');
		single.set(';');
		single.set(',');
		single.set('.');
		single.set('<');
		single.set('>');
		single.set('?');
		single.set('~');
	}

	private PushbackReader reader;

	/**
	 * Constructor.
	 *
	 * @param reader the Reader for the Java source text
	 */
	public Tokenizer(Reader reader) {
		this.reader = new PushbackReader(reader);
	}

	/**
	 * Get the next Token in the stream.
	 *
	 * @return the Token
	 */
	public Token next() throws IOException {
		skipWhitespace();
		int c = reader.read();
		if (c < 0)
			return new Token(Token.EOF);
		else if (c == '\n')
			return new Token(Token.EOL);
		else if (c == '\'' || c == '"')
			return munchString(c);
		else if (c == '/')
			return maybeComment();
		else if (single.get(c))
			return new Token(Token.SINGLE, String.valueOf((char) c));
		else {
			reader.unread(c);
			return parseWord();
		}
	}

	private void skipWhitespace() throws IOException {
		for (; ;) {
			int c = reader.read();
			if (c < 0) break;
			if (!whiteSpace.get(c)) {
				reader.unread(c);
				break;
			}
		}
	}

	private Token munchString(int delimiter) throws IOException {
		final int SCAN = 0;
		final int ESCAPE = 1;
		final int DONE = 2;

		StringBuilder result = new StringBuilder();
		result.append((char) delimiter);
		int state = SCAN;

			while (state != DONE) {
				int c = reader.read();
				if (c < 0)
					break;
				result.append((char) c);
				switch (state) {
				case SCAN:
					if (c == delimiter)
						state = DONE;
					else if (c == '\\')
						state = ESCAPE;
					break;
				case ESCAPE:
					state = SCAN;
					break;
				}
			}
		return new Token(Token.STRING, result.toString());
	}

	private Token maybeComment() throws IOException {
		int c = reader.read();
		if (c == '/') {
			// Single line comment
			StringBuilder result = new StringBuilder();
			result.append("//");
			for (; ;) {
				c = reader.read();
				if (c < 0)
					break;
				else if (c == '\n') {
					reader.unread(c);
					break;
				}
				result.append((char) c);
			}
			return new Token(Token.COMMENT, result.toString());
		} else if (c == '*') {
			// C-style multiline comment
			StringBuilder result = new StringBuilder();
			result.append("/*");
			final int SCAN = 0;
			final int STAR = 1;
			final int DONE = 2;
			int state = SCAN;
			while (state != DONE) {
				c = reader.read();
				if (c < 0)
					state = DONE;
				else
					result.append((char) c);
				switch (state) {
				case SCAN:
					if (c == '*')
						state = STAR;
					break;
				case STAR:
					if (c == '/')
						state = DONE;
					else if (c != '*')
						state = SCAN;
					break;
				case DONE:
					break;
				}
			}
			return new Token(Token.COMMENT, result.toString());
		} else {
			if (c >= 0)
				reader.unread(c);
			return new Token(Token.SINGLE, "/");
		}
	}

	private Token parseWord() throws IOException {
		StringBuilder result = new StringBuilder();
		for (; ;) {
			int c = reader.read();
			if (c < 0)
				break;
			if (whiteSpace.get(c) || c == '\n' || single.get(c)) {
				reader.unread(c);
				break;
			}
			result.append((char) c);
		}
		return new Token(Token.WORD, result.toString());
	}
}

// vim:ts=4
