package org.bndtools.utils.jface;

import java.lang.reflect.InvocationTargetException;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.operation.IRunnableContext;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.swt.widgets.Display;

/**
 * This class aims to improve the bedlam of Eclipse's progress dialogs that don't interrupt the threads, thus requiring
 * us to constantly poll the {@link IProgressMonitor#isCanceled()} method, which is not generally possible if we are
 * doing IO or other blocking tasks.
 */
public class ProgressRunner {

    /**
     * <p>
     * Execute the specified runnable within the runnable context. The work is done on a separate thread, and if the
     * user cancels the operation then the work thread will be interrupted with {@link Thread#interrupt()}. If custom
     * cancellation logic is required then the provided runnable can implement {@link CancellableTask} instead of
     * {@link IProgressMonitor} directly.
     * </p>
     * <p>
     * This method is synchronous and returns when the runnable completes, whether normally or cancellation or error.
     * </p>
     *
     * @param fork
     * @param runnable
     * @param context
     * @param display
     * @throws InvocationTargetException
     */
    public static void execute(boolean fork, final IRunnableWithProgress runnable, IRunnableContext context, final Display display) throws InvocationTargetException {
        try {
            context.run(fork, true, new IRunnableWithProgress() {
                @Override
                public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
                    OffGuiThreadProgressMonitor delegatingMonitor = new OffGuiThreadProgressMonitor(display, monitor);
                    ProgressRunnerThread thread = new ProgressRunnerThread(runnable, delegatingMonitor);
                    thread.start();

                    if (display.getThread() == Thread.currentThread()) {
                        // We are running unforked on the display thread, so keep it alive and processing events while
                        // we ping the cancellation status.
                        while (thread.isAlive()) {
                            if (!display.readAndDispatch()) {
                                if (monitor.isCanceled()) {
                                    thread.interrupt();
                                }
                                display.sleep();
                            }
                        }
                    } else {
                        // We are forked on another thread, so just sleep in between pings.
                        while (thread.isAlive()) {
                            if (monitor.isCanceled())
                                thread.interrupt();
                            Thread.sleep(500);
                        }
                    }

                    InvocationTargetException exception = thread.exception;
                    if (exception != null)
                        throw exception;
                }
            });
        } catch (InterruptedException e) {
            // ignore
        }
    }

    private static class ProgressRunnerThread extends Thread {

        private final IRunnableWithProgress runnable;
        private final IProgressMonitor monitor;

        private volatile InvocationTargetException exception = null;

        public ProgressRunnerThread(IRunnableWithProgress runnable, IProgressMonitor monitor) {
            super("Progress Runner");
            this.runnable = runnable;
            this.monitor = monitor;
        }

        @Override
        public void run() {
            try {
                runnable.run(monitor);
            } catch (InvocationTargetException e) {
                this.exception = e;
            } catch (InterruptedException e) {
                // ignore
            }
        }

        @Override
        public void interrupt() {
            try {
                if (runnable instanceof CancellableTask)
                    ((CancellableTask) runnable).cancel();
            } finally {
                super.interrupt();
            }
        }

    }

    private static class OffGuiThreadProgressMonitor implements IProgressMonitor {

        private final Display display;
        private final IProgressMonitor delegate;

        public OffGuiThreadProgressMonitor(Display display, IProgressMonitor delegate) {
            this.display = display;
            this.delegate = delegate;
        }

        @Override
        public void beginTask(final String name, final int totalWork) {
            display.asyncExec(new Runnable() {
                @Override
                public void run() {
                    delegate.beginTask(name, totalWork);
                }
            });
        }

        @Override
        public void done() {
            display.asyncExec(new Runnable() {
                @Override
                public void run() {
                    delegate.done();
                }
            });
        }

        @Override
        public void internalWorked(final double work) {
            display.asyncExec(new Runnable() {
                @Override
                public void run() {
                    delegate.internalWorked(work);
                }
            });
        }

        @Override
        public boolean isCanceled() {
            return Thread.currentThread()
                .isInterrupted();
        }

        @Override
        public void setCanceled(boolean value) {}

        @Override
        public void setTaskName(final String name) {
            display.asyncExec(new Runnable() {
                @Override
                public void run() {
                    delegate.setTaskName(name);
                }
            });
        }

        @Override
        public void subTask(final String name) {
            display.asyncExec(new Runnable() {
                @Override
                public void run() {
                    delegate.subTask(name);
                }
            });
        }

        @Override
        public void worked(final int work) {
            display.asyncExec(new Runnable() {
                @Override
                public void run() {
                    delegate.worked(work);
                }
            });
        }
    }

}
