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

import com.sun.javatest.util.DynamicArray;
import com.sun.javatest.util.Fifo;
import com.sun.javatest.util.I18NResourceBundle;

/**
 * An iterator-based interface to the tests in a test suite, as read by a test finder.
 */
public class TestFinderQueue {
    /**
     * This interface provides a means for TestFinder to report on events that
     * might be of interest as it executes.
     */
    public static interface Observer
    {
        /**
         * Another file which needs to be read has been found.
         * @param file the file which was found
         */
        void found(File file);

        /**
         * A file is being read.
         * @param file the file being read
         */
        void reading(File file);

        /**
         * A file has been read.
         * @param file the file which was read
         */
        void done(File file);

        /**
         * A test description has been found.
         * @param td the test description which was found
         */
        void found(TestDescription td);

        /**
         * A test description which was previously found, has been rejected by
         * a test filter, and so has not been put in the queue of tests to be executed.
         * @param td the test description which was rejected by the filter
         * @param f the filter which rejected the test
         */
        void ignored(TestDescription td, TestFilter f);

        /**
         * A test description that was previously put in the test finder queue
         * has been taken from the queue and passed back to the client caller.
         * @param td the test description which was taken from the queue
         */
        void done(TestDescription td);

        /**
         * The queue of tests has been flushed.
         */
        void flushed();

        /**
         * An error was reported by the test finder while reading a file.
         * @param msg a detail message describing the error
         */
        void error(String msg);

        /**
         * An error was reported by the test finder while reading a file.
         * @param td the test description to which the error applies
         * @param msg a detail message describing the error
         */
        void error(TestDescription td, String msg);
    }

    /**
     * Create a test finder queue.
     */
    public TestFinderQueue() {
    }

    /**
     * Create a test finder queue, using a specified test finder.
     * @param finder the test finder to be used to read the tests
     */
    public TestFinderQueue(TestFinder finder) {
        setTestFinder(finder);
    }

    /**
     * Get the test finder being used by this object.
     * @return the test finder being used by this object
     * @see #setTestFinder
     */
    public TestFinder getTestFinder() {
        return testFinder;
    }

    /**
     * Set the test finder to be used by this object.
     * It may only be set once.
     * @param finder the test finder to be used by this object
     * @throws NullPointerException if the finder is null
     * @throws IllegalStateException if the finder has already been set
     * @see #getTestFinder
     */
    public void setTestFinder(TestFinder finder) {
        if (finder == null)
            throw new NullPointerException();

        if (testFinder != null && testFinder != finder)
            throw new IllegalStateException();

        this.testFinder = finder;
        testFinder.setErrorHandler(new TestFinder.ErrorHandler() {
            public void error(String msg) {
                errorCount++;
                notifier.error(msg);
            }
        });
    }

    /**
     * Set an array of filters that will be used to filter the tests read by the
     * test finder. Each test must be accepted by all the filters to be put in
     * the queue.
     * @param filters the filters to be used.
     */
    public void setFilters(TestFilter[] filters) {
        this.filters = filters;
    }

    /**
     * Set the initial set of files to be read by the test finder.
     * Additional files may be read as a result of reading these and subsequent files.
     * @param initTests the initial set of files to be read by the test finder
     */
    public synchronized void setTests(String[] initTests) {
        File testSuiteRoot = testFinder.getRoot();

        // make canonical copy of tests
        // really ought not to be using File, since the tests may contain a trailing #xxx
        File[] files;
        if (initTests == null)
            // ensure not null
            files = new File[] {testSuiteRoot};
        else {
            files = new File[initTests.length];
            for (int i = 0; i < initTests.length; i++) {
                files[i] = new File(initTests[i]);
            }
        }

        rootDir = (testSuiteRoot.isDirectory() ?
                   testSuiteRoot : new File(testSuiteRoot.getParent()));

        // build up the fifo of tests to be used by readNextFile

        tests = new Fifo();
        currInitialFile = null;

        for (int pass = 0; pass < 2; pass++) {
            for (int i = 0; i < files.length; i++) {
                File f = files[i];
                String n = f.getName();
                // in pass 0, only select initial files without #
                // in pass 1, only select initial files with #
                if ((n.indexOf("#") != -1) == (pass == 0))
                    continue;

                // ensure all absolute, or if relative, make them relative
                // to rootDir
                if (!f.isAbsolute())
                    f = new File(rootDir, f.getPath());
                // ensure no trailing file separator
                String p = f.getPath();
                if (p.endsWith(File.separator))
                    f = new File(p.substring(0, p.length() - 1));
                tests.insert(f);
            }
        }

        filesRemainingCount = filesToRead.size() + tests.size();
    }

