/*
 * Copyright (c) 2015, 2024, 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 jdk.jshell;

import jdk.jshell.spi.ExecutionControl;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.PrintStream;
import java.net.InetAddress;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Objects;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.function.BiFunction;
import java.util.function.Consumer;

import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
import javax.tools.StandardJavaFileManager;
import jdk.internal.jshell.debug.InternalDebugControl;
import jdk.jshell.Snippet.Status;
import jdk.jshell.spi.ExecutionControl.EngineTerminationException;
import jdk.jshell.spi.ExecutionControl.ExecutionControlException;
import jdk.jshell.spi.ExecutionControlProvider;
import jdk.jshell.spi.ExecutionEnv;
import static jdk.jshell.Util.expunge;

/**
 * The JShell evaluation state engine.  This is the central class in the JShell
 * API.  A {@code JShell} instance holds the evolving compilation and
 * execution state.  The state is changed with the instance methods
 * {@link jdk.jshell.JShell#eval(java.lang.String) eval(String)},
 * {@link jdk.jshell.JShell#drop(jdk.jshell.Snippet) drop(Snippet)} and
 * {@link jdk.jshell.JShell#addToClasspath(java.lang.String) addToClasspath(String)}.
 * The majority of methods query the state.
 * A {@code JShell} instance also allows registering for events with
 * {@link jdk.jshell.JShell#onSnippetEvent(java.util.function.Consumer) onSnippetEvent(Consumer)}
 * and {@link jdk.jshell.JShell#onShutdown(java.util.function.Consumer) onShutdown(Consumer)}, which
 * are unregistered with
 * {@link jdk.jshell.JShell#unsubscribe(jdk.jshell.JShell.Subscription) unsubscribe(Subscription)}.
 * Access to the source analysis utilities is via
 * {@link jdk.jshell.JShell#sourceCodeAnalysis()}.
 * When complete the instance should be closed to free resources --
 * {@link jdk.jshell.JShell#close()}.
 * <p>
 * An instance of {@code JShell} is created with
 * {@code JShell.create()}.
 * <p>
 * This class is not thread safe, except as noted, all access should be through
 * a single thread.
 *
 * @author Robert Field
 * @since 9
 */
public class JShell implements AutoCloseable {

    final SnippetMaps maps;
    final KeyMap keyMap;
    final OuterWrapMap outerMap;
    final TaskFactory taskFactory;
    final InputStream in;
    final PrintStream out;
    final PrintStream err;
    final Optional<JShellConsole> console;
    final Supplier<String> tempVariableNameGenerator;
    final BiFunction<Snippet, Integer, String> idGenerator;
    final List<String> extraRemoteVMOptions;
    final List<String> extraCompilerOptions;
    final Function<StandardJavaFileManager, StandardJavaFileManager> fileManagerMapping;

    private int nextKeyIndex = 1;

    final Eval eval;
    final ClassTracker classTracker;
    private final Map<Subscription, Consumer<JShell>> shutdownListeners = new HashMap<>();
    private final Map<Subscription, Consumer<SnippetEvent>> keyStatusListeners = new HashMap<>();
    private boolean closed = false;

    private final ExecutionControl executionControl;
    private SourceCodeAnalysisImpl sourceCodeAnalysis = null;

    private static final String L10N_RB_NAME    = "jdk.jshell.resources.l10n";
    private static ResourceBundle outputRB  = null;

