/*
 * Copyright (c) 2016, 2021, 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 java.util.*;
import java.util.Map.Entry;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jdk.internal.jshell.tool.JShellTool.CompletionProvider;

import static java.util.stream.Collectors.*;
import static jdk.internal.jshell.tool.ContinuousCompletionProvider.PERFECT_MATCHER;
import static jdk.internal.jshell.tool.JShellTool.EMPTY_COMPLETION_PROVIDER;
import static jdk.internal.jshell.tool.Selector.SelectorKind;
import static jdk.internal.jshell.tool.Selector.SelectorInstanceWithDoc;
import static jdk.internal.jshell.tool.Selector.SelectorBuilder;
import static jdk.internal.jshell.tool.Selector.FormatAction;
import static jdk.internal.jshell.tool.Selector.FormatCase;
import static jdk.internal.jshell.tool.Selector.FormatErrors;
import static jdk.internal.jshell.tool.Selector.FormatResolve;
import static jdk.internal.jshell.tool.Selector.FormatUnresolved;
import static jdk.internal.jshell.tool.Selector.FormatWhen;


/**
 * Feedback customization support
 *
 * @author Robert Field
 */
class Feedback {

    // Patern for substituted fields within a customized format string
    private static final Pattern FIELD_PATTERN = Pattern.compile("\\{(.*?)\\}");

    // Internal field name for truncation length
    private static final String TRUNCATION_FIELD = "<truncation>";

    // For encoding to Properties String
    private static final String RECORD_SEPARATOR = "\u241E";

    // Selector for truncation of var value
    private static final Selector VAR_VALUE_ADD_SELECTOR = new Selector(
            FormatCase.VARVALUE,
            FormatAction.ADDED,
            FormatWhen.PRIMARY,
            FormatResolve.OK,
            FormatUnresolved.UNRESOLVED0,
            FormatErrors.ERROR0);

    // Selector for typeKind record
    private static final Selector RECORD_TYPEKIND_SELECTOR = new Selector(
            EnumSet.of(FormatCase.RECORD),
            EnumSet.noneOf(FormatAction.class),
            EnumSet.noneOf(FormatWhen.class),
            EnumSet.noneOf(FormatResolve.class),
            EnumSet.noneOf(FormatUnresolved.class),
            EnumSet.noneOf(FormatErrors.class));

    // Current mode -- initial value is placeholder during start-up
    private Mode mode = new Mode("");

    // Retained current mode -- for checks
    private Mode retainedCurrentMode = null;

    // Mapping of mode name to mode
    private final Map<String, Mode> modeMap = new HashMap<>();

    // Mapping of mode names to encoded retained mode
    private final Map<String, String> retainedMap = new HashMap<>();

    public boolean shouldDisplayCommandFluff() {
        return mode.commandFluff;
    }

    public String getPre() {
        return mode.format("pre", Selector.ANY);
    }

    public String getPost() {
        return mode.format("post", Selector.ANY);
    }

    public String getErrorPre() {
        return mode.format("errorpre", Selector.ANY);
    }

    public String getErrorPost() {
        return mode.format("errorpost", Selector.ANY);
    }

    public String format(FormatCase fc, FormatAction fa, FormatWhen fw,
                         FormatResolve fr, FormatUnresolved fu, FormatErrors fe,
                         String name, String type, String value, String unresolved, List<String> errorLines) {
        return mode.format(fc, fa, fw, fr, fu, fe,
                name, type, value, unresolved, errorLines);
    }

    public String format(String field, FormatCase fc, FormatAction fa, FormatWhen fw,
                         FormatResolve fr, FormatUnresolved fu, FormatErrors fe,
                         String name, String type, String value, String unresolved, List<String> errorLines) {
        return mode.format(field, fc, fa, fw, fr, fu, fe,
                name, type, value, unresolved, errorLines);
    }

    public String truncateVarValue(String value) {
        return mode.truncateVarValue(value);
    }

    public String getPrompt(String nextId) {
        return mode.getPrompt(nextId);
    }

    public String getContinuationPrompt(String nextId) {
        return mode.getContinuationPrompt(nextId);
    }

    public boolean setFeedback(MessageHandler messageHandler, ArgTokenizer at, Consumer<String> retainer) {
        return new Setter(messageHandler, at).setFeedback(retainer);
    }

    public boolean setFormat(MessageHandler messageHandler, ArgTokenizer at) {
        return new Setter(messageHandler, at).setFormat();
    }

