/*
 * $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.finder;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.File;
import java.util.Map;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;

import com.sun.javatest.TestFinder;
import com.sun.javatest.util.I18NResourceBundle;

/**
 * This class searches out test descriptions found in a file system
 * tree. Each attribute of the test description consists of JavaDoc-like tags
 * which provide an tag-name and associated value.  The associated test script
 * for the test suite will interpret the values.  All tags for a given test
 * description must be within the same block comment.  A file may contain
 * multiple test descriptions.
 *
 * @see TestFinder
 */
public class TagTestFinder extends TestFinder
{
    /**
     * Constructs the list of file names to exclude for pruning in the search
     * for files to examine for test descriptions.  This constructor also sets
     * the allowable comment formats.
     */
    public TagTestFinder() {
 	exclude(excludeNames);
 	addExtension(".java", JavaCommentStream.class);
    }

    /**
     * Decode the arg at a specified position in the arg array.  If overridden
     * by a subtype, the subtype should try and decode any arg it recognizes,
     * and then call super.decodeArg to give the superclass(es) a chance to
     * recognize any arguments.
     *
     * @param args The array of arguments
     * @param i    The next argument to be decoded.
     * @return     The number of elements consumed in the array; for example,
     *             for a simple option like "-v" the result should be 1; for an
     *             option with an argument like "-f file" the result should be
     *             2, etc.
     * @throws TestFinder.Fault If there is a problem with the value of the current arg,
     *             such as a bad value to an option, the Fault exception can be
     *             thrown.  The exception should NOT be thrown if the current
     *             arg is unrecognized: in that case, an implementation should
     *             delegate the call to the supertype.
     */
    protected int decodeArg(String[] args, int i) throws Fault {
	if ("-fast".equalsIgnoreCase(args[i])) {
	    fastScan = true;
	    return 1;
	}
	else
	    return super.decodeArg(args, i);
    }

    /**
     * Scan a file, looking for test descriptions and/or more files to scan.
     * @param file The file to scan
     */
    public void scan(File file) {
	currFile = file;
	if (file.isDirectory())
	    scanDirectory(file);
	else
	    scanFile(file);
    }

    /**
     * Get the name of the file currently being scanned.
     * @return the name of the file currently being scanned.
     */
    // Ideally, we should be able to get the current line number as well,
    // (for error messages)
    protected File getCurrentFile() {
	return currFile;
    }

    /**
     * Exclude all files with a particular name from being scanned.
     * This will typically be for directories like SCCS, Codemgr_wsdata, etc
     * @param name The name of files to be excluded.
     */
    public void exclude(String name) {
	excludeList.put(name, name);
    }

    /**
     * Exclude all files with particular names from being scanned.
     * This will typically be for directories like SCCS, Codemgr_wsdata, etc
     * @param names The names of files to be excluded.
     */
    public void exclude(String[] names) {
	for (int i = 0; i < names.length; i++) {
	    String name = names[i];
	    excludeList.put(name, name);
	}
    }

    /**
     * Nominate a class to read files that have a particular extension.
     * @param extn 	The extension for which this class is to be used
     * @param commentStreamClass
     *			A class to read files of a particular extension.
     *			The class must be a subtype of CommentStream
     */
    public void addExtension(String extn, Class commentStreamClass) {
	if (!extn.startsWith("."))
	    throw new IllegalArgumentException("extension must begin with `.'");
	if (!CommentStream.class.isAssignableFrom(commentStreamClass))
	    throw new IllegalArgumentException("class must be a subtype of " + CommentStream.class.getName());

	extensionTable.put(extn, commentStreamClass);
    }

    /**
     * Get the class used to handle an extension.
     * @param extn The extension in question
     * @return the class previously registered with addExtension
     */
    public Class getClassForExtension(String extn) {
	return (Class)extensionTable.get(extn);
    }

    /**
     * Set the initial tag to be checked for in a test description.
     * If set to null, no initial tag is required.  The default value
     * for the initial tag is "test".  (i.e. @test must appear in the
     * test description.)
     * @param tag The tag to be checked for.
     * @see #getInitialTag
     */
    public void setInitialTag(String tag) {
	initialTag = tag;
    }

    /**
     * Get the current value of the initial tag that is checked for
     * in a test description. If null, no tag is required.
     * @return the value of the required initial tag, or null if none required.
     * @see #setInitialTag
     */
    public String getInitialTag() {
	return initialTag;
    }


    //-----internal routines----------------------------------------------------

    /**
     * Scan a directory, looking for more files to scan
     * @param dir The directory to scan
     */
    private void scanDirectory(File dir) {
	// scan the contents of the directory, checking for
	// subdirectories and other files that should be scanned
	String[] names = dir.list();
	for (int i = 0; i < names.length; i++) {
	    String name = names[i];
	    // if the file should be ignored, skip it
	    // This is typically for directories like SCCS etc
	    if (excludeList.containsKey(name))
		continue;

	    File file = new File(dir, name);
	    if (file.isDirectory()) {
		// if its a directory, add it to the list to be scanned
		foundFile(file);
	    }
	    else {
		// if its a file, check its extension
		int dot = name.indexOf('.');
		if (dot == -1)
		    continue;
		String extn = name.substring(dot);
		if (extensionTable.containsKey(extn)) {
		    // extension has a comment reader, so add it to the
		    // list to be scanned
		    foundFile(file);
		}
	    }
	}
    }