    JShell(Builder b) throws IllegalStateException {
        this.in = b.in;
        this.out = b.out;
        this.err = b.err;
        this.console = Optional.ofNullable(b.console);
        this.tempVariableNameGenerator = b.tempVariableNameGenerator;
        this.idGenerator = b.idGenerator;
        this.extraRemoteVMOptions = b.extraRemoteVMOptions;
        this.extraCompilerOptions = b.extraCompilerOptions;
        this.fileManagerMapping = b.fileManagerMapping;
        try {
            if (b.executionControlProvider != null) {
                executionControl = b.executionControlProvider.generate(new ExecutionEnvImpl(),
                        b.executionControlParameters == null
                                ? b.executionControlProvider.defaultParameters()
                                : b.executionControlParameters);
            } else {
                String loopback = InetAddress.getLoopbackAddress().getHostAddress();
                String spec = b.executionControlSpec == null
                        ? "failover:0(jdi:hostname(" + loopback + ")),"
                          + "1(jdi:launch(true)), 2(jdi)"
                        : b.executionControlSpec;
                executionControl = ExecutionControl.generate(new ExecutionEnvImpl(), spec);
            }
        } catch (Throwable ex) {
            throw new IllegalStateException("Launching JShell execution engine threw: " + ex.getMessage(), ex);
        }

        this.maps = new SnippetMaps(this);
        this.keyMap = new KeyMap(this);
        this.outerMap = new OuterWrapMap(this);
        this.taskFactory = new TaskFactory(this);
        this.eval = new Eval(this);
        this.classTracker = new ClassTracker();
    }

    /**
     * Builder for {@code JShell} instances.
     * Create custom instances of {@code JShell} by using the setter
     * methods on this class.  After zero or more of these, use the
     * {@link #build()} method to create a {@code JShell} instance.
     * These can all be chained. For example, setting the remote output and
     * error streams:
     * <pre>
     * {@code
     *     JShell myShell =
     *       JShell.builder()
     *         .out(myOutStream)
     *         .err(myErrStream)
     *         .build(); } </pre>
     * If no special set-up is needed, just use
     * {@code JShell.builder().build()} or the short-cut equivalent
     * {@code JShell.create()}.
     */
    public static class Builder {

        InputStream in = new ByteArrayInputStream(new byte[0]);
        PrintStream out = System.out;
        PrintStream err = System.err;
        JShellConsole console = null;
        Supplier<String> tempVariableNameGenerator = null;
        BiFunction<Snippet, Integer, String> idGenerator = null;
        List<String> extraRemoteVMOptions = new ArrayList<>();
        List<String> extraCompilerOptions = new ArrayList<>();
        ExecutionControlProvider executionControlProvider;
        Map<String,String> executionControlParameters;
        String executionControlSpec;
        Function<StandardJavaFileManager, StandardJavaFileManager> fileManagerMapping;

        Builder() { }

        /**
         * Sets the input for the running evaluation (it's {@code System.in}). Note:
         * applications that use {@code System.in} for snippet or other
         * user input cannot use {@code System.in} as the input stream for
         * the remote process.
         * <p>
         * The {@code read} method of the {@code InputStream} may throw the {@link InterruptedIOException}
         * to signal the user canceled the input. The currently running snippet will be automatically
         * {@link JShell#stop() stopped}.
         * <p>
         * The default, if this is not set, is to provide an empty input stream
         * -- {@code new ByteArrayInputStream(new byte[0])}.
         *
         * @param in the {@code InputStream} to be channelled to
         * {@code System.in} in the remote execution process
         * @return the {@code Builder} instance (for use in chained
         * initialization)
         */
        public Builder in(InputStream in) {
            this.in = in;
            return this;
        }

        /**
         * Sets the output for the running evaluation (it's {@code System.out}).
         * The controlling process and
         * the remote process can share {@code System.out}.
         * <p>
         * The default, if this is not set, is {@code System.out}.
         *
         * @param out the {@code PrintStream} to be channelled to
         * {@code System.out} in the remote execution process
         * @return the {@code Builder} instance (for use in chained
         * initialization)
         */
        public Builder out(PrintStream out) {
            this.out = out;
            return this;
        }

        /**
         * Sets the error output for the running evaluation (it's
         * {@code System.err}). The controlling process and the remote
         * process can share {@code System.err}.
         * <p>
         * The default, if this is not set, is {@code System.err}.
         *
         * @param err the {@code PrintStream} to be channelled to
         * {@code System.err} in the remote execution process
         * @return the {@code Builder} instance (for use in chained
         * initialization)
         */
        public Builder err(PrintStream err) {
            this.err = err;
            return this;
        }

