/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.apache.groovy.groovysh

import groovy.cli.internal.CliBuilderInternal
import groovy.cli.internal.OptionAccessor
import org.apache.groovy.groovysh.jline.GroovyBuiltins
import org.apache.groovy.groovysh.jline.GroovyCommands
import org.apache.groovy.groovysh.jline.GroovyConsoleEngine
import org.apache.groovy.groovysh.jline.GroovyEngine
import org.apache.groovy.groovysh.jline.GroovyPosixCommands
import org.apache.groovy.groovysh.jline.GroovyPosixContext
import org.apache.groovy.groovysh.jline.GroovyPrinter
import org.apache.groovy.groovysh.jline.GroovySystemRegistry
import org.apache.groovy.groovysh.util.DocFinder
import org.codehaus.groovy.tools.shell.util.MessageSource
import org.jline.builtins.ClasspathResourceUtil
import org.jline.builtins.Completers
import org.jline.builtins.ConfigurationPath
import org.jline.builtins.Options
import org.jline.builtins.PosixCommands
import org.jline.builtins.PosixCommandsRegistry
import org.jline.builtins.SyntaxHighlighter
import org.jline.console.CommandInput
import org.jline.console.CommandMethods
import org.jline.console.CommandRegistry
import org.jline.console.ConsoleEngine
import org.jline.console.Printer
import org.jline.console.impl.JlineCommandRegistry
import org.jline.console.impl.SystemHighlighter
import org.jline.keymap.KeyMap
import org.jline.reader.Binding
import org.jline.reader.Completer
import org.jline.reader.EndOfFileException
import org.jline.reader.LineReader
import org.jline.reader.LineReader.Option
import org.jline.reader.LineReaderBuilder
import org.jline.reader.Reference
import org.jline.reader.UserInterruptException
import org.jline.reader.impl.DefaultParser
import org.jline.reader.impl.DefaultParser.Bracket
import org.jline.reader.impl.completer.ArgumentCompleter
import org.jline.reader.impl.completer.NullCompleter
import org.jline.terminal.Size
import org.jline.terminal.Terminal
import org.jline.terminal.Terminal.Signal
import org.jline.terminal.TerminalBuilder
import org.jline.utils.InfoCmp.Capability
import org.jline.utils.OSUtils
import org.jline.widget.AutosuggestionWidgets
import org.jline.widget.TailTipWidgets
import org.jline.widget.TailTipWidgets.TipType
import org.jline.widget.Widgets

import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.attribute.PosixFilePermission
import java.nio.file.attribute.PosixFilePermissions
import java.util.function.Function
import java.util.function.Supplier

import static org.jline.jansi.AnsiRenderer.render

/**
 * Boots and runs the interactive {@code groovysh} console.
 */
@SuppressWarnings('deprecation')
class Main {
    private static final MessageSource messages = new MessageSource(Main)
    /** Preference key that controls whether groovysh starts in interpreter mode. */
    public static final String INTERPRETER_MODE_PREFERENCE_KEY = 'interpreterMode'
//    private static POSIX_CMDS = []
    private static GROOVY_POSIX_CMDS = ['/ls', '/wc', '/sort', '/head', '/tail', '/cat', '/grep']

    /**
     * Registers the extra console commands that augment the interactive shell.
     */
    @SuppressWarnings("resource")
    protected static class ExtraConsoleCommands extends JlineCommandRegistry implements CommandRegistry {
        private final LineReader reader
        private final GroovyEngine scriptEngine
        private PosixCommandsRegistry posix
        private final Map<String, String[]> usage = [:]

