/*
 * $Id$
 *
 * Copyright (c) 1996, 2011, Oracle and/or its affiliates. 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.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.sun.javatest;

import com.sun.javatest.util.PropertyArray;
import com.sun.javatest.util.StringArray;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.Vector;

/**
 * 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 {
    private static File cachedRoot;
    private static String cachedRootDir;
    /**
     * 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;
    /**
     * Cached version of the root relative path.
     *
     * @see #getRootRelativeURL
     */
    private String rrurl;

    /**
     * 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) {

        synchronized (this.getClass()) {
            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<String> v = new Vector<>(0, params.size() * 2);
        for (Map.Entry<?, ?> entry : params.entrySet()) {
            insert(v, (String) entry.getKey(), (String) entry.getValue());
        }
        fields = v.toArray(new String[v.size()]);
    }

    /**
     * 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<String> 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 = v.toArray(new String[v.size()]);
    }

    /**
     * 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<String> v, String key, String value) {
        int lower = 0;
        int upper = v.size() - 2;
        int mid = 0;

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

        String last = v.get(upper);
        int cmp = key.compareTo(last);
        if (cmp > 0) {
            v.add(key);
            v.add(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 = v.get(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.add(mid, key);
        v.add(mid + 1, value);
    }

    @Override
    public int hashCode() {
        int hash = 3;
        hash = 43 * hash + Arrays.deepHashCode(this.fields);
        return hash;
    }

    @Override
    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 containing 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()}.
     *
     * @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<String> getKeywordTable() {
        String[] keys = StringArray.split(getParameter("keywords"));
        Set<String> s = new TreeSet<>();
        for (String key : keys) {
            String k = key.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));

            // normalize name
            try {
                f = f.getCanonicalFile();
            } catch (IOException ex) {
                ex.printStackTrace();
            }

            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 a list of associated files for a specified test description.
     * Normally, this will include the file containing the test description,
     * and any source files used by the test.  By default, the source files
     * are determined from the test description's "source" entry.
     *
     * @return a list of associated files for this test description
     */
    public URL[] getSourceURLs() {
        ArrayList<File> res = new ArrayList<>();

        // always include the file containing the test description
        res.add(getFile());
        // add in files given in source parameter
        res.addAll(Arrays.asList(getSourceFiles()));

        URL[] urls = new URL[res.size()];
        for (int i = 0; i < res.size(); i++) {
            try {
                urls[i] = res.get(i).toURI().toURL();
            } catch (MalformedURLException ex) {
                ex.printStackTrace();
                urls[i] = null;
            }
        }

        return urls;
    }

    /**
     * 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")} instead
     */
    @java.lang.Deprecated
    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")} instead
     */
    @java.lang.Deprecated
    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")} instead
     */
    @java.lang.Deprecated
    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")} instead
     */
    @java.lang.Deprecated
    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 No longer relevant for some test suites, so will not be supported
     * in the future.  If needed the value can be determined by asking the test
     * suite's {@code TestFinder}.
     */
    @java.lang.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() {
        if (rrurl == null) {
            String id = getParameter("id");
            rrurl = id == null ? rootRelativePath : rootRelativePath + "#" + id;
            rrurl = rrurl.intern();
        }

        return rrurl;
    }

    /**
     * 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()
     */
    @java.lang.Deprecated
    public File getRootRelativeDir() {
        String p = getRootRelativeFile().getParent();
        return p == null ? new File(".") : new File(p);
    }

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

    /**
     * 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<String> getParameterKeys() {
        return new Iterator<String>() {
            int pos = 0;

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

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

            @Override
            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.
     */
    @Override
    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<String, String> 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<String, String> p, String key, String value) {
        if (value != null) {
            p.put(key, value);
        }
    }
}