    public boolean setTruncation(MessageHandler messageHandler, ArgTokenizer at) {
        return new Setter(messageHandler, at).setTruncation();
    }

    public boolean setMode(MessageHandler messageHandler, ArgTokenizer at, Consumer<String> retainer) {
        return new Setter(messageHandler, at).setMode(retainer);
    }

    public boolean setPrompt(MessageHandler messageHandler, ArgTokenizer at) {
        return new Setter(messageHandler, at).setPrompt();
    }

    public boolean restoreEncodedModes(MessageHandler messageHandler, String encoded) {
        return new Setter(messageHandler, new ArgTokenizer("<init>", "")).restoreEncodedModes(encoded);
    }

    public void markModesReadOnly() {
        modeMap.values()
                .forEach(m -> m.readOnly = true);
    }

    JShellTool.CompletionProvider modeCompletions() {
        return modeCompletions(EMPTY_COMPLETION_PROVIDER);
    }

    JShellTool.CompletionProvider modeCompletions(CompletionProvider successor) {
        return new ContinuousCompletionProvider(
                () -> modeMap.keySet().stream()
                        .collect(toMap(Function.identity(), m -> successor)),
                PERFECT_MATCHER);
    }

    /**
     * Holds all the context of a mode mode
     */
    private static class Mode {

        // Name of mode
        final String name;

        // Display command verification/information
        boolean commandFluff;

        // Setting (including format) by field
        final Map<String, List<Setting>> byField;

        boolean readOnly = false;

        String prompt = "\n-> ";
        String continuationPrompt = ">> ";

        static class Setting {

            final String format;
            final Selector selector;

            Setting(String format, Selector selector) {
                this.format = format;
                this.selector = selector;
            }

            @Override
            public boolean equals(Object o) {
                if (o instanceof Setting) {
                    Setting ing = (Setting) o;
                    return format.equals(ing.format)
                            && selector.equals(ing.selector);
                } else {
                    return false;
                }
            }

            @Override
            public int hashCode() {
                int hash = 7;
                hash = 67 * hash + Objects.hashCode(this.selector);
                hash = 67 * hash + Objects.hashCode(this.format);
                return hash;
            }

            @Override
            public String toString() {
                return "Setting(" + format + "," + selector.toString() + ")";
            }
        }

        /**
         * Set up an empty mode.
         *
         * @param name
         */
        Mode(String name) {
            this.name = name;
            this.byField = new HashMap<>();
            set("name", "%1$s", Selector.ALWAYS);
            set("type", "%2$s", Selector.ALWAYS);
            set("value", "%3$s", Selector.ALWAYS);
            set("unresolved", "%4$s", Selector.ALWAYS);
            set("errors", "%5$s", Selector.ALWAYS);
            set("err", "%6$s", Selector.ALWAYS);

            set("errorline", "    {err}%n", Selector.ALWAYS);

            set("pre", "|  ", Selector.ALWAYS);
            set("post", "%n", Selector.ALWAYS);
            set("errorpre", "|  ", Selector.ALWAYS);
            set("errorpost", "%n", Selector.ALWAYS);
        }

        private Mode(String name, boolean commandFluff, String prompt, String continuationPrompt) {
            this.name = name;
            this.commandFluff = commandFluff;
            this.prompt = prompt;
            this.continuationPrompt = continuationPrompt;
            this.byField = new HashMap<>();
        }

        /**
         * Set up a copied mode.
         *
         * @param name
         * @param m    Mode to copy, or null for no fresh
         */
        Mode(String name, Mode m) {
            this(name, m.commandFluff, m.prompt, m.continuationPrompt);
            m.byField.forEach((fieldName, settingList) ->
                    settingList.forEach(setting -> set(fieldName, setting.format, setting.selector)));

        }

        @Override
        public boolean equals(Object o) {
            if (o instanceof Mode) {
                Mode m = (Mode) o;
                return name.equals((m.name))
                        && commandFluff == m.commandFluff
                        && prompt.equals((m.prompt))
                        && continuationPrompt.equals((m.continuationPrompt))
                        && byField.equals((m.byField));
            } else {
                return false;
            }
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(name);
        }

        /**
         * Set if this mode displays informative/confirmational messages on
         * commands.
         *
         * @param fluff the value to set
         */
        void setCommandFluff(boolean fluff) {
            commandFluff = fluff;
        }