        /**
         * Creates the auxiliary console command registry used by the shell.
         *
         * @param workDir initial working directory
         * @param scriptEngine script engine backing the shell session
         * @param reader active line reader
         */
        ExtraConsoleCommands(Path workDir, GroovyEngine scriptEngine, LineReader reader) {
            super()
            this.scriptEngine = scriptEngine
            this.reader = reader
            def terminal = reader.terminal
            def context = new GroovyPosixContext(
                terminal.input(),
                new PrintStream(terminal.output()),
                new PrintStream(terminal.output()),
                workDir,
                terminal,
                scriptEngine::get
            )
            posix = new PosixCommandsRegistry(context)
            def cmds = [
                '/clear': new CommandMethods((Function) this::clear, this::defaultCompleter),
                '/pwd'  : new CommandMethods((Function) this::pwd, this::defaultCompleter),
                '/cd'   : new CommandMethods((Function) this::cd, this::optDirCompleter),
                '/date' : new CommandMethods((Function) this::date, this::defaultCompleter),
                '/echo' : new CommandMethods((Function) this::echo, this::defaultCompleter),
                "/!"    : new CommandMethods((Function) this::shell, this::defaultCompleter)
            ]
            GROOVY_POSIX_CMDS.each { String cmd ->
                String base = cmd[1..-1]
                usage[cmd] = adjustUsage(base, cmd)
                posix.register(cmd, PosixCommands::"$base")
                cmds.put(cmd, new CommandMethods((Function) this::posixCommand, this::optFileCompleter))
            }
//            POSIX_CMDS.each { String cmd ->
//                String orig = cmd[1..-1]
//                usage[cmd] = adjustUsage(orig, cmd)
//                posix.register(cmd, PosixCommands::"$orig")
//                cmds.put(cmd, new CommandMethods((Function) this::posix, this::optFileCompleter))
//            }
            posix.register('cd', PosixCommands::cd)
            posix.register('/cd', PosixCommands::cd)
            posix.register('/pwd', PosixCommands::pwd)
            posix.register('/date', PosixCommands::date)
            posix.register('/echo', PosixCommands::echo)
            registerCommands(cmds)
        }

        /**
         * Returns the current working directory tracked by the POSIX command context.
         *
         * @return the active working directory
         */
        Path currentDir() {
            posix.context.currentDir
        }

        private String[] adjustUsage(String from, String to) {
            try {
                posix.execute(from, [from, '--help'] as String[])
            } catch (Options.HelpException e) {
                e.message.readLines()*.replaceAll("$from ", "$to ") as String[]
            }
        }

        /**
         * Returns the help-group name used for the extra console commands.
         *
         * @return the console command group name
         */
        @Override
        String name() {
            'Console Commands'
        }

        private Terminal terminal() {
            return reader?.terminal
        }

        private List<Completer> optFileCompleter(String command) {
            [new ArgumentCompleter(NullCompleter.INSTANCE, new Completers.OptionCompleter(new Completers.FilesCompleter(this::currentDir), this::commandOptions, 1))]
        }

        private List<Completer> optDirCompleter(String command) {
            [new ArgumentCompleter(NullCompleter.INSTANCE, new Completers.OptionCompleter(new Completers.DirectoriesCompleter(this::currentDir), this::commandOptions, 1))]
        }

        private void pwd(CommandInput input) {
            posix(adjustUsage('pwd', '/pwd'), input)
        }

        private void cd(CommandInput input) {
            try {
                parseOptions(adjustUsage('cd', '/cd'), input.args())
                PosixCommands.cd(context(input), ['/cd', *input.args()] as String[], { Path newPath ->
                    posix.context.currentDir = newPath
                    scriptEngine.put('PWD', newPath)
                })
            } catch (Exception e) {
                saveException(e)
            }
        }

        private void posixCommand(CommandInput input) {
            try {
                String cmd = input.command()
                String name = cmd[1..-1]
                GroovyPosixCommands."$name"(context(input), [cmd, *input.xargs()] as Object[])
            } catch (Exception e) {
                saveException(e)
            }
        }

        private GroovyPosixContext context(CommandInput input) {
            GroovyPosixContext ctx = new GroovyPosixContext(input.in(), input.out(), input.err(),
                posix.context.currentDir(), input.terminal(), scriptEngine::get)
            ctx
        }

        private void date(CommandInput input) {
            posix(adjustUsage('date', '/date'), input)
        }

        private void posix(CommandInput input) {
            posix(usage[input.command()], input)
        }

        private void posix(String[] usage, CommandInput input) {
            try {
                parseOptions(usage, input.args())
                PosixCommands."${input.command()[1..-1]}"(context(input), [input.command(), *input.args()] as String[])
//                posix.execute(input.command(), [input.command(), *input.args()] as String[])
            } catch (Exception e) {
                saveException(e)
            }
        }

        private void clear(CommandInput input) {
            final String[] usage = [
                "/clear -  clear terminal",
                "Usage: /clear",
                "  -? --help                       Displays command help"
            ]
            try {
                parseOptions(usage, input.args())
                terminal().puts(Capability.clear_screen)
                terminal().flush()
            } catch (Exception e) {
                saveException(e)
            }
        }

