/*
 * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package jdk.tools.jaotc;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import jdk.tools.jaotc.collect.ClassSearch;
import jdk.tools.jaotc.collect.ClassSource;
import jdk.tools.jaotc.collect.SearchFor;
import jdk.tools.jaotc.collect.SearchPath;
import jdk.tools.jaotc.collect.classname.ClassNameSourceProvider;
import jdk.tools.jaotc.collect.directory.DirectorySourceProvider;
import jdk.tools.jaotc.collect.jar.JarSourceProvider;
import jdk.tools.jaotc.collect.module.ModuleSourceProvider;

final class Options {
    List<SearchFor> files = new LinkedList<>();
    String osName;
    String outputName = defaultOutputName();
    String methodList;
    List<ClassSource> sources = new ArrayList<>();
    String linkerpath = null;
    SearchPath searchPath = new SearchPath();

    /**
     * We don't see scaling beyond 16 threads.
     */
    private static final int COMPILER_THREADS = 16;

    int threads = Integer.min(COMPILER_THREADS, Runtime.getRuntime().availableProcessors());

    boolean ignoreClassLoadingErrors;
    boolean exitOnError;
    boolean info;
    boolean verbose;
    boolean debug;
    boolean help;
    boolean version;
    boolean compileWithAssertions;
    boolean tiered;

    private String defaultOutputName() {
        osName = System.getProperty("os.name");
        String name = "unnamed.";
        String ext;

        switch (osName) {
            case "Linux":
            case "SunOS":
                ext = "so";
                break;
            case "Mac OS X":
                ext = "dylib";
                break;
            default:
                if (osName.startsWith("Windows")) {
                    ext = "dll";
                } else {
                    ext = "so";
                }
        }

        return name + ext;
    }

    static class BadArgs extends Exception {
        private static final long serialVersionUID = 1L;
        final String key;
        final Object[] args;
        boolean showUsage;

        BadArgs(String key, Object... args) {
            super(MessageFormat.format(key, args));
            this.key = key;
            this.args = args;
        }

        BadArgs showUsage(boolean b) {
            showUsage = b;
            return this;
        }
    }

    abstract static class Option {
        final String help;
        final boolean hasArg;
        final String[] aliases;

        Option(String help, boolean hasArg, String... aliases) {
            this.help = help;
            this.hasArg = hasArg;
            this.aliases = aliases;
        }

        boolean isHidden() {
            return false;
        }

        boolean matches(String opt) {
            for (String a : aliases) {
                if (a.equals(opt)) {
                    return true;
                } else if (opt.startsWith("--") && hasArg && opt.startsWith(a + "=")) {
                    return true;
                }
            }
            return false;
        }

        boolean ignoreRest() {
            return false;
        }

        abstract void process(Main task, String opt, String arg) throws BadArgs;
    }

    static Option[] recognizedOptions = {new Option("  --output <file>            Output file name", true, "--output") {
        @Override
        void process(Main task, String opt, String arg) {
            String name = arg;
            task.options.outputName = name;
        }
    }, new Option("  --class-name <class names> List of classes to compile", true, "--class-name", "--classname") {
        @Override
        void process(Main task, String opt, String arg) {
            task.options.files.addAll(ClassSearch.makeList(ClassNameSourceProvider.TYPE, arg));
        }
    }, new Option("  --jar <jarfiles>           List of jar files to compile", true, "--jar") {
        @Override
        void process(Main task, String opt, String arg) {
            task.options.files.addAll(ClassSearch.makeList(JarSourceProvider.TYPE, arg));
        }
    }, new Option("  --module <modules>         List of modules to compile", true, "--module") {
        @Override
        void process(Main task, String opt, String arg) {
            task.options.files.addAll(ClassSearch.makeList(ModuleSourceProvider.TYPE, arg));
        }
    }, new Option("  --directory <dirs>         List of directories where to search for files to compile", true, "--directory") {
        @Override
        void process(Main task, String opt, String arg) {
            task.options.files.addAll(ClassSearch.makeList(DirectorySourceProvider.TYPE, arg));
        }
    }, new Option("  --search-path <dirs>       List of directories where to search for specified files", true, "--search-path") {
        @Override
        void process(Main task, String opt, String arg) {
            String[] elements = arg.split(":");
            task.options.searchPath.add(elements);
        }
    }, new Option("  --compile-commands <file>  Name of file with compile commands", true, "--compile-commands") {
        @Override
        void process(Main task, String opt, String arg) {
            task.options.methodList = arg;
        }
    }, new Option("  --compile-for-tiered       Generate profiling code for tiered compilation", false, "--compile-for-tiered") {
        @Override
        void process(Main task, String opt, String arg) {
            task.options.tiered = true;
        }
    }, new Option("  --compile-with-assertions  Compile with java assertions", false, "--compile-with-assertions") {
        @Override
        void process(Main task, String opt, String arg) {
            task.options.compileWithAssertions = true;
        }
    }, new Option("  --compile-threads <number> Number of compilation threads to be used", true, "--compile-threads", "--threads") {
        @Override
        void process(Main task, String opt, String arg) {
            int threads = Integer.parseInt(arg);
            final int available = Runtime.getRuntime().availableProcessors();
            if (threads <= 0) {
                task.warning("invalid number of threads specified: {0}, using: {1}", threads, available);
                threads = available;
            }
            if (threads > available) {
                task.warning("too many threads specified: {0}, limiting to: {1}", threads, available);
            }
            task.options.threads = Integer.min(threads, available);
        }
    }, new Option("  --ignore-errors            Ignores all exceptions thrown during class loading", false, "--ignore-errors") {
        @Override
        void process(Main task, String opt, String arg) {
            task.options.ignoreClassLoadingErrors = true;
        }
    }, new Option("  --exit-on-error            Exit on compilation errors", false, "--exit-on-error") {
        @Override
        void process(Main task, String opt, String arg) {
            task.options.exitOnError = true;
        }
    }, new Option("  --info                     Print information during compilation", false, "--info") {
        @Override
        void process(Main task, String opt, String arg) throws BadArgs {
            task.options.info = true;
        }
    }, new Option("  --verbose                  Print verbose information", false, "--verbose") {
        @Override
        void process(Main task, String opt, String arg) throws BadArgs {
            task.options.info = true;
            task.options.verbose = true;
        }
    }, new Option("  --debug                    Print debug information", false, "--debug") {
        @Override
        void process(Main task, String opt, String arg) throws BadArgs {
            task.options.info = true;
            task.options.verbose = true;
            task.options.debug = true;
        }
    }, new Option("  -? -h --help               Print this help message", false, "--help", "-h", "-?") {
        @Override
        void process(Main task, String opt, String arg) {
            task.options.help = true;
        }
    }, new Option("  --version                  Version information", false, "--version") {
        @Override
        void process(Main task, String opt, String arg) {
            task.options.version = true;
        }
    }, new Option("  --linker-path              Full path to linker executable", true, "--linker-path") {
        @Override
        void process(Main task, String opt, String arg) {
            task.options.linkerpath = arg;
        }
    }, new Option("  -J<flag>                   Pass <flag> directly to the runtime system", false, "-J") {
        @Override
        void process(Main task, String opt, String arg) {
        }
    }};

    static void handleOptions(Main task, String[] args) throws BadArgs {
        if (args.length == 0) {
            task.options.help = true;
            return;
        }

        // Make checkstyle happy.
        for (int i = 0; i < args.length; i++) {
            String arg = args[i];

            if (arg.charAt(0) == '-') {
                Option option = getOption(arg);
                String param = null;

                if (option.hasArg) {
                    if (arg.startsWith("--") && arg.indexOf('=') > 0) {
                        param = arg.substring(arg.indexOf('=') + 1, arg.length());
                    } else if (i + 1 < args.length) {
                        param = args[++i];
                    }

                    if (param == null || param.isEmpty() || param.charAt(0) == '-') {
                        throw new BadArgs("missing argument for option: {0}", arg).showUsage(true);
                    }
                }

                option.process(task, arg, param);

                if (option.ignoreRest()) {
                    break;
                }
            } else {
                task.options.files.add(new SearchFor(arg));
            }
        }
    }

    static Option getOption(String name) throws BadArgs {
        for (Option o : recognizedOptions) {
            if (o.matches(name)) {
                return o;
            }
        }
        throw new BadArgs("unknown option: {0}", name).showUsage(true);
    }

}