        /**
         * Encodes the mode into a String so it can be saved in Preferences.
         *
         * @return the string representation
         */
        String encode() {
            List<String> el = new ArrayList<>();
            el.add(name);
            el.add(String.valueOf(commandFluff));
            el.add(prompt);
            el.add(continuationPrompt);
            for (Entry<String, List<Setting>> es : byField.entrySet()) {
                el.add(es.getKey());
                el.add("(");
                for (Setting ing : es.getValue()) {
                    el.add(ing.selector.toString());
                    el.add(ing.format);
                }
                el.add(")");
            }
            el.add("***");
            return String.join(RECORD_SEPARATOR, el);
        }

        private void add(String field, Setting ing) {
            List<Setting> settings = byField.get(field);
            if (settings == null) {
                settings = new ArrayList<>();
                byField.put(field, settings);
            } else {
                // remove completely obscured settings.
                // transformation of partially obscured would be confusing to user and complex
                Selector addedSelector = ing.selector;
                settings.removeIf(t -> t.selector.includedIn(addedSelector));
            }
            settings.add(ing);
        }

        void set(String field, String format, Selector selector) {
            add(field, new Setting(format, selector));
        }

        /**
         * Lookup format Replace fields with context specific formats.
         *
         * @return format string
         */
        String format(String field, Selector selector) {
            List<Setting> settings = byField.get(field);
            if (settings == null) {
                return ""; //TODO error?
            }
            String format = null;
            // Iterate backward, as most recent setting that covers the case is used
            for (int i = settings.size() - 1; i >= 0; --i) {
                Setting ing = settings.get(i);
                if (ing.selector.covers(selector)) {
                    format = ing.format;
                    break;
                }
            }
            if (format == null || format.isEmpty()) {
                return "";
            }
            Matcher m = FIELD_PATTERN.matcher(format);
            StringBuilder sb = new StringBuilder(format.length());
            while (m.find()) {
                String fieldName = m.group(1);
                String sub = format(fieldName, selector);
                m.appendReplacement(sb, Matcher.quoteReplacement(sub));
            }
            m.appendTail(sb);
            return sb.toString();
        }

        String truncateVarValue(String value) {
            return truncateValue(value, VAR_VALUE_ADD_SELECTOR);
        }

        String truncateValue(String value, Selector selector) {
            if (value==null) {
                return "";
            } else {
                // Retrieve the truncation length
                String truncField = format(TRUNCATION_FIELD, selector);
                if (truncField.isEmpty()) {
                    // No truncation set, use whole value
                    return value;
                } else {
                    // Convert truncation length to int
                    // this is safe since it has been tested before it is set
                    int trunc = Integer.parseUnsignedInt(truncField);
                    int len = value.length();
                    if (len > trunc) {
                        if (trunc <= 13) {
                            // Very short truncations have no room for "..."
                            return value.substring(0, trunc);
                        } else {
                            // Normal truncation, make total length equal truncation length
                            int endLen = trunc / 3;
                            int startLen = trunc - 5 - endLen;
                            return value.substring(0, startLen) + " ... " + value.substring(len -endLen);
                        }
                    } else {
                        // Within truncation length, use whole value
                        return value;
                    }
                }
            }
        }

        // Compute the display output given full context and values
        String format(FormatCase fc, FormatAction fa, FormatWhen fw,
                      FormatResolve fr, FormatUnresolved fu, FormatErrors fe,
                      String name, String type, String value, String unresolved, List<String> errorLines) {
            return format("display", fc, fa, fw, fr, fu, fe,
                name, type, value, unresolved, errorLines);
        }

        // Compute the display output given full context and values
        String format(String field, FormatCase fc, FormatAction fa, FormatWhen fw,
                      FormatResolve fr, FormatUnresolved fu, FormatErrors fe,
                      String name, String type, String value, String unresolved, List<String> errorLines) {
            // Convert the context into a bit representation used as selectors for store field formats
            Selector selector  = new Selector(fc, fa, fw, fr, fu, fe);
            String fname = name==null? "" : name;
            String ftype = type==null? "" : type;
            // Compute the representation of value
            String fvalue = truncateValue(value, selector);
            String funresolved = unresolved==null? "" : unresolved;
            String errors = errorLines.stream()
                    .map(el -> String.format(
                            format("errorline", selector),
                            fname, ftype, fvalue, funresolved, "*cannot-use-errors-here*", el))
                    .collect(joining());
            return String.format(
                    format(field, selector),
                    fname, ftype, fvalue, funresolved, errors, "*cannot-use-err-here*");
        }

