/*
 * Written by Dawid Kurzyniec and released to the public domain, as explained
 * at http://creativecommons.org/licenses/publicdomain
 */

package edu.emory.mathcs.util.concurrent;

import java.lang.reflect.*;
import java.security.PrivilegedAction;
import java.security.AccessController;
import java.util.*;
import edu.emory.mathcs.backport.java.util.concurrent.*;

/**
 * Low-level thread access utilities.
 *
 * @author Dawid Kurzyniec
 * @version 1.0
 */
public class ThreadUtils {

    private static Method stopThreadMethod;
    private static Method suspendThreadMethod;
    private static Method resumeThreadMethod;

    private static ThreadGroup prvTGroup = (ThreadGroup)
        AccessController.doPrivileged(new PrivilegedAction() { public Object run() {
            return new ThreadGroup(getTopLevelThreadGroup(), "_thread-utils");
        }});

    static {
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        try {
            Class threadCls = Class.forName("java.lang.Thread", true, cl);
            try {
                stopThreadMethod = threadCls.getMethod("stop", new Class[] { Throwable.class });
                suspendThreadMethod = threadCls.getMethod("suspend", null);
                resumeThreadMethod = threadCls.getMethod("resume", null);
            } catch (NoSuchMethodException e) {
                // not good
                throw new InternalError(e.getMessage());
            }
        }
        catch (ClassNotFoundException e) {
            // fatal
            throw new InternalError(e.getMessage());
        }
    }

    private ThreadUtils() {}

    /**
     * Returns the root thread group.
     * @return the root thread group
     */
    public static ThreadGroup getTopLevelThreadGroup() {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        while (tg.getParent() != null) tg = tg.getParent();
        return tg;
    }

    /**
     * Stops specified thread.
     * @param t the thread to stop
     */
    public static void stopThread(Thread t) {
        stopThread(t, new ThreadDeath());
    }

    /**
     * Stops specified thread with specified cause.
     * @param t the thread to stop
     * @param cause the stop cause
     */
    public static void stopThread(Thread t, ThreadDeath cause) {
        try {
            stopThread(t, 0, cause);
        }
        catch (InterruptedException e) {
            // can't happen
            throw new RuntimeException(e.getMessage());
        }
    }

    public static boolean isTerminated(Thread thread) {
        // on 1.5, it is easy - just call getState. But on 1.4, we have a
        // problem, since isAlive does return false also in case if the thread
        // has not yet started. Fortunately, each thread has a thread group,
        // and it is cleared upon exiting.
        if (thread.isAlive()) return false;
        ThreadGroup tg = thread.getThreadGroup();
        if (tg == null) return true;
        return false;
    }

    /**
     * Stops specified thread with specified timeout and cause.
     * @param t the thread to stop
     * @param timeout number of milliseconds to wait for stop to complete
     * @param cause the stop cause
     * @return true if thread was stopped within the specified timeout,
     *         false otherwise.
     */
    public static boolean stopThread(Thread t, long timeout, ThreadDeath cause)
        throws InterruptedException
    {
        if (stopThreadMethod != null) {
            try {
                // stop is no-op if not started yet. So make sure the thread
                // is started before we try to stop it. It may throw
                // IllegalThreadStateException if already running.
                try { t.start(); }
                catch (Exception e) {}
                stopThreadMethod.invoke(t, new Object[] { cause });
                t.interrupt();
                if (timeout == 0) {
                    return t.isAlive();
                }
                else if (timeout > 0) {
                    t.join(timeout);
                }
                else {
                    t.join();
                }
                return t.isAlive();
            } catch (InvocationTargetException e) {
                Throwable e1 = e.getTargetException();
                if (e1 instanceof Error) {
                    throw (Error)e1;
                } else if (e1 instanceof RuntimeException) {
                    throw (RuntimeException)e1;
                } else {
                    throw new InternalError(e1.getMessage());
                }
            } catch (IllegalAccessException e) {
                throw new InternalError(e.getMessage());
            }
        }
        return false;
    }

