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

import java.io.File;
import java.io.Serializable;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.Vector;
import com.sun.javatest.util.PropertyArray;
import com.sun.javatest.util.StringArray;

/**
 * TestDescription objects embody the parameters of a test and provide the
 * ability to run a test. The parameters are normally found by parsing
 * HTML files and looking for distinguished constructions whose parameters 
 * provide the necessary description of a test.
 */

public class TestDescription implements Serializable
{
    /**
     * Construct a test description from the parameters of a recognized descriptions.
     *
     * @param root	The root file of the test suite
     * @param file	The file containing the test description
     * @param params	The collected parameters of the test description
     * @throws IllegalArgumentException if the file argument is an absolute
     * filename and does not begin with the root filename.
     *
     */
    public TestDescription(File root, File file, Map params) 
		throws IllegalArgumentException {

	if (root.equals(cachedRoot))
	    rootDir = cachedRootDir;
	else {
	    if (root.exists() ? root.isFile() : root.getName().endsWith(".html"))
		rootDir = root.getParent();
	    else
		rootDir = root.getPath();

	    // cache root->rootDir map to avoid making extra files
	    cachedRoot = root;
	    cachedRootDir = rootDir;
	}

	String fp = file.getPath();
	String rootRelativeFile;
	if (file.isAbsolute()) {
	    String rp = rootDir;
	    if (! (fp.startsWith(rp) && fp.charAt(rp.length()) == File.separatorChar))
		throw new IllegalArgumentException("file must be relative to root: " + file);
	    rootRelativeFile = fp.substring(rp.length() + 1);
	}
	else
	    rootRelativeFile = fp;
	rootRelativePath = rootRelativeFile.replace(File.separatorChar, '/');

	Vector v = new Vector(0, params.size() * 2);
	for (Iterator i = params.keySet().iterator(); i.hasNext(); ) {
	    String key = (String) (i.next());
	    String value = (String)(params.get(key));
	    insert(v, key, value);
	}
	fields = new String[v.size()];
	v.copyInto(fields);
    }

    /**
     * Internal constructor used by load()
     */
    private TestDescription(String root, String file, String[] params) {
	rootDir = root;
	// skip over the root part of the filename.
	char sep = file.charAt(root.length());
	rootRelativePath = file.substring(root.length() + 1).replace(sep, '/');

	Vector v = new Vector(0, params.length);
	for (int i = 0; i < params.length; i += 2) {
	    String key = params[i];
	    if (!(key.startsWith("$") || key.equals("testsuite") ||  key.equals("file"))) {
		// don't keep synthetic values from save;
		String value = params[i+1];
		insert(v, key, value);
	    }
	}
	fields = new String[v.size()];
	v.copyInto(fields);	
    }

    public boolean equals(Object td) {
	if (!(td instanceof TestDescription))
	    return false;
	
	TestDescription otherTd = (TestDescription)td;
	
	// a quick and simple check
	if (otherTd.getParameterCount() != getParameterCount()) {
	    return false;
	}
	
	// raw compare
	int pos = 0;
	while (pos < fields.length) {
	    String otherVal = otherTd.getParameter(fields[pos]);

	    if (otherVal == null ||
		!otherVal.equals(fields[pos+1])) {
		return false;
	    }

	    pos += 2;
	}

	return true;
    }

    /**
     * Get the directory for this test description.
     * @return the directory containing this test description
     */
    public File getDir() {
	return new File(getFile().getParent());
    }

    /**
     * Get the file for this test description.
     * WARNING: If this description has been read in from a .jtr file, the rootDir
     * may be inappropriate for this system.
     * @return the file contyaining this test description
     */
    public File getFile() {
        return new File(rootDir, rootRelativePath.replace('/', File.separatorChar));
    }

    /**
     * Get the id within the file for this test description.
     * @return the id within the file of this test description
     */
    public String getId() {
	return getParameter("id");
    }

    /**
     * Get the title of this test description. This title is determined from
     * the "title" parameter, if present, defaulting to the value returned
     * by <code>getName()</code>.
     * @return the title of this test description
     */
    public String getTitle() {
	// default title to name
	String title = getParameter("title");
	if (title == null)
	    title = getName();

        return title;
    }

    /**
     * Get the name of this test description; if not given explicitly, 
     * it defaults to the filename root of the first source file.
     * @return the name of this test description
     */
    public String getName() {

	// the name used to be a parameter you could set explicitly
	// now, it is based on the file and id
	int lastSep = rootRelativePath.lastIndexOf('/');
	String name = (lastSep == -1 ? 
		       rootRelativePath : 
		       rootRelativePath.substring(lastSep + 1));

	// strip off extension
	int dot = name.indexOf('.');
	if (dot != -1)
	    name = name.substring(0, dot);

	String id = getParameter("id");
	if (id != null)
	    name = name + "_" + id;

        return name;
    }