        void setPrompts(String prompt, String continuationPrompt) {
            this.prompt = prompt;
            this.continuationPrompt = continuationPrompt;
        }

        String getPrompt(String nextId) {
            return String.format(prompt, nextId);
        }

        String getContinuationPrompt(String nextId) {
            return String.format(continuationPrompt, nextId);
        }
    }

    // Class used to set custom eval output formats
    // For both /set format  -- Parse arguments, setting custom format, or printing error
    private class Setter {

        private final ArgTokenizer at;
        private final MessageHandler messageHandler;
        boolean valid = true;

        Setter(MessageHandler messageHandler, ArgTokenizer at) {
            this.messageHandler = messageHandler;
            this.at = at;
            at.allowedOptions("-retain");
        }

        void fluff(String format, Object... args) {
            messageHandler.fluff(format, args);
        }

        void hard(String format, Object... args) {
            messageHandler.hard(format, args);
        }

        void fluffmsg(String messageKey, Object... args) {
            messageHandler.fluffmsg(messageKey, args);
        }

        void hardmsg(String messageKey, Object... args) {
            messageHandler.hardmsg(messageKey, args);
        }

        boolean showFluff() {
            return messageHandler.showFluff();
        }

        void errorat(String messageKey, Object... args) {
            if (!valid) {
                // no spew of errors
                return;
            }
            valid = false;
            Object[] a2 = Arrays.copyOf(args, args.length + 2);
            a2[args.length] = at.whole();
            messageHandler.errormsg(messageKey, a2);
        }

        // Show format settings -- in a predictable order, for testing...
        void showFormatSettings(Mode sm, String f) {
            if (sm == null) {
                modeMap.entrySet().stream()
                        .sorted((es1, es2) -> es1.getKey().compareTo(es2.getKey()))
                        .forEach(m -> showFormatSettings(m.getValue(), f));
            } else {
                sm.byField.entrySet().stream()
                        .filter(ec -> (f == null)
                            ? !ec.getKey().equals(TRUNCATION_FIELD)
                            : ec.getKey().equals(f))
                        .sorted((ec1, ec2) -> ec1.getKey().compareTo(ec2.getKey()))
                        .forEach(ec -> {
                            ec.getValue().forEach(s -> {
                                hard("/set format %s %s %s %s",
                                        sm.name, ec.getKey(), toStringLiteral(s.format),
                                        s.selector.toString());

                            });
                        });
            }
        }

        void showTruncationSettings(Mode sm) {
            if (sm == null) {
                modeMap.values().forEach(this::showTruncationSettings);
            } else {
                List<Mode.Setting> trunc = sm.byField.get(TRUNCATION_FIELD);
                if (trunc != null) {
                    trunc.forEach(s -> {
                        hard("/set truncation %s %s %s",
                                sm.name, s.format,
                                s.selector.toString());
                    });
                }
            }
        }

        void showPromptSettings(Mode sm) {
            if (sm == null) {
                modeMap.values().forEach(this::showPromptSettings);
            } else {
                hard("/set prompt %s %s %s",
                        sm.name,
                        toStringLiteral(sm.prompt),
                        toStringLiteral(sm.continuationPrompt));
            }
        }

        void showModeSettings(String umode, String msg) {
            if (umode == null) {
                modeMap.values().forEach(this::showModeSettings);
            } else {
                Mode m;
                String retained = retainedMap.get(umode);
                if (retained == null) {
                    m = searchForMode(umode, msg);
                    if (m == null) {
                        return;
                    }
                    umode = m.name;
                    retained = retainedMap.get(umode);
                } else {
                    m = modeMap.get(umode);
                }
                if (retained != null) {
                    Mode rm = buildMode(encodedModeIterator(retained));
                    showModeSettings(rm);
                    hard("/set mode -retain %s", umode);
                    if (m != null && !m.equals(rm)) {
                        hard("");
                        showModeSettings(m);
                    }
                } else {
                    showModeSettings(m);
                }
            }
        }

        void showModeSettings(Mode sm) {
            hard("/set mode %s %s",
                    sm.name, sm.commandFluff ? "-command" : "-quiet");
            showPromptSettings(sm);
            showFormatSettings(sm, null);
            showTruncationSettings(sm);
        }

