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

import com.sun.jdi.*;
import com.sun.jdi.request.*;
import com.sun.jdi.connect.*;
import com.sun.tools.example.debug.expr.ExpressionParser;
import com.sun.tools.example.debug.expr.ParseException;

import java.io.*;
import java.util.*;

import com.sun.tools.example.debug.event.*;

import javax.swing.SwingUtilities;

/**
 * Move this towards being only state and functionality
 * that spans across Sessions (and thus VMs).
 */
public class ExecutionManager {

    private Session session;

    /**
     * Get/set JDI trace mode.
     */
    int traceMode = VirtualMachine.TRACE_NONE;

  //////////////////    Listener registration    //////////////////

  // Session Listeners

    ArrayList<SessionListener> sessionListeners = new ArrayList<SessionListener>();

    public void addSessionListener(SessionListener listener) {
        sessionListeners.add(listener);
    }

    public void removeSessionListener(SessionListener listener) {
        sessionListeners.remove(listener);
    }

  // Spec Listeners

  ArrayList<SpecListener> specListeners = new ArrayList<SpecListener>();

    public void addSpecListener(SpecListener cl) {
        specListeners.add(cl);
    }

    public void removeSpecListener(SpecListener cl) {
        specListeners.remove(cl);
    }

    // JDI Listeners

    ArrayList<JDIListener> jdiListeners = new ArrayList<JDIListener>();

    /**
     * Adds a JDIListener
     */
    public void addJDIListener(JDIListener jl) {
        jdiListeners.add(jl);
    }

    /**
     * Adds a JDIListener - at the specified position
     */
    public void addJDIListener(int index, JDIListener jl) {
        jdiListeners.add(index, jl);
    }

    /**
     * Removes a JDIListener
     */
    public void removeJDIListener(JDIListener jl) {
        jdiListeners.remove(jl);
    }

  // App Echo Listeners

    private ArrayList<OutputListener> appEchoListeners = new ArrayList<OutputListener>();

    public void addApplicationEchoListener(OutputListener l) {
        appEchoListeners.add(l);
    }

    public void removeApplicationEchoListener(OutputListener l) {
        appEchoListeners.remove(l);
    }

  // App Output Listeners

    private ArrayList<OutputListener> appOutputListeners = new ArrayList<OutputListener>();

    public void addApplicationOutputListener(OutputListener l) {
        appOutputListeners.add(l);
    }

    public void removeApplicationOutputListener(OutputListener l) {
        appOutputListeners.remove(l);
    }

  // App Error Listeners

    private ArrayList<OutputListener> appErrorListeners = new ArrayList<OutputListener>();

    public void addApplicationErrorListener(OutputListener l) {
        appErrorListeners.add(l);
    }

    public void removeApplicationErrorListener(OutputListener l) {
        appErrorListeners.remove(l);
    }

  // Diagnostic Listeners

    private ArrayList<OutputListener> diagnosticsListeners = new ArrayList<OutputListener>();

    public void addDiagnosticsListener(OutputListener l) {
        diagnosticsListeners.add(l);
    }

    public void removeDiagnosticsListener(OutputListener l) {
        diagnosticsListeners.remove(l);
    }

  ///////////    End Listener Registration    //////////////

    //### We probably don't want this public
    public VirtualMachine vm() {
        return session == null ? null : session.vm;
    }

    void ensureActiveSession() throws NoSessionException {
        if (session == null) {
         throw new NoSessionException();
      }
    }

    public EventRequestManager eventRequestManager() {
        return vm() == null ? null : vm().eventRequestManager();
    }

    /**
     * Get JDI trace mode.
     */
    public int getTraceMode(int mode) {
        return traceMode;
    }

    /**
     * Set JDI trace mode.
     */
    public void setTraceMode(int mode) {
        traceMode = mode;
        if (session != null) {
            session.setTraceMode(mode);
        }
    }

    /**
     * Determine if VM is interrupted, i.e, present and not running.
     */
    public boolean isInterrupted() /* should: throws NoSessionException */ {
//      ensureActiveSession();
        return session.interrupted;
    }

    /**
     * Return a list of ReferenceType objects for all
     * currently loaded classes and interfaces.
     * Array types are not returned.
     */
    public List<ReferenceType> allClasses() throws NoSessionException {
        ensureActiveSession();
        return vm().allClasses();
    }