        /**
         * Sets the console for the running evalution.
         * <p>
         * The default, if this is not set, is no console ({@code System.console()}
         * will return {@code null} while running a snippet).
         *
         * @param console console to use while a snippet is run
         * @return the {@code Builder} instance (for use in chained
         * initialization)
         * @since 21
         */
        public Builder console(JShellConsole console) {
            this.console = console;
            return this;
        }

        /**
         * Sets a generator of temp variable names for
         * {@link jdk.jshell.VarSnippet} of
         * {@link jdk.jshell.Snippet.SubKind#TEMP_VAR_EXPRESSION_SUBKIND}.
         * <p>
         * Do not use this method unless you have explicit need for it.
         * <p>
         * The generator will be used for newly created VarSnippet
         * instances. The name of a variable is queried with
         * {@link jdk.jshell.VarSnippet#name()}.
         * <p>
         * The callback is sent during the processing of the snippet, the
         * JShell state is not stable. No calls whatsoever on the
         * {@code JShell} instance may be made from the callback.
         * <p>
         * The generated name must be unique within active snippets.
         * <p>
         * The default behavior (if this is not set or {@code generator}
         * is null) is to generate the name as a sequential number with a
         * prefixing dollar sign ("$").
         *
         * @param generator the {@code Supplier} to generate the temporary
         * variable name string or {@code null}
         * @return the {@code Builder} instance (for use in chained
         * initialization)
         */
        public Builder tempVariableNameGenerator(Supplier<String> generator) {
            this.tempVariableNameGenerator = generator;
            return this;
        }

        /**
         * Sets the generator of identifying names for Snippets.
         * <p>
         * Do not use this method unless you have explicit need for it.
         * <p>
         * The generator will be used for newly created Snippet instances. The
         * identifying name (id) is accessed with
         * {@link jdk.jshell.Snippet#id()} and can be seen in the
         * {@code StackTraceElement.getFileName()} for a
         * {@link jdk.jshell.EvalException} and
         * {@link jdk.jshell.UnresolvedReferenceException}.
         * <p>
         * The inputs to the generator are the {@link jdk.jshell.Snippet} and an
         * integer. The integer will be the same for two Snippets which would
         * overwrite one-another, but otherwise is unique.
         * <p>
         * The callback is sent during the processing of the snippet and the
         * Snippet and the state as a whole are not stable. No calls to change
         * system state (including Snippet state) should be made. Queries of
         * Snippet may be made except to {@link jdk.jshell.Snippet#id()}. No
         * calls on the {@code JShell} instance may be made from the
         * callback, except to
         * {@link #status(jdk.jshell.Snippet) status(Snippet)}.
         * <p>
         * The default behavior (if this is not set or {@code generator}
         * is null) is to generate the id as the integer converted to a string.
         *
         * @param generator the {@code BiFunction} to generate the id
         * string or {@code null}
         * @return the {@code Builder} instance (for use in chained
         * initialization)
         */
        public Builder idGenerator(BiFunction<Snippet, Integer, String> generator) {
            this.idGenerator = generator;
            return this;
        }

        /**
         * Sets additional VM options for launching the VM.
         *
         * @param options The options for the remote VM
         * @return the {@code Builder} instance (for use in chained
         * initialization)
         */
        public Builder remoteVMOptions(String... options) {
            this.extraRemoteVMOptions.addAll(Arrays.asList(options));
            return this;
        }

        /**
         * Adds compiler options.  These additional options will be used on
         * parsing, analysis, and code generation calls to the compiler.
         * Options which interfere with results are not supported and have
         * undefined effects on JShell's operation.
         *
         * @param options the addition options for compiler invocations
         * @return the {@code Builder} instance (for use in chained
         * initialization)
         */
        public Builder compilerOptions(String... options) {
            this.extraCompilerOptions.addAll(Arrays.asList(options));
            return this;
        }

