/*
 * Copyright (c) 2004, 2022, 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 sun.tools.jstat;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.regex.*;
import sun.jvmstat.monitor.Monitor;
import sun.jvmstat.monitor.VmIdentifier;

/**
 * Class for processing command line arguments and providing method
 * level access to arguments.
 *
 * @author Brian Doherty
 * @since 1.5
 */
public class Arguments {

    private static final boolean debug = Boolean.getBoolean("jstat.debug");
    private static final boolean showUnsupported =
            Boolean.getBoolean("jstat.showUnsupported");

    private static final String JVMSTAT_USERDIR = ".jvmstat";
    private static final String OPTIONS_FILENAME = "jstat_options";
    private static final String UNSUPPORTED_OPTIONS_FILENAME = "jstat_unsupported_options";
    private static final String ALL_NAMES = "\\w*";

    private Comparator<Monitor> comparator;
    private int headerRate;
    private boolean help;
    private boolean list;
    private boolean options;
    private boolean constants;
    private boolean constantsOnly;
    private boolean strings;
    private boolean timestamp;
    private boolean snap;
    private boolean verbose;
    private String specialOption;
    private String names;

    private OptionFormat optionFormat;

    private int count = -1;
    private int interval = -1;
    private String vmIdString;

    private VmIdentifier vmId;

    public static void printUsage(PrintStream ps) {
        ps.println("Usage: jstat --help|-options");
        ps.println("       jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]");
        ps.println();
        ps.println("Definitions:");
        ps.println("  <option>      An option reported by the -options option");
        ps.println("  <vmid>        Virtual Machine Identifier. A vmid takes the following form:");
        ps.println("                     <lvmid>[@<hostname>[:<port>]]");
        ps.println("                Where <lvmid> is the local vm identifier for the target");
        ps.println("                Java virtual machine, typically a process id; <hostname> is");
        ps.println("                the name of the host running the target Java virtual machine;");
        ps.println("                and <port> is the port number for the rmiregistry on the");
        ps.println("                target host. See the jvmstat documentation for a more complete");
        ps.println("                description of the Virtual Machine Identifier.");
        ps.println("  <lines>       Number of samples between header lines.");
        ps.println("  <interval>    Sampling interval. The following forms are allowed:");
        ps.println("                    <n>[\"ms\"|\"s\"]");
        ps.println("                Where <n> is an integer and the suffix specifies the units as ");
        ps.println("                milliseconds(\"ms\") or seconds(\"s\"). The default units are \"ms\".");
        ps.println("  <count>       Number of samples to take before terminating.");
        ps.println("  -J<flag>      Pass <flag> directly to the runtime system.");
        ps.println("  -? -h --help  Prints this help message.");
        ps.println("  -help         Prints this help message.");

        // undocumented options:
        //   -list [<vmid>]  - list counter names
        //   -snap <vmid>    - snapshot counter values as name=value pairs
        //   -name <pattern> - output counters matching given pattern
        //   -a              - sort in ascending order (default)
        //   -d              - sort in descending order
        //   -v              - verbose output  (-snap)
        //   -constants      - output constants with -name output
        //   -strings        - output strings with -name output
        //   -help           - same as -? ...
    }

    private static int toMillis(String s) throws IllegalArgumentException {

        String[] unitStrings = { "ms", "s" }; // ordered from most specific to
                                              // least specific
        String unitString = null;
        String valueString = s;

        for (String unit : unitStrings) {
            int index = s.indexOf(unit);
            if (index > 0) {
                unitString = s.substring(index);
                valueString = s.substring(0, index);
                break;
            }
        }

        try {
            int value = Integer.parseInt(valueString);

            if (unitString == null || unitString.equals("ms")) {
                return value;
            } else if (unitString.equals("s")) {
                return value * 1000;
            } else {
                throw new IllegalArgumentException(
                        "Unknown time unit: " + unitString);
            }
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException(
                    "Could not convert interval: " + s);
        }
    }