    /**
     * Return a ReferenceType object for the currently
     * loaded class or interface whose fully-qualified
     * class name is specified, else return null if there
     * is none.
     *
     * In general, we must return a list of types, because
     * multiple class loaders could have loaded a class
     * with the same fully-qualified name.
     */
    public List<ReferenceType> findClassesByName(String name) throws NoSessionException {
        ensureActiveSession();
        return vm().classesByName(name);
    }

    /**
     * Return a list of ReferenceType objects for all
     * currently loaded classes and interfaces whose name
     * matches the given pattern.  The pattern syntax is
     * open to some future revision, but currently consists
     * of a fully-qualified class name in which the first
     * component may optionally be a "*" character, designating
     * an arbitrary prefix.
     */
    public List<ReferenceType> findClassesMatchingPattern(String pattern)
                                                throws NoSessionException {
        ensureActiveSession();
        List<ReferenceType> result = new ArrayList<ReferenceType>();  //### Is default size OK?
        if (pattern.startsWith("*.")) {
            // Wildcard matches any leading package name.
            pattern = pattern.substring(1);
            for (ReferenceType type : vm().allClasses()) {
                if (type.name().endsWith(pattern)) {
                    result.add(type);
                }
            }
            return result;
        } else {
            // It's a class name.
            return vm().classesByName(pattern);
        }
    }

    /*
     * Return a list of ThreadReference objects corresponding
     * to the threads that are currently active in the VM.
     * A thread is removed from the list just before the
     * thread terminates.
     */

    public List<ThreadReference> allThreads() throws NoSessionException {
        ensureActiveSession();
        return vm().allThreads();
    }

    /*
     * Return a list of ThreadGroupReference objects corresponding
     * to the top-level threadgroups that are currently active in the VM.
     * Note that a thread group may be empty, or contain no threads as
     * descendents.
     */

    public List<ThreadGroupReference> topLevelThreadGroups() throws NoSessionException {
        ensureActiveSession();
        return vm().topLevelThreadGroups();
    }

    /*
     * Return the system threadgroup.
     */

    public ThreadGroupReference systemThreadGroup()
                                                throws NoSessionException {
        ensureActiveSession();
        return vm().topLevelThreadGroups().get(0);
    }

    /*
     * Evaluate an expression.
     */

    public Value evaluate(final StackFrame f, String expr)
        throws ParseException,
                                            InvocationException,
                                            InvalidTypeException,
                                            ClassNotLoadedException,
                                            NoSessionException,
                                            IncompatibleThreadStateException {
        ExpressionParser.GetFrame frameGetter = null;
        ensureActiveSession();
        if (f != null) {
            frameGetter = new ExpressionParser.GetFrame() {
                @Override
                public StackFrame get() /* throws IncompatibleThreadStateException */ {
                    return f;
                }
            };
        }
        return ExpressionParser.evaluate(expr, vm(), frameGetter);
    }


    /*
     * Start a new VM.
     */

    public void run(boolean suspended,
                    String vmArgs,
                    String className,
                    String args) throws VMLaunchFailureException {

        endSession();

        //### Set a breakpoint on 'main' method.
        //### Would be cleaner if we could just bring up VM already suspended.
        if (suspended) {
            //### Set breakpoint at 'main(java.lang.String[])'.
            List<String> argList = new ArrayList<String>(1);
            argList.add("java.lang.String[]");
            createMethodBreakpoint(className, "main", argList);
        }

        String cmdLine = className + " " + args;

        startSession(new ChildSession(this, vmArgs, cmdLine,
                                      appInput, appOutput, appError,
                                      diagnostics));
    }

    /*
     * Attach to an existing VM.
     */
    public void attach(String portName) throws VMLaunchFailureException {
        endSession();

        //### Changes made here for connectors have broken the
        //### the 'Session' abstraction.  The 'Session.attach()'
        //### method is intended to encapsulate all of the various
        //### ways in which session start-up can fail. (maddox 12/18/98)

        /*
         * Now that attaches and launches both go through Connectors,
         * it may be worth creating a new subclass of Session for
         * attach sessions.
         */
        VirtualMachineManager mgr = Bootstrap.virtualMachineManager();
        AttachingConnector connector = mgr.attachingConnectors().get(0);
        Map<String, Connector.Argument> arguments = connector.defaultArguments();
        arguments.get("port").setValue(portName);

        Session newSession = internalAttach(connector, arguments);
        if (newSession != null) {
            startSession(newSession);
        }
    }