        private void echo(CommandInput input) {
            posix(adjustUsage('echo', '/echo'), input)
        }

        private static void executeCommand(List<String> args) throws Exception {
            def sout = new StringBuilder(), serr = new StringBuilder()
            def command = OSUtils.IS_WINDOWS ? ['cmd.exe', '/c'] : ['sh', '-c']
            def proc = new ProcessBuilder().command(command + args.join(' ')).start()
            proc.consumeProcessOutput(sout, serr)
            int exitCode = proc.waitFor()
            if (sout.size()) print sout
            if (exitCode != 0) {
                if (serr.size()) print serr
                throw new Exception("Error occurred in shell!")
            }
        }

        private void shell(CommandInput input) {
            final String[] usage = [
                "/!<command> -  execute shell command",
                "Usage: /!<command>",
                "  -? --help                       Displays command help"
            ]
            if (input.args().length == 1 && (input.args()[0].equals("-?") || input.args()[0].equals("--help"))) {
                try {
                    parseOptions(usage, input.args())
                } catch (Exception e) {
                    saveException(e)
                }
            } else {
                List<String> argv = input.args().toList()
                if (!argv.isEmpty()) {
                    try {
                        executeCommand(argv)
                    } catch (Exception e) {
                        saveException(e)
                    }
                }
            }
        }

        /**
         * Returns summary text for the specified command.
         *
         * @param command command name to describe
         * @return help lines for the command
         */
        @Override
        List<String> commandInfo(String command) {
            posix.commandNames.toList()
        }
    }

    /**
     * Returns the user state directory used by groovysh for persisted files.
     *
     * @return the user-specific state directory
     */
    static Path getUserStateDirectory() {
        Path.of(System.getProperty('user.home'), '.groovy').tap { groovyHome ->
            if (!Files.exists(groovyHome)) {
                createDirectoryOwnerOnly(groovyHome)
            }
        }
    }

    private static final Set<PosixFilePermission> FILE_OWNER_ONLY = PosixFilePermissions.fromString('rw-------')
    private static final Set<PosixFilePermission> DIRECTORY_OWNER_ONLY = PosixFilePermissions.fromString('rwx------')

    /**
     * Whether the default file system records POSIX permissions. Windows and other non-POSIX file
     * systems carry their own access control, which this code does not attempt to second-guess.
     */
    private static boolean posixFileSystem() {
        FileSystems.default.supportedFileAttributeViews().contains('posix')
    }

    private static void createDirectoryOwnerOnly(Path dir) {
        if (posixFileSystem()) {
            Files.createDirectories(dir, PosixFilePermissions.asFileAttribute(DIRECTORY_OWNER_ONLY))
        } else {
            Files.createDirectories(dir)
        }
    }

    /**
     * Ensures a file under the user state directory exists and is readable only by its owner.
     * <p>
     * These files record what was typed at the shell and the values it held, so on a shared host
     * they should not be left at whatever the ambient umask happens to permit. A file that already
     * exists is tightened only when it is currently more permissive than owner-only, so a
     * deliberately-chosen mode is left alone. Failing to adjust permissions is not fatal: the file
     * is still usable, and the shell should not refuse to start over it.
     *
     * @param file the state file
     * @return the same path, for chaining
     */
    static Path createOwnerOnlyStateFile(Path file) {
        try {
            Path parent = file.parent
            if (parent != null && !Files.exists(parent)) {
                createDirectoryOwnerOnly(parent)
            }
            if (!Files.exists(file)) {
                if (posixFileSystem()) {
                    Files.createFile(file, PosixFilePermissions.asFileAttribute(FILE_OWNER_ONLY))
                } else {
                    Files.createFile(file)
                }
            }
        } catch (IOException | UnsupportedOperationException ignored) {
            // best effort: a state file we cannot create is reported by whoever needs to write it
        }
        tightenStateFile(file)
    }

    /**
     * The state files JLine writes into the same directory, which it creates at the ambient umask.
     */
    private static final List<String> JLINE_STATE_FILES = ['aliases.json', 'pipeline-names.json']