        /**
         * Sets the custom engine for execution. Snippet execution will be
         * provided by the {@link ExecutionControl} instance selected by the
         * specified execution control spec.
         * Use, at most, one of these overloaded {@code executionEngine} builder
         * methods.
         *
         * @param executionControlSpec the execution control spec,
         * which is documented in the {@link jdk.jshell.spi}
         * package documentation.
         * @return the {@code Builder} instance (for use in chained
         * initialization)
         */
        public Builder executionEngine(String executionControlSpec) {
            this.executionControlSpec = executionControlSpec;
            return this;
        }

        /**
         * Sets the custom engine for execution. Snippet execution will be
         * provided by the specified {@link ExecutionControl} instance.
         * Use, at most, one of these overloaded {@code executionEngine} builder
         * methods.
         *
         * @param executionControlProvider the provider to supply the execution
         * engine
         * @param executionControlParameters the parameters to the provider, or
         * {@code null} for default parameters
         * @return the {@code Builder} instance (for use in chained
         * initialization)
         */
        public Builder executionEngine(ExecutionControlProvider executionControlProvider,
                Map<String,String> executionControlParameters) {
            this.executionControlProvider = executionControlProvider;
            this.executionControlParameters = executionControlParameters;
            return this;
        }

        /**
         * Configure the {@code FileManager} to be used by compilation and
         * source analysis.
         * If not set or passed null, the compiler's standard file manager will
         * be used (identity mapping).
         * For use in special applications where the compiler's normal file
         * handling needs to be overridden.  See the file manager APIs for more
         * information.
         * The file manager input enables forwarding file managers, if this
         * is not needed, the incoming file manager can be ignored (constant
         * function).
         *
         * @param mapping a function that given the compiler's standard file
         * manager, returns a file manager to use
         * @return the {@code Builder} instance (for use in chained
         * initialization)
         */
        public Builder fileManager(Function<StandardJavaFileManager, StandardJavaFileManager> mapping) {
            this.fileManagerMapping = mapping;
            return this;
        }

        /**
         * Builds a JShell state engine. This is the entry-point to all JShell
         * functionality. This creates a remote process for execution. It is
         * thus important to close the returned instance.
         *
         * @throws IllegalStateException if the {@code JShell} instance could not be created.
         * @return the state engine
         */
        public JShell build() throws IllegalStateException {
            return new JShell(this);
        }
    }

    // --- public API ---

    /**
     * Create a new JShell state engine.
     * That is, create an instance of {@code JShell}.
     * <p>
     * Equivalent to {@link JShell#builder() JShell.builder()}{@link JShell.Builder#build() .build()}.
     * @throws IllegalStateException if the {@code JShell} instance could not be created.
     * @return an instance of {@code JShell}.
     */
    public static JShell create() throws IllegalStateException {
        return builder().build();
    }

    /**
     * Factory method for {@code JShell.Builder} which, in-turn, is used
     * for creating instances of {@code JShell}.
     * Create a default instance of {@code JShell} with
     * {@code JShell.builder().build()}. For more construction options
     * see {@link jdk.jshell.JShell.Builder}.
     * @return an instance of {@code Builder}.
     * @see jdk.jshell.JShell.Builder
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Access to source code analysis functionality.
     * An instance of {@code JShell} will always return the same
     * {@code SourceCodeAnalysis} instance from
     * {@code sourceCodeAnalysis()}.
     * @return an instance of {@link SourceCodeAnalysis SourceCodeAnalysis}
     * which can be used for source analysis such as completion detection and
     * completion suggestions.
     */
    public SourceCodeAnalysis sourceCodeAnalysis() {
        if (sourceCodeAnalysis == null) {
            sourceCodeAnalysis = new SourceCodeAnalysisImpl(this);
        }
        return sourceCodeAnalysis;
    }

