/*
 * Scone - The Web Enhancement Framework
 * Copyright (C) 2009 Harald Weinreich, Volkert Buchmann, Frank Wollenweber, Torsten Haß
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program 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 for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
 package scone.robot;


import java.util.Vector;


/**
 * The PageLoaderPool is a modified version of the threadpool described in the article
 * "Why Thread Pools are Important in Java" by Tarak Modi, Java Pro magazine.
 * http://www.devx.com/upload/free/features/javapro/2000/10oct00/tm0010/tm0010.asp<br><br>
 *
 * This class takes entries from the urlQueue and then starts pageLoaderThreads, that process the pages.
 *
 */


class PageLoaderPool {

    // The robot, this threadpool belongs to
    private Robot _robot;

    // max number of threads that can be created
    private int _maxThreads = -1;

    // min number of threads that must always be present
    private int _minThreads = -1;

    // max idle time after which a thread may be killed
    private int _maxIdleTime = -1;

    // the priority of the threads in the pool
    private int _priority = Thread.NORM_PRIORITY;

    // A list i.e. vector of pending jobs
    private URLQueue _urlQueue;

    // A list i.e. vector of all available threads
    private Vector _availableThreads;

    // The debug flag
    private boolean _debug = false;

    /**
     * This class represents an element in the available threads vector.
     **/

    private class ThreadElement {

        // if true then the thread can process a new job
        private boolean _idle;

        // serves as the key
        private Thread _thread;

        /**
         * constructor
         * @param thread
         **/
        public ThreadElement(Thread thread) {
            _thread = thread;
            _idle = true;
        }
    }


    /**
     * Each thread in the pool is an instance of this class
     **/

    private class PoolThread extends Thread {

        private Object _lock;
        // pass in the pool instance for synchronization.


        /**
         * constructor
         * @param lock
         **/
        public PoolThread(Object lock) {
            _lock = lock;
            setPriority(_priority);
        }

        /**
         * This is where all the action is...
         **/
        public void run() {
            Runnable job = null;

            while (true) {
                while (true) {
                    synchronized (_lock) {

                        // Keep processing jobs until none availble
                        if (_urlQueue.size() == 0) {
                            if (_debug) {
                                System.out.println("Idle Thread...");
                            }
                            int index = findMe();

                            if (index == -1) {
                                return;
                            }
                            ((ThreadElement) _availableThreads.get(index))._idle = true;
                            break;
                        }

                        // Remove the job from the pending list.
                        QueueEntry qe = null;

                        synchronized (_urlQueue) {
                            qe = _urlQueue.dequeue();
                            qe.getRobotTask().addOpenUri(qe);
                            qe.getRobotTask().incOpenThreads();
                        }
                        if (qe.getRobotTask().getStartTime() == -1) {
                            qe.getRobotTask().setStartTime();
                        }
                        job = new PageLoaderThread(_robot, qe); // Makes a new job of the queueEntry
                    }

                    // run the job
                    job.run();
                    job = null;
                }
                try {
                    synchronized (this) {

                        // if no idle time specified, wait till notified.
                        if (_maxIdleTime == -1) {
                            super.wait();
                        } else {
                            super.wait(_maxIdleTime);
                        }
                    }
                } catch (InterruptedException e) {

                    // Cleanup if interrupted
                    synchronized (_lock) {
                        if (_debug) {
                            System.out.println("Interrupted...");
                        }
                        removeMe();
                    }
                    return;
                }

                // Just been notified or the wait timed out
                synchronized (_lock) {

                    // If there are no jobs, that means we "idled" out.
                    if (_urlQueue.size() == 0) {
                        if (_minThreads != -1
                                && _availableThreads.size() > _minThreads) {
                            if (_debug) {
                                System.out.println("Thread timed out...");
                            }
                            removeMe();
                            return;
                        }
                    }
                }
            }
        }
    }

