/*
 * $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.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

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


/**
 * A TestFinder to read a compressed binary file containing the
 * previously "compiled" results of some other test finder.
 */

public class BinaryTestFinder extends TestFinder
{
    /**
     * Create an uninitialized binary test finder. Use one of the init
     * methods to initialize it.
     */
    public BinaryTestFinder() { }

    /**
     * Create and initialize a binary test finder.
     * @param jtdFile   The binary file containing the precompiled test descriptions.
     *                  The file should be an absolute file.
     * @throws TestFinder.Fault
     *                  if there is a problem while reading the data file.
     * @deprecated Use BinaryTestFinder(File testSuiteRoot, File jtdFile) instead
     * @see #BinaryTestFinder(File, File)
     */
    public BinaryTestFinder(File jtdFile) throws Fault {
        this.jtdFile = jtdFile;
        openBinaryFile();
    }

    /**
     * Create and initialize a binary test finder.
     * @param jtdFile   The binary file containing the precompiled test descriptions.
     *                  If the file is relative, it will be evaluated relative to testSuiteRoot.
     * @param testSuiteRoot The root file of the Test Suite.
     *                  This is usually a file called testsuite.html
     * @throws TestFinder.Fault
     *                  if there is a problem while reading the data file.
     * @deprecated
     */
    public BinaryTestFinder(File testSuiteRoot, File jtdFile) throws Fault {
        init(testSuiteRoot, jtdFile);
    }

    /**
     * Initialize a new BinaryTestFinder. This method opens the binary file to read
     *          from, calls methods to read the data, and initializes other variables.
     *
     * @param args      Any arguments needed by the TestFinder. You must pass "-binary"
     *                  followed by the path to the binary file. If the file is relative,
     *                  it will be evaluated relative to testSuiteRoot.
     * @param testSuiteRoot The root file of the Test Suite.
     *                  This is usually a file called testsuite.html
     * @param env       Environment file for the TestFinder. Not used by BinaryTestFinder.
     * @throws TestFinder.Fault
     *                  if there is a problem while reading the data file.
     */
    public void init(String[] args, File testSuiteRoot, TestEnvironment env) throws Fault {
        super.init(args, testSuiteRoot, env);
        openBinaryFile();
    }

    /**
     * Initialize a new BinaryTestFinder. This method opens the binary file to read
     *          from, calls methods to read the data, and initializes other variables.
     *
     * @param testSuiteRoot The root file of the Test Suite.
     *                  This is usually a file called testsuite.html
     * @param jtdFile   The file containing the binary data for the test descriptions.
     *                  If the file is relative, it will be evaluated relative to testSuiteRoot.
     * @throws TestFinder.Fault
     *                  if there is a problem while reading the data file.
     */
    public void init(File testSuiteRoot, File jtdFile) throws Fault {
        super.init(null, testSuiteRoot, null);
        this.jtdFile = jtdFile;
        openBinaryFile();
    }

    /**
     * Decodes any args needed by BinaryTestFinder. The only supported args
     * are "-binary" or "-jtd" followed by the name of the binary file,
     * or the name of the binary file as the last argument.
     * @param args      An array of arguments, containing the next argument to be
     *                  decoded.
     * @param i         The position in the args array of the next argument to be
     *                  decoded.
     * @return          The number of entries in the args array that were
     *                  taken as part of the next argument.
     * @throws TestFinder.Fault
     *                  if any problems occur while decoding the next argument
     */
    protected int decodeArg(String[] args, int i) throws Fault {
        if (args[i].equalsIgnoreCase("-binary") || args[i].equalsIgnoreCase("-jtd")) {
            String e = args[i+1];
            jtdFile = new File(e);
            return 2;
        }
        else if (i == args.length - 1 &&  !args[i].startsWith("-")) {
            jtdFile = new File(args[i]);
            return 1;
        }
        else
            return super.decodeArg(args, i);
    }

