/*
 * $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.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.util.Hashtable;
import java.util.Vector;

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

/**
 * This class searches out test descriptions as represented by
 * certain HTML elements in a hierarchy of HTML files.
 *
 * @see TestFinder
 */
public class HTMLTestFinder extends TestFinder
{
    /**
     * Create an HTMLTestFinder.
     */
    public HTMLTestFinder() {
	// init the tables used when scanning directories
	// if necessary, the tables could be dynamically updated by the
	// init args; this is not currently supported
	excludeList = new Hashtable(excludeNames.length);
	for (int i = 0; i < excludeNames.length; i++)
	    excludeList.put(excludeNames[i], excludeNames[i]);

	extensionTable = new Hashtable(extensions.length);
	for (int i = 0; i < extensions.length; i++)
	    extensionTable.put(extensions[i], extensions[i]);
    }

    protected int decodeArg(String[] args, int i) throws Fault {
	if (args[i].equalsIgnoreCase("-webWalk")) {
	    mode = WEB_WALK;
	    return 1;
	}
	else if (args[i].equalsIgnoreCase("-dirWalk")) {
	    mode = DIR_WALK;
	    return 1;
	}  
	else if (args[i].equalsIgnoreCase("-IGNORE-ERRORS")) {
	    ignoreErrors = true;
	    return 1;
	}  
	else 
	    return super.decodeArg(args, i);
    }

    public File getRoot() {
	// we assume that providedRoot exists - see Parameters class
	File providedRoot = super.getRoot();

	if (mode == DIR_WALK && providedRoot.isFile()) {
	    validatedRoot = new File(providedRoot.getParent());
	}
	else if (mode == WEB_WALK && providedRoot.isDirectory()) {
	    throw new IllegalStateException (i18n.getString("html.badRootForWebWalk", providedRoot.getPath()));
	}
	else {
	    validatedRoot = new File(providedRoot.getPath());
	}

	return validatedRoot;
    }

    protected void localizedError(String msg) {
	if (!ignoreErrors)
	    super.localizedError(msg);
    }

    /**
     * Specify the mode for how this test finder determines the child files
     * to be scanned.
     * @param mode One of {@link #WEB_WALK} or {@link #DIR_WALK}
     * @see #getMode
     */
    public void setMode(int mode) {
	switch (mode) {
	case DIR_WALK:
	case WEB_WALK:
	    this.mode = mode;
	    break;

	default:
	    throw new IllegalArgumentException();
	}
    }

    /**
     * Get the current mode for how this test finder determines the child files
     * to be scanned.
     * @return One of {@link #WEB_WALK} or {@link #DIR_WALK}
     * @see #setMode
     */
    public int getMode() {
	return mode;
    }

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


    protected void scan(File file) {
	if (file.isDirectory())
	    scanDirectory(file);
	else
	    scanFile(file);
    }