    /**
     * Set a flag indicating whether it is OK to find no tests in the
     * specified set of files. If set to false, and if no tests have
     * been found by the time the last file has been read, an error
     * will be notified to any observers.
     * @param zeroTestsOK set to true to suppress an error being generated
     * if no tests are found by the time that all files have been read
     */
    public void setZeroTestsOK(boolean zeroTestsOK) {
        this.zeroTestsOK = zeroTestsOK;
    }

    /**
     * Set the queue to "repeat" a set of test descriptions by putting
     * them in the test found queue again.
     * @param tds the test descriptions to be "found again".
     * @deprecated retained for historical purposes
     */
    public void repeat(TestDescription[] tds) {
        if (tests == null)
            tests = new Fifo(); // for now
        for (int i = 0; i < tds.length; i++) {
            TestDescription td = tds[i];
            testDescsFound.insert(td);
            testsFoundCount++;
            notifier.found(td);
        }
    }



    /**
     * Get the next test description if one is available, or null when all have
     * been returned.
     *
     * @return     A test description or null.
     */
    public TestDescription next() {
        TestDescription td;

        synchronized (this) {
            while (needReadAhead() && readNextFile()) /*NO-OP*/;

            // read files until there is a test description available or there
            // are no more files.
            while ((td = (TestDescription)(testDescsFound.remove())) == null) {
                boolean ok = readNextFile();
                if (!ok)
                    return null;
            }

            // note testsDone, for readAhead
            testsDoneCount++;
        }

        notifier.done(td);
        return td;
    }


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

    /**
     * Get the root directory for the test finder.
     * @return the root directory, as set in the test finder
     */
    public File getRoot() {
        return rootDir;
    }

    //--------------------------------------------------------------------------
    //
    // these are all carefully arranges to not need to be synchronized

    /**
     * Get the number of files that have been found so far.
     * @return the number of files that have been found so far
     */
    public int getFilesFoundCount() {
        return filesFound.size();
    }

    /**
     * Get the number of files that have been found and read so far.
     * @return the number of files that have been found and read so far
     */
    public int getFilesDoneCount() {
        return filesDoneCount;
    }

    /**
     * Get the number of files that have been found but not yet read so far.
     * @return the number of files that have been found but not yet read so far
     */
    public int getFilesRemainingCount() {
        return filesRemainingCount;
    }

    /**
     * Get the number of tests that have been found so far.
     * @return the number of tests that have been found so far
     */
    public int getTestsFoundCount() {
        return testsFoundCount;
    }

    /**
     * Get the number of tests that have been read from this object so far.
     * @return the number of tests that have been read from this object so far
     */
    public int getTestsDoneCount() {
        return testsDoneCount;
    }

    /**
     * Get the number of tests which have been found but not yet from this
     * object so far.
     * @return the number of tests which have been found but not yet read
     * from this object so far
     */
    public int getTestsRemainingCount() {
        return testDescsFound.size();
    }

    /**
     * Get the number of errors that have been found so far by the test finder
     * while reading the tests.
     * @return the number of errors that have been found so far by the test finder
     * while reading the tests.
     */
    public int getErrorCount() {
        return errorCount;
    }

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

    /**
     * Add an observer to monitor the progress of the TestFinder.
     * @param o the observer
     */
    public void addObserver(Observer o) {
        notifier.addObserver(o);
    }

    /**
     * Remove an observer form the set currently monitoring the progress
     * of the TestFinder.
     * @param o the observer
     */
    public void removeObserver(Observer o) {
        notifier.removeObserver(o);
    }

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


