/*
 * $Id$
 *
 * Copyright (c) 1996, 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.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * 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 com.sun.javatest.agent;

import com.sun.javatest.JavaTestSecurityManager;
import com.sun.javatest.Status;

import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

import static com.sun.javatest.agent.Agent.MILLIS_PER_SECOND;

/**
 * Start an agent, based on command line arguments.
 * No GUI is used.  To create an agent with a GUI, see
 * AgentApplet or AgentFrame.
 *
 * @see Agent
 **/
public class AgentMain {

    private static final int ACTIVE = 1;
    private static final int PASSIVE = 2;
    private static final int SERIAL = 3;
    private boolean helpRequested = false;
    private int mode = 0;
    private int modeCheck = 0;
    private String activeHost = null;
    private int activePort = Agent.defaultActivePort;
    private int passivePort = Agent.defaultPassivePort;
    private String serialPort = null;
    private int concurrency = 1;
    private String mapFile = null;
    private Map<String, String> mappedArgs = new HashMap<>();
    private String observerClassName;
    private boolean tracing;

    /**
     * Create and start an Agent, based on the supplied command line arguments.
     *
     * @param args The command line arguments
     *             <table>
     *             <tr><td> -help                           <td> print a short summary of the command usage
     *             <tr><td> -usage                          <td> print a short summary of the command usage
     *             <tr><td> -active                         <td> set mode to be active
     *             <tr><td> -activeHost  <em>hostname</em>  <td> set the host for active connections (implies -active)
     *             <tr><td> -activePort  <em>port</em>      <td> set the port for active connections (implies -active)
     *             <tr><td> -passive                        <td> set mode to be passive
     *             <tr><td> -passivePort <em>port</em>      <td> set the port for passive connections (implies -passive)
     *             <tr><td> -concurrency <em>number</em>    <td> set the maximum number of simultaneous connections
     *             <tr><td> -map         <em>file</em>      <td> map file for translating arguments of incoming requests
     *             <tr><td> -trace                          <td> trace the execution of the agent
     *             <tr><td> -observer    <em>classname</em> <td> add an observer to the agent that is used
     *             </table>
     */
    public static void main(String... args) {
        AgentMain m = new AgentMain();
        m.runAndExit(args);
    }

    /**
     * Unwrap an InvocationTargetException.
     *
     * @param t the exception to be unwrapped
     * @return the argument's target exception if the argument is an
     * InvocationTargetException; otherwise the argument itself is returned.
     */
    protected static Throwable unwrapInvocationTargetException(Throwable t) {
        if (t instanceof InvocationTargetException) {
            return ((InvocationTargetException) t).getTargetException();
        } else {
            return t;
        }
    }

    private static int max(int a, int b) {
        return a > b ? a : b;
    }

    /**
     * Create and start an Agent, based on the supplied command line arguments.
     * This is for use by subtypes with their own main(String[]) entry point.
     *
     * @param args the command line arguments for the run
     */
    protected void runAndExit(String... args) {
        // install our own permissive security manager, to prevent anyone else
        // installing a less permissive one; moan if it can't be installed.
        JavaTestSecurityManager.install();

        if (Boolean.getBoolean("javatest.trace.printargs") &&
                args != null && args.length > 0) {
            StringBuilder fullCmd = new StringBuilder();
            StringBuilder incrementalCmd = new StringBuilder();

            for (String arg : args) {
                fullCmd.append(arg);
                fullCmd.append(" ");

                incrementalCmd.append("// ");
                incrementalCmd.append(arg);
                incrementalCmd.append("\n");
            }   // for

            System.out.println(fullCmd.toString().trim());
            System.out.println(incrementalCmd.toString());
        }

        int rc;
        try {
            run(args);
            rc = 0;
        } catch (BadArgs e) {
            System.err.println("Error: Bad arguments");
            String[] msgs = e.getMessages();
            for (String msg : msgs) {
                System.err.println(msg);
            }
            System.err.println();
            usage(System.err);
            rc = 1;
        } catch (Fault e) {
            String[] msgs = e.getMessages();
            for (String msg : msgs) {
                System.err.println(msg);
            }
            rc = 2;
        } catch (Throwable t) {
            t.printStackTrace();
            rc = 3;
        }

        // If the JT security manager is installed, it won't allow a call of
        // System.exit unless we ask it nicely..
        SecurityManager sc = System.getSecurityManager();
        if (sc instanceof JavaTestSecurityManager) {
            JavaTestSecurityManager.setAllowExit(true);
        }

        System.exit(rc);
    }