    /**
     * Scan a file, looking for comments and in the comments, for test
     * description data.
     * @param file The file to scan
     */
    protected void scanFile(File file) {
	int testDescNumber = 0;
	String name = file.getName();
	int dot = name.indexOf('.');
	if (dot == -1)
	    return;
	String extn = name.substring(dot);
	Class csc = (Class)(extensionTable.get(extn));
	if (csc == null) {
	    error(i18n, "tag.noParser", new Object[] {file, extn});
	    return;
	}
	CommentStream cs = null;
	try {
	    cs = (CommentStream)(csc.newInstance());
	}
	catch (InstantiationException e) {
	    error(i18n, "tag.cantCreateClass", new Object[] {csc.getName(), extn});
	    return;
	}
	catch (IllegalAccessException e) {
	    error(i18n, "tag.cantAccessClass", new Object[] {csc.getName(), extn});
	    return;
	}

	try {
	    cs.init(new BufferedReader(new FileReader(file)));
	    if (fastScan)
		cs.setFastScan(true);

	    String comment;
	    while ((comment = cs.readComment()) != null) {
		Map tagValues = parseComment(comment, file);
		if (tagValues.isEmpty())
		    continue;

		if (tagValues.get("id") == null) {
		    if (testDescNumber  != 0)
			tagValues.put("id", "id" + (new Integer(testDescNumber)).toString());
		    testDescNumber++;
		}

		// The "test" marker can now be removed so that we don't waste
		// space unnecessarily.  We need to do the remove *after* the
		// isEmpty() check because of the potential to interfere with
		// defaults based on file extension. (i.e. The TD /* @test */
		// still needs to evaluate to a valid test description.)
		tagValues.remove("test");

		foundTestDescription(tagValues, file, /*line*/0);
	    }
	}
	catch (FileNotFoundException e) {
	    error(i18n, "tag.cantFindFile", file);
	}
	catch (IOException e) {
	    error(i18n, "tag.ioError", file);
	}
	finally {
	    try {
		cs.close();
	    }
	    catch (IOException e) {
	    }
	}
    }


    /**
     * Given a comment, find all tags of interest.  Return a map
     * containing the name-value pairs for those tags.  If a duplicate
     * name is found, the last name-value will be returned.
     * @param comment 	The comment to be parsed.
     * @param currFile 	The name of the file currently being read.
     * @return A map containing the name-value pairs read from the comment.
     */
    protected Map parseComment(String comment, File currFile) {
	Map tagValues = new HashMap();
	int tagStart = 0;
	int tagEnd   = 0;

//    	System.out.println(comment);
	while (true) {
	    tagStart = findTagStart(comment, tagEnd);
	    if (tagStart == -1)
		return tagValues;

	    tagEnd = findTagEnd(comment, tagStart);
	    String tag = comment.substring(tagStart, tagEnd);

	    int pos = 0;
	    while ((pos < tag.length())
		   && !Character.isWhitespace(tag.charAt(pos)))
		pos++;

	    String name = tag.substring(1, pos);
	    while ((pos < tag.length())
		   && Character.isWhitespace(tag.charAt(pos)))
		pos++;
	    String value = tag.substring(pos);
 	    value = value.replace('\n', ' ').replace('\r', ' ').trim();

	    // The first token of the leading comment in the defining file
	    // must be "@test" (or whatever the initialTag is set to.)
	    if (tagValues.isEmpty() && initialTag != null && !name.equals(initialTag))
		return tagValues; // i.e. empty

	    processEntry(tagValues, name, value);
	}
    }

    /**
     * Identify the start of the next tag.  The start tag is identified as the
     * first token which begins with '@'.  Tokens are space-separated, thus we
     * need to look for the pattern: " @.".  If the very first token of the
     * string begins with '@' then the pattern is "@.".
     *
     * @param s    The string to search.
     * @param pos  The position in the string to start looking for the beginning
     *             of the tag.
     * @return     The position in the string of the start of the next tag.
     */
    private int findTagStart(String s, int pos) {
	while (true) {
	    pos = s.indexOf("@", pos);
	    if ((pos == -1) || (pos >= (s.length()-1)))
		return -1;
	    if (((pos == 0) && !Character.isWhitespace(s.charAt(pos+1)))
		|| ((pos > 0) && !Character.isWhitespace(s.charAt(pos+1))
		    && Character.isWhitespace(s.charAt(pos-1))))
		return pos;
	    pos++;
	}
    }

    /**
     * Identify the end of the next tag.  The end is identified as either the
     * position before the next tag start or the end of the string.
     *
     * @param s    The string to search.
     * @param tagStart The position in the string which marks the beginning of
     *             the tag.
     * @return     The position in the string of the end of the current tag.
     */
    private int findTagEnd(String s, int tagStart) {
	int end = findTagStart(s, tagStart+1);
	if (end == -1)
	    return s.length();
	return end;
    }

    //----------member variables------------------------------------------------

    private File currFile;
    private HashMap excludeList   = new HashMap();
    private HashMap extensionTable = new HashMap();
    private boolean fastScan = false;
    private String initialTag = "test";

    //private int testDescNumber;
    private static final String[] excludeNames = {
 	"SCCS", "deleted_files"
    };

    private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(TagTestFinder.class);

}