    /**
     * Evaluate the input String, including definition and/or execution, if
     * applicable. The input is checked for errors, unless the errors can be
     * deferred (as is the case with some unresolvedDependencies references),
     * errors will abort evaluation.
     * <p>
     * The input should be
     * exactly one complete snippet of source code, that is, one expression,
     * statement, variable declaration, method declaration, class declaration,
     * or import.
     * To break arbitrary input into individual complete snippets, use
     * {@link SourceCodeAnalysis#analyzeCompletion(String)}.
     * <p>
     * For imports, the import is added.  Classes, interfaces. methods,
     * and variables are defined.  The initializer of variables, statements,
     * and expressions are executed.
     * The modifiers public, protected, private, static, and final are not
     * allowed on op-level declarations and are ignored with a warning.
     * Synchronized, native, abstract, and default top-level methods are not
     * allowed and are errors.
     * If a previous definition of a declaration is overwritten then there will
     * be an event showing its status changed to OVERWRITTEN, this will not
     * occur for dropped, rejected, or already overwritten declarations.
     * <p>
     * If execution environment is out of process, as is the default case, then
     * if the evaluated code
     * causes the execution environment to terminate, this {@code JShell}
     * instance will be closed but the calling process and VM remain valid.
     * @param input The input String to evaluate
     * @return the list of events directly or indirectly caused by this evaluation.
     * @throws IllegalStateException if this {@code JShell} instance is closed.
     * @see SourceCodeAnalysis#analyzeCompletion(String)
     * @see JShell#onShutdown(java.util.function.Consumer)
     */
    public List<SnippetEvent> eval(String input) throws IllegalStateException {
        SourceCodeAnalysisImpl a = sourceCodeAnalysis;
        if (a != null) {
            a.suspendIndexing();
        }
        try {
            checkIfAlive();
            List<SnippetEvent> events = eval.eval(input);
            events.forEach(this::notifyKeyStatusEvent);
            return Collections.unmodifiableList(events);
        } finally {
            if (a != null) {
                a.resumeIndexing();
            }
        }
    }

    /**
     * Remove a declaration from the state.  That is, if the snippet is an
     * {@linkplain jdk.jshell.Snippet.Status#isActive() active}
     * {@linkplain jdk.jshell.PersistentSnippet persistent} snippet, remove the
     * snippet and update the JShell evaluation state accordingly.
     * For all active snippets, change the {@linkplain #status status} to
     * {@link jdk.jshell.Snippet.Status#DROPPED DROPPED}.
     * @param snippet The snippet to remove
     * @return The list of events from updating declarations dependent on the
     * dropped snippet.
     * @throws IllegalStateException if this {@code JShell} instance is closed.
     * @throws IllegalArgumentException if the snippet is not associated with
     * this {@code JShell} instance.
     */
    public List<SnippetEvent> drop(Snippet snippet) throws IllegalStateException {
        checkIfAlive();
        checkValidSnippet(snippet);
        List<SnippetEvent> events = eval.drop(snippet);
        events.forEach(this::notifyKeyStatusEvent);
        return Collections.unmodifiableList(events);
    }

    /**
     * The specified path is added to the end of the classpath used in eval().
     * Note that the unnamed package is not accessible from the package in which
     * {@link JShell#eval(String)} code is placed.
     * @param path the path to add to the classpath.
     * @throws IllegalStateException if this {@code JShell} instance is closed.
     */
    public void addToClasspath(String path) {
        checkIfAlive();
        // Compiler
        taskFactory.addToClasspath(path);
        // Runtime
        try {
            executionControl().addToClasspath(path);
        } catch (ExecutionControlException ex) {
            debug(ex, "on addToClasspath(" + path + ")");
        }
        if (sourceCodeAnalysis != null) {
            sourceCodeAnalysis.classpathChanged();
        }
    }

    /**
     * Attempt to stop currently running evaluation. When called while
     * the {@link #eval(java.lang.String) } method is running and the
     * user's code being executed, an attempt will be made to stop user's code.
     * Note that typically this method needs to be called from a different thread
     * than the one running the {@code eval} method.
     * <p>
     * If the {@link #eval(java.lang.String) } method is not running, does nothing.
     * <p>
     * The attempt to stop the user's code may fail in some case, which may include
     * when the execution is blocked on an I/O operation, or when the user's code is
     * catching the {@link ThreadDeath} exception.
     */
    public void stop() {
        if (executionControl != null) {
            try {
                executionControl.stop();
            } catch (ExecutionControlException ex) {
                debug(ex, "on stop()");
            }
        }
    }