    /**
     * constructor
     * @param props Properties for this ThreadPool
     * @param robot A reference to the robot. Only used to pass it to the new created pageLoaderThreads
     * @param urlqueue The pageLoaderPool uses this queue to store the tasks which should be executed by the threads in the pool.
     **/
    PageLoaderPool(java.util.Properties props, Robot robot, URLQueue urlqueue) throws NumberFormatException, IllegalArgumentException {
        this._robot = robot;
        this._urlQueue = urlqueue;
        _availableThreads = new Vector();
        if (props == null) {
            return;
        }
        Object o = props.getProperty("maxThreads");

        if (o != null) {
            int n = java.lang.Integer.parseInt((String) o);

            if (n < 1) {
                throw new IllegalArgumentException("maxThreads must be an integral value greater than 0");
            }
            _maxThreads = n;
        }
        o = props.getProperty("minThreads");
        if (o != null) {
            int n = java.lang.Integer.parseInt((String) o);

            if (n < 0) {
                throw new IllegalArgumentException("minThreads must be an integral " + "value greater than or equal to 0");
            }
            if (n > _maxThreads) {
                throw new IllegalArgumentException("minThreads cannot be greater than maxThreads");
            }
            _minThreads = n;
        }
        o = props.getProperty("maxIdleTime");
        if (o != null) {
            int n = java.lang.Integer.parseInt((String) o);

            if (n < 1) {
                throw new IllegalArgumentException("maxIdleTime must be an integral value greater than 0");
            }
            _maxIdleTime = n;
        }
        o = props.getProperty("priority");
        if (o != null) {
            int n = java.lang.Integer.parseInt((String) o);

            if ((n < Thread.MIN_PRIORITY) || (n > Thread.MAX_PRIORITY)) {
                throw new IllegalArgumentException("priority must be an integral value between " + Thread.MIN_PRIORITY + " and " + Thread.MAX_PRIORITY);
            }
            _priority = n;
        }
        o = props.getProperty("debug");
        if (o != null) {
            _debug = true;
        }
    }

    /**
     * Queues an entry for execution.
     * @param qe A pageLoaderThread (which implements the Interface Runnable) is created to be run by one of the tasks in the pool
     **/
    void queue(QueueEntry qe) {
        synchronized (_availableThreads) {
            _urlQueue.queue(qe);
            int index = findFirstIdleThread();

            if (index == -1) {

                // All threads are busy
                if (_maxThreads == -1 || _availableThreads.size() < _maxThreads) {

                    // We can create another thread...
                    if (_debug) {
                        System.out.println("Creating a new Thread...");
                    }
                    ThreadElement e = new ThreadElement(new PoolThread(_availableThreads));

                    e._idle = false;
                    e._thread.start();
                    _availableThreads.add(e);
                    return;
                }

                // We are not allowed to create any more threads
                // So, just return.

                // When one of the busy threads is done,
                // it will check the pending queue and will see this job.
                if (_debug) {
                    System.out.println("Max Threads created and all threads" + " in the pool are busy.");
                }
            } else {

                // There is at least one idle thread.
                if (_debug) {
                    System.out.println("Using an existing thread...");
                }
                ((ThreadElement) _availableThreads.get(index))._idle = false;
                synchronized (((ThreadElement) _availableThreads.get(index))._thread) {
                    ((ThreadElement) _availableThreads.get(index))._thread.notify();
                }
            }
        }
    }

    /**
     * Returns some statistics about the ThreadPool
     * @return A PageLoaderPoolStats object containing the statistics
     **/
    PageLoaderPoolStats getStats() {
        synchronized (_availableThreads) {
            PageLoaderPoolStats stats = new PageLoaderPoolStats();

            stats.maxThreads = _maxThreads;
            stats.minThreads = _minThreads;
            stats.maxIdleTime = _maxIdleTime;
            stats.pendingJobs = _urlQueue.size();
            stats.numThreads = _availableThreads.size();
            stats.jobsInProgress = _availableThreads.size()
                    - findNumIdleThreads();
            return (stats);
        }
    }

    // *****************************************
    // Important...
    // All private methods must always be
    // called from a synchronized method!!
    // *****************************************


    /**
     * Called by the thread pool to find the number of idle threads
     **/
    private int findNumIdleThreads() {
        int idleThreads = 0;
        int size = _availableThreads.size();

        for (int i = 0; i < size; i++) {
            if (((ThreadElement) _availableThreads.get(i))._idle) {
                idleThreads++;
            }
        }
        return (idleThreads);
    }

    /**
     * Called by the thread pool to find the first idle thread
     **/
    private int findFirstIdleThread() {
        int size = _availableThreads.size();

        for (int i = 0; i < size; i++) {
            if (((ThreadElement) _availableThreads.get(i))._idle) {
                return (i);
            }
        }
        return (-1);
    }

    /**
     *  Called by a pool thread to find itself in the vector of available threads.
     **/
    private int findMe() {
        int size = _availableThreads.size();

        for (int i = 0; i < size; i++) {
            if (((ThreadElement) _availableThreads.get(i))._thread
                    == Thread.currentThread()) {
                return (i);
            }
        }
        return (-1);
    }

    /**
     * Called by a pool thread to remove itself from the vector of available threads
     **/
    private void removeMe() {
        int size = _availableThreads.size();

        for (int i = 0; i < size; i++) {
            if (((ThreadElement) _availableThreads.get(i))._thread
                    == Thread.currentThread()) {
                _availableThreads.remove(i);
                return;
            }
        }
    }
}