    /**
     * Set the amount of read-ahead done by the finder.
     * @param mode      acceptable values are as follows:
     * <dl> <dt> 0: no read ahead
     * <dd> Files are not read ahead more than necessary
     * <dt> 1: low read ahead
     * <dd> A low priority thread is created to read the test files
     * when the system is otherwise idle
     * <dt> 2: medium read ahead
     * <dd> A low priority thread is created to read the test files
     * when the system is otherwise idle. In addition, if the number
     * of tests done approaches the number of tests read, then more
     * tests will be read.
     * <dt> 3: full and immediate read ahead
     * <dd> All the tests will be read now
     * </dl>
     */
    public synchronized void setReadAheadMode(byte mode) {
        switch (mode) {
        case NO_READ_AHEAD:
        case FULL_READ_AHEAD:
            readAheadMode = mode;
            readAheadWorker = null; // worker will note this and go away
            break;

        case LOW_READ_AHEAD:
        case MEDIUM_READ_AHEAD:
            readAheadMode = mode;
            if (readAheadWorker == null) {
                readAheadWorker = new Thread() {
                    public void run() {
                        // This is intended to be interruptible and
                        // relies on safe atomic access to worker
                        while ((readAheadWorker == this) && readNextFile()) /*NO-OP*/;
                        // when thread exits; flatten pointer if still current
                        synchronized (TestFinderQueue.this) {
                            if (readAheadWorker == this)
                                readAheadWorker = null;
                        }
                    }
                };
                readAheadWorker.setName("TestFinderQueue:Worker:" + workerIndex++);
                readAheadWorker.setPriority(Thread.MIN_PRIORITY);
                readAheadWorker.start();
            }
            break;

        default:
            throw new IllegalArgumentException("invalid value for mode");
        }
    }

    /**
     * A constant specifying that the test finder queue should not perform
     * any read ahead.
     */
    public static final byte NO_READ_AHEAD = 0;

    /**
     * A constant specifying the test finder queue should perform minimal
     * read ahead.
     */
    public static final byte LOW_READ_AHEAD = 1;

    /**
     * A constant specifying the test finder queue should perform medium
     * (typical) read ahead.
     */
    public static final byte MEDIUM_READ_AHEAD = 2;

    /**
     * A constant specifying the test finder queue should perform complete
     * read ahead, reading all tests from the test finder before returning any
     * from this object.
     */
    public static final byte FULL_READ_AHEAD = 3;

    /**
     * Flush all readahead.
     */
    public void flush() {
        synchronized (this) {
            filesToRead.setSize(0);
            tests.flush();
            testDescsFound.flush();
            filesRemainingCount = 0;
        }
        notifier.flushed();
    }


    /**
     * This method is called from next() to determine if any readAhead
     * should be done there and then, before getting the next TestDescription
     * for the client.
     */
    private boolean needReadAhead() {
        switch (readAheadMode) {
        case FULL_READ_AHEAD:
            return true;
        case MEDIUM_READ_AHEAD:
            // return true if not many tests read yet, or if testsDoneCount
            // is greater than a certain percentage of testsFoundCount.
            // This percentage increases inverse exponentially on the
            // number of tests found. The intent is to try and keep
            // progress meters based on testsDoneCount and testsFoundCount helpful,
            // while permitting readAhead to be done.
            // The formula has the following datapoints:
            // testsFoundCount: 1000   percent: 18%
            // testsFoundCount: 10000  percent: 86%
            if (testsFoundCount < 100)
                // don't let tests start until at least 100 have been read
                return true;
            else {
                double percent = 1 - Math.exp(-0.0002 * testsFoundCount);
                return (testsDoneCount > (testsFoundCount * percent));
            }
        default:
            return false;
        }
    }

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

    private synchronized boolean readNextFile() {
        if (filesToRead.isEmpty()) {
            // have we finished reading an initial file and found no test descriptions in it?
            // if so, inform the caller
            if (currInitialFile != null
                && testsFoundCountBeforeCurrInitialFile == testsFoundCount
                && !zeroTestsOK) {
                errorCount++;
                notifier.error(i18n.getString("finder.noTests", currInitialFile));
            }

            // are there any more tests that have not been read?
            // check until we find one (just one).
            while (filesToRead.isEmpty() && !tests.isEmpty()) {
                currInitialFile = (File)tests.remove();
                foundFile(currInitialFile);
            }

            // if we didn't find any more initial files, there is nothing more to do
            if (filesToRead.isEmpty()) {
                currInitialFile = null;
                return false;
            }
            else
                testsFoundCountBeforeCurrInitialFile = testsFoundCount;
        }


        File f = (File)(filesToRead.lastElement());
        filesToRead.setSize(filesToRead.size() - 1);
        filesRemainingCount = filesToRead.size() + tests.size();

        String path = f.getPath();
        int index = path.indexOf('#');
        if (index != -1) {
            selectedId = path.substring(index + 1);
            f = new File(path.substring(0, index));
        }

        // The filesToRead are maintained in a vector that approximates a stack:
        // new entries are added at or near the end, and entries are taken from
        // the end. The subtlety is to add the new files found in file in reverse
        // order, so that when removed from the end, they are used in the
        // correct order. This is done by inserting each new file found at a fixed
        // place, corresponding to the end of the vector as it is when scan() starts.
        // The net effect is to push up files added earlier in this scan, so they
        // they will be used first.   The overall net effect is that of a depth-first
        // search for test descriptions.
        fileInsertPosn = filesToRead.size();

        notifier.reading(f);
        try {
            testFinder.read(f);
        }
        finally {
            TestDescription[] tds = testFinder.getTests();
            for (int i = 0; i < tds.length; i++) {
                foundTestDescription(tds[i]);
            }

            File[] files = testFinder.getFiles();
            for (int i = 0; i < files.length; i++) {
                foundFile(files[i]);
            }

            // done limiting tests to this id
            selectedId = null;
            filesDoneCount++;
            notifier.done(f);

            /*
            if (filesToRead.isEmpty()) {
                // we have read all the files we can
                // flush various tables to free up any space used
                filesFound.clear();
                testsInFile.clear();
            }
            */

        }

        // read a file OK
        return true;
    }

