/*
 * Copyright (c) 1998, 2022, 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.
 */

/*
 * This source code is provided to illustrate the usage of a given feature
 * or technique and has been deliberately simplified. Additional steps
 * required for a production-quality application, such as security checks,
 * input validation and proper error handling, might not be present in
 * this sample code.
 */


package com.sun.tools.example.debug.tty;

import com.sun.jdi.*;
import com.sun.jdi.event.*;
import com.sun.jdi.request.EventRequest;
import com.sun.jdi.request.EventRequestManager;
import com.sun.jdi.request.ThreadDeathRequest;

public class EventHandler implements Runnable {

    EventNotifier notifier;
    Thread thread;
    volatile boolean connected = true;
    boolean completed = false;
    String shutdownMessageKey;
    boolean stopOnVMStart;
    private final boolean trackVthreads;

    EventHandler(EventNotifier notifier, boolean stopOnVMStart, boolean trackVthreads) {
        this.notifier = notifier;
        this.stopOnVMStart = stopOnVMStart;
        this.trackVthreads = trackVthreads;
        this.thread = new Thread(this, "event-handler");
        this.thread.start();
    }

    synchronized void shutdown() {
        connected = false;  // force run() loop termination
        thread.interrupt();
        while (!completed) {
            try {wait();} catch (InterruptedException exc) {}
        }
    }

    @Override
    public void run() {
        EventQueue queue = Env.vm().eventQueue();
        while (connected) {
            try {
                EventSet eventSet = queue.remove();
                boolean resumeStoppedApp = false;
                EventIterator it = eventSet.eventIterator();
                while (it.hasNext()) {
                    resumeStoppedApp |= !handleEvent(it.nextEvent());
                }

                if (resumeStoppedApp) {
                    eventSet.resume();
                } else if (eventSet.suspendPolicy() == EventRequest.SUSPEND_ALL) {
                    setCurrentThread(eventSet);
                    notifier.vmInterrupted();
                }
            } catch (InterruptedException exc) {
                // Do nothing. Any changes will be seen at top of loop.
            } catch (VMDisconnectedException discExc) {
                handleDisconnectedException();
                break;
            }
        }
        synchronized (this) {
            completed = true;
            notifyAll();
        }
    }

    private boolean handleEvent(Event event) {
        notifier.receivedEvent(event);

        /*
         * See if the event thread is a vthread that we need to start tracking.
         */
        ThreadReference eventThread = null;
        if (event instanceof ClassPrepareEvent evt) {
            eventThread = evt.thread();
        } else if (event instanceof LocatableEvent evt) {
            eventThread = evt.thread();
        }
        if (eventThread != null) {
            // This might be a vthread we haven't seen before, so add it to the list.
            boolean added = ThreadInfo.addThread(eventThread);
            if (added) {
                // If added, it should be a vthread. Platform threads are always added
                // when the ThreadStart event arrives, so should already be in the list.
                assert eventThread.isVirtual();
            }
            // If we added it, we need to make sure it eventually gets removed. Usually
            // this happens when the ThreadDeathEvent comes in, but if !trackVthreads,
            // then the ThreadDeathRequest was setup to filter out vthreads. So we'll need
            // to create a special ThreadDeathRequest just to detect when this vthread dies.
            if (added && !trackVthreads) {
                EventRequestManager erm = Env.vm().eventRequestManager();
                ThreadDeathRequest tdr = erm.createThreadDeathRequest();
                tdr.addThreadFilter(eventThread);
                tdr.enable();
            }
        }

        if (event instanceof ExceptionEvent) {
            return exceptionEvent(event);
        } else if (event instanceof BreakpointEvent) {
            return breakpointEvent(event);
        } else if (event instanceof WatchpointEvent) {
            return fieldWatchEvent(event);
        } else if (event instanceof StepEvent) {
            return stepEvent(event);
        } else if (event instanceof MethodEntryEvent) {
            return methodEntryEvent(event);
        } else if (event instanceof MethodExitEvent) {
            return methodExitEvent(event);
        } else if (event instanceof ClassPrepareEvent) {
            return classPrepareEvent(event);
        } else if (event instanceof ClassUnloadEvent) {
            return classUnloadEvent(event);
        } else if (event instanceof ThreadStartEvent) {
            return threadStartEvent(event);
        } else if (event instanceof ThreadDeathEvent) {
            return threadDeathEvent(event);
        } else if (event instanceof VMStartEvent) {
            return vmStartEvent(event);
        } else {
            return handleExitEvent(event);
        }
    }

    private boolean vmDied = false;
    private boolean handleExitEvent(Event event) {
        if (event instanceof VMDeathEvent) {
            vmDied = true;
            return vmDeathEvent(event);
        } else if (event instanceof VMDisconnectEvent) {
            connected = false;
            if (!vmDied) {
                vmDisconnectEvent(event);
            }
            /*
             * Inform jdb command line processor that jdb is being shutdown. JDK-8154144.
             */
            ((TTY)notifier).setShuttingDown(true);
            Env.shutdown(shutdownMessageKey);
            return false;
        } else {
            throw new InternalError(MessageOutput.format("Unexpected event type",
                                                         new Object[] {event.getClass()}));
        }
    }

