/*
 * Copyright (c) 2015, 2018, 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.internal.jshell.tool;


import jdk.jshell.SourceCodeAnalysis.Documentation;
import jdk.jshell.SourceCodeAnalysis.QualifiedNames;
import jdk.jshell.SourceCodeAnalysis.Suggestion;

import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import jdk.internal.shellsupport.doc.JavadocFormatter;
import jdk.internal.jshell.tool.StopDetectingInputStream.State;
import jdk.internal.misc.Signal;
import jdk.internal.misc.Signal.Handler;
import jdk.internal.org.jline.keymap.KeyMap;
import jdk.internal.org.jline.reader.Binding;
import jdk.internal.org.jline.reader.EOFError;
import jdk.internal.org.jline.reader.EndOfFileException;
import jdk.internal.org.jline.reader.History;
import jdk.internal.org.jline.reader.LineReader;
import jdk.internal.org.jline.reader.LineReader.Option;
import jdk.internal.org.jline.reader.Parser;
import jdk.internal.org.jline.reader.UserInterruptException;
import jdk.internal.org.jline.reader.Widget;
import jdk.internal.org.jline.reader.impl.LineReaderImpl;
import jdk.internal.org.jline.reader.impl.completer.ArgumentCompleter.ArgumentLine;
import jdk.internal.org.jline.reader.impl.history.DefaultHistory;
import jdk.internal.org.jline.terminal.impl.LineDisciplineTerminal;
import jdk.internal.org.jline.terminal.Attributes;
import jdk.internal.org.jline.terminal.Attributes.ControlChar;
import jdk.internal.org.jline.terminal.Attributes.LocalFlag;
import jdk.internal.org.jline.terminal.Size;
import jdk.internal.org.jline.terminal.Terminal;
import jdk.internal.org.jline.terminal.TerminalBuilder;
import jdk.internal.org.jline.utils.Display;
import jdk.internal.org.jline.utils.NonBlockingReader;
import jdk.jshell.ExpressionSnippet;
import jdk.jshell.Snippet;
import jdk.jshell.Snippet.SubKind;
import jdk.jshell.SourceCodeAnalysis.CompletionInfo;
import jdk.jshell.VarSnippet;

class ConsoleIOContext extends IOContext {

    private static final String HISTORY_LINE_PREFIX = "HISTORY_LINE_";

    final boolean allowIncompleteInputs;
    final JShellTool repl;
    final StopDetectingInputStream input;
    final Attributes originalAttributes;
    final LineReaderImpl in;
    final History userInputHistory = new DefaultHistory();
    final Instant historyLoad;

    String prefix = "";

    ConsoleIOContext(JShellTool repl, InputStream cmdin, PrintStream cmdout) throws Exception {
        this.allowIncompleteInputs = Boolean.getBoolean("jshell.test.allow.incomplete.inputs");
        this.repl = repl;
        Map<String, Object> variables = new HashMap<>();
        this.input = new StopDetectingInputStream(() -> repl.stop(),
                                                  ex -> repl.hard("Error on input: %s", ex));
        Terminal terminal;
        if (System.getProperty("test.jdk") != null) {
            terminal = new TestTerminal(input, cmdout);
            input.setInputStream(cmdin);
        } else {
            terminal = TerminalBuilder.builder().inputStreamWrapper(in -> {
                input.setInputStream(in);
                return input;
            }).build();
        }
        originalAttributes = terminal.getAttributes();
        Attributes noIntr = new Attributes(originalAttributes);
        noIntr.setControlChar(ControlChar.VINTR, 0);
        terminal.setAttributes(noIntr);
        terminal.enterRawMode();
        LineReaderImpl reader = new LineReaderImpl(terminal, "jshell", variables) {
            {
                //jline can handle the CONT signal on its own, but (currently) requires sun.misc for it
                try {
                    Signal.handle(new Signal("CONT"), new Handler() {
                        @Override public void handle(Signal sig) {
                            try {
                                handleSignal(jdk.internal.org.jline.terminal.Terminal.Signal.CONT);
                            } catch (Exception ex) {
                                ex.printStackTrace();
                            }
                        }
                    });
                } catch (IllegalArgumentException ignored) {
                    //the CONT signal does not exist on this platform
                }
            }
            CompletionState completionState = new CompletionState();
            @Override
            protected boolean doComplete(CompletionType lst, boolean useMenu, boolean prefix) {
                return ConsoleIOContext.this.complete(completionState);
            }
            @Override
            public Binding readBinding(KeyMap<Binding> keys, KeyMap<Binding> local) {
                completionState.actionCount++;
                return super.readBinding(keys, local);
            }
        };

        reader.setOpt(Option.DISABLE_EVENT_EXPANSION);

        reader.setParser((line, cursor, context) -> {
            if (!allowIncompleteInputs && !repl.isComplete(line)) {
                throw new EOFError(cursor, cursor, line);
            }
            return new ArgumentLine(line, cursor);
        });

        reader.getKeyMaps().get(LineReader.MAIN)
              .bind((Widget) () -> fixes(), FIXES_SHORTCUT);
        reader.getKeyMaps().get(LineReader.MAIN)
              .bind((Widget) () -> { throw new UserInterruptException(""); }, "\003");

        List<String> loadHistory = new ArrayList<>();
        Stream.of(repl.prefs.keys())
              .filter(key -> key.startsWith(HISTORY_LINE_PREFIX))
              .sorted()
              .map(key -> repl.prefs.get(key))
              .forEach(loadHistory::add);

        for (ListIterator<String> it = loadHistory.listIterator(); it.hasNext(); ) {
            String current = it.next();

            int trailingBackSlashes = countTrailintBackslashes(current);
            boolean continuation = trailingBackSlashes % 2 != 0;
            current = current.substring(0, current.length() - trailingBackSlashes / 2 - (continuation ? 1 : 0));
            if (continuation && it.hasNext()) {
                String next = it.next();
                it.remove();
                it.previous();
                current += "\n" + next;
            }

            it.set(current);
        }

        historyLoad = Instant.MIN;
        loadHistory.forEach(line -> reader.getHistory().add(historyLoad, line));

        in = reader;
    }

    @Override
    public String readLine(String firstLinePrompt, String continuationPrompt,
                           boolean firstLine, String prefix) throws IOException, InputInterruptedException {
        assert firstLine || allowIncompleteInputs;
        this.prefix = prefix;
        try {
            in.setVariable(LineReader.SECONDARY_PROMPT_PATTERN, continuationPrompt);
            return in.readLine(firstLinePrompt);
        } catch (UserInterruptException ex) {
            throw (InputInterruptedException) new InputInterruptedException().initCause(ex);
        } catch (EndOfFileException ex) {
            return null;
        }
    }

    @Override
    public boolean interactiveOutput() {
        return true;
    }

    @Override
    public Iterable<String> history(boolean currentSession) {
        return StreamSupport.stream(getHistory().spliterator(), false)
                            .filter(entry -> !currentSession || !historyLoad.equals(entry.time()))
                            .map(entry -> entry.line())
                            .collect(Collectors.toList());
    }

    @Override
    public void close() throws IOException {
        //save history:
        for (String key : repl.prefs.keys()) {
            if (key.startsWith(HISTORY_LINE_PREFIX)) {
                repl.prefs.remove(key);
            }
        }
        Collection<String> savedHistory =
            StreamSupport.stream(in.getHistory().spliterator(), false)
                         .map(History.Entry::line)
                         .flatMap(this::toSplitEntries)
                         .collect(Collectors.toList());
        if (!savedHistory.isEmpty()) {
            int len = (int) Math.ceil(Math.log10(savedHistory.size()+1));
            String format = HISTORY_LINE_PREFIX + "%0" + len + "d";
            int index = 0;
            for (String historyLine : savedHistory) {
                repl.prefs.put(String.format(format, index++), historyLine);
            }
        }
        repl.prefs.flush();
        try {
            in.getTerminal().setAttributes(originalAttributes);
            in.getTerminal().close();
        } catch (Exception ex) {
            throw new IOException(ex);
        }
        input.shutdown();
    }

    private Stream<String> toSplitEntries(String entry) {
        String[] lines = entry.split("\n");
        List<String> result = new ArrayList<>();

        for (int i = 0; i < lines.length; i++) {
            StringBuilder historyLine = new StringBuilder(lines[i]);
            int trailingBackSlashes = countTrailintBackslashes(historyLine);
            for (int j = 0; j < trailingBackSlashes; j++) {
                historyLine.append("\\");
            }
            if (i + 1 < lines.length) {
                historyLine.append("\\");
            }
            result.add(historyLine.toString());
        }

        return result.stream();
    }

    private int countTrailintBackslashes(CharSequence text) {
        int count = 0;

        for (int i = text.length() - 1; i >= 0; i--) {
            if (text.charAt(i) == '\\') {
                count++;
            } else {
                break;
            }
        }

        return count;
    }

    private static final String FIXES_SHORTCUT = "\033\133\132"; //Shift-TAB

    private static final String LINE_SEPARATOR = System.getProperty("line.separator");
    private static final String LINE_SEPARATORS2 = LINE_SEPARATOR + LINE_SEPARATOR;

    /*XXX:*/private static final int AUTOPRINT_THRESHOLD = 100;
    @SuppressWarnings("fallthrough")
    private boolean complete(CompletionState completionState) {
        //The completion has multiple states (invoked by subsequent presses of <tab>).
        //On the first invocation in a given sequence, all steps are precomputed
        //and placed into the todo list (completionState.todo). The todo list is
        //then followed on both the first and subsequent completion invocations:
        try {
            String text = in.getBuffer().toString();
            int cursor = in.getBuffer().cursor();

            List<CompletionTask> todo = completionState.todo;

            if (todo.isEmpty() || completionState.actionCount != 1) {
                ConsoleIOContextTestSupport.willComputeCompletion();
                int[] anchor = new int[] {-1};
                List<Suggestion> suggestions;
                List<String> doc;
                boolean command = prefix.isEmpty() && text.startsWith("/");
                if (command) {
                    suggestions = repl.commandCompletionSuggestions(text, cursor, anchor);
                    doc = repl.commandDocumentation(text, cursor, true);
                } else {
                    int prefixLength = prefix.length();
                    suggestions = repl.analysis.completionSuggestions(prefix + text, cursor + prefixLength, anchor);
                    anchor[0] -= prefixLength;
                    doc = repl.analysis.documentation(prefix + text, cursor + prefix.length(), false)
                                       .stream()
                                       .map(Documentation::signature)
                                       .collect(Collectors.toList());
                }
                long smartCount = suggestions.stream().filter(Suggestion::matchesType).count();
                boolean hasSmart = smartCount > 0 && smartCount <= /*in.getAutoprintThreshold()*/AUTOPRINT_THRESHOLD;
                boolean hasBoth = hasSmart &&
                                  suggestions.stream()
                                             .map(s -> s.matchesType())
                                             .distinct()
                                             .count() == 2;
                boolean tooManyItems = suggestions.size() > /*in.getAutoprintThreshold()*/AUTOPRINT_THRESHOLD;
                CompletionTask ordinaryCompletion =
                        new OrdinaryCompletionTask(suggestions,
                                                   anchor[0],
                                                   !command && !doc.isEmpty(),
                                                   hasBoth);
                CompletionTask allCompletion = new AllSuggestionsCompletionTask(suggestions, anchor[0]);

                todo = new ArrayList<>();

                //the main decission tree:
                if (command) {
                    CompletionTask shortDocumentation = new CommandSynopsisTask(doc);
                    CompletionTask fullDocumentation = new CommandFullDocumentationTask(todo);

                    if (!doc.isEmpty()) {
                        if (tooManyItems) {
                            todo.add(new NoopCompletionTask());
                            todo.add(allCompletion);
                        } else {
                            todo.add(ordinaryCompletion);
                        }
                        todo.add(shortDocumentation);
                        todo.add(fullDocumentation);
                    } else {
                        todo.add(new NoSuchCommandCompletionTask());
                    }
                } else {
                    if (doc.isEmpty()) {
                        if (hasSmart) {
                            todo.add(ordinaryCompletion);
                        } else if (tooManyItems) {
                            todo.add(new NoopCompletionTask());
                        }
                        if (!hasSmart || hasBoth) {
                            todo.add(allCompletion);
                        }
                    } else {
                        CompletionTask shortDocumentation = new ExpressionSignaturesTask(doc);
                        CompletionTask fullDocumentation = new ExpressionJavadocTask(todo);

                        if (hasSmart) {
                            todo.add(ordinaryCompletion);
                        }
                        todo.add(shortDocumentation);
                        if (!hasSmart || hasBoth) {
                            todo.add(allCompletion);
                        }
                        if (tooManyItems) {
                            todo.add(todo.size() - 1, fullDocumentation);
                        } else {
                            todo.add(fullDocumentation);
                        }
                    }
                }
            }

            boolean success = false;
            boolean repaint = true;

            OUTER: while (!todo.isEmpty()) {
                CompletionTask.Result result = todo.remove(0).perform(text, cursor);

                switch (result) {
                    case CONTINUE:
                        break;
                    case SKIP_NOREPAINT:
                        repaint = false;
                    case SKIP:
                        todo.clear();
                        //intentional fall-through
                    case FINISH:
                        success = true;
                        //intentional fall-through
                    case NO_DATA:
                        if (!todo.isEmpty()) {
                            in.getTerminal().writer().println();
                            in.getTerminal().writer().println(todo.get(0).description());
                        }
                        break OUTER;
                }
            }

            completionState.actionCount = 0;
            completionState.todo = todo;

            if (repaint) {
                in.redrawLine();
                in.flush();
            }

            return success;
        } catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }

    private CompletionTask.Result doPrintFullDocumentation(List<CompletionTask> todo, List<String> doc, boolean command) {
        if (doc != null && !doc.isEmpty()) {
            Terminal term = in.getTerminal();
            int pageHeight = term.getHeight() - NEEDED_LINES;
            List<CompletionTask> thisTODO = new ArrayList<>();

            for (Iterator<String> docIt = doc.iterator(); docIt.hasNext(); ) {
                String currentDoc = docIt.next();
                String[] lines = currentDoc.split("\n");
                int firstLine = 0;

                while (firstLine < lines.length) {
                    boolean first = firstLine == 0;
                    String[] thisPageLines =
                            Arrays.copyOfRange(lines,
                                               firstLine,
                                               Math.min(firstLine + pageHeight, lines.length));

                    thisTODO.add(new CompletionTask() {
                        @Override
                        public String description() {
                            String key =  !first ? "jshell.console.see.next.page"
                                                 : command ? "jshell.console.see.next.command.doc"
                                                           : "jshell.console.see.next.javadoc";

                            return repl.getResourceString(key);
                        }

                        @Override
                        public Result perform(String text, int cursor) throws IOException {
                            in.getTerminal().writer().println();
                            for (String line : thisPageLines) {
                                in.getTerminal().writer().println(line);
                            }
                            return Result.FINISH;
                        }
                    });

                    firstLine += pageHeight;
                }
            }

            todo.addAll(0, thisTODO);

            return CompletionTask.Result.CONTINUE;
        }

        return CompletionTask.Result.FINISH;
    }
    //where:
        private static final int NEEDED_LINES = 4;

    private static String commonPrefix(String str1, String str2) {
        for (int i = 0; i < str2.length(); i++) {
            if (!str1.startsWith(str2.substring(0, i + 1))) {
                return str2.substring(0, i);
            }
        }

        return str2;
    }

    private interface CompletionTask {
        public String description();
        public Result perform(String text, int cursor) throws IOException;

        enum Result {
            NO_DATA,
            CONTINUE,
            FINISH,
            SKIP,
            SKIP_NOREPAINT;
        }
    }

    private final class NoopCompletionTask implements CompletionTask {

        @Override
        public String description() {
            throw new UnsupportedOperationException("Should not get here.");
        }

        @Override
        public Result perform(String text, int cursor) throws IOException {
            return Result.FINISH;
        }

    }

    private final class NoSuchCommandCompletionTask implements CompletionTask {

        @Override
        public String description() {
            throw new UnsupportedOperationException("Should not get here.");
        }

        @Override
        public Result perform(String text, int cursor) throws IOException {
            in.getTerminal().writer().println();
            in.getTerminal().writer().println(repl.getResourceString("jshell.console.no.such.command"));
            in.getTerminal().writer().println();
            return Result.SKIP;
        }

    }

    private final class OrdinaryCompletionTask implements CompletionTask {
        private final List<Suggestion> suggestions;
        private final int anchor;
        private final boolean cont;
        private final boolean showSmart;

        public OrdinaryCompletionTask(List<Suggestion> suggestions,
                                      int anchor,
                                      boolean cont,
                                      boolean showSmart) {
            this.suggestions = suggestions;
            this.anchor = anchor;
            this.cont = cont;
            this.showSmart = showSmart;
        }

        @Override
        public String description() {
            throw new UnsupportedOperationException("Should not get here.");
        }

        @Override
        public Result perform(String text, int cursor) throws IOException {
            List<CharSequence> toShow;

            if (showSmart) {
                toShow =
                    suggestions.stream()
                               .filter(Suggestion::matchesType)
                               .map(Suggestion::continuation)
                               .distinct()
                               .collect(Collectors.toList());
            } else {
                toShow =
                    suggestions.stream()
                               .map(Suggestion::continuation)
                               .distinct()
                               .collect(Collectors.toList());
            }

            if (toShow.isEmpty()) {
                return Result.CONTINUE;
            }

            Optional<String> prefix =
                    suggestions.stream()
                               .map(Suggestion::continuation)
                               .reduce(ConsoleIOContext::commonPrefix);

            String prefixStr = prefix.orElse("").substring(cursor - anchor);
            in.putString(prefixStr);

            boolean showItems = toShow.size() > 1 || showSmart;

            if (showItems) {
                in.getTerminal().writer().println();
                printColumns(toShow);
            }

            if (!prefixStr.isEmpty())
                return showItems ? Result.FINISH : Result.SKIP_NOREPAINT;

            return cont ? Result.CONTINUE : Result.FINISH;
        }

    }

    private final class AllSuggestionsCompletionTask implements CompletionTask {
        private final List<Suggestion> suggestions;
        private final int anchor;

        public AllSuggestionsCompletionTask(List<Suggestion> suggestions,
                                            int anchor) {
            this.suggestions = suggestions;
            this.anchor = anchor;
        }

        @Override
        public String description() {
            if (suggestions.size() <= /*in.getAutoprintThreshold()*/AUTOPRINT_THRESHOLD) {
                return repl.getResourceString("jshell.console.completion.all.completions");
            } else {
                return repl.messageFormat("jshell.console.completion.all.completions.number", suggestions.size());
            }
        }

        @Override
        public Result perform(String text, int cursor) throws IOException {
            List<String> candidates =
                    suggestions.stream()
                               .map(Suggestion::continuation)
                               .distinct()
                               .collect(Collectors.toList());

            Optional<String> prefix =
                    candidates.stream()
                              .reduce(ConsoleIOContext::commonPrefix);

            String prefixStr = prefix.map(str -> str.substring(cursor - anchor)).orElse("");
            in.putString(prefixStr);
            if (candidates.size() > 1) {
                in.getTerminal().writer().println();
                printColumns(candidates);
            }
            return suggestions.isEmpty() ? Result.NO_DATA : Result.FINISH;
        }

    }

    private void printColumns(List<? extends CharSequence> candidates) {
        if (candidates.isEmpty()) return ;
        int size = candidates.stream().mapToInt(CharSequence::length).max().getAsInt() + 3;
        int columns = in.getTerminal().getWidth() / size;
        int c = 0;
        for (CharSequence cand : candidates) {
            in.getTerminal().writer().print(cand);
            for (int s = cand.length(); s < size; s++) {
                in.getTerminal().writer().print(" ");
            }
            if (++c == columns) {
                in.getTerminal().writer().println();
                c = 0;
            }
        }
        if (c != 0) {
            in.getTerminal().writer().println();
        }
    }

    private final class CommandSynopsisTask implements CompletionTask {

        private final List<String> synopsis;

        public CommandSynopsisTask(List<String> synposis) {
            this.synopsis = synposis;
        }

        @Override
        public String description() {
            return repl.getResourceString("jshell.console.see.synopsis");
        }

        @Override
        public Result perform(String text, int cursor) throws IOException {
//            try {
                in.getTerminal().writer().println();
                in.getTerminal().writer().println(synopsis.stream()
                                   .map(l -> l.replaceAll("\n", LINE_SEPARATOR))
                                   .collect(Collectors.joining(LINE_SEPARATORS2)));
//            } catch (IOException ex) {
//                throw new IllegalStateException(ex);
//            }
            return Result.FINISH;
        }

    }

    private final class CommandFullDocumentationTask implements CompletionTask {

        private final List<CompletionTask> todo;

        public CommandFullDocumentationTask(List<CompletionTask> todo) {
            this.todo = todo;
        }

        @Override
        public String description() {
            return repl.getResourceString("jshell.console.see.full.documentation");
        }

        @Override
        public Result perform(String text, int cursor) throws IOException {
            List<String> fullDoc = repl.commandDocumentation(text, cursor, false);
            return doPrintFullDocumentation(todo, fullDoc, true);
        }

    }

    private final class ExpressionSignaturesTask implements CompletionTask {

        private final List<String> doc;

        public ExpressionSignaturesTask(List<String> doc) {
            this.doc = doc;
        }

        @Override
        public String description() {
            throw new UnsupportedOperationException("Should not get here.");
        }

        @Override
        public Result perform(String text, int cursor) throws IOException {
            in.getTerminal().writer().println();
            in.getTerminal().writer().println(repl.getResourceString("jshell.console.completion.current.signatures"));
            in.getTerminal().writer().println(doc.stream().collect(Collectors.joining(LINE_SEPARATOR)));
            return Result.FINISH;
        }

    }

    private final class ExpressionJavadocTask implements CompletionTask {

        private final List<CompletionTask> todo;

        public ExpressionJavadocTask(List<CompletionTask> todo) {
            this.todo = todo;
        }

        @Override
        public String description() {
            return repl.getResourceString("jshell.console.see.documentation");
        }

        @Override
        public Result perform(String text, int cursor) throws IOException {
            //schedule showing javadoc:
            Terminal term = in.getTerminal();
            JavadocFormatter formatter = new JavadocFormatter(term.getWidth(),
                                                              true);
            Function<Documentation, String> convertor = d -> formatter.formatJavadoc(d.signature(), d.javadoc()) +
                             (d.javadoc() == null ? repl.messageFormat("jshell.console.no.javadoc")
                                                  : "");
            List<String> doc = repl.analysis.documentation(prefix + text, cursor + prefix.length(), true)
                                            .stream()
                                            .map(convertor)
                                            .collect(Collectors.toList());
            return doPrintFullDocumentation(todo, doc, false);
        }

    }

    @Override
    public boolean terminalEditorRunning() {
        Terminal terminal = in.getTerminal();
        return !terminal.getAttributes().getLocalFlag(LocalFlag.ICANON);
    }

    @Override
    public void suspend() {
    }

    @Override
    public void resume() {
    }

    @Override
    public void beforeUserCode() {
        synchronized (this) {
            inputBytes = null;
        }
        input.setState(State.BUFFER);
    }

    @Override
    public void afterUserCode() {
        input.setState(State.WAIT);
    }

    @Override
    public void replaceLastHistoryEntry(String source) {
        var it = in.getHistory().iterator();
        while (it.hasNext()) {
            it.next();
        }
        it.remove();
        in.getHistory().add(source);
    }

    private static final long ESCAPE_TIMEOUT = 100;

    private boolean fixes() {
        try {
            int c = in.getTerminal().input().read();

            if (c == (-1)) {
                return true; //TODO: true or false???
            }

            for (FixComputer computer : FIX_COMPUTERS) {
                if (computer.shortcut == c) {
                    fixes(computer);
                    return true; //TODO: true of false???
                }
            }

            readOutRemainingEscape(c);

            in.beep();
            in.getTerminal().writer().println();
            in.getTerminal().writer().println(repl.getResourceString("jshell.fix.wrong.shortcut"));
            in.redrawLine();
            in.flush();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        return true;
    }

    private void readOutRemainingEscape(int c) throws IOException {
        if (c == '\033') {
            //escape, consume waiting input:
            NonBlockingReader inp = in.getTerminal().reader();

            while (inp.peek(ESCAPE_TIMEOUT) > 0) {
                inp.read();
            }
        }
    }

    //compute possible options/Fixes based on the selected FixComputer, present them to the user,
    //and perform the selected one:
    private void fixes(FixComputer computer) {
        String input = prefix + in.getBuffer().toString();
        int cursor = prefix.length() + in.getBuffer().cursor();
        FixResult candidates = computer.compute(repl, input, cursor);

        try {
            final boolean printError = candidates.error != null && !candidates.error.isEmpty();
            if (printError) {
                in.getTerminal().writer().println(candidates.error);
            }
            if (candidates.fixes.isEmpty()) {
                in.beep();
                if (printError) {
                    in.redrawLine();
                    in.flush();
                }
            } else if (candidates.fixes.size() == 1 && !computer.showMenu) {
                if (printError) {
                    in.redrawLine();
                    in.flush();
                }
                candidates.fixes.get(0).perform(in);
            } else {
                List<Fix> fixes = new ArrayList<>(candidates.fixes);
                fixes.add(0, new Fix() {
                    @Override
                    public String displayName() {
                        return repl.messageFormat("jshell.console.do.nothing");
                    }

                    @Override
                    public void perform(LineReaderImpl in) throws IOException {
                        in.redrawLine();
                    }
                });

                Map<Character, Fix> char2Fix = new HashMap<>();
                in.getTerminal().writer().println();
                for (int i = 0; i < fixes.size(); i++) {
                    Fix fix = fixes.get(i);
                    char2Fix.put((char) ('0' + i), fix);
                    in.getTerminal().writer().println("" + i + ": " + fixes.get(i).displayName());
                }
                in.getTerminal().writer().print(repl.messageFormat("jshell.console.choice"));
                in.flush();
                int read;

                read = in.readCharacter();

                Fix fix = char2Fix.get((char) read);

                if (fix == null) {
                    in.beep();
                    fix = fixes.get(0);
                }

                in.getTerminal().writer().println();

                fix.perform(in);

                in.flush();
            }
        } catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }

    private byte[] inputBytes;
    private int inputBytesPointer;

    @Override
    public synchronized int readUserInput() throws IOException {
        while (inputBytes == null || inputBytes.length <= inputBytesPointer) {
            History prevHistory = in.getHistory();
            boolean prevDisableCr = Display.DISABLE_CR;
            Parser prevParser = in.getParser();

            try {
                in.setParser((line, cursor, context) -> new ArgumentLine(line, cursor));
                input.setState(State.WAIT);
                Display.DISABLE_CR = true;
                in.setHistory(userInputHistory);
                inputBytes = (in.readLine("") + System.getProperty("line.separator")).getBytes();
                inputBytesPointer = 0;
            } catch (UserInterruptException ex) {
                throw new InterruptedIOException();
            } finally {
                in.setParser(prevParser);
                in.setHistory(prevHistory);
                input.setState(State.BUFFER);
                Display.DISABLE_CR = prevDisableCr;
            }
        }
        return inputBytes[inputBytesPointer++];
    }

    
A possible action which the user can choose to perform.
/** * A possible action which the user can choose to perform. */
public interface Fix {
A name that should be shown to the user.
/** * A name that should be shown to the user. */
public String displayName();
Perform the given action.
/** * Perform the given action. */
public void perform(LineReaderImpl in) throws IOException; }
A factory for Fixes.
/** * A factory for {@link Fix}es. */
public abstract static class FixComputer { private final char shortcut; private final boolean showMenu;
Construct a new FixComputer. shortcut defines the key which should trigger this FixComputer. If showMenu is false, and this computer returns exactly one Fix, no options will be show to the user, and the given Fix will be performed.
/** * Construct a new FixComputer. {@code shortcut} defines the key which should trigger this FixComputer. * If {@code showMenu} is {@code false}, and this computer returns exactly one {@code Fix}, * no options will be show to the user, and the given {@code Fix} will be performed. */
public FixComputer(char shortcut, boolean showMenu) { this.shortcut = shortcut; this.showMenu = showMenu; }
Compute possible actions for the given code.
/** * Compute possible actions for the given code. */
public abstract FixResult compute(JShellTool repl, String code, int cursor); }
A list of Fixes with a possible error that should be shown to the user.
/** * A list of {@code Fix}es with a possible error that should be shown to the user. */
public static class FixResult { public final List<Fix> fixes; public final String error; public FixResult(List<Fix> fixes, String error) { this.fixes = fixes; this.error = error; } } private static final FixComputer[] FIX_COMPUTERS = new FixComputer[] { new FixComputer('v', false) { //compute "Introduce variable" Fix: private void performToVar(LineReaderImpl in, String type) throws IOException { in.redrawLine(); in.getBuffer().cursor(0); in.putString(type + " = "); in.getBuffer().cursor(in.getBuffer().cursor() - 3); in.flush(); } @Override public FixResult compute(JShellTool repl, String code, int cursor) { String type = repl.analysis.analyzeType(code, cursor); if (type == null) { return new FixResult(Collections.emptyList(), null); } List<Fix> fixes = new ArrayList<>(); fixes.add(new Fix() { @Override public String displayName() { return repl.messageFormat("jshell.console.create.variable"); } @Override public void perform(LineReaderImpl in) throws IOException { performToVar(in, type); } }); int idx = type.lastIndexOf("."); if (idx > 0) { String stype = type.substring(idx + 1); QualifiedNames res = repl.analysis.listQualifiedNames(stype, stype.length()); if (res.isUpToDate() && res.getNames().contains(type) && !res.isResolvable()) { fixes.add(new Fix() { @Override public String displayName() { return "import: " + type + ". " + repl.messageFormat("jshell.console.create.variable"); } @Override public void perform(LineReaderImpl in) throws IOException { repl.processSource("import " + type + ";"); in.getTerminal().writer().println("Imported: " + type); performToVar(in, stype); } }); } } return new FixResult(fixes, null); } }, new FixComputer('m', false) { //compute "Introduce method" Fix: private void performToMethod(LineReaderImpl in, String type, String code) throws IOException { in.redrawLine(); if (!code.trim().endsWith(";")) { in.putString(";"); } in.putString(" }"); in.getBuffer().cursor(0); String afterCursor = type.equals("void") ? "() { " : "() { return "; in.putString(type + " " + afterCursor); // position the cursor where the method name should be entered (before parens) in.getBuffer().cursor(in.getBuffer().cursor() - afterCursor.length()); in.flush(); } private FixResult reject(JShellTool repl, String messageKey) { return new FixResult(Collections.emptyList(), repl.messageFormat(messageKey)); } @Override public FixResult compute(JShellTool repl, String code, int cursor) { final String codeToCursor = code.substring(0, cursor); final String type; final CompletionInfo ci = repl.analysis.analyzeCompletion(codeToCursor); if (!ci.remaining().isEmpty()) { return reject(repl, "jshell.console.exprstmt"); } switch (ci.completeness()) { case COMPLETE: case COMPLETE_WITH_SEMI: case CONSIDERED_INCOMPLETE: break; case EMPTY: return reject(repl, "jshell.console.empty"); case DEFINITELY_INCOMPLETE: case UNKNOWN: default: return reject(repl, "jshell.console.erroneous"); } List<Snippet> snl = repl.analysis.sourceToSnippets(ci.source()); if (snl.size() != 1) { return reject(repl, "jshell.console.erroneous"); } Snippet sn = snl.get(0); switch (sn.kind()) { case EXPRESSION: type = ((ExpressionSnippet) sn).typeName(); break; case STATEMENT: type = "void"; break; case VAR: if (sn.subKind() != SubKind.TEMP_VAR_EXPRESSION_SUBKIND) { // only valid var is an expression turned into a temp var return reject(repl, "jshell.console.exprstmt"); } type = ((VarSnippet) sn).typeName(); break; case IMPORT: case METHOD: case TYPE_DECL: return reject(repl, "jshell.console.exprstmt"); case ERRONEOUS: default: return reject(repl, "jshell.console.erroneous"); } List<Fix> fixes = new ArrayList<>(); fixes.add(new Fix() { @Override public String displayName() { return repl.messageFormat("jshell.console.create.method"); } @Override public void perform(LineReaderImpl in) throws IOException { performToMethod(in, type, codeToCursor); } }); int idx = type.lastIndexOf("."); if (idx > 0) { String stype = type.substring(idx + 1); QualifiedNames res = repl.analysis.listQualifiedNames(stype, stype.length()); if (res.isUpToDate() && res.getNames().contains(type) && !res.isResolvable()) { fixes.add(new Fix() { @Override public String displayName() { return "import: " + type + ". " + repl.messageFormat("jshell.console.create.method"); } @Override public void perform(LineReaderImpl in) throws IOException { repl.processSource("import " + type + ";"); in.getTerminal().writer().println("Imported: " + type); performToMethod(in, stype, codeToCursor); } }); } } return new FixResult(fixes, null); } }, new FixComputer('i', true) { //compute "Add import" Fixes: @Override public FixResult compute(JShellTool repl, String code, int cursor) { QualifiedNames res = repl.analysis.listQualifiedNames(code, cursor); List<Fix> fixes = new ArrayList<>(); for (String fqn : res.getNames()) { fixes.add(new Fix() { @Override public String displayName() { return "import: " + fqn; } @Override public void perform(LineReaderImpl in) throws IOException { repl.processSource("import " + fqn + ";"); in.getTerminal().writer().println("Imported: " + fqn); in.redrawLine(); } }); } if (res.isResolvable()) { return new FixResult(Collections.emptyList(), repl.messageFormat("jshell.console.resolvable")); } else { String error = ""; if (fixes.isEmpty()) { error = repl.messageFormat("jshell.console.no.candidate"); } if (!res.isUpToDate()) { error += repl.messageFormat("jshell.console.incomplete"); } return new FixResult(fixes, error); } } } }; private History getHistory() { return in.getHistory(); } private static final class TestTerminal extends LineDisciplineTerminal { private static final int DEFAULT_HEIGHT = 24; public TestTerminal(StopDetectingInputStream input, OutputStream output) throws Exception { super("test", "ansi", output, Charset.forName("UTF-8")); // setAnsiSupported(true); // setEchoEnabled(false); // this.input = input; Attributes a = new Attributes(getAttributes()); a.setLocalFlag(LocalFlag.ECHO, false); setAttributes(attributes); int h = DEFAULT_HEIGHT; try { String hp = System.getProperty("test.terminal.height"); if (hp != null && !hp.isEmpty()) { h = Integer.parseInt(hp); } } catch (Throwable ex) { // ignore } setSize(new Size(80, h)); new Thread(() -> { int r; try { while ((r = input.read()) != (-1)) { processInputByte(r); } slaveInput.close(); } catch (IOException ex) { throw new IllegalStateException(ex); } }).start(); } } private static final class CompletionState {
The number of actions since the last completion invocation. Will be 1 when completion is invoked immediately after the last completion invocation.
/**The number of actions since the last completion invocation. Will be 1 when completion is * invoked immediately after the last completion invocation.*/
public int actionCount;
Precomputed completion actions. Should only be reused if actionCount == 1.
/**Precomputed completion actions. Should only be reused if actionCount == 1.*/
public List<CompletionTask> todo = Collections.emptyList(); } }