    public long lastModified(File f) {
        // with BTF, the file will never change, so rescanning should never be
        // needed.  And, after the first scan, 1 will always be earlier than the
        // current date, so rescanning won't be triggered again.
        return 1;
    }

    public boolean isFolder(File path) {
        if (zipFile != null)
            readBinaryFile();

        File rootDir = getRootDir();
        String relPath = getRelativePath(rootDir, path);
        TestTree.Node node = (testTree == null ? null : testTree.getNode(relPath));

        /*
        System.out.println("isFolder() for " + path.getPath() + " = " +
            (node == null ? false : true));
        */

        // temporary fix which will handle most TCKs
        // need to find algorithm to determine if the path is a test in
        // a leightweight manner
        if (path.getPath().endsWith(".html") ||
            path.getPath().endsWith(".java") ||
            path.getPath().endsWith(".xml"))
            return false;

        if (node != null)
            return true;
        else
            return false;
    }

    /**
     * Given a File, scan to look for other files or tests. These can
     * then be used through getTests() and getFiles().
     *
     * This method first takes the file path and makes it relative to the testsuite
     * root. It then finds the corresponding node in the test tree, and calls
     * foundFile and foundTestDescription based on the info in that node, thereby
     * recreating the effect of the original read of this file by the original
     * test finder.
     */
    protected void scan(File file) {
        if (zipFile != null)
            readBinaryFile();

        try {
            // locate the node for this file
            File rootDir = getRootDir();
            String relPath = getRelativePath(rootDir, file);
            TestTree.Node node = (testTree == null ? null : testTree.getNode(relPath));

            if (node == null) {
                error(i18n, "bin.cantFindPath", file.getPath());
                return;
            }

            // simulate files to be scanned for child nodes
            TestTree.Node[] children = node.children;
            if (children != null) {
                for (int i = 0; i < children.length; i++) {
                    File f = (relPath.length() == 0 ? new File(children[i].name) :
                                            new File(relPath, children[i].name));
                    foundFile(f);
                }
            }

            // regenerate test descriptions
            if (node.testIndexes != null) {
                File testFile = new File(relPath);
                for (int i = 0; i < node.testIndexes.length; i++) {
                    TestDescription td = node.getTest(i, rootDir, testFile, testTable);
                    foundTestDescription(td);
                }
            }
        }
        catch (IOException e) {
            error(i18n, "bin.internalIOError", new Object[] {file.getPath(), e});
        }
    }

    /**
     * Create the relative path of a filename relative to a root directory.
     * @throws IllegalArgumentException is the file is not under the root
     * directory.
     */
    private String getRelativePath(File rootDir, File file) {
        if (file.isAbsolute()) {
            String rootDirPath = rootDir.getPath();
            String filePath = file.getPath();
            if (filePath.startsWith(rootDirPath)) {
                return (filePath.equals(rootDirPath)
                        ? ""
                        : filePath.substring(rootDirPath.length() + 1));
            }
            else
                throw new IllegalArgumentException();
        }
        else
            return file.getPath();
    }

    /**
     * Read the binary file. The input file should be a zip file as written by
     * BinaryTestWriter. All three sections are reed, but only the string table
     * and test tree are parsed at this point. The test table is read from an
     * internal byte array as required.
     */
    private void openBinaryFile() throws Fault {
        try {
            if (jtdFile == null)
                throw new Fault(i18n, "bin.noFile");

            // open these all here to take the hit of exceptions
            // as early as possible
            File root = getRoot();
            File f = (jtdFile.isAbsolute() || root == null ? jtdFile : new File(root, jtdFile.getPath()));
            zipFile = new ZipFile(f);
            stringsEntry = zipFile.getEntry("strings");
            testsEntry = zipFile.getEntry("tests");
            treeEntry = zipFile.getEntry("tree");

            if (stringsEntry == null || testsEntry == null || treeEntry == null)
                throw new Fault(i18n, "bin.badBinFile", zipFile.getName());
        }
        catch (FileNotFoundException e) {
            throw new Fault(i18n, "bin.cantFindFile", jtdFile.getPath());
        }
        catch (IOException e) {
            throw new Fault(i18n, "bin.ioError", new Object[] {jtdFile.getPath(), e});
        }
    }