    /**
     * Close this state engine. Frees resources. Should be called when this
     * state engine is no longer needed.
     */
    @Override
    public void close() {
        closeDown();
    }

    /**
     * Return all snippets.
     * @return the snippets for all current snippets in id order.
     */
    public Stream<Snippet> snippets() {
        return maps.snippetList().stream();
    }

    /**
     * Returns the active variable snippets.
     * This convenience method is equivalent to {@code snippets()} filtered for
     * {@link jdk.jshell.Snippet.Status#isActive() status(snippet).isActive()}
     * {@code && snippet.kind() == Kind.VARIABLE}
     * and cast to {@code VarSnippet}.
     * @return the active declared variables.
     */
    public Stream<VarSnippet> variables() {
        return snippets()
                     .filter(sn -> status(sn).isActive() && sn.kind() == Snippet.Kind.VAR)
                     .map(sn -> (VarSnippet) sn);
    }

    /**
     * Returns the active method snippets.
     * This convenience method is equivalent to {@code snippets()} filtered for
     * {@link jdk.jshell.Snippet.Status#isActive() status(snippet).isActive()}
     * {@code && snippet.kind() == Kind.METHOD}
     * and cast to MethodSnippet.
     * @return the active declared methods.
     */
    public Stream<MethodSnippet> methods() {
        return snippets()
                     .filter(sn -> status(sn).isActive() && sn.kind() == Snippet.Kind.METHOD)
                     .map(sn -> (MethodSnippet)sn);
    }

    /**
     * Returns the active type declaration (class, interface, annotation type, and enum) snippets.
     * This convenience method is equivalent to {@code snippets()} filtered for
     * {@link jdk.jshell.Snippet.Status#isActive() status(snippet).isActive()}
     * {@code && snippet.kind() == Kind.TYPE_DECL}
     * and cast to TypeDeclSnippet.
     * @return the active declared type declarations.
     */
    public Stream<TypeDeclSnippet> types() {
        return snippets()
                .filter(sn -> status(sn).isActive() && sn.kind() == Snippet.Kind.TYPE_DECL)
                .map(sn -> (TypeDeclSnippet) sn);
    }

    /**
     * Returns the active import snippets.
     * This convenience method is equivalent to {@code snippets()} filtered for
     * {@link jdk.jshell.Snippet.Status#isActive() status(snippet).isActive()}
     * {@code && snippet.kind() == Kind.IMPORT}
     * and cast to ImportSnippet.
     * @return the active declared import declarations.
     */
    public Stream<ImportSnippet> imports() {
        return snippets()
                .filter(sn -> status(sn).isActive() && sn.kind() == Snippet.Kind.IMPORT)
                .map(sn -> (ImportSnippet) sn);
    }

    /**
     * Return the status of the snippet.
     * This is updated either because of an explicit {@code eval()} call or
     * an automatic update triggered by a dependency.
     * @param snippet the {@code Snippet} to look up
     * @return the status corresponding to this snippet
     * @throws IllegalStateException if this {@code JShell} instance is closed.
     * @throws IllegalArgumentException if the snippet is not associated with
     * this {@code JShell} instance.
     */
    public Status status(Snippet snippet) {
        return checkValidSnippet(snippet).status();
    }

    /**
     * Return the diagnostics of the most recent evaluation of the snippet.
     * The evaluation can either because of an explicit {@code eval()} call or
     * an automatic update triggered by a dependency.
     *
     * <p>This method will return best-effort diagnostics for snippets returned
     * from {@link SourceCodeAnalysis#sourceToSnippets(java.lang.String) }. The
     * diagnostics returned for such snippets may differ from diagnostics provided
     * after the snippet is {@link #eval(java.lang.String) }-ed.
     *
     * @param snippet the {@code Snippet} to look up
     * @return the diagnostics corresponding to this snippet.  This does not
     * include unresolvedDependencies references reported in {@code unresolvedDependencies()}.
     * @throws IllegalStateException if this {@code JShell} instance is closed.
     * @throws IllegalArgumentException if the snippet is not associated with
     * this {@code JShell} instance.
     */
    public Stream<Diag> diagnostics(Snippet snippet) {
        return checkValidSnippet(snippet, true).diagnostics().stream();
    }