    private Session internalAttach(AttachingConnector connector,
                                   Map<String, Connector.Argument> arguments) {
        try {
            VirtualMachine vm = connector.attach(arguments);
            return new Session(vm, this, diagnostics);
        } catch (IOException ioe) {
            diagnostics.putString("\n Unable to attach to target VM: " +
                                  ioe.getMessage());
        } catch (IllegalConnectorArgumentsException icae) {
            diagnostics.putString("\n Invalid connector arguments: " +
                                  icae.getMessage());
        }
        return null;
    }

    private Session internalListen(ListeningConnector connector,
                                   Map<String, Connector.Argument> arguments) {
        try {
            VirtualMachine vm = connector.accept(arguments);
            return new Session(vm, this, diagnostics);
        } catch (IOException ioe) {
            diagnostics.putString(
                  "\n Unable to accept connection to target VM: " +
                                  ioe.getMessage());
        } catch (IllegalConnectorArgumentsException icae) {
            diagnostics.putString("\n Invalid connector arguments: " +
                                  icae.getMessage());
        }
        return null;
    }

    /*
     * Connect via user specified arguments
     * @return true on success
     */
    public boolean explictStart(Connector connector, Map<String, Connector.Argument> arguments)
                                           throws VMLaunchFailureException {
        Session newSession = null;

        endSession();

        if (connector instanceof LaunchingConnector) {
            // we were launched, use ChildSession
            newSession = new ChildSession(this, (LaunchingConnector)connector,
                                          arguments,
                                          appInput, appOutput, appError,
                                          diagnostics);
        } else if (connector instanceof AttachingConnector) {
            newSession = internalAttach((AttachingConnector)connector,
                                        arguments);
        } else if (connector instanceof ListeningConnector) {
            newSession = internalListen((ListeningConnector)connector,
                                        arguments);
        } else {
            diagnostics.putString("\n Unknown connector: " + connector);
        }
        if (newSession != null) {
            startSession(newSession);
        }
        return newSession != null;
    }

    /*
     * Detach from VM.  If VM was started by debugger, terminate it.
     */
    public void detach() throws NoSessionException {
        ensureActiveSession();
        endSession();
    }