    /**
     * Run with the specified command-lines options.
     * This consists of the following steps:
     * <ul>
     * <li>The command line options are decoded with decodeAllArgs
     * <li>Validity checks (such as option inter-dependencies) are done by validateArgs
     * <li>An agent is created with createAgent
     * <li>The agent is run.
     * </ul>
     *
     * @param args An array of strings, typically provided via the command line
     * @throws AgentMain.BadArgs if a problem is found in the arguments provided
     * @throws AgentMain.Fault   if a fault is found while running
     * @see #main
     * @see #decodeAllArgs
     * @see #validateArgs
     * @see #createAgent
     */
    public void run(String... args) throws BadArgs, Fault {
        decodeAllArgs(args);

        if (helpRequested) {
            usage(System.err);
            return;
        }

        validateArgs();

        Agent agent = createAgent();
        agent.addObserver(new ErrorObserver());
        agent.run();
    }

    /**
     * Decode an array of command line options, by calling decodeArg for
     * successive options in the array.
     *
     * @param args the array of command line options
     * @throws AgentMain.BadArgs if a problem is found decoding the args
     */
    protected void decodeAllArgs(String... args) throws BadArgs {
        int i = 0;
        while (i < args.length) {
            int used = decodeArg(args, i);
            if (used == 0) {
                throw new BadArgs("Unrecognised option: " + args[i]);
            }
            i += used;
        }
    }