    /**
     * For {@link jdk.jshell.Snippet.Status#RECOVERABLE_DEFINED RECOVERABLE_DEFINED} or
     * {@link jdk.jshell.Snippet.Status#RECOVERABLE_NOT_DEFINED RECOVERABLE_NOT_DEFINED}
     * declarations, the names of current unresolved dependencies for
     * the snippet.
     * The returned value of this method, for a given method may change when an
     * {@code eval()} or {@code drop()} of another snippet causes
     * an update of a dependency.
     * @param snippet the declaration {@code Snippet} to look up
     * @return a stream of symbol names that are currently unresolvedDependencies.
     * @throws IllegalStateException if this {@code JShell} instance is closed.
     * @throws IllegalArgumentException if the snippet is not associated with
     * this {@code JShell} instance.
     */
    public Stream<String> unresolvedDependencies(DeclarationSnippet snippet) {
        return checkValidSnippet(snippet).unresolved().stream();
    }

    /**
     * Get the current value of a variable.
     * @param snippet the variable Snippet whose value is queried.
     * @return the current value of the variable referenced by snippet.
     * @throws IllegalStateException if this {@code JShell} instance is closed.
     * @throws IllegalArgumentException if the snippet is not associated with
     * this {@code JShell} instance.
     * @throws IllegalArgumentException if the variable's status is anything but
     * {@link jdk.jshell.Snippet.Status#VALID}.
     */
    public String varValue(VarSnippet snippet) throws IllegalStateException {
        checkIfAlive();
        checkValidSnippet(snippet);
        if (snippet.status() != Status.VALID) {
            throw new IllegalArgumentException(
                    messageFormat("jshell.exc.var.not.valid",  snippet, snippet.status()));
        }
        String value;
        try {
            value = executionControl().varValue(snippet.classFullName(), snippet.fieldName());
        } catch (EngineTerminationException ex) {
            throw new IllegalStateException(ex.getMessage());
        } catch (ExecutionControlException ex) {
            debug(ex, "In varValue()");
            return "[" + ex.getMessage() + "]";
        }
        return expunge(value);
    }

    /**
     * Register a callback to be called when the Status of a snippet changes.
     * Each call adds a new subscription.
     * @param listener Action to perform when the Status changes.
     * @return A token which can be used to {@linkplain JShell#unsubscribe unsubscribe} this subscription.
     * @throws IllegalStateException if this {@code JShell} instance is closed.
     */
    public Subscription onSnippetEvent(Consumer<SnippetEvent> listener)
            throws IllegalStateException {
        return onX(keyStatusListeners, listener);
    }

    /**
     * Register a callback to be called when this JShell instance terminates.
     * This occurs either because the client process has ended (e.g. called System.exit(0))
     * or the connection has been shutdown, as by close().
     * Each call adds a new subscription.
     * @param listener Action to perform when the state terminates.
     * @return A token which can be used to {@linkplain JShell#unsubscribe unsubscribe} this subscription.
     * @throws IllegalStateException if this JShell instance is closed
     */
    public Subscription onShutdown(Consumer<JShell> listener)
            throws IllegalStateException {
        return onX(shutdownListeners, listener);
    }

    /**
     * Cancel a callback subscription.
     * @param token The token corresponding to the subscription to be unsubscribed.
     */
    public void unsubscribe(Subscription token) {
        synchronized (this) {
            token.remover.accept(token);
        }
    }

    /**
     * Subscription is a token for referring to subscriptions so they can
     * be {@linkplain JShell#unsubscribe unsubscribed}.
     */
    public class Subscription {

        Consumer<Subscription> remover;

        Subscription(Consumer<Subscription> remover) {
            this.remover = remover;
        }
    }

    /**
     * Provide the environment for a execution engine.
     */
    class ExecutionEnvImpl implements ExecutionEnv {

        @Override
        public InputStream userIn() {
            return in;
        }