    public static boolean suspendThread(Thread t) {
        if (suspendThreadMethod != null) {
            try {
                suspendThreadMethod.invoke(t, null);
                return true;
            } catch (InvocationTargetException e) {
                Throwable e1 = e.getTargetException();
                if (e1 instanceof Error) {
                    throw (Error)e1;
                } else if (e1 instanceof RuntimeException) {
                    throw (RuntimeException)e1;
                } else {
                    throw new InternalError(e1.getMessage());
                }
            } catch (IllegalAccessException e) {
                throw new InternalError(e.getMessage());
            }
        }
        return false;
    }

    public static void resumeThread(Thread t) {
        if (resumeThreadMethod != null) {
            try {
                resumeThreadMethod.invoke(t, null);
            } catch (InvocationTargetException e) {
                throw new InternalError(e.getMessage());
            } catch (IllegalAccessException e) {
                throw new InternalError(e.getMessage());
            }
        }
    }

    public static boolean destroyThreadGroup(ThreadGroup tg, long timeout)
        throws InterruptedException, ExecutionException, TimeoutException
    {
        return sync(asyncDestroyThreadGroup(tg, null), timeout);
    }

    public static Future asyncDestroyThreadGroup(final ThreadGroup tg,
        Callback cb)
    {
        return asyncDestroyThreadGroup(tg, cb, new ThreadDeath());
    }

    public static boolean destroyThreadGroup(ThreadGroup tg, long timeout,
                                             ThreadDeath cause)
        throws InterruptedException, ExecutionException, TimeoutException
    {
        return sync(asyncDestroyThreadGroup(tg, null, null, cause), timeout);
    }

    public static Future asyncDestroyThreadGroup(final ThreadGroup tg,
        Callback cb, ThreadDeath cause)
    {
        return Executioner.doOp(Executioner.DESTROY, tg, null, cause, cb);
    }

    public static boolean destroyThreadGroup(ThreadGroup tg, long timeout,
                                             boolean suicide, ThreadDeath cause)
        throws InterruptedException, ExecutionException, TimeoutException
    {
        return sync(asyncDestroyThreadGroup(tg, suicide, null, cause), timeout);
    }

    public static Future asyncDestroyThreadGroup(final ThreadGroup tg,
                                                 boolean suicide, Callback cb,
                                                 ThreadDeath cause)
    {
        return Executioner.doOp(Executioner.DESTROY, tg,
            suicide ? null : new Thread[] { Thread.currentThread() }, cause, cb);
    }

    public static boolean destroyThreadGroup(ThreadGroup tg, long timeout,
                                             Thread[] waitFor, ThreadDeath cause)
        throws InterruptedException, ExecutionException, TimeoutException
    {
        return sync(asyncDestroyThreadGroup(tg, waitFor, null, cause), timeout);
    }

    public static Future asyncDestroyThreadGroup(final ThreadGroup tg,
        Thread[] waitFor, Callback cb, ThreadDeath cause)
    {
        return Executioner.doOp(Executioner.DESTROY, tg, waitFor, cause, cb);
    }

    public static boolean stopThreadGroup(ThreadGroup tg, long timeout)
        throws InterruptedException, ExecutionException, TimeoutException
    {
        return sync(asyncStopThreadGroup(tg, null), timeout);
    }

    public static Future asyncStopThreadGroup(ThreadGroup tg,
        Callback cb)
    {
        return asyncStopThreadGroup(tg, true, cb, new ThreadDeath());
    }

    public static boolean stopThreadGroup(ThreadGroup tg, long timeout,
                                          boolean suicide, ThreadDeath cause)
        throws InterruptedException, ExecutionException, TimeoutException
    {
        return sync(asyncStopThreadGroup(tg, suicide, null, cause), timeout);
    }

    public static Future asyncStopThreadGroup(ThreadGroup tg, boolean suicide,
                                              Callback cb, ThreadDeath cause)
    {
        return asyncStopThreadGroup(tg,
            suicide ? null : new Thread[] { Thread.currentThread() }, cb, cause);
    }

