/*
 * Copyright (c) 2017, 2021, Oracle and/or its affiliates.
 * Copyright (c) 2013, Regents of the University of California
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this list of
 * conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
 * conditions and the following disclaimer in the documentation and/or other materials provided
 * with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
 * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.oracle.graal.python.shell;

import java.io.EOFException;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.management.ManagementFactory;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;

import org.graalvm.launcher.AbstractLanguageLauncher;
import org.graalvm.nativeimage.ImageInfo;
import org.graalvm.nativeimage.ProcessProperties;
import org.graalvm.options.OptionCategory;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Context.Builder;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.PolyglotException.StackFrame;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.SourceSection;
import org.graalvm.polyglot.Value;
import org.graalvm.shadowed.org.jline.reader.UserInterruptException;

public class GraalPythonMain extends AbstractLanguageLauncher {

    public static final String SHORT_HELP = "usage: python [option] ... [-c cmd | -m mod | file | -] [arg] ...\n" +
                    "Try `python -h' for more information.";

    public static void main(String[] args) {
        GraalPythonMain.setStartupTime();
        new GraalPythonMain().launch(args);
    }

    private static final String LANGUAGE_ID = "python";

    // provided by GraalVM bash launchers, ignored in native image mode
    protected static final String BASH_LAUNCHER_EXEC_NAME = System.getProperty("org.graalvm.launcher.executablename");

    private static long startupWallClockTime = -1;
    private static long startupNanoTime = -1;

    private ArrayList<String> programArgs = null;
    private String commandString = null;
    private String inputFile = null;
    private boolean isolateFlag = false;
    private boolean ignoreEnv = false;
    private boolean inspectFlag = false;
    private boolean verboseFlag = false;
    private boolean quietFlag = false;
    private boolean noUserSite = false;
    private boolean noSite = false;
    private final boolean stdinIsInteractive = System.console() != null;
    private boolean unbufferedIO = false;
    private boolean multiContext = false;
    private boolean snaptshotStartup = false;
    private VersionAction versionAction = VersionAction.None;
    private List<String> givenArguments;
    private List<String> relaunchArgs;
    private boolean wantsExperimental = false;
    private Map<String, String> enginePolyglotOptions;
    private boolean dontWriteBytecode = false;
    private String warnOptions = null;
    private String checkHashPycsMode = "default";

    protected static void setStartupTime() {
        if (GraalPythonMain.startupNanoTime == -1) {
            GraalPythonMain.startupNanoTime = System.nanoTime();
        }
        if (GraalPythonMain.startupWallClockTime == -1) {
            GraalPythonMain.startupWallClockTime = System.currentTimeMillis();
        }
    }

    @Override
    protected List<String> preprocessArguments(List<String> givenArgs, Map<String, String> polyglotOptions) {
        ArrayList<String> unrecognized = new ArrayList<>();
        List<String> defaultEnvironmentArgs = getDefaultEnvironmentArgs();
        ArrayList<String> inputArgs = new ArrayList<>(defaultEnvironmentArgs);
        inputArgs.addAll(givenArgs);
        givenArguments = new ArrayList<>(inputArgs);
        List<String> arguments = new ArrayList<>(inputArgs);
        List<String> subprocessArgs = new ArrayList<>();
        programArgs = new ArrayList<>();
        boolean posixBackendSpecified = false;
        for (Iterator<String> argumentIterator = arguments.iterator(); argumentIterator.hasNext();) {
            String arg = argumentIterator.next();
            if (arg.startsWith("-")) {
                if (arg.length() == 1) {
                    // Lone dash should just be skipped
                    continue;
                }
                /*
                 * Our internal options with single-dash `-long-option` format should be tried first
                 * to resolve ambiguity with short options taking arguments
                 */
                if (wantsExperimental) {
                    switch (arg) {
                        case "-debug-java":
                            if (!isAOT()) {
                                subprocessArgs.add("agentlib:jdwp=transport=dt_socket,server=y,address=8000,suspend=y");
                                inputArgs.remove("-debug-java");
                            }
                            continue;
                        case "-debug-perf":
                            unrecognized.add("--engine.TraceCompilation");
                            unrecognized.add("--engine.TraceCompilationDetails");
                            unrecognized.add("--engine.TraceInlining");
                            unrecognized.add("--engine.TraceSplitting");
                            unrecognized.add("--engine.TraceCompilationPolymorphism");
                            unrecognized.add("--engine.TraceAssumptions");
                            unrecognized.add("--engine.TraceTransferToInterpreter");
                            unrecognized.add("--engine.TracePerformanceWarnings=all");
                            unrecognized.add("--engine.CompilationFailureAction=Print");
                            inputArgs.remove("-debug-perf");
                            continue;
                        case "-multi-context":
                            multiContext = true;
                            continue;
                        case "-dump":
                            subprocessArgs.add("Dgraal.Dump=");
                            inputArgs.add("--engine.BackgroundCompilation=false");
                            inputArgs.remove("-dump");
                            continue;
                        case "-snapshot-startup":
                            snaptshotStartup = true;
                            continue;
                    }
                }
                if (arg.startsWith("--")) {
                    // Long options
                    switch (arg) {
                        // --help gets passed through as unrecognized
                        case "--version":
                            versionAction = VersionAction.PrintAndExit;
                            continue;
                        case "--show-version":
                            versionAction = VersionAction.PrintAndContinue;
                            continue;
                        case "--experimental-options":
                        case "--experimental-options=true":
                            /*
                             * This is the default Truffle experimental option flag. We also use it
                             * for our custom launcher options
                             */
                            wantsExperimental = true;
                            addRelaunchArg(arg);
                            unrecognized.add(arg);
                            continue;
                        case "--check-hash-based-pycs":
                            if (!argumentIterator.hasNext()) {
                                throw abort("Argument expected for the --check-hash-based-pycs option\n" + SHORT_HELP, 2);
                            }
                            checkHashPycsMode = argumentIterator.next();
                            continue;
                        default:
                            if (arg.startsWith("--llvm.") ||
                                            matchesPythonOption(arg, "CoreHome") ||
                                            matchesPythonOption(arg, "StdLibHome") ||
                                            matchesPythonOption(arg, "CAPI") ||
                                            matchesPythonOption(arg, "PosixModuleBackend")) {
                                addRelaunchArg(arg);
                            }
                            if (matchesPythonOption(arg, "PosixModuleBackend")) {
                                posixBackendSpecified = true;
                            }
                            // possibly a polyglot argument
                            unrecognized.add(arg);
                            continue;
                    }
                }
                // Short options
                /*
                 * Multiple options can be clustered together (`-vE`). They can also be repeated
                 * (`-OO`). And some of them can take a parameter that may be in the next argument
                 * (`-m some_module`) or follow immediately (`-msome_module`).
                 */
                String remainder = arg.substring(1);
                shortOptionLoop: while (!remainder.isEmpty()) {
                    char option = remainder.charAt(0);
                    remainder = remainder.substring(1);
                    switch (option) {
                        case 'b':
                            // TODO implement
                            /*
                             * Issue warnings about str(bytes_instance), str(bytearray_instance) and
                             * comparing bytes/bytearray with str. (-bb: issue errors)
                             */
                            break;
                        case 'B':
                            dontWriteBytecode = true;
                            break;
                        case 'c':
                            programArgs.add("-c");
                            commandString = getShortOptionParameter(argumentIterator, remainder, 'c');
                            break shortOptionLoop;
                        case 'd':
                            // TODO implement
                            /* Turn on parser debugging output */
                            break;
                        case 'E':
                            ignoreEnv = true;
                            break;
                        case '?':
                        case 'h':
                            unrecognized.add("--help");
                            break;
                        case 'i':
                            inspectFlag = true;
                            break;
                        case 'I':
                            noUserSite = true;
                            ignoreEnv = true;
                            isolateFlag = true;
                            break;
                        case 'm':
                            programArgs.add("-m");
                            String module = getShortOptionParameter(argumentIterator, remainder, 'm');
                            commandString = "import runpy; runpy._run_module_as_main('" + module + "')";
                            break shortOptionLoop;
                        case 'O':
                            // TODO implement
                            /*
                             * Remove assert statements and any code conditional on the value of
                             * __debug__; augment the filename for compiled (bytecode) files by
                             * adding .opt-1 before the .pyc extension.
                             */
                            break;
                        case 'R':
                            // TODO implement
                            break;
                        case 'q':
                            quietFlag = true;
                            break;
                        case 's':
                            noUserSite = true;
                            break;
                        case 'S':
                            noSite = true;
                            break;
                        case 't':
                            // Ignored even in CPython, for backwards compatibility
                            break;
                        case 'W':
                            if (warnOptions == null) {
                                warnOptions = "";
                            } else {
                                warnOptions += ",";
                            }
                            warnOptions += getShortOptionParameter(argumentIterator, remainder, 'W');
                            break shortOptionLoop;
                        case 'u':
                            unbufferedIO = true;
                            break;
                        case 'v':
                            verboseFlag = true;
                            break;
                        case 'V':
                            versionAction = VersionAction.PrintAndExit;
                            break;
                        case 'X':
                            // CPython ignores unknown/unsupported -X options, so we can do that too
                            getShortOptionParameter(argumentIterator, remainder, 'X');
                            break shortOptionLoop;
                        default:
                            throw abort(String.format("Unknown option -%c\n", option) + SHORT_HELP, 2);
                    }
                }
            } else {
                // Not an option, has to be a file name
                inputFile = arg;
                programArgs.add(arg);
            }

            if (inputFile != null || commandString != null) {
                while (argumentIterator.hasNext()) {
                    programArgs.add(argumentIterator.next());
                }
                break;
            }
        }

        // According to CPython if no arguments are given, they contain an empty string.
        if (programArgs.isEmpty()) {
            programArgs.add("");
        }

        if (!subprocessArgs.isEmpty()) {
            subExec(inputArgs, subprocessArgs);
        }
        if (!posixBackendSpecified) {
            polyglotOptions.put("python.PosixModuleBackend", "native");
        }
        // Never emit warnings that mess up the output
        unrecognized.add("--engine.WarnInterpreterOnly=false");
        return unrecognized;
    }

    private String getShortOptionParameter(Iterator<String> argumentIterator, String remainder, char option) {
        if (remainder.isEmpty()) {
            if (!argumentIterator.hasNext()) {
                throw abort(String.format("Argument expected for the -%c option\n", option) + SHORT_HELP, 2);
            }
            return argumentIterator.next();
        } else {
            return remainder;
        }
    }

    @Override
    protected AbortException abortUnrecognizedArgument(String argument) {
        throw abort(String.format("Unknown option %s\n", argument) + SHORT_HELP, 2);
    }

    @Override
    protected void validateArguments(Map<String, String> polyglotOptions) {
        if (multiContext) {
            // Hack to pass polyglot options to the shared engine, not to the context which would
            // refuse them
            this.enginePolyglotOptions = new HashMap<>(polyglotOptions);
            polyglotOptions.clear();
        }
    }

    private void addRelaunchArg(String arg) {
        if (relaunchArgs == null) {
            relaunchArgs = new ArrayList<>();
        }
        relaunchArgs.add(arg);
    }

    private String[] execListWithRelaunchArgs(String executableName) {
        if (relaunchArgs == null) {
            return new String[]{executableName};
        } else {
            ArrayList<String> execList = new ArrayList<>(relaunchArgs.size() + 1);
            execList.add(executableName);
            execList.addAll(relaunchArgs);
            return execList.toArray(new String[execList.size()]);
        }
    }

    private static void print(String string) {
        System.err.println(string);
    }

    private static String getLauncherExecName() {
        if (ImageInfo.inImageCode()) {
            String binPathName = null;
            if (ProcessProperties.getArgumentVectorBlockSize() > 0) {
                binPathName = calculateProgramFullPath(ProcessProperties.getArgumentVectorProgramName());
            }
            if (binPathName != null) {
                return binPathName;
            }
            return ProcessProperties.getExecutableName();
        }
        return GraalPythonMain.BASH_LAUNCHER_EXEC_NAME;
    }

    /**
     * Follows the same semantics as CPython's {@code getpath.c:calculate_program_full_path} to
     * determine the full program path if we just got a non-absolute program name. This method
     * handles the following cases:
     * <dl>
     * <dt><b>Program name is an absolute path</b></dt>
     * <dd>Just return {@code program}.</dd>
     * <dt><b>Program name is a relative path</b></dt>
     * <dd>it will resolve it to an absolute path. E.g. {@code "./python3"} will become {@code
     * "<current_working_dir>/python3"}/dd>
     * <dt><b>Program name is neither an absolute nor a relative path</b></dt>
     * <dd>It will resolve the program name wrt. to the {@code PATH} env variable. Since it may be
     * that the {@code PATH} variable is not available, this method will return {@code null}</dd>
     * </dl>
     *
     * @param program The program name as passed in the process' argument vector (position 0).
     * @return The absolute path to the program or {@code null}.
     */
    private static String calculateProgramFullPath(String program) {
        Path programPath = Paths.get(program);

        // If this is an absolute path, we are already fine.
        if (programPath.isAbsolute()) {
            return program;
        }

        /*
         * If there is no slash in the arg[0] path, then we have to assume python is on the user's
         * $PATH, since there's no other way to find a directory to start the search from. If $PATH
         * isn't exported, you lose.
         */
        if (programPath.getNameCount() < 2) {
            // Resolve the program name with respect to the PATH variable.
            String path = System.getenv("PATH");
            if (path != null) {
                int last = 0;
                for (int i = path.indexOf(File.pathSeparatorChar); i != -1; i = path.indexOf(File.pathSeparatorChar, last)) {
                    Path resolvedProgramName = Paths.get(path.substring(last, i)).resolve(programPath);
                    if (Files.isExecutable(resolvedProgramName)) {
                        return resolvedProgramName.toString();
                    }

                    // next start is the char after the separator because we have "path0:path1" and
                    // 'i' points to ':'
                    last = i + 1;
                }
            }
            return null;
        }
        // It's seemingly a relative path, so we can just resolve it to an absolute one.
        assert !programPath.isAbsolute();
        /*
         * Another special case (see: CPython function "getpath.c:copy_absolute"): If the program
         * name starts with "./" (on Unix; or similar on other systems) then the path is
         * canonicalized.
         */
        if (".".equals(programPath.getName(0).toString())) {
            return programPath.toAbsolutePath().normalize().toString();
        }
        return programPath.toAbsolutePath().toString();
    }

    private String[] getExecutableList() {
        String launcherExecName = getLauncherExecName();
        if (launcherExecName != null) {
            return execListWithRelaunchArgs(launcherExecName);
        }

        // This should only be reached if this main is directly executed via Java.
        if (!ImageInfo.inImageCode()) {
            StringBuilder sb = new StringBuilder();
            ArrayList<String> exec_list = new ArrayList<>();
            sb.append(System.getProperty("java.home")).append(File.separator).append("bin").append(File.separator).append("java");
            exec_list.add(sb.toString());
            String javaOptions = System.getenv("_JAVA_OPTIONS");
            String javaToolOptions = System.getenv("JAVA_TOOL_OPTIONS");
            for (String arg : ManagementFactory.getRuntimeMXBean().getInputArguments()) {
                if (arg.matches("(-Xrunjdwp:|-agentlib:jdwp=).*suspend=y.*")) {
                    arg = arg.replace("suspend=y", "suspend=n");
                }
                if ((javaOptions != null && javaOptions.contains(arg)) || (javaToolOptions != null && javaToolOptions.contains(arg))) {
                    // both _JAVA_OPTIONS and JAVA_TOOL_OPTIONS are adeed during
                    // JVM startup automatically. We do not want to repeat these
                    // for subprocesses, because they should also pick up those
                    // variables.
                    continue;
                }
                exec_list.add(arg);
            }
            exec_list.add("-classpath");
            exec_list.add(System.getProperty("java.class.path"));
            exec_list.add(GraalPythonMain.class.getName());
            if (relaunchArgs != null) {
                exec_list.addAll(relaunchArgs);
            }
            return exec_list.toArray(new String[exec_list.size()]);
        }

        return new String[]{""};
    }

    private String getExecutable() {
        if (ImageInfo.inImageBuildtimeCode()) {
            return "";
        } else {
            String launcherExecName = getLauncherExecName();
            if (launcherExecName != null) {
                return launcherExecName;
            }
            String[] executableList = getExecutableList();
            for (int i = 0; i < executableList.length; i++) {
                if (executableList[i].matches("\\s")) {
                    executableList[i] = "'" + executableList[i].replace("'", "\\'") + "'";
                }
            }
            return String.join(" ", executableList);
        }
    }

    @Override
    protected void launch(Builder contextBuilder) {
        // prevent the use of System.out/err - they are PrintStreams which suppresses exceptions
        contextBuilder.out(new FileOutputStream(FileDescriptor.out));
        contextBuilder.err(new FileOutputStream(FileDescriptor.err));
        if (!ignoreEnv) {
            String pythonpath = System.getenv("PYTHONPATH");
            if (pythonpath != null) {
                contextBuilder.option("python.PythonPath", pythonpath);
            }
            inspectFlag = inspectFlag || System.getenv("PYTHONINSPECT") != null;
            noUserSite = noUserSite || System.getenv("PYTHONNOUSERSITE") != null;
            verboseFlag = verboseFlag || System.getenv("PYTHONVERBOSE") != null;
            unbufferedIO = unbufferedIO || System.getenv("PYTHONUNBUFFERED") != null;
            dontWriteBytecode = dontWriteBytecode || System.getenv("PYTHONDONTWRITEBYTECODE") != null;

            String hashSeed = System.getenv("PYTHONHASHSEED");
            if (hashSeed != null) {
                contextBuilder.option("python.HashSeed", hashSeed);
            }

            String envWarnOptions = System.getenv("PYTHONWARNINGS");
            if (envWarnOptions != null && !envWarnOptions.isEmpty()) {
                if (warnOptions == null) {
                    warnOptions = envWarnOptions;
                } else {
                    warnOptions = envWarnOptions + "," + warnOptions;
                }
            }
            String cachePrefix = System.getenv("PYTHONPYCACHEPREFIX");
            if (cachePrefix != null) {
                contextBuilder.option("python.PyCachePrefix", cachePrefix);
            }

            String encoding = System.getenv("PYTHONIOENCODING");
            if (encoding != null) {
                contextBuilder.option("python.StandardStreamEncoding", encoding);
            }
        }
        if (warnOptions == null || warnOptions.isEmpty()) {
            warnOptions = "";
        }
        String executable = getContextOptionIfSetViaCommandLine("Executable");
        if (executable != null) {
            contextBuilder.option("python.ExecutableList", executable);
        } else {
            contextBuilder.option("python.Executable", getExecutable());
            // The unlikely separator is used because options need to be
            // strings. See PythonOptions.getExecutableList()
            contextBuilder.option("python.ExecutableList", String.join("🏆", getExecutableList()));
        }

        // setting this to make sure our TopLevelExceptionHandler calls the excepthook
        // to print Python exceptions
        contextBuilder.option("python.AlwaysRunExcepthook", "true");
        contextBuilder.option("python.InspectFlag", Boolean.toString(inspectFlag));
        contextBuilder.option("python.VerboseFlag", Boolean.toString(verboseFlag));
        contextBuilder.option("python.IsolateFlag", Boolean.toString(isolateFlag));
        contextBuilder.option("python.WarnOptions", warnOptions);
        contextBuilder.option("python.DontWriteBytecodeFlag", Boolean.toString(dontWriteBytecode));
        if (verboseFlag) {
            contextBuilder.option("log.python.level", "FINE");
        }
        contextBuilder.option("python.QuietFlag", Boolean.toString(quietFlag));
        contextBuilder.option("python.NoUserSiteFlag", Boolean.toString(noUserSite));
        contextBuilder.option("python.NoSiteFlag", Boolean.toString(noSite));
        if (!noSite) {
            contextBuilder.option("python.ForceImportSite", "true");
        }
        contextBuilder.option("python.IgnoreEnvironmentFlag", Boolean.toString(ignoreEnv));
        contextBuilder.option("python.UnbufferedIO", Boolean.toString(unbufferedIO));

        ConsoleHandler consoleHandler = createConsoleHandler(System.in, System.out);
        contextBuilder.arguments(getLanguageId(), programArgs.toArray(new String[programArgs.size()]));
        contextBuilder.in(consoleHandler.createInputStream());
        contextBuilder.option("python.TerminalIsInteractive", Boolean.toString(stdinIsInteractive));
        contextBuilder.option("python.TerminalWidth", Integer.toString(consoleHandler.getTerminalWidth()));
        contextBuilder.option("python.TerminalHeight", Integer.toString(consoleHandler.getTerminalHeight()));

        contextBuilder.option("python.CheckHashPycsMode", checkHashPycsMode);
        contextBuilder.option("python.RunViaLauncher", "true");
        if (inputFile != null) {
            contextBuilder.option("python.InputFilePath", inputFile);
        }

        if (multiContext) {
            contextBuilder.engine(Engine.newBuilder().allowExperimentalOptions(true).options(enginePolyglotOptions).build());
        }

        int rc = 1;
        try (Context context = contextBuilder.build()) {
            runVersionAction(versionAction, context.getEngine());

            if (snaptshotStartup) {
                evalInternal(context, "__graalpython__.startup_wall_clock_ts = " + startupWallClockTime + "; __graalpython__.startup_nano = " + startupNanoTime);
            }

            if (!quietFlag && (verboseFlag || (commandString == null && inputFile == null && stdinIsInteractive))) {
                print("Python " + evalInternal(context, "import sys; sys.version + ' on ' + sys.platform").asString());
                if (!noSite) {
                    print("Type \"help\", \"copyright\", \"credits\" or \"license\" for more information.");
                }
            }
            consoleHandler.setContext(context);

            if (commandString != null || inputFile != null) {
                try {
                    evalNonInteractive(context, consoleHandler);
                    rc = 0;
                } catch (PolyglotException e) {
                    if (!e.isExit()) {
                        printPythonLikeStackTrace(e);
                    } else {
                        rc = e.getExitStatus();
                    }
                } catch (NoSuchFileException e) {
                    printFileNotFoundException(e);
                }
            }
            if ((commandString == null && inputFile == null) || inspectFlag) {
                inspectFlag = false;
                rc = readEvalPrint(context, consoleHandler);
            }
        } catch (IOException e) {
            rc = 1;
            e.printStackTrace();
        } finally {
            consoleHandler.setContext(null);
        }
        System.exit(rc);
    }

    private static boolean matchesPythonOption(String arg, String key) {
        assert !key.startsWith("python.");
        return arg.startsWith("--python." + key) || arg.startsWith("--" + key);
    }

    private String getContextOptionIfSetViaCommandLine(String key) {
        if (System.getProperty("polyglot.python." + key) != null) {
            return System.getProperty("polyglot.python." + key);
        }
        for (String f : givenArguments) {
            if (matchesPythonOption(f, key)) {
                String[] splits = f.split("=", 2);
                if (splits.length > 1) {
                    return splits[1];
                } else {
                    return "true";
                }
            }
        }
        return null;
    }

    private static void printFileNotFoundException(NoSuchFileException e) {
        String reason = e.getReason();
        if (reason == null) {
            reason = "No such file or directory";
        }
        System.err.println(GraalPythonMain.class.getCanonicalName() + ": can't open file '" + e.getFile() + "': " + reason);
    }

    private static void printPythonLikeStackTrace(PolyglotException e) {
        // If we're running through the launcher and an exception escapes to here,
        // we didn't go through the Python code to print it. That may be because
        // it's an exception from another language. In this case, we still would
        // like to print it like a Python exception.
        ArrayList<String> stack = new ArrayList<>();
        for (StackFrame frame : e.getPolyglotStackTrace()) {
            if (frame.isGuestFrame()) {
                StringBuilder sb = new StringBuilder();
                SourceSection sourceSection = frame.getSourceLocation();
                String rootName = frame.getRootName();
                if (sourceSection != null) {
                    sb.append("  ");
                    String path = sourceSection.getSource().getPath();
                    if (path != null) {
                        sb.append("File ");
                    }
                    sb.append('"');
                    sb.append(sourceSection.getSource().getName());
                    sb.append("\", line ");
                    sb.append(sourceSection.getStartLine());
                    sb.append(", in ");
                    sb.append(rootName);
                    stack.add(sb.toString());
                }
            }
        }
        System.err.println("Traceback (most recent call last):");
        ListIterator<String> listIterator = stack.listIterator(stack.size());
        while (listIterator.hasPrevious()) {
            System.err.println(listIterator.previous());
        }
        System.err.println(e.getMessage());
    }

    private void evalNonInteractive(Context context, ConsoleHandler consoleHandler) throws IOException {
        // We need to setup the terminal even when not running the REPL because code may request
        // input from the terminal.
        setupTerminal(consoleHandler);

        Source src;
        if (commandString != null) {
            src = Source.newBuilder(getLanguageId(), commandString, "<string>").build();
        } else {
            assert inputFile != null;
            // the path is passed through a context option
            src = Source.newBuilder(getLanguageId(), "__graalpython__.run_path()", "<internal>").internal(true).build();
        }
        context.eval(src);
    }

    @Override
    protected String getLanguageId() {
        return LANGUAGE_ID;
    }

    @Override
    protected void printHelp(OptionCategory maxCategory) {
        System.out.println("usage: python [option] ... (-c cmd | file) [arg] ...\n" +
                        "Options and arguments (and corresponding environment variables):\n" +
                        "-B     : this disables writing .py[co] files on import\n" +
                        "-c cmd : program passed in as string (terminates option list)\n" +
                        // "-d : debug output from parser; also PYTHONDEBUG=x\n" +
                        "-E     : ignore PYTHON* environment variables (such as PYTHONPATH)\n" +
                        "-h     : print this help message and exit (also --help)\n" +
                        "-i     : inspect interactively after running script; forces a prompt even\n" +
                        "         if stdin does not appear to be a terminal; also PYTHONINSPECT=x\n" +
                        "-m mod : run library module as a script (terminates option list)\n" +
                        "-O     : on CPython, this optimizes generated bytecode slightly;\n" +
                        "         GraalPython does not use bytecode, and thus this flag has no effect\n" +
                        "-OO    : remove doc-strings in addition to the -O optimizations;\n" +
                        "         GraalPython does not use bytecode, and thus this flag has no effect\n" +
                        "-R     : on CPython, this enables the use of a pseudo-random salt to make\n" +
                        "         hash()values of various types be unpredictable between separate\n" +
                        "         invocations of the interpreter, as a defense against denial-of-service\n" +
                        "         attacks; GraalPython always enables this and the flag has no effect.\n" +
                        // "-Q arg : division options: -Qold (default), -Qwarn, -Qwarnall, -Qnew\n"
                        // +
                        "-q     : don't print version and copyright messages on interactive startup\n" +
                        "-I     : don't add user site and script directory to sys.path; also PYTHONNOUSERSITE\n" +
                        "-s     : don't add user site directory to sys.path; also PYTHONNOUSERSITE\n" +
                        "-S     : don't imply 'import site' on initialization\n" +
                        // "-t : issue warnings about inconsistent tab usage (-tt: issue errors)\n"
                        // +
                        "-u     : unbuffered binary stdout and stderr; also PYTHONUNBUFFERED=x\n" +
                        "-v     : verbose (trace import statements); also PYTHONVERBOSE=x\n" +
                        "         can be supplied multiple times to increase verbosity\n" +
                        "-V     : print the Python version number and exit (also --version)\n" +
                        "         when given twice, print more information about the build\n" +
                        "-X opt : CPython implementation-specific options. Ignored on GraalPython\n" +
                        "-W arg : warning control; arg is action:message:category:module:lineno\n" +
                        "         also PYTHONWARNINGS=arg\n" +
                        // "-x : skip first line of source, allowing use of non-Unix forms of
                        // #!cmd\n" +
                        // "-3 : warn about Python 3.x incompatibilities that 2to3 cannot trivially
                        // fix\n" +
                        "file   : program read from script file\n" +
                        "-      : program read from stdin\n" +
                        "arg ...: arguments passed to program in sys.argv[1:]\n" +
                        "\n" +
                        "Other environment variables:\n" +
                        "PYTHONSTARTUP: file executed on interactive startup (no default)\n" +
                        "PYTHONPATH   : ':'-separated list of directories prefixed to the\n" +
                        "               default module search path.  The result is sys.path.\n" +
                        "PYTHONHOME   : alternate <prefix> directory (or <prefix>:<exec_prefix>).\n" +
                        "               The default module search path uses <prefix>/pythonX.X.\n" +
                        "PYTHONCASEOK : ignore case in 'import' statements (Windows).\n" +
                        "PYTHONIOENCODING: Encoding[:errors] used for stdin/stdout/stderr.\n" +
                        "PYTHONHASHSEED: if this variable is set to 'random', the effect is the same\n" +
                        "   as specifying the -R option: a random value is used to seed the hashes of\n" +
                        "   str, bytes and datetime objects.  It can also be set to an integer\n" +
                        "   in the range [0,4294967295] to get hash values with a predictable seed.\n" +
                        "PYTHONPYCACHEPREFIX: if this is set, GraalPython will write .pyc files in a mirror\n" +
                        "   directory tree at this path, instead of in __pycache__ directories within the source tree.\n" +
                        "GRAAL_PYTHON_ARGS: the value is added as arguments as if passed on the\n" +
                        "   commandline. There are two special cases: any `$$' in the value is replaced\n" +
                        "   with the current process id, and any $UUID$ is replaced with random unique string\n" +
                        "   that may contain letters, digits, and '-'. To pass a literal `$$', you must escape the\n" +
                        "   second `$' like so: `$\\$'\n" +
                        (wantsExperimental ? "\nArguments specific to the Graal Python launcher:\n" +
                                        "--show-version : print the Python version number and continue.\n" +
                                        "-CC            : run the C compiler used for generating GraalPython C extensions.\n" +
                                        "                 All following arguments are passed to the compiler.\n" +
                                        "-LD            : run the linker used for generating GraalPython C extensions.\n" +
                                        "                 All following arguments are passed to the linker.\n" +
                                        "\nEnvironment variables specific to the Graal Python launcher:\n" +
                                        "SULONG_LIBRARY_PATH: Specifies the library path for Sulong.\n" +
                                        "   This is required when starting subprocesses of python.\n" : ""));
    }

    @Override
    protected String[] getDefaultLanguages() {
        return new String[]{getLanguageId(), "llvm", "regex"};
    }

    @Override
    protected void collectArguments(Set<String> options) {
        // This list of arguments is used when we are launched through the Polyglot
        // launcher
        options.add("-c");
        options.add("-h");
        options.add("-V");
        options.add("--version");
        options.add("--show-version");
    }

    private ConsoleHandler createConsoleHandler(InputStream inStream, OutputStream outStream) {
        if (!stdinIsInteractive) {
            return new DefaultConsoleHandler(inStream);
        } else {
            return new JLineConsoleHandler(inStream, outStream, false);
        }
    }

    /**
     * The read-eval-print loop, which can take input from a console, command line expression or a
     * file. There are two ways the repl can terminate:
     * <ol>
     * <li>A {@code quit} command is executed successfully.</li>
     * <li>EOF on the input.</li>
     * </ol>
     * In case 2, we must implicitly execute a {@code quit("default, 0L, TRUE} command before
     * exiting. So,in either case, we never return.
     */
    public int readEvalPrint(Context context, ConsoleHandler consoleHandler) {
        int lastStatus = 0;
        try {
            setupREPL(context, consoleHandler);
            Value sys = evalInternal(context, "import sys; sys");

            while (true) { // processing inputs
                boolean doEcho = doEcho(context);
                consoleHandler.setPrompt(doEcho ? sys.getMember("ps1").asString() : null);

                try {
                    String input = consoleHandler.readLine();
                    if (input == null) {
                        throw new EOFException();
                    }
                    if (canSkipFromEval(input)) {
                        // nothing to parse
                        continue;
                    }

                    String continuePrompt = null;
                    StringBuilder sb = new StringBuilder(input).append('\n');
                    while (true) { // processing subsequent lines while input is incomplete
                        lastStatus = 0;
                        try {
                            context.eval(Source.newBuilder(getLanguageId(), sb.toString(), "<stdin>").interactive(true).buildLiteral());
                        } catch (PolyglotException e) {
                            if (continuePrompt == null) {
                                continuePrompt = doEcho ? sys.getMember("ps2").asString() : null;
                            }
                            if (e.isIncompleteSource()) {
                                // read more input until we get an empty line
                                consoleHandler.setPrompt(continuePrompt);
                                String additionalInput;
                                boolean isIncompleteCode = false; // this for cases like empty lines
                                                                  // in tripplecode, where the
                                                                  // additional input can be empty,
                                                                  // but we should still continue
                                do {
                                    additionalInput = consoleHandler.readLine();
                                    sb.append(additionalInput).append('\n');
                                    try {
                                        // We try to parse every time, when an additional input is
                                        // added to find out if there is continuation exception or
                                        // other error. If there is other error, we have to stop
                                        // to ask for additional input.
                                        context.parse(Source.newBuilder(getLanguageId(), sb.toString(), "<stdin>").interactive(true).buildLiteral());
                                        e = null;   // the parsing was ok -> try to eval
                                                    // the code in outer while loop
                                        isIncompleteCode = false;
                                    } catch (PolyglotException pe) {
                                        if (!pe.isIncompleteSource()) {
                                            e = pe;
                                            break;
                                        } else {
                                            isIncompleteCode = true;
                                        }
                                    }
                                } while (additionalInput != null && (!additionalInput.isEmpty() || isIncompleteCode));
                                // Here we can be in these cases:
                                // The parsing of the code with additional code was ok
                                // The parsing of the code with additional code thrown an error,
                                // which is not IncompleteSourceException
                                if (additionalInput == null) {
                                    throw new EOFException();
                                }
                                if (e == null) {
                                    // the last source (with additional input) was parsed correctly,
                                    // so we can execute it.
                                    continue;
                                }
                            }
                            // process the exception from eval or from the last parsing of the input
                            // + additional source
                            if (e.isExit()) {
                                // usually from quit
                                throw new ExitException(e.getExitStatus());
                            } else if (e.isHostException()) {
                                // we continue the repl even though the system may be broken
                                lastStatus = 1;
                                System.out.println(e.getMessage());
                            } else if (e.isInternalError()) {
                                System.err.println("An internal error occurred:");
                                printPythonLikeStackTrace(e);

                                // we continue the repl even though the system may be broken
                                lastStatus = 1;
                            } else if (e.isGuestException()) {
                                // drop through to continue REPL and remember last eval was an error
                                lastStatus = 1;
                            }
                        }
                        break;
                    }
                } catch (EOFException e) {
                    if (!noSite) {
                        try {
                            evalInternal(context, "import site; exit()\n");
                        } catch (PolyglotException e2) {
                            if (e2.isExit()) {
                                // don't use the exit code from the PolyglotException
                                return lastStatus;
                            } else if (e2.isCancelled()) {
                                continue;
                            }
                            throw new RuntimeException("error while calling exit", e);
                        }
                    }
                    System.out.println();
                    return lastStatus;
                } catch (UserInterruptException e) {
                    // interrupted by ctrl-c
                }
            }
        } catch (ExitException e) {
            return e.code;
        }
    }

    private static boolean canSkipFromEval(String input) {
        String[] split = input.split("\n");
        for (String s : split) {
            if (!s.isEmpty() && !s.startsWith("#")) {
                return false;
            }
        }
        return true;
    }

    private Value evalInternal(Context context, String code) {
        return context.eval(Source.newBuilder(getLanguageId(), code, "<internal>").internal(true).buildLiteral());
    }

    private void setupREPL(Context context, ConsoleHandler consoleHandler) {
        // Then we can get the readline module and see if any completers were registered and use its
        // history feature
        evalInternal(context, "import sys\ngetattr(sys, '__interactivehook__', lambda: None)()\n");
        final Value readline = evalInternal(context, "import readline; readline");
        final Value getCompleter = readline.getMember("get_completer").execute();
        final Value shouldRecord = readline.getMember("get_auto_history");
        final Value addHistory = readline.getMember("add_history");
        final Value getHistoryItem = readline.getMember("get_history_item");
        final Value setHistoryItem = readline.getMember("replace_history_item");
        final Value deleteHistoryItem = readline.getMember("remove_history_item");
        final Value clearHistory = readline.getMember("clear_history");
        final Value getHistorySize = readline.getMember("get_current_history_length");

        Function<String, List<String>> completer = null;
        if (getCompleter.canExecute()) {
            completer = (buffer) -> {
                List<String> candidates = new ArrayList<>();
                Value candidate = getCompleter.execute(buffer, candidates.size());
                while (candidate.isString()) {
                    candidates.add(candidate.asString());
                    candidate = getCompleter.execute(buffer, candidates.size());
                }
                return candidates;
            };
        }
        consoleHandler.setupReader(
                        () -> shouldRecord.execute().asBoolean(),
                        () -> getHistorySize.execute().asInt(),
                        (item) -> addHistory.execute(item),
                        (pos) -> getHistoryItem.execute(pos).asString(),
                        (pos, item) -> setHistoryItem.execute(pos, item),
                        (pos) -> deleteHistoryItem.execute(pos),
                        () -> clearHistory.execute(),
                        completer);

    }

    private static void setupTerminal(ConsoleHandler consoleHandler) {
        consoleHandler.setupReader(() -> false, () -> 0, (item) -> {
        }, (pos) -> null, (pos, item) -> {
        }, (pos) -> {
        }, () -> {
        }, null);
    }

    /**
     * Some system properties have already been read at this point, so to change them, we just
     * re-execute the process with the additional options.
     */
    private static void subExec(List<String> args, List<String> subProcessDefs) {
        List<String> cmd = getCmdline(args, subProcessDefs);
        try {
            System.exit(new ProcessBuilder(cmd.toArray(new String[0])).inheritIO().start().waitFor());
        } catch (IOException | InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println(e.getMessage());
            System.exit(-1);
        }
    }

    static List<String> getCmdline(List<String> args, List<String> subProcessDefs) {
        List<String> cmd = new ArrayList<>();
        if (isAOT()) {
            cmd.add(ProcessProperties.getExecutableName());
            for (String subProcArg : subProcessDefs) {
                assert subProcArg.startsWith("D");
                cmd.add("--native." + subProcArg);
            }
        } else {
            cmd.add(System.getProperty("java.home") + File.separator + "bin" + File.separator + "java");
            switch (System.getProperty("java.vm.name")) {
                case "Java HotSpot(TM) 64-Bit Server VM":
                    cmd.add("-server");
                    break;
                case "Java HotSpot(TM) 64-Bit Client VM":
                    cmd.add("-client");
                    break;
                default:
                    break;
            }
            cmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
            cmd.add("-cp");
            cmd.add(ManagementFactory.getRuntimeMXBean().getClassPath());
            for (String subProcArg : subProcessDefs) {
                assert subProcArg.startsWith("D") || subProcArg.startsWith("agent");
                cmd.add("-" + subProcArg);
            }
            cmd.add(GraalPythonMain.class.getName());
        }

        cmd.addAll(args);
        return cmd;
    }

    private static final class ExitException extends RuntimeException {
        private static final long serialVersionUID = 1L;
        private final int code;

        ExitException(int code) {
            this.code = code;
        }
    }

    private static enum State {
        NORMAL,
        SINGLE_QUOTE,
        DOUBLE_QUOTE,
        ESCAPE_SINGLE_QUOTE,
        ESCAPE_DOUBLE_QUOTE,
    }

    private static List<String> getDefaultEnvironmentArgs() {
        String pid;
        if (isAOT()) {
            pid = String.valueOf(ProcessProperties.getProcessID());
        } else {
            pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
        }
        String uuid = UUID.randomUUID().toString();
        String envArgsOpt = System.getenv("GRAAL_PYTHON_ARGS");
        ArrayList<String> envArgs = new ArrayList<>();
        if (envArgsOpt != null) {
            State s = State.NORMAL;
            StringBuilder sb = new StringBuilder();
            for (char x : envArgsOpt.toCharArray()) {
                if (s == State.NORMAL && Character.isWhitespace(x)) {
                    addArgument(pid, uuid, envArgs, sb);
                } else {
                    if (x == '"') {
                        if (s == State.NORMAL) {
                            s = State.DOUBLE_QUOTE;
                        } else if (s == State.DOUBLE_QUOTE) {
                            s = State.NORMAL;
                        } else if (s == State.ESCAPE_DOUBLE_QUOTE) {
                            s = State.DOUBLE_QUOTE;
                            sb.append(x);
                        }
                    } else if (x == '\'') {
                        if (s == State.NORMAL) {
                            s = State.SINGLE_QUOTE;
                        } else if (s == State.SINGLE_QUOTE) {
                            s = State.NORMAL;
                        } else if (s == State.ESCAPE_SINGLE_QUOTE) {
                            s = State.SINGLE_QUOTE;
                            sb.append(x);
                        }
                    } else if (x == '\\') {
                        if (s == State.SINGLE_QUOTE) {
                            s = State.ESCAPE_SINGLE_QUOTE;
                        } else if (s == State.DOUBLE_QUOTE) {
                            s = State.ESCAPE_DOUBLE_QUOTE;
                        }
                    } else {
                        sb.append(x);
                    }
                }
            }
            addArgument(pid, uuid, envArgs, sb);
        }
        return envArgs;
    }

    private static void addArgument(String pid, String uuid, ArrayList<String> envArgs, StringBuilder sb) {
        if (sb.length() > 0) {
            String arg = sb.toString().replace("$UUID$", uuid).replace("$$", pid).replace("\\$", "$");
            envArgs.add(arg);
            sb.setLength(0);
        }
    }

    private static boolean doEcho(@SuppressWarnings("unused") Context context) {
        return true;
    }
}