    private void scanDirectory(File dir) {
	if (mode == WEB_WALK)
	    return;

	// 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.lastIndexOf('.');
		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);
		}
	    }
	}	
    }

    private void scanFile(File file) {
	String tag;

	input = null;
	lastName = null;

	// We want to check that the names defined in the file are unique. 
	// Maintain the following tables:
	namesInFile.clear();

	try {
	    input = new BufferedReader(new FileReader(file));
	    currFile = file;
	    line = 1;
	    nextCh();
	    while (c >= 0) {
		switch (c) {
		case '<' :
		    nextCh();
		    skipSpace();
		    switch (c) {
		      case '!':
			nextCh();
			if (c == '-') {
			    nextCh();
			    if (c == '-') {
				nextCh();
				skipComment();
			    }
			}
			break;

		      case '/':
			nextCh();
			tag = scanIdentifier();
			if (tag.equals("dl"))
			    endDefList();
			else if (tag.equals("td") || tag.equals("th"))
			    endTableData();
			else if (tag.equals("tr") || tag.equals("table"))
			    endTableRow();
			skipTag();

			if (inTestDescription() && tag.equals(endTestDescriptionTag)) {
			    foundTestDescription(params, file, line);
			    params = null;
			}
		        break;

		      default:
		        tag = scanIdentifier();
			if (tag.equals("a"))
			    scanLink(file);
			else if (tag.equals("table"))
			    scanTable(file);
			else if (tag.equals("tr"))
			    scanTableRow();
			else if (tag.equals("td") || tag.equals("th"))
			    scanTableData();
			else if (tag.equals("dl"))
			    scanDefList(file);
			else if (tag.equals("dt"))
			    scanDefTerm();
			else if (tag.equals("dd"))
			    scanDefData();
			else
			    skipTag();
		    }
		    break;

		case ' ':
		case '\f':
		case '\t':
		case '\r':
		case '\n':
		    if (text != null && text.length() > 0 && text.charAt(text.length()-1) != ' ')
			text.append(' ');
		    nextCh();
		    break;

		case '&':
		    String replace = null;
		    nextCh();
		    if (c == '#') {
			int n = 0;
			nextCh();
			tag = "#";
			while ('0' <= c && c <= '9') {
			    tag += ((char) c);
			    n = (n * 10) + (c - '0');
			    nextCh();
			}
			replace = "" + ((char)n);
		    } 
		    else {
			tag = scanIdentifier();
			if (tag.equals("lt"))
			    replace = "<";
			else if (tag.equals("gt"))
			    replace = ">";
			else if (tag.equals("amp"))
			    replace = "&";
			else if (tag.equals("copy"))
			    replace = "©";
			if (replace == null)
			    replace = "&" + tag + ((char) c);
		    }
		    if (c != ';') {
			if (!Character.isWhitespace((char) c))
			    tag += ((char) c);
			error(i18n, "html.badEscape", new Object[] {tag, file});
		    }
		    if (text != null)
			text.append(replace);
		    nextCh();
		    break;

		default:
		    if (text != null)
			text.append((char)c);
		    nextCh();
		}
	    }
	}
	catch (FileNotFoundException ex) {
	    error(i18n, "html.cantFindFile", file);
	}
	catch (IOException ex) {
	    error(i18n, "html.ioError", new Object[] {file, new Integer(line), ex});
	}
	finally {
	    if (input != null) {
	      try {
		  input.close();
	      }
	      catch (IOException e) {
	      }
	      input = null;
	    }
	}
    }

    /**
     * Get the name of the file currently being read.
     * @return the name of the file currently being read.
     */
    protected File getCurrentFile() {
	return currFile;
    }

    private void nextCh() throws IOException {
	c = input.read();
	if (c == '\n')
	    line++;
    }

    private boolean inTestDescription() {
	return (params != null);
    }

    //-----internal routines------------------------------------------------------
    //
    // detect test descriptions in tables

    private void scanTable(File context) throws IOException {
	if (debug) 
	    System.err.println("scanning table starting in line " + line);

	String id = lastName; // default
	skipSpace();
	while (c != '>') {
	    String att = scanIdentifier();
	    String value = scanValue();
	    skipSpace();
	    if (att.equals("class") && "TestDescription".equals(value)) {
	        params = new Hashtable();
		endTestDescriptionTag = "table";
	    } else if (att.equals("id"))
		id = value;
	}
	nextCh();
	if (params != null && id != null)
	    processEntry(params, "id", id);
    }

    private void scanTableRow() throws IOException {
	skipTag();
	if (params != null) {
	    endTableRow();
	    tableRow = new Vector();
	}
    }

    private void endTableRow() throws IOException {
        if (params != null && tableRow != null) {
	    // ensure any outstanding <td> is closed
	    endTableData();
	    if (tableRow.size() == 2)
		processEntry(params, (String)tableRow.elementAt(0), (String)tableRow.elementAt(1));
	    tableRow = null;
	}
    }

    private void scanTableData() throws IOException {
	skipTag();
	if (params != null && tableRow != null) {
	    // ensure any outstanding <td> is closed
	    endTableData();
	    text = new StringBuffer();
	}
    }

    private void endTableData() throws IOException {
        if (params != null && tableRow != null && text != null) {
	    while (text.length() > 0 && text.charAt(text.length() - 1) == ' ')
		text.setLength(text.length() -1);
	    tableRow.addElement(new String(text));
	    text = null;
	}
    }


    //-----internal routines----------------------------------------------------
    //
    // detect test descriptions in definition lists

    private void scanDefList(File context) throws IOException {
	String id = lastName; // default
	skipSpace();
	while (c != '>') {
	    String att = scanIdentifier();
	    String value = scanValue();
	    skipSpace();
	    if (att.equals("class") && "TestDescription".equals(value)) {
	        params = new Hashtable();
		endTestDescriptionTag = "dl";
	    } else if (att.equals("id"))
		id = value;
	}
	nextCh();
	if (params != null && id != null)
	    processEntry(params, "id", id);
    }

    private void scanDefTerm() throws IOException {
	skipTag();
	if (params != null) {
	    if (defTerm != null && text != null) {
		while (text.length() > 0 && text.charAt(text.length() - 1) == ' ')
		    text.setLength(text.length() -1);
		String defData = new String(text);
		processEntry(params, defTerm, defData);
	    }
	    defTerm = null;
	    text = new StringBuffer();
	}
    }

    private void scanDefData() throws IOException {
	skipTag();
	if (params != null && text != null) {
	    while (text.length() > 0 && text.charAt(text.length() - 1) == ' ')
		text.setLength(text.length() -1);
	    defTerm = new String(text);
	    text = new StringBuffer();
	}
    }

    private void endDefList() throws IOException {
	if (params != null) {
	    if (defTerm != null && text != null) {
		while (text.length() > 0 && text.charAt(text.length() - 1) == ' ')
		    text.setLength(text.length() -1);
		String defData = new String(text);
		processEntry(params, defTerm, defData);
	    }
	    defTerm = null;
	    text = null;
	}
    }


    //-----internal routines----------------------------------------------------
    //
    // general lexical support

    private String scanIdentifier() throws IOException {
	StringBuffer buf = new StringBuffer();
	while (true) {
	    if ((c >= 'a') && (c <= 'z')) {
		buf.append((char)c);
		nextCh();
	    } else if ((c >= 'A') && (c <= 'Z')) {
	        buf.append((char)('a' + (c - 'A')));
	        nextCh();
	    } else if ((c >= '0') && (c <= '9')) {
	        buf.append((char)c);
	        nextCh();
	    } else if (c == '-') {  // needed for <META HTTP-EQUIV ....>
	        buf.append((char)c);
	        nextCh();
	    } else
	        if (buf.length() == 0)
		    throw new IOException("Identifier expected");
		else
	            return buf.toString();
	}
    }

    private void scanLink(File context) throws IOException {
	skipSpace();
	while (c != '>') {
	    String att = scanIdentifier();
	    String value = scanValue();
	    skipSpace();
	    if (mode == WEB_WALK &&
		att.equals("href") &&
		value.indexOf(':') == -1 &&     // no protocol
		!value.startsWith("/") &&	// no host or port or absolute files
		!value.startsWith("../")) {	// no backing up the tree
		// remove trailing #ref, if any
		int refStart = value.lastIndexOf('#');
		if (refStart != -1)
		    value = value.substring(0, refStart);

		// strip trailing whitespace
		value = value.trim();

		File file = new File(context.getParent(), value.replace('/', File.separatorChar));
		String f = file.getPath();
		if ((f.endsWith(".html") || f.endsWith(".htm")))
		    foundFile(file);
	    }
	    if (att.equals("name")) {
		lastName = value;
		Integer here = new Integer(line);
		Integer prev = (Integer)namesInFile.put(value, here);
		if (prev != null) {
		    error(i18n, "html.multipleName", 
			  new Object[] {value, context, here, prev});
		}
	    }
	}
	nextCh();
	//long exitTime = System.currentTimeMillis();
	//System.out.println(exitTime-enterTime);
    }

    private String scanValue() throws IOException {
	skipSpace();
	if (c != '=')
	    return "";

	int quote = -1;
	nextCh();
	skipSpace();
	if ((c == '\'') || (c == '\"')) {
	    quote = c;
	    nextCh();
	    skipSpace();
	}
	StringBuffer buf = new StringBuffer();
	while (((quote < 0) && (c != ' ') && (c != '\t') &&
		(c != '\n') && (c != '\r') && (c != '>')) ||
	       ((quote >= 0) && (c != quote))) {
	    if (c == -1 || c == '\n' || c == '\r') {
		error(i18n, "html.quoteMismatch", new Object[] {currFile, new Integer(line)});
		break;
	    }
	    buf.append((char)c);
	    nextCh();
	}
	if (c == quote)
	    nextCh();
	skipSpace();
	return buf.toString();
    }

    private void skipComment() throws IOException {
	// a comment sequence is "<!--" ... "-->"
	// at the time this is called, "<!--" has been read;
	int numHyphens = 0;
	//System.out.print("SKIPCOMMENT: <!--" + (char)c);
	while (c != -1 && (numHyphens < 2 || c != '>')) {
	    if (c == '-')
		numHyphens++;
	    else
		numHyphens = 0;
	    nextCh();
	    //System.out.print((char)c);
	}
	//System.out.println("END SKIPCOMMENT");
	nextCh();
    }

    private void skipSpace() throws IOException {
	while ((c == ' ') || (c == '\t') || (c == '\n') || (c == '\r')) {
	    nextCh();
	}
    }

    private void skipTag() throws IOException {
	skipSpace();
	while (c != '>') {
	    String att = scanIdentifier();
	    if (att == "")
	        throw new IOException("error parsing HTML input");
	    String value = scanValue();
	    skipSpace();
	}
	nextCh();
    }

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

    private Hashtable namesInFile = new Hashtable();
    private Hashtable excludeList;
    private static final String[] excludeNames = {"SCCS", "deleted_files"};
    private Hashtable extensionTable;
    private static final String[] extensions = {".html", ".htm"};

    /**
     * A value for {@link #setMode} to specify that the child files
     * within the test tree should be determined from the HTML &lt;a href=...&gt; tags.
     */
    public static final int WEB_WALK = 1;

    /**
     * A value for {@link #setMode} to specify that the child files
     * within the test tree should be determined by sub-directories and
     * HTML files within directories.
     */
    public static final int DIR_WALK = 2;

    private int mode = DIR_WALK; // default
    private boolean ignoreErrors = false;

    private File validatedRoot;
    private File currFile;
    private Reader input;
    private int c;
    private int line;
    private StringBuffer text;

    private String endTestDescriptionTag;
    private Hashtable params;
    private String defTerm;   // collects test description parameter name
    private Vector tableRow;  // collects test description info from <TR><TD>....</TD>....etc...</TR>
    private String lastName;

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