    /**
     * Get the set of keywords for this test description, 
     * as specified by the "keywords" parameter.
     * @return the set of keywords
     */
    public String[] getKeywords() {
	return StringArray.split(getParameter("keywords"));
    }

    /**
     * Get the set of keywords for this test description, 
     * as specified by the "keywords" parameter. 
     * They are returned in canonical form (lower-case).
     * @return the set of keywords
     */
    public Set getKeywordTable() {
	String[] keys = StringArray.split(getParameter("keywords"));
	Set s = new TreeSet();
	for (int i = 0; i < keys.length; i++) {
	    String k = keys[i].toLowerCase();
	    s.add(k);
	}
	return s;
    }

    /**
     * Get the set of source files for this test description,
     * as specified by the "source" parameter.
     * @return The sources as specified in the HTML test description
     * @see #getSourceFiles
     */
    public String[] getSources() {
	return StringArray.split(getParameter("source"));
    }

    /**
     * Get the set of source files for this test description,
     * as specified by the "source" parameter. The files in the
     * "source" parameter should normally be relative, in which
     * case, they will be evaluated relative to the directory
     * containing this test description. Then, if any of the
     * files are under the user's current directory, they will
     * be returned relative to that directory; otherwise, they 
     * will be returned as absolute filenames.
     * @return filenames specified by the source parameter.
     * @see #getSources
     */
    public File[] getSourceFiles() {
	String dir  = getFile().getParent();
	String[] srcs = getSources();
	File[] sourceFiles = new File[srcs.length];
    	String userCurrDir = System.getProperty("user.dir") + File.separator;
	for (int i = 0; i < srcs.length; i++) {
	    File f = new File(dir, srcs[i].replace('/', File.separatorChar));
	    String s = f.getPath();
	    if (s.startsWith(userCurrDir)) {	
	        s = s.substring(userCurrDir.length());	
		sourceFiles[i] = new File(s);
	    } else
		sourceFiles[i] = f;
	}
	return sourceFiles;
    }

    /**
     * Get the optional class directory for this test description,
     * as specified by the "classDir" parameter.
     * @return the class directory, or null if not specified
     * @deprecated use <code>getParameter("classDir")</code> instead
     */
    public String getClassDir() {
	return getParameter("classDir");
    }

    /**
     * Get the execution class for this test description,
     * as specified by the "executeClass" parameter.
     * @return the execute class name, or null if not specified
     * @deprecated use <code>getParameter("executeClass")</code> instead
     */
    public String getExecuteClass() {
	return getParameter("executeClass");
    }

    /**
     * Get the execution args for this test description,
     * as specified by the "executeArgs" parameter.
     * @return the execute args, or null if not specified
     * @deprecated use <code>getParameter("executeArgs")</code> instead
     */
    public String getExecuteArgs() {
	return getParameter("executeArgs");
    }

    /**
     * Get the requested timeout value for this test description,
     * as specified by the "timeout" parameter.
     * @return the timeout value, or 0 if not specified
     * @deprecated  use <code>getParameter("timeout")</code> instead
     */
    public int getTimeout() {
	String t = getParameter("timeout");
	if (t == null) 
	    return 0;
	else
	    return Integer.parseInt(t);
    }

    /**
     * Get the root file for this test suite;
     * THIS IS PROVIDED FOR BACKWARDS COMPATIBILTY FOR JCK ONLY.
     * It returns the name of testsuite.html within the root directory
     * of the test suite.
     * WARNING: If this description has been read in from a .jtr file, the rootDir
     * may be inappropriate for this system.
     * @return the root file for this test suite
     * @see #getRootDir
     * @deprecated 
     */
    public File getRoot() {
        return new File(rootDir, "testsuite.html");
    }

    /**
     * Get the root directory for this test suite
     * WARNING: If this description has been read in from a .jtr file, the rootDir
     * may be inappropriate for this system.
     * @return the root directory for this test suite
     */
    public String getRootDir() {
        return rootDir;
    }

    /**
     * Get the path of the test, relative to the root dir for the test suite.
     * This is the path to the source file for this description.
     * The internal separator is always '/'.
     * @return the path for this test description within the test suite
     */
    public String getRootRelativePath() {
	return rootRelativePath;
    }

    /**
     * Get the file of the test, relative to the root dir for the test suite.
     * @return A platform specific path to the source file.
     */
    public File getRootRelativeFile() {
	return new File(rootRelativePath.replace('/', File.separatorChar));
    }

    /**
     * Get the url of the test, relative to the root dir for the test suite.
     * This is the path to the source file for this description, plus the
     * test id if necessary.  Again, the path separator is always '/'.
     * @return a relative URL for this test within the test suite
     */
    public String getRootRelativeURL() {
	String id = getParameter("id");
	return (id == null ?  rootRelativePath : rootRelativePath + "#" + id);
    }