    public Arguments(String[] args) throws IllegalArgumentException {
        int argc = 0;

        if (args.length == 0) {
            help = true;
            return;
        }

        if ((args[0].equals("-?"))
                || (args[0].equals("-h"))
                || (args[0].equals("--help"))
                // -help: legacy.
                || (args[0].equals("-help"))) {
            help = true;
            return;
        } else if (args[0].equals("-options")) {
            options = true;
            return;
        } else if (args[0].equals("-list")) {
            list = true;
            if (args.length > 2) {
              throw new IllegalArgumentException("invalid argument count");
            }
            // list can take one arg - a vmid - fall through for arg processing
            argc++;
        }

        for ( ; (argc < args.length) && (args[argc].startsWith("-")); argc++) {
            String arg = args[argc];

            if (arg.equals("-a")) {
                comparator = new AscendingMonitorComparator();
            } else if (arg.equals("-d")) {
                comparator =  new DescendingMonitorComparator();
            } else if (arg.equals("-t")) {
                timestamp = true;
            } else if (arg.equals("-v")) {
                verbose = true;
            } else if ((arg.equals("-constants"))
                       || (arg.equals("-c"))) {
                constants = true;
            } else if ((arg.equals("-strings"))
                       || (arg.equals("-s"))) {
                strings = true;
            } else if (arg.startsWith("-h")) {
                String value;
                if (!arg.equals("-h")) {
                    value = arg.substring(2);
                } else {
                    argc++;
                    if (argc >= args.length) {
                        throw new IllegalArgumentException(
                                "-h requires an integer argument");
                    }
                    value = args[argc];
                }
                try {
                    headerRate = Integer.parseInt(value);
                } catch (NumberFormatException e) {
                    headerRate = -1;
                }
                if (headerRate < 0) {
                    throw new IllegalArgumentException(
                            "illegal -h argument: " + value);
                }
            } else if (arg.startsWith("-name")) {
                if (arg.startsWith("-name=")) {
                    names = arg.substring(7);
                } else {
                    argc++;
                    if (argc >= args.length) {
                        throw new IllegalArgumentException(
                                "option argument expected");
                    }
                    names = args[argc];
                }
            } else {
                /*
                 * there are scenarios here: special jstat_options file option
                 * or the rare case of a negative lvmid. The negative lvmid
                 * can occur in some operating environments (such as Windows
                 * 95/98/ME), so we provide for this case here by checking if
                 * the argument has any numerical characters. This assumes that
                 * there are no special jstat_options that contain numerical
                 * characters in their name.
                 */

                // extract the lvmid part of possible lvmid@host.domain:port
                String lvmidStr = null;
                int at_index = args[argc].indexOf('@');
                if (at_index < 0) {
                    lvmidStr = args[argc];
                } else {
                    lvmidStr = args[argc].substring(0, at_index);
                }

                // try to parse the lvmid part as an integer
                try {
                    int vmid = Integer.parseInt(lvmidStr);
                    // it parsed, assume a negative lvmid and continue
                    break;
                } catch (NumberFormatException nfe) {
                    // it didn't parse. check for the -snap or jstat_options
                    // file options.
                    if ((argc == 0) && (args[argc].equals("-snap"))) {
                        snap = true;
                    } else if (argc == 0) {
                        specialOption = args[argc].substring(1);
                    } else {
                        throw new IllegalArgumentException(
                                "illegal argument: " + args[argc]);
                    }
                }
            }
        }

        // prevent 'jstat <pid>' from being accepted as a valid argument
        if (!(specialOption != null || list || snap || names != null)) {
            throw new IllegalArgumentException("-<option> required");
        }

        switch (args.length - argc) {
        case 3:
            if (snap) {
                throw new IllegalArgumentException("invalid argument count");
            }
            try {
                count = Integer.parseInt(args[args.length-1]);
            } catch (NumberFormatException e) {
                throw new IllegalArgumentException("illegal count value: "
                                                   + args[args.length-1]);
            }
            interval = toMillis(args[args.length-2]);
            vmIdString = args[args.length-3];
            break;
        case 2:
            if (snap) {
                throw new IllegalArgumentException("invalid argument count");
            }
            interval = toMillis(args[args.length-1]);
            vmIdString = args[args.length-2];
            break;
        case 1:
            vmIdString = args[args.length-1];
            break;
        case 0:
            if (!list) {
                throw new IllegalArgumentException("invalid argument count");
            }
            break;
        default:
            throw new IllegalArgumentException("invalid argument count");
        }

        // set count and interval to their default values if not set above.
        if (count == -1 && interval == -1) {
            // default is for a single sample
            count = 1;
            interval = 0;
        }

        // validate arguments
        if (comparator == null) {
            comparator = new AscendingMonitorComparator();
        }

        // allow ',' characters to separate names, convert to '|' chars
        names = (names == null) ? ALL_NAMES : names.replace(',', '|');

        // verify that the given pattern parses without errors
        try {
            Pattern pattern = Pattern.compile(names);
        } catch (PatternSyntaxException e) {
            throw new IllegalArgumentException("Bad name pattern: "
                                               + e.getMessage());
        }

        // verify that the special option is valid and get it's formatter
        if (specialOption != null) {
            OptionFinder finder = new OptionFinder(optionsSources());
            optionFormat = finder.getOptionFormat(specialOption, timestamp);
            if (optionFormat == null) {
                throw new IllegalArgumentException("Unknown option: -"
                                                   + specialOption);
            }
        }

        // verify that the vm identifier is valid
        try {
            vmId = new VmIdentifier(vmIdString);
        } catch (URISyntaxException e) {
            IllegalArgumentException iae = new IllegalArgumentException(
                    "Malformed VM Identifier: " + vmIdString);
            iae.initCause(e);
            throw iae;
        }
    }

