/*
 * Copyright (c) 1998, 2006, Oracle and/or its affiliates. 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.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.tools.jdi;

import com.sun.jdi.*;
import com.sun.jdi.event.EventQueue;
import com.sun.jdi.event.EventSet;

import java.util.*;

public class EventQueueImpl extends MirrorImpl implements EventQueue {

    /*
     * Note this is not a synchronized list. Iteration/update should be
     * protected through the 'this' monitor.
     */
    LinkedList<EventSet> eventSets = new LinkedList<EventSet>();

    TargetVM target;
    boolean closed = false;

    EventQueueImpl(VirtualMachine vm, TargetVM target) {
        super(vm);
        this.target = target;
        target.addEventQueue(this);
    }

    /*
     * Override superclass back to default equality
     */
    public boolean equals(Object obj) {
        return this == obj;
    }

    public int hashCode() {
        return System.identityHashCode(this);
    }

    synchronized void enqueue(EventSet eventSet) {
        eventSets.add(eventSet);
        notifyAll();
    }

    synchronized int size() {
        return eventSets.size();
    }

    synchronized void close() {
        if (!closed) {
            closed = true; // OK for this the be first since synchronized

            // place VMDisconnectEvent into queue
            enqueue(new EventSetImpl(vm,
                                     (byte)JDWP.EventKind.VM_DISCONNECTED));
        }
    }

    public EventSet remove() throws InterruptedException {
        return remove(0);
    }

    /**
     * Filter out events not for user's eyes.
     * Then filter out empty sets.
     */
    public EventSet remove(long timeout) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("Timeout cannot be negative");
        }

        EventSet eventSet;
        while (true) {
            EventSetImpl fullEventSet = removeUnfiltered(timeout);
            if (fullEventSet == null) {
                eventSet = null;  // timeout
                break;
            }
            /*
             * Remove events from the event set for which
             * there is no corresponding enabled request (
             * this includes our internally requested events.)
             * This never returns null
             */
            eventSet = fullEventSet.userFilter();
            if (!eventSet.isEmpty()) {
                break;
            }
        }

        if ((eventSet != null) && (eventSet.suspendPolicy() == JDWP.SuspendPolicy.ALL)) {
            vm.notifySuspend();
        }

        return eventSet;
    }

    EventSet removeInternal() throws InterruptedException {
        EventSet eventSet;
        do {
            // Waiting forever, so removeUnfiltered() is never null
            eventSet = removeUnfiltered(0).internalFilter();
        } while (eventSet == null || eventSet.isEmpty());

        /*
         * Currently, no internal events are requested with a suspend
         * policy other than none, so we don't check for notifySuspend()
         * here. If this changes in the future, there is much
         * infrastructure that needs to be updated.
         */

        return eventSet;
    }

    private TimerThread startTimerThread(long timeout) {
        TimerThread thread = new TimerThread(timeout);
        thread.setDaemon(true);
        thread.start();
        return thread;
    }

    private boolean shouldWait(TimerThread timerThread) {
        return !closed && eventSets.isEmpty() &&
               ((timerThread == null) ? true : !timerThread.timedOut());
    }

    private EventSetImpl removeUnfiltered(long timeout)
                                               throws InterruptedException {
        EventSetImpl eventSet = null;

        /*
         * Make sure the VM has completed initialization before
         * trying to build events.
         */
        vm.waitInitCompletion();

        synchronized(this) {
            if (!eventSets.isEmpty()) {
                /*
                 * If there's already something there, no need
                 * for anything elaborate.
                 */
                eventSet = (EventSetImpl)eventSets.removeFirst();
            } else {
                /*
                 * If a timeout was specified, create a thread to
                 * notify this one when a timeout
                 * occurs. We can't use the timed version of wait()
                 * because it is possible for multiple enqueue() calls
                 * before we see something in the eventSet queue
                 * (this is possible when multiple threads call
                 * remove() concurrently -- not a great idea, but
                 * it should be supported). Even if enqueue() did a
                 * notify() instead of notifyAll() we are not able to
                 * use a timed wait because there's no way to distinguish
                 * a timeout from a notify.  That limitation implies a
                 * possible race condition between a timed out thread
                 * and a notified thread.
                 */
                TimerThread timerThread = null;
                try {
                    if (timeout > 0) {
                        timerThread = startTimerThread(timeout);
                    }

                    while (shouldWait(timerThread)) {
                        this.wait();
                    }
                } finally {
                    if ((timerThread != null) && !timerThread.timedOut()) {
                        timerThread.interrupt();
                    }
                }

                if (eventSets.isEmpty()) {
                    if (closed) {
                        throw new VMDisconnectedException();
                    }
                } else {
                    eventSet = (EventSetImpl)eventSets.removeFirst();
                }
            }
        }

        // The build is synchronized on the event set, don't hold
        // the queue lock.
        if (eventSet != null) {
            target.notifyDequeueEventSet();
            eventSet.build();
        }
        return eventSet;
    }

    private class TimerThread extends Thread {
        private boolean timedOut = false;
        private long timeout;

        TimerThread(long timeout) {
            super(vm.threadGroupForJDI(), "JDI Event Queue Timer");
            this.timeout = timeout;
        }

        boolean timedOut() {
            return timedOut;
        }

        public void run() {
            try {
                Thread.sleep(timeout);
                EventQueueImpl queue = EventQueueImpl.this;
                synchronized(queue) {
                    timedOut = true;
                    queue.notifyAll();
                }
            } catch (InterruptedException e) {
                // Exit without notifying
            }
        }
    }
}