    /**
     * Read the binary file. The input file should be a zip file as written by
     * BinaryTestWriter. All three sections are reed, but only the string table
     * and test tree are parsed at this point. The test table is read from an
     * internal byte array as required.
     */
    private void readBinaryFile() {
        if (zipFile == null)
            throw new IllegalStateException();

        try {
            stringTable = StringTable.read(zipFile, stringsEntry);
            testTable = TestTable.read(zipFile, testsEntry, stringTable);
            testTree = TestTree.read(zipFile, treeEntry);
        }
        catch (Fault e) {
            error(i18n, "bin.fault", new Object[] {jtdFile.getPath(), e});
        }
        catch (IOException e) {
            error(i18n, "bin.ioError", new Object[] {jtdFile.getPath(), e});
        }
        finally {
            try {
                zipFile.close();
            }
            catch (IOException e) {
                error(i18n, "bin.ioError", new Object[] {jtdFile.getPath(), e});
            }
            stringsEntry = null;
            testsEntry = null;
            treeEntry = null;
            zipFile = null;
        }
    }

    //------------------------------------------------------------------------------------------

    /**
     * Read an int from a stream using a variable length encoding.
     * The integer is read in 7 bit chunks in big-endian order, with
     * the top bit in each byte indicating more data to be read.
     */
    private static int readInt(DataInputStream in) throws IOException {
        int n = 0;
        int b;
        while ((b = in.readUnsignedByte()) >= 0x80)
            n = (n << 7) | (b & 0x7f);
        n = (n << 7) | b;
        return n;
    }

    //------------------------------------------------------------------------------------------

    private File jtdFile;
    private ZipFile zipFile;
    private ZipEntry stringsEntry;
    private ZipEntry testsEntry;
    private ZipEntry treeEntry;
    private StringTable stringTable;
    private TestTable testTable;
    private TestTree testTree;

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

    //------------------------------------------------------------------------------------------

    /**
     * A StringTable is an array of strings. References to these strings
     * are written either as an index into the string array, or as zero
     * followed by the string inline.
     * @see BinaryTestWriter.StringTable
     */
    static class StringTable {
        /**
         * Read a string table from a entry called "strings" in a zip file.
         */
        static StringTable read(ZipFile zf, ZipEntry ze) throws IOException, Fault {
            DataInputStream in = new DataInputStream(new BufferedInputStream(zf.getInputStream(ze)));
            return new StringTable(in);
        }

        /**
         * Read a string table from a stream.
         */
        StringTable(DataInputStream in) throws IOException {
            int count = readInt(in);
            strings = new String[count];
            for (int i = 0; i < count; i++)
                strings[i] = in.readUTF();
        }

        /**
         * Read a reference to a string in the string table. It will either
         * be the index of an entry in the table, or 0 followed by the inline
         * string, if the string is not to be found in the table.
         */
        String readRef(DataInputStream in) throws IOException {
            int index = readInt(in);
            if (index == 0)
                return in.readUTF();
            else
                return strings[index];
        }

        /**
         * Get a string from the table.
         */
        String get(int i) {
            return strings[i];
        }

        private String[] strings;
    }

    //------------------------------------------------------------------------------------------

    /**
     * A TestTable is a collection of TestDescriptions.
     */
    static class TestTable {
        /**
         * Read a test table from an entry called "tests" in a zip file.
         * The data of the entry is read and stored, but only analysed
         * as required.
         */
        static TestTable read(ZipFile zf, ZipEntry ze, StringTable stringTable) throws IOException, Fault {
            byte[] bytes = new byte[(int) (ze.getSize())];
            InputStream in = zf.getInputStream(ze);
            for (int total = 0; total < bytes.length; ) {
                total += in.read(bytes, total, bytes.length - total);
            }
            return new TestTable(bytes, stringTable);
        }

