/*
 * $Id$
 *
 * Copyright 1996-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.javatest.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Vector;

/**
 * A class to parse lines of words in a stream. Words may be unquoted sequences
 * of non-blank characters, or may be quoted strings. Comments can be introduced
 * by '#' and extend to the next newline character. Lines may be terminated
 * by newline, semicolon or a comment.
 */
public class LineParser 
{
    /**
     * This exception is used to report problems while using a line parser.
     */
    public static class Fault extends Exception
    {
	/**
	 * Create a Fault.
	 * @param i18n A resource bundle in which to find the detail message.
	 * @param s The key for the detail message.
	 */
	Fault(I18NResourceBundle i18n, String s) {
	    super(i18n.getString(s));
	}

	/**
	 * Create a Fault.
	 * @param i18n A resource bundle in which to find the detail message.
	 * @param s The key for the detail message.
	 * @param o An argument to be formatted with the detail message by
	 * {@link java.text.MessageFormat#format}
	 */
	Fault(I18NResourceBundle i18n, String s, Object o) {
	    super(i18n.getString(s, o));
	}

	/**
	 * Create a Fault.
	 * @param i18n A resource bundle in which to find the detail message.
	 * @param s The key for the detail message.
	 * @param o An array of arguments to be formatted with the detail message by
	 * {@link java.text.MessageFormat#format}
	 */
	Fault(I18NResourceBundle i18n, String s, Object[] o) {
	    super(i18n.getString(s, o));
	}
    }

    /**
     * Create a line parser, reading the data in a given file.
     * @param file the file to be read
     * @throws FileNotFoundException if the file was not found
     * @throws IOException if there is some problem opening the file
     * or reading the initial characters of the file
     */
    public LineParser(File file) 
	throws FileNotFoundException, IOException 
    {
	this(file, new BufferedReader(new FileReader(file)));
    }

    /**
     * Create a line parser, reading data from an anonymous stream.
     * @param in the stream from which to read the data
     */
    public LineParser(Reader in) 
    {
	this(null, in);
    }

    /**
     * Create a line parser, reading the data from a file for which a stream
     * has already been opened. The name of the file will be included in any
     * appropriate error messages.
     * @param file the file from which the data is being read
     * @param in the stream from which to read the data
     */
    private LineParser(File file, Reader in) {
	this.file = file;
	this.in = in;
	currLine = new Vector();
	lineNumber = 1;
	ch = ' ';
    }

    /**
     * Get the file being read, or null if it is not available.
     * @return the file being read, or null if not available
     */
    public File getFile() {
	return file;
    }

    /**
     * Get the current line number within the stream being read.
     * @return the current line number within nthe stream being read
     */
    public int getLineNumber() {
	return lineNumber;
    }

    /**
     * Read the next line of words from the input stream.
     * @return the next line of words from the input stream
     * @throws LineParser.Fault if there is a problem reading the required data --
     * such as an unterminated string
     */
    public String[] readLine() throws Fault {
	try {
	    while (ch != -1) {
		switch (ch) {
		case ' ':
		case '\t':
		    // end current word
		    endWord_nextCh();
		    break;
		    
		case '\r':
		case '\n':
		    // end current word, return if curr line not empty
		    endWord_nextCh();
		    if (currLine.size() > 0)
			return endLine();
		    break;

		case ';':
		    // end current word, return if curr line not empty
		    endWord_nextCh();
		    if (currLine.size() > 0)
			return endLine();
		    break;

		case '#':
		    // skip to end of line
		    // return if curr line not empty
		    endWord_nextCh();
		    while (ch != -1 && ch != '\r' && ch != '\n')
			nextCh();
		    if (currLine.size() > 0)
			return endLine();
		    break;

		case '\\':
		    // read next character; if newline, skip whitespace
		    // else add next character uninterpreted
		    nextCh();
		    if (ch == '\r')
			nextCh();
		    if (ch == '\n') {
			nextCh();
			while (ch == ' ' || ch == '\t')
			    nextCh();
		    }
		    else 
			append_nextCh();
		    break;

		case '"':
		case '\'':
		    readString((char) ch);
		    break;

		default:
		    append_nextCh();
		    break;
		    
		}
	    }

	    // at end of file ...
	    // flush last word found, if any
	    if (currWord != null) {
		currLine.add(currWord.toString());
		currWord = null;
	    }

	    return (currLine.size() > 0 ? endLine() : null);
	}
	catch (IOException e) {
	    throw new Fault(i18n, "lineParser.ioError", 
			    new Object[] {new Integer(file == null ? 0 : 1),
					  file,
					  new Integer(lineNumber), 
					  e } );
	}
    }

    private void readString(char termCh) throws IOException, Fault {
	if (currWord == null)
	    currWord = new StringBuffer();
	nextCh();
	while (ch != -1) {
	    switch (ch) {
	    case '\r':
	    case '\n':
		throw new Fault(i18n, "lineParser.unterminatedString",
				new Object[] {new Integer(file == null ? 0 : 1),
					      file,
					      new Integer(lineNumber) } );
		    
	    case '\\':
		nextCh();
		append_nextCh();
		break;

	    default:
		if (ch == termCh) {
		    nextCh();
		    return;
		}
		else
		    append_nextCh();
	    }
	}
    }

    private void append_nextCh() throws IOException {
	if (currWord == null)
	    currWord = new StringBuffer();
	currWord.append((char) ch);
	nextCh();
    }

    private void endWord_nextCh() throws IOException {
	if (currWord != null) {
	    //System.err.println("endWord_nextCh: `" + currWord + "'");
	    currLine.add(currWord.toString());
	    currWord = null;
	}
	nextCh();
    }

    private String[] endLine() {
	String[] line = new String[currLine.size()];
	currLine.copyInto(line);
	currLine.setSize(0);
	return line;
    }

    private void nextCh() throws IOException {
	ch = in.read();
	if (ch == '\n')
	    lineNumber++;
    }

    private File file;
    private Reader in;
    private int ch;   
    private int lineNumber;
    private StringBuffer currWord;
    private Vector currLine;

    private static final I18NResourceBundle i18n = 
	I18NResourceBundle.getBundleForClass(LineParser.class);
}