        @Override
        public PrintStream userOut() {
            return out;
        }

        @Override
        public PrintStream userErr() {
            return err;
        }

        @Override
        public List<String> extraRemoteVMOptions() {
            return extraRemoteVMOptions;
        }

        @Override
        public void closeDown() {
            JShell.this.closeDown();
        }

        @Override
        public Optional<JShellConsole> console() {
            return console;
        }

    }

    // --- private / package-private implementation support ---

    ExecutionControl executionControl() {
        return executionControl;
    }

    void debug(int flags, String format, Object... args) {
        InternalDebugControl.debug(this, err, flags, format, args);
    }

    void debug(Throwable ex, String where) {
        InternalDebugControl.debug(this, err, ex, where);
    }

    /**
     * Generate the next key index, indicating a unique snippet signature.
     *
     * @return the next key index
     */
    int nextKeyIndex() {
        return nextKeyIndex++;
    }

    private synchronized <T> Subscription onX(Map<Subscription, Consumer<T>> map, Consumer<T> listener)
            throws IllegalStateException {
        Objects.requireNonNull(listener);
        checkIfAlive();
        Subscription token = new Subscription(map::remove);
        map.put(token, listener);
        return token;
    }

    private synchronized void notifyKeyStatusEvent(SnippetEvent event) {
        keyStatusListeners.values().forEach(l -> l.accept(event));
    }

    private synchronized void notifyShutdownEvent(JShell state) {
        shutdownListeners.values().forEach(l -> l.accept(state));
    }

    void closeDown() {
        if (!closed) {
            // Send only once
            closed = true;
            try {
                notifyShutdownEvent(this);
            } catch (Throwable thr) {
                // Don't care about dying exceptions
            }
            try {
                executionControl().close();
            } catch (Throwable ex) {
                // don't care about exceptions on close
            }
            if (sourceCodeAnalysis != null) {
                sourceCodeAnalysis.close();
            }
            InternalDebugControl.release(this);
        }
    }

    /**
     * Check if this JShell has been closed
     * @throws IllegalStateException if it is closed
     */
    void checkIfAlive()  throws IllegalStateException {
        if (closed) {
            throw new IllegalStateException(messageFormat("jshell.exc.closed", this));
        }
    }

    /**
     * Check a Snippet parameter coming from the API user
     * @param sn the Snippet to check
     * @throws NullPointerException if Snippet parameter is null
     * @throws IllegalArgumentException if Snippet is not from this JShell
     * @return the input Snippet (for chained calls)
     */
    private Snippet checkValidSnippet(Snippet sn) {
        return checkValidSnippet(sn, false);
    }

    /**
     * Check a Snippet parameter coming from the API user
     * @param sn the Snippet to check
     * @param acceptUnassociated  accept snippets that are unassociated
     * @throws NullPointerException if Snippet parameter is null
     * @throws IllegalArgumentException if Snippet is not from this JShell
     * @return the input Snippet (for chained calls)
     */
    private Snippet checkValidSnippet(Snippet sn, boolean acceptUnassociated) {
        if (sn == null) {
            throw new NullPointerException(messageFormat("jshell.exc.null"));
        } else {
            if (sn.key().state() != this || (!acceptUnassociated && sn.id() == Snippet.UNASSOCIATED_ID)) {
                throw new IllegalArgumentException(messageFormat("jshell.exc.alien", sn.toString()));
            }
            return sn;
        }
    }

    /**
     * Format using resource bundle look-up using MessageFormat
     *
     * @param key the resource key
     * @param args
     */
    String messageFormat(String key, Object... args) {
        if (outputRB == null) {
            try {
                outputRB = ResourceBundle.getBundle(L10N_RB_NAME);
            } catch (MissingResourceException mre) {
                throw new InternalError("Cannot find ResourceBundle: " + L10N_RB_NAME);
            }
        }
        String s;
        try {
            s = outputRB.getString(key);
        } catch (MissingResourceException mre) {
            throw new InternalError("Missing resource: " + key + " in " + L10N_RB_NAME);
        }
        return MessageFormat.format(s, args);
    }

}