    /**
     * Add a file to the queue of files waiting to be read.
     * It will be added to the queue if it has not already been read or is
     * on the queue waiting to be read, and if the finder is not looking for
     * a specific test in the current file.
     * @param newFile The file to be queued
     */
    private void foundFile(File newFile) {
        // only accept new files if not looking for a specific test in the
        // current file
        if (selectedId == null) {
            Object prev = filesFound.put(newFile.getPath(), newFile);
            if (prev == null) {
                filesToRead.insertElementAt(newFile, fileInsertPosn);
                notifier.found(newFile);
            }
        }
    }

    private void foundTestDescription(TestDescription td) {
        // if we are not searching for a specific test, or if we are and we have
        // found it, then add the test to the list of tests we have found
        if (selectedId == null || selectedId.equals(td.getId())) {
            if (filters != null) {
                for (int i = 0; i < filters.length; i++) {
                    TestFilter filter = filters[i];
                    try {
                        if (!filter.accepts(td)) {
                            notifier.ignored(td, filter);
                            return;
                        }
                    }
                    catch (TestFilter.Fault e) {
                        errorCount++;
                        notifier.error(td, e.getMessage());
                        return;
                    }
                }
            }

            testDescsFound.insert(td);
            testsFoundCount++;
            notifier.found(td);
        }
    }

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

    private static class Notifier implements Observer {
        public synchronized void addObserver(Observer o) {
            observers = (Observer[])DynamicArray.append(observers, o);
        }

        public synchronized void removeObserver(Observer o) {
            observers = (Observer[])DynamicArray.remove(observers, o);
        }

        public synchronized void found(File file) {
            for (int i = 0; i < observers.length; i++)
                observers[i].found(file);
        }

        public synchronized void reading(File file) {
            for (int i = 0; i < observers.length; i++)
                observers[i].reading(file);
        }

        public synchronized void done(File file) {
            for (int i = 0; i < observers.length; i++)
                observers[i].done(file);
        }

        public synchronized void found(TestDescription td) {
            for (int i = 0; i < observers.length; i++)
                observers[i].found(td);
        }

        public synchronized void ignored(TestDescription td, TestFilter f) {
            for (int i = 0; i < observers.length; i++)
                observers[i].ignored(td, f);
        }

        public synchronized void done(TestDescription td) {
            for (int i = 0; i < observers.length; i++)
                observers[i].done(td);
        }

        public synchronized void flushed() {
            for (int i = 0; i < observers.length; i++)
                observers[i].flushed();
        }

        public synchronized void error(String msg) {
            for (int i = 0; i < observers.length; i++)
                observers[i].error(msg);
        }

        public synchronized void error(TestDescription td, String msg) {
            for (int i = 0; i < observers.length; i++)
                observers[i].error(td, msg);
        }

        private Observer[] observers = new Observer[0];
    }

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


    private TestFinder testFinder;
    private Fifo tests;
    private TestFilter[] filters;
    private String selectedId;
    private File rootDir;
    private File currInitialFile;
    private int testsFoundCountBeforeCurrInitialFile;
    private boolean zeroTestsOK;

    private Vector filesToRead  = new Vector(32, 8);
    private int fileInsertPosn;
    private Fifo testDescsFound = new Fifo();
    private int filesRemainingCount;
    private int filesDoneCount;
    private int testsDoneCount;
    private int testsFoundCount;
    private int errorCount;

    private Hashtable filesFound  = new Hashtable();

    private byte readAheadMode;
    private Thread readAheadWorker;
    private static int workerIndex;

    private Notifier notifier = new Notifier();
    private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(TestFinder.class);
}