    private void startSession(Session s) throws VMLaunchFailureException {
        if (!s.attach()) {
            throw new VMLaunchFailureException();
        }
        session = s;
        EventRequestManager em = vm().eventRequestManager();
        ClassPrepareRequest classPrepareRequest = em.createClassPrepareRequest();
        //### We must allow the deferred breakpoints to be resolved before
        //### we continue executing the class.  We could optimize if there
        //### were no deferred breakpoints outstanding for a particular class.
        //### Can we do this with JDI?
        classPrepareRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL);
        classPrepareRequest.enable();
        ClassUnloadRequest classUnloadRequest = em.createClassUnloadRequest();
        classUnloadRequest.setSuspendPolicy(EventRequest.SUSPEND_NONE);
        classUnloadRequest.enable();
        ThreadStartRequest threadStartRequest = em.createThreadStartRequest();
        threadStartRequest.setSuspendPolicy(EventRequest.SUSPEND_NONE);
        threadStartRequest.enable();
        ThreadDeathRequest threadDeathRequest = em.createThreadDeathRequest();
        threadDeathRequest.setSuspendPolicy(EventRequest.SUSPEND_NONE);
        threadDeathRequest.enable();
        ExceptionRequest exceptionRequest =
                                em.createExceptionRequest(null, false, true);
        exceptionRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL);
        exceptionRequest.enable();
        validateThreadInfo();
        session.interrupted = true;
        notifySessionStart();
    }

    void endSession() {
        if (session != null) {
            session.detach();
            session = null;
            invalidateThreadInfo();
            notifySessionDeath();
        }
    }

    /*
     * Suspend all VM activity.
     */

    public void interrupt() throws NoSessionException {
        ensureActiveSession();
        vm().suspend();
        //### Is it guaranteed that the interrupt has happened?
        validateThreadInfo();
        session.interrupted = true;
        notifyInterrupted();
    }

    /*
     * Resume interrupted VM.
     */

    public void go() throws NoSessionException, VMNotInterruptedException {
        ensureActiveSession();
        invalidateThreadInfo();
        session.interrupted = false;
        notifyContinued();
        vm().resume();
    }

    /*
     * Stepping.
     */
    void clearPreviousStep(ThreadReference thread) {
        /*
         * A previous step may not have completed on this thread;
         * if so, it gets removed here.
         */
         EventRequestManager mgr = vm().eventRequestManager();
         for (StepRequest request : mgr.stepRequests()) {
             if (request.thread().equals(thread)) {
                 mgr.deleteEventRequest(request);
                 break;
             }
         }
    }

    private void generalStep(ThreadReference thread, int size, int depth)
                        throws NoSessionException {
        ensureActiveSession();
        invalidateThreadInfo();
        session.interrupted = false;
        notifyContinued();

        clearPreviousStep(thread);
        EventRequestManager reqMgr = vm().eventRequestManager();
        StepRequest request = reqMgr.createStepRequest(thread,
                                                       size, depth);
        // We want just the next step event and no others
        request.addCountFilter(1);
        request.enable();
        vm().resume();
    }

    public void stepIntoInstruction(ThreadReference thread)
                        throws NoSessionException {
        generalStep(thread, StepRequest.STEP_MIN, StepRequest.STEP_INTO);
    }

    public void stepOverInstruction(ThreadReference thread)
                        throws NoSessionException {
        generalStep(thread, StepRequest.STEP_MIN, StepRequest.STEP_OVER);
    }

    public void stepIntoLine(ThreadReference thread)
                        throws NoSessionException,
                        AbsentInformationException {
        generalStep(thread, StepRequest.STEP_LINE, StepRequest.STEP_INTO);
    }

    public void stepOverLine(ThreadReference thread)
                        throws NoSessionException,
                        AbsentInformationException {
        generalStep(thread, StepRequest.STEP_LINE, StepRequest.STEP_OVER);
    }

    public void stepOut(ThreadReference thread)
                        throws NoSessionException {
        generalStep(thread, StepRequest.STEP_MIN, StepRequest.STEP_OUT);
    }

    /*
     * Thread control.
     */

    public void suspendThread(ThreadReference thread) throws NoSessionException {
        ensureActiveSession();
        thread.suspend();
    }

    public void resumeThread(ThreadReference thread) throws NoSessionException {
        ensureActiveSession();
        thread.resume();
    }

    public void stopThread(ThreadReference thread) throws NoSessionException {
        ensureActiveSession();
        //### Need an exception now.  Which one to use?
        //thread.stop();
    }

    /*
     * ThreadInfo objects -- Allow query of thread status and stack.
     */

    private List<ThreadInfo> threadInfoList = new LinkedList<ThreadInfo>();
    //### Should be weak! (in the value, not the key)
    private HashMap<ThreadReference, ThreadInfo> threadInfoMap = new HashMap<ThreadReference, ThreadInfo>();

    public ThreadInfo threadInfo(ThreadReference thread) {
        if (session == null || thread == null) {
            return null;
        }
        ThreadInfo info = threadInfoMap.get(thread);
        if (info == null) {
            //### Should not hardcode initial frame count and prefetch here!
            //info = new ThreadInfo(thread, 10, 10);
            info = new ThreadInfo(thread);
            if (session.interrupted) {
                info.validate();
            }
            threadInfoList.add(info);
            threadInfoMap.put(thread, info);
        }
        return info;
    }

     void validateThreadInfo() {
        session.interrupted = true;
        for (ThreadInfo threadInfo : threadInfoList) {
            threadInfo.validate();
            }
    }

    private void invalidateThreadInfo() {
        if (session != null) {
            session.interrupted = false;
            for (ThreadInfo threadInfo : threadInfoList) {
                threadInfo.invalidate();
            }
        }
    }

    void removeThreadInfo(ThreadReference thread) {
        ThreadInfo info = threadInfoMap.get(thread);
        if (info != null) {
            info.invalidate();
            threadInfoMap.remove(thread);
            threadInfoList.remove(info);
        }
    }

    /*
     * Listen for Session control events.
     */

    private void notifyInterrupted() {
      ArrayList<SessionListener> l = new ArrayList<SessionListener>(sessionListeners);
        EventObject evt = new EventObject(this);
        for (int i = 0; i < l.size(); i++) {
            l.get(i).sessionInterrupt(evt);
        }
    }

    private void notifyContinued() {
        ArrayList<SessionListener> l = new ArrayList<SessionListener>(sessionListeners);
        EventObject evt = new EventObject(this);
        for (int i = 0; i < l.size(); i++) {
            l.get(i).sessionContinue(evt);
        }
    }

    private void notifySessionStart() {
        ArrayList<SessionListener> l = new ArrayList<SessionListener>(sessionListeners);
        EventObject evt = new EventObject(this);
        for (int i = 0; i < l.size(); i++) {
            l.get(i).sessionStart(evt);
        }
    }

    private void notifySessionDeath() {
/*** noop for now
        ArrayList<SessionListener> l = new ArrayList<SessionListener>(sessionListeners);
        EventObject evt = new EventObject(this);
        for (int i = 0; i < l.size(); i++) {
            ((SessionListener)l.get(i)).sessionDeath(evt);
        }
****/
    }

    /*
     * Listen for input and output requests from the application
     * being debugged.  These are generated only when the debuggee
     * is spawned as a child of the debugger.
     */

    private Object inputLock = new Object();
    private LinkedList<String> inputBuffer = new LinkedList<String>();

    private void resetInputBuffer() {
        synchronized (inputLock) {
            inputBuffer = new LinkedList<String>();
        }
    }

    public void sendLineToApplication(String line) {
        synchronized (inputLock) {
            inputBuffer.addFirst(line);
            inputLock.notifyAll();
        }
    }

    private InputListener appInput = new InputListener() {
        @Override
        public String getLine() {
            // Don't allow reader to be interrupted -- catch and retry.
            String line = null;
            while (line == null) {
                synchronized (inputLock) {
                    try {
                        while (inputBuffer.size() < 1) {
                            inputLock.wait();
                        }
                        line = inputBuffer.removeLast();
                    } catch (InterruptedException e) {}
                }
            }
            // We must not be holding inputLock here, as the listener
            // that we call to echo a line might call us re-entrantly
            // to provide another line of input.
            // Run in Swing event dispatcher thread.
            final String input = line;
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    echoInputLine(input);
                }
            });
            return line;
        }
    };

    private static String newline = System.getProperty("line.separator");

    private void echoInputLine(String line) {
        ArrayList<OutputListener> l = new ArrayList<OutputListener>(appEchoListeners);
        for (int i = 0; i < l.size(); i++) {
            OutputListener ol = l.get(i);
            ol.putString(line);
            ol.putString(newline);
        }
    }

    private OutputListener appOutput = new OutputListener() {
      @Override
        public void putString(String string) {
            ArrayList<OutputListener> l = new ArrayList<OutputListener>(appEchoListeners);
            for (int i = 0; i < l.size(); i++) {
                l.get(i).putString(string);
            }
        }
    };

    private OutputListener appError = new OutputListener() {
      @Override
        public void putString(String string) {
            ArrayList<OutputListener> l = new ArrayList<OutputListener>(appEchoListeners);
            for (int i = 0; i < l.size(); i++) {
                l.get(i).putString(string);
            }
        }
    };

   private OutputListener diagnostics = new OutputListener() {
      @Override
        public void putString(String string) {
            ArrayList<OutputListener> l = new ArrayList<OutputListener>(diagnosticsListeners);
            for (int i = 0; i < l.size(); i++) {
                l.get(i).putString(string);
            }
        }
   };

  /////////////    Spec Request Creation/Deletion/Query   ///////////

    private EventRequestSpecList specList = new EventRequestSpecList(this);

    public BreakpointSpec
    createSourceLineBreakpoint(String sourceName, int line) {
        return specList.createSourceLineBreakpoint(sourceName, line);
    }

    public BreakpointSpec
    createClassLineBreakpoint(String classPattern, int line) {
        return specList.createClassLineBreakpoint(classPattern, line);
    }

    public BreakpointSpec
    createMethodBreakpoint(String classPattern,
                           String methodId, List<String> methodArgs) {
        return specList.createMethodBreakpoint(classPattern,
                                                 methodId, methodArgs);
    }

    public ExceptionSpec
    createExceptionIntercept(String classPattern,
                             boolean notifyCaught,
                             boolean notifyUncaught) {
        return specList.createExceptionIntercept(classPattern,
                                                   notifyCaught,
                                                   notifyUncaught);
    }

    public AccessWatchpointSpec
    createAccessWatchpoint(String classPattern, String fieldId) {
        return specList.createAccessWatchpoint(classPattern, fieldId);
    }

    public ModificationWatchpointSpec
    createModificationWatchpoint(String classPattern, String fieldId) {
        return specList.createModificationWatchpoint(classPattern,
                                                       fieldId);
    }

    public void delete(EventRequestSpec spec) {
        specList.delete(spec);
    }

    void resolve(ReferenceType refType) {
        specList.resolve(refType);
    }

    public void install(EventRequestSpec spec) {
        specList.install(spec, vm());
    }

    public List<EventRequestSpec> eventRequestSpecs() {
        return specList.eventRequestSpecs();
    }
}