        /**
         * Create a TestTable from a byte array as written by BinaryTestWriter.
         * The data of the entry is stored, and analysed as required.
         */
        TestTable(byte[] data, StringTable stringTable) {
            bais = new ByteArrayInputStream(data);
            this.stringTable = stringTable;
        }

        /**
         * Get a test description from the table. It is reconsituted from the
         * data given when the table was created. The root and file for the
         * test description must be supplied separately; the index is a byte
         * offset within the data of where the test description data is to be found.
         * The test description data is encoded as a count, and then that many
         * name value pairs to be put in the te4st description.
         */
        TestDescription get(File root, File file, int index) throws IOException {
            bais.reset();
            bais.skip(index);
            Map m = new HashMap();
            DataInputStream in = new DataInputStream(bais);
            int n = readInt(in);
            for (int i = 0; i < n; i++) {
                String key = stringTable.readRef(in);
                String value = stringTable.readRef(in);
                m.put(key, value);
            }
            return new TestDescription(root, file, m);
        }

        private ByteArrayInputStream bais;
        private StringTable stringTable;
    }

    //------------------------------------------------------------------------------------------

    /**
     * A TestTree represents a tree of test descriptions; the data of the individual
     * descriptions is stored in a test table.
     */
    static class TestTree {
        /**
         * Read a test tree from an entry called "tree" in a zip file.
         */
        static TestTree read(ZipFile zf, ZipEntry ze) throws IOException, Fault {
            DataInputStream in = new DataInputStream(new BufferedInputStream(zf.getInputStream(ze)));
            Node root = new Node(in);
            return new TestTree(root);
        }

        /**
         * Create a test tree from a root node.
         */
        TestTree(Node root) {
            this.root = root;
        }

        /**
         * Get the node for a path within the tree.
         */
        Node getNode(String path) {
            return (path.length() == 0 ? root : root.getNode(path));
        }

        private Node root;

        /**
         * A node within the test tree. It has a name, a set of indices that can be
         * used to find the test descriptions with the test table, and a set of child
         * nodes.
         */
        static class Node {
            /**
             * Read a node from an input stream. The node is represented as
             * a name, a count, followed by that many test indexes into the
             * corresponding test table, and a count, followed (recursively)
             * by that many child nodes.
             */
            Node(DataInputStream in) throws IOException {
                name = in.readUTF();
                int testCount = readInt(in);
                if (testCount > 0) {
                    testIndexes = new int[testCount];
                    for (int i = 0; i < testIndexes.length; i++)
                        testIndexes[i] = readInt(in);
                }
                int childCount = readInt(in);
                if (childCount > 0) {
                    children = new Node[childCount];
                    for (int i = 0; i < children.length; i++)
                        children[i] = new Node(in);
                }
            }

            /**
             * Get a child node as indicated by a relative path.
             */
            Node getNode(String path) {
                int sep = path.indexOf(File.separatorChar);
                String head = (sep == -1 ? path : path.substring(0, sep));
                for (int i = 0; i < children.length; i++) {
                    Node child = children[i];
                    if (child.name.equals(head))
                        return (sep == -1 ? child : child.getNode(path.substring(sep + 1)));
                }
                // not found
                return null;
            }

            /**
             * Get a TestDescription from this node, using the data in a
             * test table.
             */
            TestDescription getTest(int index, File root, File path, TestTable testTable) throws IOException {
                if (testIndexes == null || index > testIndexes.length)
                    throw new IllegalArgumentException();
                return testTable.get(root, path, testIndexes[index]);
            }

            private String name;
            private int[] testIndexes;
            private Node[] children;
        }
    }
}
