/*******************************************************************************
 * Copyright (c) 2005, 2009 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Matthew Hall - bugs 118516, 281723, 286533
 *******************************************************************************/
package org.eclipse.core.tests.databinding.observable;

import java.util.LinkedList;

import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.core.runtime.Assert;

/**
 * {@link Realm} that enforces execution to be within a specific
 * {@link Thread}.
 *
 * @since 3.2
 */
public class ThreadRealm extends Realm {
    private Thread thread;

    private final LinkedList<Runnable> queue = new LinkedList<Runnable>();

    private volatile boolean block;

    /**
     * Initializes the realm.
     *
     * @param thread
     */
    public synchronized void init(Thread thread) {
        if (thread == null) {
            throw new IllegalArgumentException("Parameter thread was null."); //$NON-NLS-1$
        }
        Assert.isTrue(this.thread == null, "Realm can only be initialized once.");

        this.thread = thread;
    }

    /**
     * @return <code>true</code> if the current thread is the thread for
     *         the realm
     */
    @Override
	public boolean isCurrent() {
        return Thread.currentThread() == thread;
    }

    /**
     * @return thread, <code>null</code> if not
     *         {@link #init(Thread) initialized}
     */
    public Thread getThread() {
        return thread;
    }

    /**
     * Queues the provided <code>runnable</code>.
     *
     * @param runnable
     */
    @Override
	public void asyncExec(Runnable runnable) {
        synchronized (queue) {
            queue.add(runnable);
            queue.notifyAll();
        }
    }

    /**
     * Returns after the realm has completed all runnables currently on its
     * queue.  Do not call from the realm's thread.
     *
     * @throws IllegalStateException
     *             if the ThreadRealm is not blocking on its thread.
     * @throws IllegalStateException
     *             if invoked from the realm's own thread.
     */
    public void processQueue() {
        if (Thread.currentThread() == thread) {
            throw new IllegalStateException(
                    "Cannot execute this method in the realm's own thread");
        }

        try {
            synchronized (queue) {
                while (!queue.isEmpty()) {
                    if (!block)
                        throw new IllegalStateException(
                                "Cannot process queue, ThreadRealm is not blocking on its thread");
                    queue.wait();
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public boolean isBlocking() {
        return block;
    }

    /**
     * Blocks the current thread invoking runnables.
     */
    public void block() {
        if (block) {
            throw new IllegalStateException("Realm is already blocking.");
        }

        if (Thread.currentThread() != thread) {
            throw new IllegalStateException("The current thread is not the correct thread.");
        }

        try {
            block = true;

            synchronized (queue) {
                queue.notifyAll(); // so waitUntilBlocking can return
            }

            while (block) {
                Runnable runnable = null;
                synchronized (queue) {
                    if (queue.isEmpty()) {
                        queue.wait();
                    } else {
                        runnable = queue.getFirst();
                    }
                }

                if (runnable != null) {
                    safeRun(runnable);
                    synchronized (queue) {
                        // Don't remove the runnable from the queue until after
                        // it has run, or else processQueue() may return before
                        // the last runnable has finished
                        queue.removeFirst();
                        queue.notifyAll();
                    }
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            block = false;
        }
    }

    /**
     * Unblocks the thread.
     */
    public void unblock() {
        block = false;

        // Awaken the thread if waiting.
        synchronized (queue) {
            queue.notifyAll();
        }
    }

    /**
     * Blocks until the ThreadRealm is blocking on its own thread.
     */
    public void waitUntilBlocking() {
        if (Thread.currentThread() == thread) {
            throw new IllegalStateException(
                    "Cannot execute this method in the realm's own thread");
        }

        while (!block) {
            synchronized (queue) {
                try {
                    queue.wait();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
}
