/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2010, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.process;

import static java.security.AccessController.doPrivileged;
import static org.jboss.as.process.ProcessMessages.MESSAGES;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Handler;
import java.util.logging.Logger;

import javax.net.ServerSocketFactory;

import org.jboss.as.process.protocol.ProtocolServer;
import org.wildfly.security.manager.action.GetAccessControlContextAction;
import org.jboss.as.version.ProductConfig;
import org.jboss.as.version.Version;
import org.jboss.logging.MDC;
import org.jboss.logmanager.handlers.ConsoleHandler;
import org.jboss.modules.Module;
import org.jboss.threads.JBossThreadFactory;
import org.wildfly.security.manager.WildFlySecurityManager;

/**
 * The main entry point for the process controller.
 *
 * @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
 */
public final class Main {

    public static String getVersionString() {
        return Version.AS_VERSION;
    }

    private static void usage() {
        CommandLineArgumentUsageImpl.printUsage(System.out);
    }

    private Main() {
    }

    public static final String HOST_CONTROLLER_PROCESS_NAME = "Host Controller";
    public static final String HOST_CONTROLLER_MODULE = "org.jboss.as.host-controller";

    public static void main(String[] args) throws IOException {

        start(args);
    }

    public static ProcessController start(String[] args) throws IOException {
        MDC.put("process", "process controller");

        String javaHome = WildFlySecurityManager.getPropertyPrivileged("java.home", ".");
        String jvmName = javaHome + "/bin/java";
        String jbossHome = WildFlySecurityManager.getPropertyPrivileged("jboss.home.dir", ".");
        String modulePath = null;
        String bootJar = null;
        String bootModule = HOST_CONTROLLER_MODULE;
        final PCSocketConfig pcSocketConfig = new PCSocketConfig();

        String currentWorkingDir = WildFlySecurityManager.getPropertyPrivileged("user.dir", null);

        final List<String> javaOptions = new ArrayList<String>();
        final List<String> smOptions = new ArrayList<String>();

        // target module is always SM
        // -mp is my module path
        // -jar is jboss-modules.jar in jboss-home
        // log config should be fixed loc

        OUT: for (int i = 0; i < args.length; i++) {
            String arg = args[i];
            if ("-jvm".equals(arg)) {
                jvmName = args[++i];
            } else if ("-jboss-home".equals(arg)) {
                jbossHome = args[++i];
            } else if ("-mp".equals(arg)) {
                modulePath = args[++i];
            } else if ("-jar".equals(arg)) {
                bootJar = args[++i];
            } else if ("--".equals(arg)) {
                for (i++; i < args.length; i++) {
                    arg = args[i];
                    if ("--".equals(arg)) {
                        for (i++; i < args.length; i++) {
                            arg = args[i];
                            if (handleHelpOrVersion(arg, jbossHome)) {
                                return null;
                            } else if (pcSocketConfig.processPCSocketConfigArgument(arg, args, i)) {
                                if (pcSocketConfig.isParseFailed()) {
                                    return null;
                                }
                                i += pcSocketConfig.getArgIncrement();
                            } else if (arg.startsWith("-D" + CommandLineConstants.PREFER_IPV4_STACK + "=")) {
                                // AS7-5409 set the property for this process and pass it to HC via javaOptions
                                String val = parseValue(arg, "-D" + CommandLineConstants.PREFER_IPV4_STACK);
                                WildFlySecurityManager.setPropertyPrivileged(CommandLineConstants.PREFER_IPV4_STACK, val);
                                addJavaOption(arg, javaOptions);
                            } else if (arg.startsWith("-D" + CommandLineConstants.PREFER_IPV6_ADDRESSES + "=")) {
                                // AS7-5409 set the property for this process and pass it to HC via javaOptions
                                String val = parseValue(arg, "-D" + CommandLineConstants.PREFER_IPV6_ADDRESSES);
                                WildFlySecurityManager.setPropertyPrivileged(CommandLineConstants.PREFER_IPV6_ADDRESSES, val);
                                addJavaOption(arg, javaOptions);

                            } else {
                                addJavaOption(arg, smOptions);
                            }
                        }
                        break OUT;
                    } else if (handleHelpOrVersion(arg, jbossHome)) {
                        // This would normally come in via the nested if ("--".equals(arg)) case above, but in case someone tweaks the
                        // script to set it directly, we've handled it
                        return null;
                    } else if (pcSocketConfig.processPCSocketConfigArgument(arg, args, i)) {
                        // This would normally come in via the nested if ("--".equals(arg)) case above, but in case someone tweaks the
                        // script to set it directly, we've handled it
                        if (pcSocketConfig.isParseFailed()) {
                            return null;
                        }
                        i += pcSocketConfig.getArgIncrement();
                    } else {
                        addJavaOption(arg, javaOptions);
                    }
                }
                break OUT;
            } else if (handleHelpOrVersion(arg, jbossHome)) {
                // This would normally come in via the if ("--".equals(arg)) cases above, but in case someone tweaks the
                // script to set it directly, we've handled it)
                return null;
            } else if (pcSocketConfig.processPCSocketConfigArgument(arg, args, i)) {
                // This would normally come in via the if ("--".equals(arg)) cases above, but in case someone tweaks the
                // script to set it directly, we've handled it
                if (pcSocketConfig.isParseFailed()) {
                    return null;
                }
                i += pcSocketConfig.getArgIncrement();
            } else {
                throw MESSAGES.invalidOption(arg);
            }
        }
        if (modulePath == null) {
            // if "-mp" (i.e. module path) wasn't part of the command line args, then check the system property.
            // if system property not set, then default to JBOSS_HOME/modules
            // TODO: jboss-modules setting module.path is not a reliable API; log a WARN or something if we get here
            modulePath = WildFlySecurityManager.getPropertyPrivileged("module.path", jbossHome + File.separator + "modules");
        }
        if (bootJar == null) {
            // if "-jar" wasn't part of the command line args, then default to JBOSS_HOME/jboss-modules.jar
            bootJar = jbossHome + File.separator + "jboss-modules.jar";
        }


        Handler consoleHandler = null;

        final Logger rootLogger = Logger.getLogger("");
        for (Handler handler : rootLogger.getHandlers()) {
            if (handler instanceof ConsoleHandler) {
                if (consoleHandler != null) {
                    // duplicate handlers
                    rootLogger.removeHandler(handler);
                } else {
                    consoleHandler = handler;
                    ((ConsoleHandler)consoleHandler).setWriter(new SynchronizedWriter(System.out));
                }
            }
        }

        final ProtocolServer.Configuration configuration = new ProtocolServer.Configuration();
        InetAddress pcInetAddress = InetAddress.getByName(pcSocketConfig.getBindAddress());
        InetSocketAddress pcInetSocketAddress = new InetSocketAddress(pcInetAddress, pcSocketConfig.getBindPort());
        configuration.setBindAddress(pcInetSocketAddress);
        configuration.setSocketFactory(ServerSocketFactory.getDefault());
        final ThreadFactory threadFactory = new JBossThreadFactory(new ThreadGroup("ProcessController-threads"), Boolean.FALSE, null, "%G - %t", null, null, doPrivileged(GetAccessControlContextAction.getInstance()));
        configuration.setThreadFactory(threadFactory);
        configuration.setReadExecutor(Executors.newCachedThreadPool(threadFactory));

        final ProcessController processController = new ProcessController(configuration, System.out, System.err);
        final InetSocketAddress boundAddress = processController.getServer().getBoundAddress();

        final List<String> initialCommand = new ArrayList<String>();
        initialCommand.add(jvmName);
        initialCommand.add("-D[" + HOST_CONTROLLER_PROCESS_NAME + "]");
        initialCommand.addAll(javaOptions);
        initialCommand.add("-jar");
        initialCommand.add(bootJar);
        initialCommand.add("-mp");
        initialCommand.add(modulePath);
        initialCommand.add(bootModule);
        initialCommand.add("-mp");  // Repeat the module path so HostController's Main sees it
        initialCommand.add(modulePath);
        initialCommand.add(CommandLineConstants.PROCESS_CONTROLLER_BIND_ADDR);
        initialCommand.add(boundAddress.getAddress().getHostAddress());
        initialCommand.add(CommandLineConstants.PROCESS_CONTROLLER_BIND_PORT);
        initialCommand.add(Integer.toString(boundAddress.getPort()));
        initialCommand.addAll(smOptions);
        initialCommand.add("-D" + "jboss.home.dir=" + jbossHome);

        processController.addProcess(HOST_CONTROLLER_PROCESS_NAME, initialCommand, Collections.<String, String>emptyMap(), currentWorkingDir, true, true);
        processController.startProcess(HOST_CONTROLLER_PROCESS_NAME);

        final Thread shutdownThread = new Thread(new Runnable() {
            public void run() {
                processController.shutdown();
            }
        }, "Shutdown thread");
        shutdownThread.setDaemon(false);
        Runtime.getRuntime().addShutdownHook(shutdownThread);

        return processController;
    }