    synchronized void handleDisconnectedException() {
        /*
         * A VMDisconnectedException has happened while dealing with
         * another event. We need to flush the event queue, dealing only
         * with exit events (VMDeath, VMDisconnect) so that we terminate
         * correctly.
         */
        EventQueue queue = Env.vm().eventQueue();
        while (connected) {
            try {
                EventSet eventSet = queue.remove();
                EventIterator iter = eventSet.eventIterator();
                while (iter.hasNext()) {
                    handleExitEvent(iter.next());
                }
            } catch (InterruptedException exc) {
                // ignore
            } catch (InternalError exc) {
                // ignore
            }
        }
    }

    private ThreadReference eventThread(Event event) {
        if (event instanceof ClassPrepareEvent) {
            return ((ClassPrepareEvent)event).thread();
        } else if (event instanceof LocatableEvent) {
            return ((LocatableEvent)event).thread();
        } else if (event instanceof ThreadStartEvent) {
            return ((ThreadStartEvent)event).thread();
        } else if (event instanceof ThreadDeathEvent) {
            return ((ThreadDeathEvent)event).thread();
        } else if (event instanceof VMStartEvent) {
            return ((VMStartEvent)event).thread();
        } else {
            return null;
        }
    }

    private void setCurrentThread(EventSet set) {
        ThreadReference thread;
        if (set.size() > 0) {
            /*
             * If any event in the set has a thread associated with it,
             * they all will, so just grab the first one.
             */
            Event event = set.iterator().next(); // Is there a better way?
            thread = eventThread(event);
        } else {
            thread = null;
        }
        setCurrentThread(thread);
    }

    private void setCurrentThread(ThreadReference thread) {
        ThreadInfo.invalidateAll();
        ThreadInfo.setCurrentThread(thread);
    }

    private boolean vmStartEvent(Event event)  {
        VMStartEvent se = (VMStartEvent)event;
        notifier.vmStartEvent(se);
        return stopOnVMStart;
    }

    private boolean breakpointEvent(Event event)  {
        BreakpointEvent be = (BreakpointEvent)event;
        notifier.breakpointEvent(be);
        return true;
    }

    private boolean methodEntryEvent(Event event)  {
        MethodEntryEvent me = (MethodEntryEvent)event;
        notifier.methodEntryEvent(me);
        return true;
    }

    private boolean methodExitEvent(Event event)  {
        MethodExitEvent me = (MethodExitEvent)event;
        return notifier.methodExitEvent(me);
    }

    private boolean fieldWatchEvent(Event event)  {
        WatchpointEvent fwe = (WatchpointEvent)event;
        notifier.fieldWatchEvent(fwe);
        return true;
    }

    private boolean stepEvent(Event event)  {
        StepEvent se = (StepEvent)event;
        notifier.stepEvent(se);
        return true;
    }

    private boolean classPrepareEvent(Event event)  {
        ClassPrepareEvent cle = (ClassPrepareEvent)event;
        notifier.classPrepareEvent(cle);

        if (!Env.specList.resolve(cle)) {
            MessageOutput.lnprint("Stopping due to deferred breakpoint errors.");
            return true;
        } else {
            return false;
        }
    }

    private boolean classUnloadEvent(Event event)  {
        ClassUnloadEvent cue = (ClassUnloadEvent)event;
        notifier.classUnloadEvent(cue);
        return false;
    }

    private boolean exceptionEvent(Event event) {
        ExceptionEvent ee = (ExceptionEvent)event;
        notifier.exceptionEvent(ee);
        return true;
    }

    private boolean threadDeathEvent(Event event) {
        ThreadDeathEvent tde = (ThreadDeathEvent)event;
        ThreadReference thread = tde.thread();
        ThreadInfo.removeThread(thread);

        if (!trackVthreads && thread.isVirtual()) {
            // Remove the ThreadDeathRequest used for this event since it was created
            // just to remove this one vthread.
            EventRequestManager erm = Env.vm().eventRequestManager();
            erm.deleteEventRequest(event.request());
        }
        return false;
    }

    private boolean threadStartEvent(Event event) {
        ThreadStartEvent tse = (ThreadStartEvent)event;
        ThreadInfo.addThread(tse.thread());
        notifier.threadStartEvent(tse);
        return false;
    }

    public boolean vmDeathEvent(Event event) {
        shutdownMessageKey = "The application exited";
        notifier.vmDeathEvent((VMDeathEvent)event);
        return false;
    }

    public boolean vmDisconnectEvent(Event event) {
        shutdownMessageKey = "The application has been disconnected";
        notifier.vmDisconnectEvent((VMDisconnectEvent)event);
        return false;
    }
}