        void showFeedbackSetting() {
            if (retainedCurrentMode != null) {
                hard("/set feedback -retain %s", retainedCurrentMode.name);
            }
            if (mode != retainedCurrentMode) {
                hard("/set feedback %s", mode.name);
            }
        }

        // For /set prompt <mode> "<prompt>" "<continuation-prompt>"
        boolean setPrompt() {
            Mode m = nextMode();
            String prompt = nextFormat();
            String continuationPrompt = nextFormat();
            checkOptionsAndRemainingInput();
            if (valid && prompt == null) {
                showPromptSettings(m);
                return valid;
            }
            if (valid && m.readOnly) {
                errorat("jshell.err.not.valid.with.predefined.mode", m.name);
            } else if (continuationPrompt == null) {
                errorat("jshell.err.continuation.prompt.required");
            }
            if (valid) {
                m.setPrompts(prompt, continuationPrompt);
            } else {
                fluffmsg("jshell.msg.see", "/help /set prompt");
            }
            return valid;
        }

        /**
         * Set mode. Create, changed, or delete a feedback mode. For {@code /set
         * mode <mode> [<old-mode>] [-command|-quiet|-delete|-retain]}.
         *
         * @return true if successful
         */
        boolean setMode(Consumer<String> retainer) {
            class SetMode {

                final String umode;
                final String omode;
                final boolean commandOption;
                final boolean quietOption;
                final boolean deleteOption;
                final boolean retainOption;

                SetMode() {
                    at.allowedOptions("-command", "-quiet", "-delete", "-retain");
                    umode = nextModeIdentifier();
                    omode = nextModeIdentifier();
                    checkOptionsAndRemainingInput();
                    commandOption = at.hasOption("-command");
                    quietOption = at.hasOption("-quiet");
                    deleteOption = at.hasOption("-delete");
                    retainOption = at.hasOption("-retain");
                }

                void delete() {
                    // Note: delete, for safety reasons, does NOT do name matching
                    if (commandOption || quietOption) {
                        errorat("jshell.err.conflicting.options");
                    } else if (retainOption
                            ? !retainedMap.containsKey(umode) && !modeMap.containsKey(umode)
                            : !modeMap.containsKey(umode)) {
                        // Cannot delete a mode that does not exist
                        errorat("jshell.err.mode.unknown", umode);
                    } else if (omode != null) {
                        // old mode is for creation
                        errorat("jshell.err.unexpected.at.end", omode);
                    } else if (mode.name.equals(umode)) {
                        // Cannot delete the current mode out from under us
                        errorat("jshell.err.cannot.delete.current.mode", umode);
                    } else if (retainOption && retainedCurrentMode != null &&
                             retainedCurrentMode.name.equals(umode)) {
                        // Cannot delete the retained mode or re-start will have an error
                        errorat("jshell.err.cannot.delete.retained.mode", umode);
                    } else {
                        Mode m = modeMap.get(umode);
                        if (m != null && m.readOnly) {
                            errorat("jshell.err.not.valid.with.predefined.mode", umode);
                        } else {
                            // Remove the mode
                            modeMap.remove(umode);
                            if (retainOption) {
                                // Remove the retained mode
                                retainedMap.remove(umode);
                                updateRetainedModes();
                            }
                        }
                    }
                }

                void retain() {
                    if (commandOption || quietOption) {
                        errorat("jshell.err.conflicting.options");
                    } else if (omode != null) {
                        // old mode is for creation
                        errorat("jshell.err.unexpected.at.end", omode);
                    } else {
                        Mode m = modeMap.get(umode);
                        if (m == null) {
                            // can only retain existing modes
                            errorat("jshell.err.mode.unknown", umode);
                        } else if (m.readOnly) {
                            errorat("jshell.err.not.valid.with.predefined.mode", umode);
                        } else {
                            // Add to local cache of retained current encodings
                            retainedMap.put(m.name, m.encode());
                            updateRetainedModes();
                        }
                    }
                }

                void updateRetainedModes() {
                    // Join all the retained encodings
                    String encoded = String.join(RECORD_SEPARATOR, retainedMap.values());
                    // Retain it
                    retainer.accept(encoded);
                }

                void create() {
                    if (commandOption && quietOption) {
                        errorat("jshell.err.conflicting.options");
                    } else if (!commandOption && !quietOption) {
                        errorat("jshell.err.mode.creation");
                    } else if (modeMap.containsKey(umode)) {
                        // Mode already exists
                        errorat("jshell.err.mode.exists", umode);
                    } else {
                        Mode om = searchForMode(omode);
                        if (valid) {
                            // We are copying an existing mode and/or creating a
                            // brand-new mode -- in either case create from scratch
                            Mode m = (om != null)
                                    ? new Mode(umode, om)
                                    : new Mode(umode);
                            modeMap.put(umode, m);
                            fluffmsg("jshell.msg.feedback.new.mode", m.name);
                            m.setCommandFluff(commandOption);
                        }
                    }
                }

                boolean set() {
                    if (valid && !commandOption && !quietOption && !deleteOption &&
                            omode == null && !retainOption) {
                        // Not a creation, deletion, or retain -- show mode(s)
                        showModeSettings(umode, "jshell.err.mode.creation");
                    } else if (valid && umode == null) {
                        errorat("jshell.err.missing.mode");
                    } else if (valid && deleteOption) {
                        delete();
                    } else if (valid && retainOption) {
                        retain();
                    } else if (valid) {
                        create();
                    }
                    if (!valid) {
                        fluffmsg("jshell.msg.see", "/help /set mode");
                    }
                    return valid;
                }
            }
            return new SetMode().set();
        }