    private static String parseValue(final String arg, final String key) {
        String value = null;
        int splitPos = key.length();
        if (arg.length() <= splitPos + 1 || arg.charAt(splitPos) != '=') {
            System.out.println(MESSAGES.noArgValue(key));
            usage();
        } else {
            value = arg.substring(splitPos + 1);
        }
        return value;
    }

    private static void addJavaOption(String option, List<String> javaOptions) {

        // Remove any existing -D options that this one replaces
        if (option.startsWith("-D")) {
            String key;
            int splitPos = option.indexOf('=');
            if (splitPos < 0) {
                key = option;
            } else {
                key = option.substring(0, splitPos);
            }
            for (Iterator<String> iter = javaOptions.iterator(); iter.hasNext();) {
                String existingOp = iter.next();
                if (existingOp.equals(key) || (existingOp.startsWith(key) && existingOp.indexOf('=') == key.length())) {
                    iter.remove();
                }
            }
        }

        javaOptions.add(option);
    }

    private static boolean handleHelpOrVersion(String arg, String jbossHome) {
        if (CommandLineConstants.HELP.equals(arg) || CommandLineConstants.SHORT_HELP.equals(arg)
            || CommandLineConstants.OLD_HELP.equals(arg)) {
            usage();
            return true;
        } else if (CommandLineConstants.VERSION.equals(arg) || CommandLineConstants.SHORT_VERSION.equals(arg)
                || CommandLineConstants.OLD_VERSION.equals(arg) || CommandLineConstants.OLD_SHORT_VERSION.equals(arg)) {
            System.out.println(new ProductConfig(Module.getBootModuleLoader(), jbossHome, null).getPrettyVersionString());
            return true;
        }
        return false;
    }