    public static boolean stopThreadGroup(ThreadGroup tg, long timeout,
                                          Thread[] exclude, ThreadDeath cause)
        throws InterruptedException, ExecutionException, TimeoutException
    {
        return sync(asyncStopThreadGroup(tg, exclude, null, cause), timeout);
    }

    public static Future asyncStopThreadGroup(ThreadGroup tg,
        Thread[] exclude, Callback cb, ThreadDeath cause)
    {
        return Executioner.doOp(Executioner.STOP, tg, exclude, cause, cb);
    }

    public static boolean suspendThreadGroup(ThreadGroup tg, long timeout,
                                             boolean suicide)
        throws InterruptedException, ExecutionException, TimeoutException
    {
        return sync(asyncSuspendThreadGroup(tg, suicide, null), timeout);
    }

    public static Future asyncSuspendThreadGroup(ThreadGroup tg,
        boolean suicide, Callback cb)
    {
        return asyncSuspendThreadGroup(tg,
            suicide ? null : new Thread[] { Thread.currentThread() }, cb);
    }

    public static boolean suspendThreadGroup(ThreadGroup tg, long timeout,
                                             Thread[] exclude)
        throws InterruptedException, ExecutionException, TimeoutException
    {
        return sync(asyncSuspendThreadGroup(tg, exclude, null), timeout);
    }

    public static Future asyncSuspendThreadGroup(ThreadGroup tg,
        Thread[] exclude, Callback cb)
    {
        return Executioner.doOp(Executioner.SUSPEND, tg, exclude, null, cb);
    }

    public static boolean resumeThreadGroup(ThreadGroup tg, long timeout,
                                            Thread[] exclude)
        throws InterruptedException, ExecutionException, TimeoutException
    {
        return sync(asyncResumeThreadGroup(tg, exclude, null), timeout);
    }

    public static Future asyncResumeThreadGroup(ThreadGroup tg,
                                                Thread[] exclude, Callback cb)
    {
        return Executioner.doOp(Executioner.RESUME, tg, exclude, null, cb);
    }

    private static class Executioner extends Thread implements Callable {
        static final int DESTROY = 1;
        static final int STOP    = 2;
        static final int SUSPEND = 3;
        static final int RESUME  = 4;
        final ThreadGroup tg;
        final int op;
        final Thread[] exclude;
        volatile boolean done = false;
        final ThreadDeath cause;
        final Result result;

        private static class Result extends AsyncTask {

            Result(Callback cb) {
                super(cb);
            }
            protected Runnable getPerformer(Callable call) {
                return super.createPerformer(call, true);
            }
            protected void setFailed(Throwable cause) {
                super.setFailed(cause);
            }
        }

        Executioner(ThreadGroup tg, int op, Thread[] exclude, ThreadDeath cause,
                    Callback cb)
        {
            super(prvTGroup, "exec");
            this.tg = tg;
            this.op = op;
            this.setPriority(Thread.MAX_PRIORITY);
            this.cause = cause;
            this.result = new Result(cb);

            if (exclude == null) exclude = new Thread[0];
            List excludeList = new ArrayList(exclude.length);
            for (int i = 0; i < exclude.length; i++) {
                ThreadGroup localTg = exclude[i].getThreadGroup();
                while (localTg != null && localTg != tg) {
                    localTg = localTg.getParent();
                }
                if (localTg != null) {
                    // exclude[i] is indeed within this tg, add it
                    excludeList.add(exclude[i]);
                }
            }
            exclude = (Thread[])excludeList.toArray(new Thread[excludeList.size()]);
            Arrays.sort(exclude, 0, exclude.length, threadComparator);
            this.exclude = exclude;
        }

        public static Future doOp(final int doWhat, final ThreadGroup tg,
            final Thread[] exclude, final ThreadDeath cause, final Callback cb)
        {
            return (Future)AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    Executioner executioner = new Executioner(tg, doWhat, exclude,
                                                              cause, cb);
                    return executioner.exec();
                }
            });
        }