        // For /set format <mode> <field> "<format>" <selector>...
        boolean setFormat() {
            Mode m = nextMode();
            String field = toIdentifier(next(), "jshell.err.field.name");
            String format = nextFormat();
            if (valid && format == null) {
                if (field != null && m != null && !m.byField.containsKey(field)) {
                    errorat("jshell.err.field.name", field);
                } else {
                    showFormatSettings(m, field);
                }
            } else {
                installFormat(m, field, format, "/help /set format");
            }
            return valid;
        }

        // For /set truncation <mode> <length> <selector>...
        boolean setTruncation() {
            Mode m = nextMode();
            String length = next();
            if (length == null) {
                showTruncationSettings(m);
            } else {
                try {
                    // Assure that integer format is correct
                    Integer.parseUnsignedInt(length);
                } catch (NumberFormatException ex) {
                    errorat("jshell.err.truncation.length.not.integer", length);
                }
                // install length into an internal format field
                installFormat(m, TRUNCATION_FIELD, length, "/help /set truncation");
            }
            return valid;
        }

        // For /set feedback <mode>
        boolean setFeedback(Consumer<String> retainer) {
            String umode = next();
            checkOptionsAndRemainingInput();
            boolean retainOption = at.hasOption("-retain");
            if (valid && umode == null && !retainOption) {
                showFeedbackSetting();
                hard("");
                showFeedbackModes();
                return true;
            }
            if (valid) {
                Mode m = umode == null
                        ? mode
                        : searchForMode(toModeIdentifier(umode));
                if (valid && retainOption && !m.readOnly && !retainedMap.containsKey(m.name)) {
                    errorat("jshell.err.retained.feedback.mode.must.be.retained.or.predefined");
                }
                if (valid) {
                    if (umode != null) {
                        mode = m;
                        fluffmsg("jshell.msg.feedback.mode", mode.name);
                    }
                    if (retainOption) {
                        retainedCurrentMode = m;
                        retainer.accept(m.name);
                    }
                }
            }
            if (!valid) {
                fluffmsg("jshell.msg.see", "/help /set feedback");
                return false;
            }
            return true;
        }

        boolean restoreEncodedModes(String allEncoded) {
            try {
                // Iterate over each record in each encoded mode
                Iterator<String> itr = encodedModeIterator(allEncoded);
                while (itr.hasNext()) {
                    // Reconstruct the encoded mode
                    Mode m = buildMode(itr);
                    modeMap.put(m.name, m);
                    // Continue to retain if a new retains occur
                    retainedMap.put(m.name, m.encode());
                }
                return true;
            } catch (Throwable exc) {
                // Catastrophic corruption -- clear map
                errorat("jshell.err.retained.mode.failure", exc);
                retainedMap.clear();
                return false;
            }
        }