    private static class PCSocketConfig {
        private String bindAddress;
        private int bindPort = 0;
        private int argIncrement = 0;
        private boolean parseFailed;

        private PCSocketConfig() {
        }

        private String getBindAddress() {
            if (bindAddress != null) {
                return bindAddress;
            } else {
                boolean v4Stack = Boolean.valueOf(WildFlySecurityManager.getPropertyPrivileged(CommandLineConstants.PREFER_IPV4_STACK, "false"));
                boolean useV6 = !v4Stack && Boolean.valueOf(WildFlySecurityManager.getPropertyPrivileged(CommandLineConstants.PREFER_IPV6_ADDRESSES, "false"));
                return useV6 ? "::1" : "127.0.0.1";
            }
        }

        private int getBindPort() {
            return bindPort;
        }

        private int getArgIncrement() {
            return argIncrement;
        }

        private boolean isParseFailed() {
            return parseFailed;
        }

        private boolean processPCSocketConfigArgument(final String arg, final String[] args, final int index) {
            boolean isPCSocketArg = true;

            argIncrement = 0;

            if (CommandLineConstants.PROCESS_CONTROLLER_BIND_ADDR.equals(arg) || CommandLineConstants.OLD_PROCESS_CONTROLLER_BIND_ADDR.equals(arg)) {
                bindAddress = args[index +1];
                argIncrement = 1;
            } else if (arg.startsWith(CommandLineConstants.PROCESS_CONTROLLER_BIND_ADDR)) {
                String addr = parseValue(arg, CommandLineConstants.PROCESS_CONTROLLER_BIND_ADDR);
                if (addr == null) {
                    parseFailed = true;
                } else {
                    bindAddress = addr;
                }
            } else if (arg.startsWith(CommandLineConstants.OLD_PROCESS_CONTROLLER_BIND_ADDR)) {
                String addr = parseValue(arg, CommandLineConstants.OLD_PROCESS_CONTROLLER_BIND_ADDR);
                if (addr == null) {
                    parseFailed = true;
                } else {
                    bindAddress = addr;
                }
            } else if (CommandLineConstants.PROCESS_CONTROLLER_BIND_PORT.equals(arg) || CommandLineConstants.OLD_PROCESS_CONTROLLER_BIND_PORT.equals(arg)) {
                bindPort = Integer.parseInt(args[index + 1]);
                argIncrement = 1;
            } else if (arg.startsWith(CommandLineConstants.PROCESS_CONTROLLER_BIND_PORT)) {
                String port = parseValue(arg, CommandLineConstants.PROCESS_CONTROLLER_BIND_PORT);
                if (port == null) {
                    parseFailed = true;
                } else {
                    bindPort = Integer.parseInt(port);
                }
            } else if (arg.startsWith(CommandLineConstants.OLD_PROCESS_CONTROLLER_BIND_PORT)) {
                String port = parseValue(arg, CommandLineConstants.OLD_PROCESS_CONTROLLER_BIND_PORT);
                if (port == null) {
                    parseFailed = true;
                } else {
                    bindPort = Integer.parseInt(port);
                }
            } else {
                isPCSocketArg = false;
            }

            return isPCSocketArg;
        }
    }
}