    /**
     * Restricts JLine's own state files to their owner once they exist.
     * <p>
     * JLine creates them through {@code ConfigurationPath.getUserConfig}, which calls
     * {@code Files.createFile} with no permission attributes; as of 4.4.1 that is still the case,
     * even though {@code DefaultHistory} was hardened for exactly this. They are not created here,
     * because handing JLine an empty file to parse as JSON would be worse than leaving the mode
     * alone. A file JLine creates during this session is therefore tightened on the next startup
     * rather than this one.
     * <p>
     * The lasting fix belongs upstream, in {@code getUserConfig} itself.
     */
    private static void tightenJLineStateFiles() {
        Path stateDirectory = userStateDirectory
        JLINE_STATE_FILES.each { name -> tightenStateFile(stateDirectory.resolve(name)) }
    }

    /**
     * Restricts an existing state file to its owner when it is currently more permissive, and does
     * nothing at all when the file is absent.
     * <p>
     * JLine warns about an over-permissive history file rather than changing it, so as not to
     * surprise the user. This is the one place groovysh goes further: the file records what was
     * typed at a shell prompt, the warning is easy to miss in a log, and the directory it lives in
     * belongs to groovysh. A mode stricter than owner-only is still left alone.
     *
     * @param file the state file
     * @return the same path, for chaining
     */
    static Path tightenStateFile(Path file) {
        try {
            if (posixFileSystem() && Files.exists(file)
                    && !FILE_OWNER_ONLY.containsAll(Files.getPosixFilePermissions(file))) {
                Files.setPosixFilePermissions(file, FILE_OWNER_ONLY)
            }
        } catch (IOException | UnsupportedOperationException ignored) {
            // best effort: a state file we cannot tighten is still a usable state file
        }
        file
    }