    /**
     * Decode the next command line option in an array of options.
     *
     * @param args  the array of command line options
     * @param index the position of the next option to be decoded
     * @return the number of elements consumed from the array
     * @throws AgentMain.BadArgs if a problem is found decoding the args
     */
    protected int decodeArg(String[] args, int index) throws BadArgs {
        int i = index;
        try {
            if (args[i].equalsIgnoreCase("-active")) {
                mode = ACTIVE;
                modeCheck |= 1 << mode;
                return 1;
            } else if (args[i].equalsIgnoreCase("-passive")) {
                mode = PASSIVE;
                modeCheck |= 1 << mode;
                return 1;
            } else if (args[i].equalsIgnoreCase("-activeHost")) {
                mode = ACTIVE;
                modeCheck |= 1 << mode;
                activeHost = args[++i];
                return 2;
            } else if (args[i].equalsIgnoreCase("-activePort")) {
                mode = ACTIVE;
                modeCheck |= 1 << mode;
                activePort = Integer.parseInt(args[++i]);
                return 2;
            } else if (args[i].equalsIgnoreCase("-passivePort")) {
                mode = PASSIVE;
                modeCheck |= 1 << mode;
                passivePort = Integer.parseInt(args[++i]);
                return 2;
            } else if (args[i].equalsIgnoreCase("-serialPort")) {
                mode = SERIAL;
                modeCheck |= 1 << mode;
                serialPort = args[++i];
                return 2;
            } else if (args[i].equalsIgnoreCase("-concurrency")) {
                concurrency = Integer.parseInt(args[++i]);
                return 2;
            } else if (args[i].equalsIgnoreCase("-map")) {
                mapFile = args[++i];
                return 2;
            } else if (args[i].equalsIgnoreCase("-mapArg")) {
                mappedArgs.put(args[++i], args[++i]);
                return 3;
            } else if (args[i].equalsIgnoreCase("-trace")) {
                tracing = true;
                return 1;
            } else if ("-observer".equalsIgnoreCase(args[i]) && i < args.length - 1) {
                if (observerClassName != null) {
                    throw new BadArgs("duplicate use of -observer");
                }
                observerClassName = args[++i];
                return 2;
            } else if (args[i].equalsIgnoreCase("-help") || args[i].equalsIgnoreCase("-usage")) {
                helpRequested = true;
                return args.length - index; // consume remaining args
            } else {
                return 0;   // unrecognized
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            throw new BadArgs("Missing argument for " + args[args.length - 1]);
        } catch (NumberFormatException e) {
            throw new BadArgs("Number expected: " + args[i]);
        }
    }

    /**
     * Validate the values decoded by decodeAllArgs.
     *
     * @throws AgentMain.BadArgs if a problem is found validating the args that
     *                           is likely caused by a misunderstanding of the command line options or syntax
     */
    protected void validateArgs() throws BadArgs {
        if (modeCheck == 0) {
            throw new BadArgs("No connection options given");
        }

        if (modeCheck != (1 << mode)) {
            throw new BadArgs("Conflicting options for connection to JT Harness harness");
        }

        switch (mode) {
            case ACTIVE:
                if (activeHost == null || activeHost.isEmpty()) {
                    throw new BadArgs("No active host specified");
                }
                if (activePort <= 0) {
                    throw new BadArgs("No active port specified");
                }
                break;

            case SERIAL:
                if (serialPort == null) {
                    throw new BadArgs("No serial port specified");
                }
        }

        if (!Agent.isValidConcurrency(concurrency)) {
            throw new BadArgs("Bad value for concurrency: " + concurrency);
        }
    }

    /**
     * Create a connection factory based on the values decoded by decodeAllArgs
     * Normally called from createAgent.
     *
     * @return a connection factory based on the values decoded by decodeAllArgs
     * @throws AgentMain.Fault if there is a problem creating the factory
     */
    protected ConnectionFactory createConnectionFactory() throws Fault {
        String s = AgentMain.class.getName();
        String pkg = s.substring(0, s.lastIndexOf('.'));

        switch (mode) {
            case ACTIVE:
                try {
                    Class<? extends ConnectionFactory> clazz =
                            Class.forName(pkg + ".ActiveConnectionFactory").asSubclass(ConnectionFactory.class);
                    return clazz.getConstructor(String.class, int.class).newInstance(activeHost, activePort);
                } catch (Throwable e) {
                    Throwable t = unwrapInvocationTargetException(e);
                    String[] msgs = {
                            "Error occurred while trying to start an active agent",
                            t.toString(),
                            "Are the java.net classes available?"
                    };
                    throw new Fault(msgs);
                }

            case PASSIVE:
                try {
                    Class<? extends ConnectionFactory> clazz =
                            Class.forName(pkg + ".PassiveConnectionFactory").asSubclass(ConnectionFactory.class);
                    return clazz.getConstructor(int.class, int.class).newInstance(passivePort, concurrency + 1);
                } catch (Throwable e) {
                    Throwable t = unwrapInvocationTargetException(e);
                    if (t instanceof IOException) {
                        throw new Fault("Cannot create socket on port " + passivePort);
                    } else {
                        String[] msgs = {
                                "Error occurred while trying to start a passive agent",
                                t.toString(),
                                "Are the java.net classes available?"
                        };
                        throw new Fault(msgs);
                    }
                }

            case SERIAL:
                try {
                    Class<? extends ConnectionFactory> clazz =
                            Class.forName(pkg + ".SerialPortConnectionFactory").asSubclass(ConnectionFactory.class);
                    return clazz.getConstructor(String.class, String.class, int.class)
                            .newInstance(serialPort, Agent.PRODUCT_NAME, Integer.valueOf(10 * MILLIS_PER_SECOND));
                } catch (InvocationTargetException e) {
                    Throwable t = e.getTargetException();
                    if (t instanceof IllegalArgumentException ||
                            t.getClass().getName().equals("gnu.io.NoSuchPortException")) {
                        throw new Fault(serialPort + " is not a valid port");
                    } else {
                        String[] msgs = {
                                "Error occurred while trying to access the communication ports",
                                t.toString(),
                                "Is the gnu.io extension installed?"
                        };
                        throw new Fault(msgs);
                    }
                } catch (Throwable e) {
                    String[] msgs = {
                            "Error occurred while trying to access the communication ports",
                            e.toString(),
                            "Is the gnu.io extension installed?"
                    };
                    throw new Fault(msgs);
                }

            default:
                throw new Error("unexpected mode");
        }
    }

    /**
     * Create an agent based on the command line options previously decoded.
     * createConnectionFactory() is used to create the connection factory required
     * by the agent.
     *
     * @return an agent based on the values decoded by decodeAllArgs
     * @throws AgentMain.Fault if there is a problem creating the agent
     */
    protected Agent createAgent() throws Fault {
        ConnectionFactory cf = createConnectionFactory();
        Agent agent = new Agent(cf, concurrency);
        agent.setTracing(tracing);

        if (observerClassName != null) {
            try {
                Class<? extends Agent.Observer> observerClass =
                        Class.forName(observerClassName).asSubclass(Agent.Observer.class);
                Agent.Observer observer = observerClass.getDeclaredConstructor().newInstance();
                agent.addObserver(observer);
            } catch (ClassCastException e) {
                throw new Fault("observer is not of type " +
                        Agent.Observer.class.getName() + ": " + observerClassName);
            } catch (ClassNotFoundException e) {
                throw new Fault("cannot find observer class: " + observerClassName);
            } catch (Exception e) {
                throw new Fault("problem instantiating observer: " + e);
            }
        }

        // for now, we only read a map file if one is explicitly specified;
        // in JDK 1.1, it might be nice to check if <<HOSTNAME>>.jtm exists
        // or something like that. In JDK 1.0.2, getting the HOSTNAME is
        // problematic, because INetAddress fails the verifier.
        if (mapFile != null) {
            try {
                agent.setMap(ConfigValuesMap.readFileOrURL(mapFile));
            } catch (IOException e) {
                String[] msgs = {"Problem reading map file", e.toString()};
                throw new Fault(msgs);
            }
        } else if (!mappedArgs.isEmpty()) {
            agent.setMap(new ConfigValuesMap(mappedArgs));
        }

        Integer delay = Integer.getInteger("agent.retry.delay");
        if (delay != null) {
            agent.setRetryDelay(delay.intValue());
        }

        return agent;
    }

    /**
     * Display the set of options recognized by main(String[] args).
     *
     * @param out a stream to which to write the information
     */
    public void usage(PrintStream out) {
        String className = getClass().getName();
        out.println("Usage:");
        out.println("    java " + className + " [options]");
        out.println("        -help             print this message");
        out.println("        -usage            print this message");
        out.println("        -active           set mode to be active");
        out.println("        -activeHost host  set the host for active connections (implies -active)");
        out.println("        -activePort port  set the port for active connections (implies -active)");
        out.println("        -passive          set mode to be passive");
        out.println("        -passivePort port set the port for passive connections (implies -passive)");
        out.println("        -serialPort port  set the port for serial port connections");
        out.println("        -map file         map file for translating arguments of incoming requests");
        out.println("        -mapArg from to   map \"from\" arg to \"to\" arg for incoming requests");
        out.println("        -concurrency num  set the maximum number of simultaneous connections");
        out.println("        -trace            trace the execution of the agent");
        out.println("        -observer class   add an observer to the agent");
    }

    /**
     * This exception is used to report bad command line arguments.
     */
    public static class BadArgs extends Exception {
        private String[] msgs;

        /**
         * Create a BadArgs exception.
         *
         * @param msg A detail message about an error that has been found.
         */
        public BadArgs(String msg) {
            this(new String[]{msg});
        }

        /**
         * Create a BadArgs object.
         *
         * @param msgs Detailed message about an error that has been found.
         */
        public BadArgs(String... msgs) {
            super(msgs[0]);
            this.msgs = msgs;
        }

        /**
         * Get the detail messages.
         *
         * @return the messages given when this exception was created.
         */
        public String[] getMessages() {
            return msgs;
        }
    }

    /**
     * This exception is used to report problems that occur while running.
     */
    public static class Fault extends Exception {
        private String[] msgs;

        /**
         * Create a Fault exception.
         *
         * @param msg A detail message about a fault that has occurred.
         */
        public Fault(String msg) {
            this(new String[]{msg});
        }

        /**
         * Create a Fault object.
         *
         * @param msgs A detail message about a fault that has been found.
         */
        public Fault(String... msgs) {
            super(msgs[0]);
            this.msgs = msgs;
        }

        /**
         * Get the detail messages.
         *
         * @return the messages given when this exception was created.
         */
        public String[] getMessages() {
            return msgs;
        }
    }

    private static class ErrorObserver implements Agent.Observer {
        private long lastNotRespondMsgTime = 0;
        private int lastNotRespondMsgInterval =
                max(Integer.getInteger("notResponding.message.interval", 60).intValue(), 10) * MILLIS_PER_SECOND;
        private Class<?> connectExceptionClass;
        private Class<?> unknownHostExceptionClass;
        ErrorObserver() {
            try {
                connectExceptionClass = Class.forName("java.net.ConnectException", true, ClassLoader.getSystemClassLoader());
            } catch (Throwable t) {
                // ignore
            }

            try {
                unknownHostExceptionClass = Class.forName("java.net.UnknownHostException", true, ClassLoader.getSystemClassLoader());
            } catch (Throwable t) {
                // ignore
            }
        }

        @Override
        public void started(Agent agent) {
        }

        @Override
        public void errorOpeningConnection(Agent agent, Exception e) {
            if (connectExceptionClass != null && connectExceptionClass.isInstance(e)) {
                long now = System.currentTimeMillis();
                if (lastNotRespondMsgTime + lastNotRespondMsgInterval < now) {
                    System.err.println("host not responding: " + e.getMessage());
                    lastNotRespondMsgTime = now;
                }
            } else if (unknownHostExceptionClass != null && unknownHostExceptionClass.isInstance(e)) {
                System.err.println("unknown host: " + e.getMessage());
            } else {
                System.err.println("error connecting to host: " + e);
            }
        }

        @Override
        public void finished(Agent agent) {
        }

        @Override
        public void openedConnection(Agent agent, Connection c) {
        }

        @Override
        public void execTest(Agent agent, Connection c, String tag, String className, String... args) {
        }

        @Override
        public void execCommand(Agent agent, Connection c, String tag, String className, String... args) {
        }

        @Override
        public void execMain(Agent agent, Connection c, String tag, String className, String... args) {
        }

        @Override
        public void result(Agent agent, Connection c, Status result) {
        }

        @Override
        public void exception(Agent agent, Connection c, Throwable e) {
        }

        @Override
        public void completed(Agent agent, Connection c) {
        }
    }
}
