/*
 *  Copyright 2008 Hannes Wallnoefer <hannes@helma.at>
 *
 *  Licensed 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.ringojs.tools;

import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.WrappedException;
import org.ringojs.engine.ModuleScope;
import org.ringojs.engine.RhinoEngine;
import org.ringojs.engine.RingoConfig;
import org.ringojs.engine.RingoWrapFactory;
import org.ringojs.engine.ScriptError;
import org.ringojs.repository.FileRepository;
import org.ringojs.repository.Repository;
import org.ringojs.repository.ZipRepository;
import org.ringojs.util.StringUtils;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.RhinoException;

import static java.lang.System.err;
import static java.lang.System.out;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Formatter;
import java.io.IOException;
import java.io.File;
import java.io.PrintStream;

public class RingoRunner {

    RingoConfig config;
    RhinoEngine engine;
    Context cx;
    Scriptable module;
    int optlevel;
    String scriptName = null;
    String[] scriptArgs = new String[0];
    String expr = null;
    Path history = null;
    String charset;
    boolean runShell = false;
    boolean debug = false;
    boolean verbose = false;
    boolean silent = false;
    boolean legacyMode = false;
    boolean productionMode = false;
    List<String> bootScripts;
    final List<String> userModules = new ArrayList<>();

    static final String[][] options = {
        {"b", "bootscript", "Run additional bootstrap script", "FILE"},
        {"c", "charset", "Set character encoding for scripts (default: utf-8)", "CHARSET"},
        {"D", "java-property", "Set Java system property K to value V", "K=V"},
        {"d", "debug", "Run with debugger GUI", ""},
        {"e", "expression", "Run the given expression as script", "EXPR"},
        {"h", "help", "Display this help message", ""},
        {"H", "history", "Use custom history file (default: ~/.ringo-history)", "FILE"},
        {"i", "interactive", "Start shell after script file has run", ""},
        {"l", "legacy-mode", "Enable __parent__ and __proto__ and suppress warnings", ""},
        {"m", "modules", "Add a directory to the module search path", "DIR"},
        {"o", "optlevel", "Set Rhino optimization level (-1 to 9)", "OPT"},
        {"p", "production", "Disable module reloading and warnings", ""},
        {"s", "silent", "Disable shell prompt and echo for piped stdin/stdout", ""},
        {"V", "verbose", "Print java stack traces on errors", ""},
        {"v", "version", "Print version number and exit", ""},
    };

    public RingoRunner() {
    }

    public static void main(String[] args) {
        RingoRunner runner = new RingoRunner();
        runner.run(args);
    }

    public void parseArgs(String[] args) throws Exception {

        if (args != null && args.length > 0) {
            int i = parseOptions(args);
            if (i < args.length) {
                scriptName = args[i];
                scriptArgs = new String[args.length - i];
                System.arraycopy(args, i, scriptArgs, 0, scriptArgs.length);
            }
        }

        String ringoHome = System.getProperty("ringo.home");
        if (ringoHome == null) {
            ringoHome = System.getenv("RINGO_HOME");
        }
        if (ringoHome == null) {
            ringoHome = ".";
        }
        File file = new File(ringoHome);
        Repository home = file.isFile() && StringUtils.isZipOrJarFile(ringoHome) ?
                new ZipRepository(file) : new FileRepository(file);
        String extraPath = System.getProperty("ringo.modulepath");
        if (extraPath == null) {
            extraPath = System.getenv("RINGO_MODULE_PATH");
        }
        if (extraPath != null) {
            Collections.addAll(userModules,
                    StringUtils.split(extraPath, File.pathSeparator));
        }
        String[] systemModulePath = {"modules", "packages"};
        String[] userModulePath = userModules.toArray(new String[userModules.size()]);

        config = new RingoConfig(home, userModulePath, systemModulePath);
        config.setMainScript(scriptName);
        config.setArguments(scriptArgs);
        config.setOptLevel(optlevel);
        config.setBootstrapScripts(bootScripts);
        config.setDebug(debug);
        config.setVerbose(verbose);
        config.setParentProtoProperties(legacyMode);
        config.setStrictVars(!legacyMode && !productionMode);
        config.setReloading(!productionMode);
        if (charset != null) {
            config.setCharset(charset);
        }
        engine = new RhinoEngine(config, null);
    }

    public void run(String[] args) {
        try {
            parseArgs(args);
            if (expr != null) {
                engine.evaluateExpression(expr);
            }
            if (scriptName != null) {
                engine.runScript(config.getMainResource(), scriptArgs);
            }
            if ((scriptName == null && expr == null)  || runShell) {
                // autodetect --silent option if stdin or stdout is redirected
                if (!silent) {
                    silent = System.console() == null;
                }
                new RingoShell(engine, history, silent).run();
            } else {
                // wait for daemon or scheduler threads to terminate
                engine.waitForAsyncTasks();
            }
        } catch (Exception x) {
            List<ScriptError> errors = engine == null ? null : engine.getMainErrors();
            reportError(x, err, errors, verbose);
            System.exit(-1);
        }
    }

    public void init(String[] args) {
        try {
            parseArgs(args);
            if (scriptName == null) {
                throw new RuntimeException("daemon interface requires a script argument");
            }
            cx = engine.getContextFactory().enterContext(null);
            String moduleId = config.getMainResource().getModuleName();
            module = engine.loadModule(cx, moduleId, null);
            if (module instanceof ModuleScope) {
                module = ((ModuleScope)module).getExports();
            }
            engine.invoke(module, "init");
        } catch (NoSuchMethodException nsm) {
            // daemon life-cycle method not implemented
        } catch (Exception x) {
            reportError(x, err, engine.getMainErrors(), verbose);
            System.exit(-1);
        }
    }

    public void start() {
        if (cx != null) {
            try {
                engine.invoke(module, "start");
            } catch (NoSuchMethodException nsm) {
                // daemon life-cycle method not implemented
            } catch (Exception x) {
                reportError(x, err, engine.getMainErrors(), verbose);
                System.exit(-1);
            }
        }
    }


    public void stop() {
        if (cx != null) {
            try {
                engine.invoke(module, "stop");
            } catch (NoSuchMethodException nsm) {
                // daemon life-cycle method not implemented
            } catch (Exception x) {
                reportError(x, err, engine.getMainErrors(), verbose);
                System.exit(-1);
            }
        }
    }

    public void destroy() {
        if (cx != null) {
            try {
                engine.invoke(module, "destroy");
            } catch (NoSuchMethodException nsm) {
                // daemon life-cycle method not implemented
            } catch (Exception x) {
                reportError(x, err, engine.getMainErrors(), verbose);
                System.exit(-1);
            }
        }
    }

    private int parseOptions(String[] args) throws IOException {
        int i;
        for (i = 0; i < args.length; i++) {
            String option = args[i];
            if (!option.startsWith("-")) {
                break;
            }
            String nextArg = i < args.length - 1 ? args[i + 1] : null;
            int result;
            if (option.startsWith("--")) {
                result = parseLongOption(option.substring(2), nextArg);
            } else {
                result = parseShortOption(option.substring(1), nextArg);
            }
            if (result < 0) {
                break;
            } else {
                i += result;
            }
        }
        return i;
    }

    private int parseShortOption(String opt, String nextArg) throws IOException {
        int length = opt.length();
        for (int i = 0; i < length; i++) {
            String[] def = null;
            char c = opt.charAt(i);
            for (String[] d : options) {
                if (d[0].length() == 1 && d[0].charAt(0) == c) {
                    def = d;
                    break;
                }
            }
            if (def == null) {
                unknownOptionError("-" + c);
            }
            String optarg = null;
            int consumedNext = 0;
            if (!def[3].equals("")) {
                if (i == length - 1) {
                    if (nextArg == null) {
                        missingValueError("-" + def[0]);
                    }
                    optarg = nextArg;
                    consumedNext = 1;
                } else {
                    optarg = opt.substring(i + 1);
                    if (optarg.length() == 0) {
                        missingValueError("-" + def[0]);
                    }
                }
                i = length;
            }
            processOption(def[1], optarg);
            if (i >= length) {
                return consumedNext;
            }
        }
        return 0;
    }

    private int parseLongOption(String opt, String nextArg) throws IOException {
        String[] def = null;
        for (String[] d : options) {
            if (opt.equals(d[1]) || (opt.startsWith(d[1])
                                 && opt.charAt(d[1].length()) == '=')) {
                def = d;
                break;
            }
        }
        if (def == null) {
            unknownOptionError("--" + opt);
        }
        String optarg = null;
        int consumedNext = 0;
        if (!def[3].equals("")) {
            if (opt.equals(def[1])) {
                if (nextArg == null) {
                    missingValueError("--" + def[1]);
                }
                optarg = nextArg;
                consumedNext = 1;
            } else {
                int length = def[1].length();
                if (opt.charAt(length) != '=') {
                    missingValueError("--" + def[1]);
                }
                optarg = opt.substring(length + 1);
            }
        }
        processOption(def[1], optarg);
        return consumedNext;
    }

    private void processOption(String option, String arg) {
        if ("help".equals(option)) {
            printUsage();
            System.exit(0);
        } else if ("interactive".equals(option)) {
            runShell = true;
        } else if ("debug".equals(option)) {
            debug = true;
        } else if ("optlevel".equals(option)) {
            try {
                optlevel = Integer.parseInt(arg);
            } catch (NumberFormatException x) {
                rangeError(option);
            }
            if (optlevel < -1 || optlevel > 9) {
                rangeError(option);
            }
        } else if ("history".equals(option)) {
            history = Paths.get(arg);
        } else if ("modules".equals(option)) {
            Collections.addAll(userModules,
                    StringUtils.split(arg, File.pathSeparator));
        } else if ("java-property".equals(option)) {
            if (arg.contains("=")) {
                String[] property = arg.split("=", 2);
                System.setProperty(property[0], property[1]);
            }
        } else if ("bootscript".equals(option)) {
            if (bootScripts == null) {
                bootScripts = new ArrayList<>();
            }
            bootScripts.add(arg);
        } else if ("charset".equals(option)) {
            charset = arg;
        } else if ("expression".equals(option)) {
            expr = arg;
        } else if ("silent".equals(option)) {
            silent = runShell = true;
        } else if ("production".equals(option)) {
            productionMode = true;
        } else if ("verbose".equals(option)) {
            verbose = true;
        } else if ("legacy-mode".equals(option)) {
            legacyMode = true;
        } else if ("version".equals(option)) {
            printVersion();
            System.exit(0);
        }
    }

    private static void missingValueError(String option) {
        exitWithError(option + " option requires a value.", -1);
    }

    private static void rangeError(String option) {
        exitWithError(option + " value must be a number between -1 and 9.", -1);
    }

    private static void unknownOptionError(String option) {
        exitWithError("Unknown option: " + option, -1);
    }

    private static void exitWithError(String message, int code) {
        err.println(message);
        err.println("Use -h or --help for a list of supported options.");
        System.exit(code);
    }

    public static void reportError(Throwable x, PrintStream output,
                                   List<ScriptError> errors, boolean debug) {
        if (x instanceof RhinoException) {
            output.println(((RhinoException)x).details());
        } else {
            output.println(x.toString());
        }
        if (errors != null && !errors.isEmpty()) {
            for (ScriptError error : errors) {
                output.println(error);
            }
        }
        if (x instanceof RhinoException) {
            output.print(((RhinoException) x).getScriptStackTrace());
        }
        if (debug) {
            if (x instanceof WrappedException) {
                x = ((WrappedException)x).getWrappedException();
            }
            output.print("Java Exception: ");
            x.printStackTrace(output);
        }
        output.println();
    }

    public static void printUsage() {
        out.println("Usage:");
        out.println("  ringo [option] ... [script] [arg] ...");
        out.println("Options:");
        Formatter formatter = new Formatter(out);
        for (String[] opt : options) {
            String format = opt[0].length() == 0 ? "   --%2$s %4$s" : "-%1$s --%2$s %4$s";
            String def = new Formatter().format(format, (Object[]) opt).toString();
            formatter.format("  %1$-23s %2$s%n", def, opt[2]);
        }
    }

    public static void printVersion() {
        out.print("RingoJS version ");
        out.println(RhinoEngine.VERSION.get(0) + "." + RhinoEngine.VERSION.get(1) + "." + RhinoEngine.VERSION.get(2));
    }

}