    /**
     * Programmatic entry point for embedding groovysh.
     *
     * @param args CLI-like arguments (same as {@link #main(String[])}).
     * @param initialBindings binding variables for the GroovyEngine
     * @return process exit code (0 for success)
     *
     * @since 6.0.0
     */
    static int start(Map<String, ?> initialBindings = Collections.emptyMap(), String[] args = new String[0]) {
        def cli = new CliBuilderInternal(usage: 'groovysh [options] [...]', stopAtNonOption: false,
            header: messages['cli.option.header'])
        cli.with {
            _(names: ['-cp', '-classpath', '--classpath'], messages['cli.option.classpath.description'])
            h(longOpt: 'help', messages['cli.option.help.description'])
            V(longOpt: 'version', messages['cli.option.version.description'])
            v(longOpt: 'verbose', messages['cli.option.verbose.description'])
            q(longOpt: 'quiet', messages['cli.option.quiet.description'])
            c(longOpt: 'encoding', args: 1, argName: 'CHARSET', optionalArg: false, messages['cli.option.encoding.description'])
            d(longOpt: 'debug', messages['cli.option.debug.description'])
            e(longOpt: 'evaluate', args: 1, argName: 'CODE', optionalArg: false, messages['cli.option.evaluate.description'])
            C(longOpt: 'color', args: 1, argName: 'FLAG', optionalArg: true, messages['cli.option.color.description'])
            D(longOpt: 'define', type: Map, argName: 'name=value', messages['cli.option.define.description'])
            T(longOpt: 'terminal', args: 1, argName: 'TYPE', messages['cli.option.terminal.description'])
            pa(longOpt: 'parameters', messages['cli.option.parameters.description'])
            pr(longOpt: 'enable-preview', messages['cli.option.enable.preview.description'])
        }
        OptionAccessor options = cli.parse(args)

        if (options == null) {
            // CliBuilder prints error
            return 22 // Invalid Args
        }

        if (options.h) {
            cli.usage()
            return 0
        }

        if (options.V) {
            println render(messages.format('cli.info.version', GroovySystem.version))
            return 0
        }
        String evaluate = options.e ?: null

        try {
            DefaultParser parser = new DefaultParser(
                regexCommand: /\/?[a-zA-Z!]\S*/,
                eofOnUnclosedQuote: true,
                eofOnEscapedNewLine: true
            )
            parser.blockCommentDelims(new DefaultParser.BlockCommentDelims('/*', '*/'))
                .lineCommentDelims(new String[]{'//'})
                .setEofOnUnclosedBracket(Bracket.CURLY, Bracket.ROUND, Bracket.SQUARE)
            Terminal terminal = TerminalBuilder.builder().tap{
                if (options.T) {
                    type(options.T)
                }
                if (options.c) {
                    encoding(options.c)
                }
                if (options.C) {
                    color(options.C as boolean)
                }
                name('groovysh')
            }.build()
            if (terminal.columns == 0 || terminal.rows == 0) {
                terminal.size = new Size(120, 40) // hard-coded terminal size when redirecting
            }
            Thread executeThread = Thread.currentThread()
            terminal.handle(Signal.INT, signal -> executeThread.interrupt())

            def rootURL = Main.getResource('/nanorc')
            Path root = ClasspathResourceUtil.getResourcePath(rootURL)
            ConfigurationPath configPath = new ConfigurationPath(root, userStateDirectory)

            // ScriptEngine and command registries
            GroovyEngine scriptEngine = new GroovyEngine()

            if (initialBindings) {
                initialBindings.each { k, v ->
                    if (k != null) {
                        scriptEngine.put(k, v)
                    }
                }
            }

            scriptEngine.put('ROOT', rootURL.toString())
            if (!scriptEngine.hasVariable('CONSOLE_OPTIONS')) {
                scriptEngine.put('CONSOLE_OPTIONS', [:])
            }
            def interpreterMode = Boolean.parseBoolean(System.getProperty("groovysh.interpreterMode", "true"))
            scriptEngine.put('GROOVYSH_OPTIONS', [interpreterMode: interpreterMode])
            Printer printer = new GroovyPrinter(scriptEngine, configPath)

            scriptEngine.put(GroovyEngine.NANORC_VALUE, rootURL.toString())
            Path jnanorc = root.resolve('jnanorc')
            def commandHighlighter = SyntaxHighlighter.build(jnanorc, "COMMAND")
            def argsHighlighter = SyntaxHighlighter.build(jnanorc, "ARGS")
            def groovyHighlighter = SyntaxHighlighter.build(jnanorc, "Groovy")

            LineReader reader = LineReaderBuilder.builder()
                .terminal(terminal)
                .parser(parser)
                .variable(LineReader.SECONDARY_PROMPT_PATTERN, "%M%P > ")
                .variable(LineReader.INDENTATION, 2)
                .variable(LineReader.LIST_MAX, 100)
                // Left for JLine to create: DefaultHistory creates the history file owner-only,
                // handles the concurrent-creation race, and falls back on non-POSIX file systems.
                // Creating it here first is what used to defeat that and leave it at the umask
                // default. Only an already-open file is adjusted.
                .variable(LineReader.HISTORY_FILE,
                    tightenStateFile(userStateDirectory.resolve('groovysh_history')))
                .option(Option.INSERT_BRACKET, true)
                .option(Option.EMPTY_WORD_OPTIONS, false)
                .option(Option.USE_FORWARD_SLASH, true)
                .option(Option.DISABLE_EVENT_EXPANSION, true)
                .build()
            if (OSUtils.IS_WINDOWS) {
                reader.setVariable(
                    LineReader.BLINK_MATCHING_PAREN, 0) // if enabled cursor remains in begin parenthesis (gitbash)
            }

            def extra = new ExtraConsoleCommands(Paths.get(System.getProperty('user.dir')), scriptEngine, reader)
            Supplier<Path> workDir = extra::currentDir
            scriptEngine.put('PWD', workDir.get())

            CommandRegistry groovy = new GroovyCommands(scriptEngine, workDir, printer, groovyHighlighter)

            ConsoleEngine consoleEngine = new GroovyConsoleEngine(scriptEngine, printer, workDir, configPath, reader)
            consoleEngine.setConsoleOption('docs', new DocFinder())
            tightenJLineStateFiles()

            CommandRegistry builtins = new GroovyBuiltins(scriptEngine, workDir, configPath, reader, (String fun) ->
                new ConsoleEngine.WidgetCreator(consoleEngine, fun)
            )

            GroovySystemRegistry systemRegistry = new GroovySystemRegistry(parser, terminal, workDir, configPath).tap {
                groupCommandsInHelp(false)
                setCommandRegistries(extra, consoleEngine, builtins, groovy)
                addCompleter(scriptEngine.scriptCompleter)
                setScriptDescription(scriptEngine::scriptDescription)
                renameLocal 'exit', '/exit'
                renameLocal 'help', '/help'
                invoke '/alias', '/x', '/exit'
                invoke '/alias', '/q', '/exit'
                invoke '/alias', '/h', '/help'
                setConsoleOption "ignoreUnknownPipes", true
            }

            def highlighter = new SystemHighlighter(commandHighlighter, argsHighlighter, groovyHighlighter).tap {
                if (!OSUtils.IS_WINDOWS) {
                    setSpecificHighlighter("/!", SyntaxHighlighter.build(jnanorc, "SH-REPL"))
                }
                addFileHighlight('/nano', '/less', '/slurp', '/load', '/save', '/img', *GROOVY_POSIX_CMDS, '/cd')
                addFileHighlight('/classloader', null, ['-a', '--add'])
                addExternalHighlighterRefresh(printer::refresh)
                addExternalHighlighterRefresh(scriptEngine::refresh)
            }

            reader.highlighter = highlighter
            reader.completer = systemRegistry.completer()

            // widgets and console initialization
            new TailTipWidgets(reader, systemRegistry::commandDescription, 5, TipType.COMPLETER)
            new AutosuggestionWidgets(reader)
            KeyMap<Binding> keyMap = reader.keyMaps.get("main")
            keyMap.bind(new Reference(Widgets.TAILTIP_TOGGLE), KeyMap.alt("s"))
            keyMap.bind(new Reference(Widgets.AUTOSUGGEST_TOGGLE), KeyMap.alt("v"))
            def init = configPath.getUserConfig('groovysh_init.groovy')
            if (init) {
                systemRegistry.initialize(init.toFile())
            }

            if (options.q) {
                println render(messages.format('cli.info.version', GroovySystem.version))
            } else {
                println render(messages.format('startup_banner.0', GroovySystem.version, System.properties['java.version'], terminal.type))
                println render(messages['startup_banner.1'])
                println render(messages['startup_banner.2'])
            }
            println '-' * (terminal.columns - 1)
// for debugging
//            def index = 0
//            def lines = ['/slurp /Users/paulk/Projects/groovy/subprojects/groovy-json/src/test/resources/groovy9802.json',
//                         'println _',
//                         'x = /slurp /Users/paulk/Projects/groovy/subprojects/groovy-json/src/test/resources/groovy9802.json',
//                        'println x',
//                         '/q']
            // REPL-loop
            while (true) {
                try {
                    systemRegistry.cleanUp() // delete temporary variables and reset output streams
                    String line
                    if (evaluate) {
                        line = evaluate
                        evaluate = null
                    } else {
                        // for debugging
//                        line = lines[index++]
                        line = reader.readLine("groovy> ")
                    }
                    line = line.readLines().collect{ s ->
                        // remove Groovy continuation character for repl not Groovy's sake
                        s.endsWith(' \\') ? s[0..-3] : s
                    }.join('\n').replaceAll($/(/[a-z]+\s+)(https?://\S+)/$, '$1"$2"') // quote URLs after commands
                    if (line.startsWith(':')) {
                        // some simple legacy support for ':' commands
                        def maybeCmd = line.split()[0].replaceFirst(':', '/')
                        if (systemRegistry.hasCommand(maybeCmd) || systemRegistry.isCommandAlias(maybeCmd)) {
                            line = line.replaceFirst(':', '/')
                        }
                    }
                    Object result = systemRegistry.execute(line)
                    consoleEngine.println(result?.toString())
//                    consoleEngine.println([(Printer.OBJECT_TO_STRING): [(Object) : {  o -> o.toString() }] ], result)
                } catch (UserInterruptException e) {
                    // Ignore
                } catch (EndOfFileException e) {
                    String pl = e.getPartialLine()
                    if (pl != null) { // execute last line from redirected file (required for Windows)
                        try {
                            consoleEngine.println(systemRegistry.execute(pl))
                        } catch (Exception e2) {
                            systemRegistry.trace(e2)
                        }
                    }
                    break
                } catch (Exception | Error e) {
                    systemRegistry.trace(e) // print exception and save it to console variable
                }
            }
            systemRegistry.close() // persist pipeline completer names etc

            boolean groovyRunning = Thread.getAllStackTraces().keySet().any { it.name.startsWith("AWT-Shut") }
            if (groovyRunning) {
                consoleEngine.println("Please, close Groovy Consoles/Object Browsers!")
            }
        } catch (Throwable t) {
            t.printStackTrace()
            return 1
        }
        return 0
    }

    /**
     * Programmatic entry point for embedding groovysh.
     *
     * @param args CLI-like arguments (same as {@link #main(String[])}).
     * @return process exit code (0 for success)
     *
     * @since 6.0.0
     */
    static int start(String[] args) {
        start([:], args)
    }

    /**
     * Launches groovysh as a standalone JVM process.
     *
     * @param args command-line arguments
     */
    static void main(String[] args) {
        System.exit(start(args))
    }
}