    public Comparator<Monitor> comparator() {
        return comparator;
    }

    public boolean isHelp() {
        return help;
    }

    public boolean isList() {
        return list;
    }

    public boolean isSnap() {
        return snap;
    }

    public boolean isOptions() {
        return options;
    }

    public boolean isVerbose() {
        return verbose;
    }

    public boolean printConstants() {
        return constants;
    }

    public boolean isConstantsOnly() {
        return constantsOnly;
    }

    public boolean printStrings() {
        return strings;
    }

    public boolean showUnsupported() {
        return showUnsupported;
    }

    public int headerRate() {
        return headerRate;
    }

    public String counterNames() {
        return names;
    }

    public VmIdentifier vmId() {
        return vmId;
    }

    public String vmIdString() {
        return vmIdString;
    }

    public int sampleInterval() {
        return interval;
    }

    public int sampleCount() {
        return count;
    }

    public boolean isTimestamp() {
        return timestamp;
    }

    public boolean isSpecialOption() {
        return specialOption != null;
    }

    public String specialOption() {
        return specialOption;
    }

    public OptionFormat optionFormat() {
        return optionFormat;
    }

    public List<URL> optionsSources() {
        List<URL> sources = new ArrayList<URL>();
        int i = 0;

        String filename = OPTIONS_FILENAME;

        try {
            String userHome = System.getProperty("user.home");
            String userDir = userHome + "/" + JVMSTAT_USERDIR;
            File home = new File(userDir + "/" + filename);
            sources.add(home.toURI().toURL());
        } catch (Exception e) {
            if (debug) {
                System.err.println(e.getMessage());
                e.printStackTrace();
            }
            throw new IllegalArgumentException("Internal Error: Bad URL: "
                                               + e.getMessage());
        }
        URL u = this.getClass().getResource("resources/" + filename);
        assert u != null;
        sources.add(u);

        if (showUnsupported) {
            u = this.getClass().getResource("resources/" +  UNSUPPORTED_OPTIONS_FILENAME);
            assert u != null;
            sources.add(u);
        }
        return sources;
    }
}