    /**
     * Get the path of the test directory, relative to the root directory for the test suite.
     * @return the a relative path to the directory containing this test description
     *
     * @deprecated Use getRootRelativeFile().getParent()
     */
    public File getRootRelativeDir() {
	String p = getRootRelativeFile().getParent();
	return (p == null ? new File(".") : new File(p));
    }

    /**
     * Get the number of parameters contained in this test description.
     * @return the number of parameters
     */
    public int getParameterCount() {
	return fields.length / 2;
    }

    /**
     * Get an iterator for the names of the parameters contained in
     * this test description.
     * @return an iterator for the names of the parameters
     */
    public Iterator getParameterKeys() {
        return new Iterator() {
            int pos = 0;

            public boolean hasNext() {
                if(fields == null || fields.length == 0 ||
                   pos >= fields.length) {
                   return false;
                } else {
                   return true;
                }
            }

            public Object next() {
                if(fields == null || fields.length == 0 ||
                   pos == fields.length) {
                   return null;
                } else {
                   String current = fields[pos];
                   pos += 2;
                   return current;
                }
            }

	    public void remove() {
		throw new UnsupportedOperationException();
	    }
        };
    }

    /**
     * Get a parameter of the test description by name.
     * @param key the name of the parameter value to be returned
     * @return the value of the specified parameter, or null if not found
     */
    public String getParameter(String key) {
	int lower = 0;
	int upper = fields.length - 2;
	int mid;

	if (upper < 0)
	    return null;

	String last = fields[upper];
	int cmp = key.compareTo(last);
	if (cmp > 0)
	    return null;

	while (lower <= upper) {
	    // in next line, take care to ensure that mid is always even
	    mid = lower + ((upper - lower) / 4) * 2;
	    String e = fields[mid];
	    cmp = key.compareTo(e);
	    if (cmp < 0) {
		upper = mid - 2;
	    }
	    else if (cmp > 0) {
		lower = mid + 2;
	    }
	    else 
		return fields[mid+1];
	}

	// did not find an exact match 
	return null;
    }

    /**
     * Simple standard debugging output.
     */
    public String toString() {
	return ("TestDescription[" + getTitle() + "]");
    }

    /**
     * Save TestDescription to a dictionary
     * WARNING: If this description has been read in from a .jtr file, the rootDir
     * may be inappropriate for this system.
     */
    void save(Map p) {
	saveField(p, "$root", rootDir);
	saveField(p, "$file", getFile().getPath());
	for (int i = 0; i < fields.length; i+=2) {
	    saveField(p, fields[i], fields[i+1]);
	}
    }

    private void saveField(Map p, String key, String value) {
	if (value != null)
	    p.put(key, value);
    }

    /**
     * Recover TestDescription from saved dictionary
     */
    static TestDescription load(String[] params) {
	//File r = new File((String)d.get("testsuite"));
	//if (!r.isDirectory())
	//    r = new File(r.getParent());
	//File f = new File((String)d.get("file"));
	String r = PropertyArray.get(params, "$root");
	if (r == null)
	    r = PropertyArray.get(params, "testsuite");
	String f = PropertyArray.get(params, "$file");
	if (f == null)
	    f = PropertyArray.get(params, "file");
	return new TestDescription(r, f, params);
    }

    private static void insert(Vector v, String key, String value) {
	int lower = 0;
	int upper = v.size() - 2;
	int mid = 0;

	if (upper < 0) {
	    v.addElement(key);
	    v.addElement(value);
	    return;
	}

	String last = (String)v.elementAt(upper);
	int cmp = key.compareTo(last);
	if (cmp > 0) {
	    v.addElement(key);
	    v.addElement(value);
	    return;
	}

	while (lower <= upper) {
	    // in next line, take care to ensure that mid is always even
	    mid = lower + ((upper - lower) / 4) * 2;
	    String e = (String)(v.elementAt(mid));
	    cmp = key.compareTo(e);
	    if (cmp < 0) {
		upper = mid - 2;
	    }
	    else if (cmp > 0) {
		lower = mid + 2;
	    }
	    else 
		throw new Error("should not happen");
	}

	// did not find an exact match (we did not expect to) 
	// adjust the insert point
	if (cmp > 0)
	    mid += 2;

	v.insertElementAt(key, mid);
	v.insertElementAt(value, mid+1);
    }

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

    /**
     * Root directory for the test suite
     * WARNING: If this description has been read in from a .jtr file, the rootDir
     * may be inappropriate for this system.
     * @serial
     */
    private String rootDir;

    /**
     * Root relative path for this test description within the test suite
     * @serial
     */
    private String rootRelativePath;

    /**
     * The data for this test description, organized as a sequence of
     * name-value pairs.
     * @serial
     */
    private String[] fields;

    private static File cachedRoot;
    private static String cachedRootDir;
}