        /**
         * Set up a mode reconstituted from a preferences string.
         *
         * @param it the encoded Mode broken into String chunks, may contain
         *           subsequent encoded modes
         */
        private Mode buildMode(Iterator<String> it) {
            Mode newMode = new Mode(it.next(), Boolean.parseBoolean(it.next()),  it.next(), it.next());
            Map<String, List<Mode.Setting>> fields = new HashMap<>();
            long suspiciousBits = Selector.OLD_ALWAYS.asBits();
            boolean suspicious = false;
            String field;
            while (!(field = it.next()).equals("***")) {
                String open = it.next();
                assert open.equals("(");
                List<Mode.Setting> settings = new ArrayList<>();
                String selectorText;
                while (!(selectorText = it.next()).equals(")")) {
                    String format = it.next();
                    Selector selector;
                    if (selectorText.isEmpty()) {
                        selector = Selector.ALWAYS;
                    } else if (Character.isDigit(selectorText.charAt(0))) {
                        // legacy format, bits
                        long bits = Long.parseLong(selectorText);
                        suspicious |= bits == suspiciousBits;
                        selector = new Selector(bits);
                    } else {
                        selector = parseSelector(selectorText);
                    }
                    Mode.Setting ing = new Mode.Setting(format, selector);
                    settings.add(ing);
                }
                fields.put(field, settings);
            }
            List<Mode.Setting> tk;
            List<Mode.Setting> errf;
            // If suspicious that this is a pre-JDK-14 settings, check deeper...
            if (suspicious
                    // Super simple might not define typeKind, otherwise check for JDK-14 presence of record
                    && ((tk = fields.get("typeKind")) == null
                    || !tk.stream().anyMatch(tkc -> tkc.selector.equals(RECORD_TYPEKIND_SELECTOR)))
                    // no record typeKind, now check for corruption
                    && ((errf = fields.get("err")) == null
                    || errf.stream().anyMatch(tkc -> tkc.selector.equals(Selector.OLD_ALWAYS)))) {
                // Pre-JDK-14 setting found, convert them

                // start with solid base, ideally normal
                Mode base = modeMap.get("normal");
                if (base == null) {
                    base = mode;
                }

                // Make sure any current fields/selectors are covered: filling in with the base (normal)
                base.byField.forEach((fieldName, settingList) ->
                        settingList.forEach(setting -> newMode.set(fieldName, setting.format, setting.selector)));

                // Now, overlay with user's settings (position adjusted).
                // Assume any setting for class applies to record, except for typeKind definition where base definition
                // should fall through.
                fields.forEach((fieldName, settingList) -> {
                        settingList.forEach(setting -> newMode.set(fieldName, setting.format,
                                Selector.fromPreJDK14(setting.selector, !fieldName.equals("typeKind"))));
                        });
            } else {
                fields.forEach((fieldName, settingList) ->
                        settingList.forEach(setting -> newMode.set(fieldName, setting.format, setting.selector)));
            }
            return newMode;
        }

        Iterator<String> encodedModeIterator(String encoded) {
            String[] ms = encoded.split(RECORD_SEPARATOR);
            return Arrays.asList(ms).iterator();
        }

        // install the format of a field under parsed selectors
        void installFormat(Mode m, String field, String format, String help) {
            String slRaw;
            List<Selector> selectorList = new ArrayList<>();
            while (valid && (slRaw = next()) != null) {
                selectorList.add(parseSelector(slRaw));
            }
            checkOptionsAndRemainingInput();
            if (valid) {
                if (m.readOnly) {
                    errorat("jshell.err.not.valid.with.predefined.mode", m.name);
                } else if (selectorList.isEmpty()) {
                    // No selectors specified, then always use the format
                    m.set(field, format, Selector.ALWAYS);
                } else {
                    // Set the format of the field for specified selector
                    selectorList.forEach(sel -> m.set(field, format, sel));
                }
            } else {
                fluffmsg("jshell.msg.see", help);
            }
        }

        void checkOptionsAndRemainingInput() {
            String junk = at.remainder();
            if (!junk.isEmpty()) {
                errorat("jshell.err.unexpected.at.end", junk);
            } else {
                String bad = at.badOptions();
                if (!bad.isEmpty()) {
                    errorat("jshell.err.unknown.option", bad);
                }
            }
        }

        String next() {
            String s = at.next();
            if (s == null) {
                checkOptionsAndRemainingInput();
            }
            return s;
        }

        /**
         * Check that the specified string is an identifier (Java identifier).
         * If null display the missing error. If it is not an identifier,
         * display the error.
         *
         * @param id the string to check, MUST be the most recently retrieved
         * token from 'at'.
         * @param err the resource error to display if not an identifier
         * @return the identifier string, or null if null or not an identifier
         */
        private String toIdentifier(String id, String err) {
            if (!valid || id == null) {
                return null;
            }
            if (at.isQuoted() ||
                    !id.codePoints().allMatch(Character::isJavaIdentifierPart)) {
                errorat(err, id);
                return null;
            }
            return id;
        }