//        private boolean exec(long tout) {
//            start();
//            try {
//                if (tout >= 0) {
//                    this.join(tout);
//                } else {
//                    this.join();
//                }
//                if (!done) interrupt();
//                return done;
//            } catch (InterruptedException e) {
//                // can happen that the current thread is stopped (or suspended,
//                // or resumed) as a result of this operation; in this case,
//                // the returned value does not matter
//                Thread.currentThread().interrupt();
//                return true;
//            }
//        }

        private Future exec() {
            start();
            return result;
        }

        public void run() {
            result.getPerformer(this).run();
        }

        public Object call() throws InterruptedException {
            int nthreads;
            switch (op) {
                case SUSPEND:
                case RESUME:
                    execTGOperation(tg, exclude, op);
                    return null;
                case STOP:
                    while (true) {
                        execTGOperation(tg, exclude, op);
                        nthreads = tg.activeCount();
                        if (nthreads == 0) {
                            return null;
                        }
                        else if (nthreads <= exclude.length) {
                            // maybe they are all in exclude?...
                            Thread[] check = new Thread[nthreads];
                            nthreads = tg.enumerate(check, true);
                            for (int i=nthreads-1; i>=0; i--) {
                                if (Arrays.binarySearch(exclude, check[i],
                                                       threadComparator) >= 0)
                                {
                                    nthreads--;
                                }
                            }
                            if (nthreads == 0) {
                                return null;
                            }
                        }
                        Thread.yield();
                    }
                case DESTROY:
                    while (true) {
                        execTGOperation(tg, exclude, op);
                        nthreads = tg.activeCount();
                        if (nthreads > 0 && nthreads <= exclude.length) {
                            // maybe they are all in exclude?...
                            Thread[] check = new Thread[nthreads];
                            nthreads = tg.enumerate(check, true);
                            for (int i=nthreads-1; i>=0; i--) {
                                if (Arrays.binarySearch(exclude, check[i],
                                                       threadComparator) >= 0)
                                {
                                    // wait for the thread to complete
                                    check[i].join();
                                    nthreads--;
                                }
                            }
                        }
                        if (tg.activeCount() == 0) {
                            tg.destroy();
                        }
                        if (nthreads == 0) {
                            return null;
                        }
                    }
                default:
                    throw new RuntimeException();
            }
        }

        private static Comparator threadComparator = new Comparator() {
            public int compare(Object o1, Object o2) {
                if (o1 == o2) return 0;
                Thread t1 = (Thread)o1;
                Thread t2 = (Thread)o2;
                int h1 = t1.hashCode();
                int h2 = t2.hashCode();
                if (h1 != h2) return h2-h1;
                // hash codes equal; must be more fancy
                String s1 = t1.toString();
                String s2 = t2.toString();
                return s1.compareTo(s2);
            }
        };

        private void execTGOperation(ThreadGroup tg, Thread[] exclude, int op)
            throws InterruptedException
        {
            int nthreads = tg.activeCount();
            Thread[] threads = new Thread[nthreads];
            Set processed = new HashSet(nthreads*2);
            boolean all;
            do {
                if (interrupted()) throw new InterruptedException();
                all = true;
                int active = tg.enumerate(threads, true);
                for (int i = 0; i < active; i++) {
                    Thread t = threads[i];
                    if (processed.contains(t) ||
                        Arrays.binarySearch(exclude, t, threadComparator) >= 0) {
                        continue;
                    }
                    switch (op) {
                        case STOP:
                        case DESTROY:
                            stopThread(threads[i], cause);
                            break;
                        case SUSPEND:
                            suspendThread(threads[i]);
                            break;
                        case RESUME:
                            resumeThread(threads[i]);
                            break;
                        default:
                            throw new RuntimeException();
                    }
                    processed.add(t);
                    all = false;
                }
            }
            while (!all);
        }
    }

    private static boolean sync(Future future, long timeout)
        throws InterruptedException, ExecutionException, TimeoutException
    {
        if (timeout < 0) {
            future.get();
        }
        else {
            future.get(timeout, TimeUnit.MILLISECONDS);
        }
        return true;
    }
}