        private String toModeIdentifier(String id) {
            return toIdentifier(id, "jshell.err.mode.name");
        }

        private String nextModeIdentifier() {
            return toModeIdentifier(next());
        }

        private Mode nextMode() {
            String umode = nextModeIdentifier();
            return searchForMode(umode);
        }

        private Mode searchForMode(String umode) {
            return searchForMode(umode, null);
        }

        private Mode searchForMode(String umode, String msg) {
            if (!valid || umode == null) {
                return null;
            }
            Mode m = modeMap.get(umode);
            if (m != null) {
                return m;
            }
            // Failing an exact match, go searching
            Mode[] matches = modeMap.entrySet().stream()
                    .filter(e -> e.getKey().startsWith(umode))
                    .map(Entry::getValue)
                    .toArray(Mode[]::new);
            if (matches.length == 1) {
                return matches[0];
            } else {
                if (msg != null) {
                    hardmsg(msg, "");
                }
                if (matches.length == 0) {
                    errorat("jshell.err.feedback.does.not.match.mode", umode);
                } else {
                    errorat("jshell.err.feedback.ambiguous.mode", umode);
                }
                if (showFluff()) {
                    showFeedbackModes();
                }
                return null;
            }
        }

        void showFeedbackModes() {
            if (!retainedMap.isEmpty()) {
                hardmsg("jshell.msg.feedback.retained.mode.following");
                retainedMap.keySet().stream()
                        .sorted()
                        .forEach(mk -> hard("   %s", mk));
            }
            hardmsg("jshell.msg.feedback.mode.following");
            modeMap.keySet().stream()
                    .sorted()
                    .forEach(mk -> hard("   %s", mk));
        }

        // Read and test if the format string is correctly
        private String nextFormat() {
            return toFormat(next());
        }

        // Test if the format string is correctly
        private String toFormat(String format) {
            if (!valid || format == null) {
                return null;
            }
            if (!at.isQuoted()) {
                errorat("jshell.err.feedback.must.be.quoted", format);
               return null;
            }
            return format;
        }

        // Convert to a quoted string
        private String toStringLiteral(String s) {
            StringBuilder sb = new StringBuilder();
            sb.append('"');
            final int length = s.length();
            for (int offset = 0; offset < length;) {
                final int codepoint = s.codePointAt(offset);

                switch (codepoint) {
                    case '\b':
                        sb.append("\\b");
                        break;
                    case '\t':
                        sb.append("\\t");
                        break;
                    case '\n':
                        sb.append("\\n");
                        break;
                    case '\f':
                        sb.append("\\f");
                        break;
                    case '\r':
                        sb.append("\\r");
                        break;
                    case '\"':
                        sb.append("\\\"");
                        break;
                    case '\'':
                        sb.append("\\'");
                        break;
                    case '\\':
                        sb.append("\\\\");
                        break;
                    default:
                        if (codepoint < 040) {
                            sb.append(String.format("\\%o", codepoint));
                        } else {
                            sb.appendCodePoint(codepoint);
                        }
                        break;
                }

                // do something with the codepoint
                offset += Character.charCount(codepoint);

            }
            sb.append('"');
            return sb.toString();
        }

        private Selector parseSelector(String selectorText) {
            SelectorBuilder seb = new SelectorBuilder(selectorText);
            EnumSet<SelectorKind> seen = EnumSet.noneOf(SelectorKind.class);
            for (String s : selectorText.split("-")) {
                SelectorKind lastKind = null;
                for (String as : s.split(",")) {
                    if (!as.isEmpty()) {
                        SelectorInstanceWithDoc<?> sel = Selector.selectorMap.get(as);
                        if (sel == null) {
                            errorat("jshell.err.feedback.not.a.valid.selector", as, s);
                            return Selector.ALWAYS;
                        }
                        SelectorKind kind = sel.kind();
                        if (lastKind == null) {
                            if (seen.contains(kind)) {
                                errorat("jshell.err.feedback.multiple.sections", as, s);
                                return Selector.ALWAYS;
                            }
                        } else if (kind != lastKind) {
                            errorat("jshell.err.feedback.different.selector.kinds", as, s);
                            return Selector.ALWAYS;
                        }
                        seb.add(sel);
                        seen.add(kind);
                        lastKind = kind;
                    }
                }
            }
            return seb.toSelector();
         }
    }
}
