/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.ignite.startup.cmdline;

import java.io.PrintStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

/**
 * This class is a workaround for a versatile problems with passing arguments to the Ignite Windows batch launcher
 * scripts. It transforms command line options passed to the *.bat launcher scripts into a reformatted, adapted for
 * Windows batch usage variables.
 * <p>
 * The idea behind the workaround is:<br>
 * 1. User runs ignite.bat with some args.<br>
 * 2. ignite.bat calls parseargs.bat with all that args.<br>
 * 3. parsearg.bat runs this class with all that args.<br>
 * 4. This class transforms the args and prints them out.<br>
 * 5. parseargs.bat splits the output of this class and sets each variable it found.<br>
 * 6. ignite.bat uses environment variables as if they were defined by a user.
 * <p>
 *
 * @see <a href="http://jcommander.org/">JCommander command line parameters parsing library</a>
 */
public class CommandLineTransformer {
    /** Prefix for every custom JVM option. */
    static final String JVM_OPTION_PREFIX = "-J";

    /** Flag to check in step 5 of the Workaround before splitting the output. */
    private static final String TRANSFORMATION_FAILED_FLAG = "CommandLineTransformerError";

    /** Delimiter used in step 5 of the Workaround. */
    private static final String ARGS_DELIMITER = " ";

    /** Interactive mode. */
    private boolean interactive;

    /** Verbose mode. */
    private boolean verbose;

    /** No pause mode. */
    private boolean noPause;

    /** Supported parameter, parsed manually. */
    private String jvmOptions = "";

    /** Supported parameter, parsed manually. */
    private String springCfgPath = "";

    /**
     * Private constructor to promote usage of a factory method {@link #transform(String...)}.
     */
    private CommandLineTransformer() {
        // No-op
    }

    /**
     * Main method being triggered in step 3 of the Workaround.
     * <p>
     * Method prints out TRANSFORMATION_FAILED_FLAG if something went wrong.
     *
     * @param args Command line arguments passed by a user in step 1 of the Workaround.
     */
    public static void main(String[] args) {
        PrintStream ps = null;

        try {
            String encoding = System.getProperty("file.encoding", Charset.defaultCharset().name());

            ps = new PrintStream(System.out, true, encoding);

            ps.println(transform(args));
        }
        catch (Throwable t) {
            t.printStackTrace();

            if (ps != null)
                ps.println(TRANSFORMATION_FAILED_FLAG);

            if (t instanceof Error)
                throw (Error)t;
        }
    }

    /**
     * @param args Arguments.
     * @return Transformed arguments.
     */
    public static String transform(String... args) {
        assert args != null;

        return new CommandLineTransformer().doTransformation(args);
    }

    /**
     * Actually does arguments transformation.
     *
     * @param args Arguments.
     * @return Transformed arguments.
     */
    private String doTransformation(String[] args) {
        List<String> argsList = new ArrayList<>();

        for (String arg : args) {
            switch (arg) {
                case "-i":
                    interactive = true;

                    break;

                case "-v":
                    verbose = true;

                    break;

                case "-np":
                    noPause = true;

                    break;

                default:
                    argsList.add(arg);
            }
        }

        return reformatArguments(argsList);
    }

    /**
     * Transforms parsed arguments into a string.
     *
     * @param args Non-standard arguments.
     * @return Transformed arguments.
     */
    private String reformatArguments(List<String> args) {
        StringBuilder sb = new StringBuilder();

        addArgWithValue(sb, "INTERACTIVE", formatBooleanValue(interactive));
        addArgWithValue(sb, "QUIET", "-DIGNITE_QUIET=" + !verbose);
        addArgWithValue(sb, "NO_PAUSE", formatBooleanValue(noPause));

        parseJvmOptionsAndSpringConfig(args);

        addArgWithValue(sb, "JVM_XOPTS", jvmOptions);
        addArgWithValue(sb, "CONFIG", springCfgPath);

        return sb.toString().trim();
    }

    /**
     * Formats boolean value into a string one.
     *
     * @param val Boolean value to be formatted.
     * @return 1 if val is true 0 otherwise.
     */
    private String formatBooleanValue(boolean val) {
        return String.valueOf(val ? 1 : 0);
    }

    /**
     * Transforms one argument.
     *
     * @param sb StringBuilder where transformation result will be written.
     * @param arg Argument's name.
     * @param val Argument's value.
     */
    private void addArgWithValue(StringBuilder sb, String arg, Object val) {
        sb.append("\"");
        sb.append(arg);
        sb.append("=");
        sb.append(val);
        sb.append("\"");
        sb.append(ARGS_DELIMITER);
    }

    /**
     * Manually parses non-trivial (from JCommander point of view) arguments.
     *
     * @param args Collection of unknown (from JCommander point of view) arguments.
     */
    private void parseJvmOptionsAndSpringConfig(Iterable<String> args) {
        boolean hadFileEncoding = false;

        for (String arg : args) {
            if (arg.startsWith(JVM_OPTION_PREFIX)) {
                String jvmOpt = arg.substring(JVM_OPTION_PREFIX.length());

                if (jvmOpt.startsWith("-Dfile.encoding="))
                    hadFileEncoding = true;

                if (!checkJVMOptionIsSupported(jvmOpt))
                    throw new RuntimeException(JVM_OPTION_PREFIX + " JVM parameters for Ignite batch scripts " +
                        "with double quotes are not supported. " +
                        "Use JVM_OPTS environment variable to pass any custom JVM option.");

                jvmOptions = jvmOptions.isEmpty() ? jvmOpt : jvmOptions + " " + jvmOpt;
            }
            else {
                if (springCfgPath.isEmpty())
                    springCfgPath = arg;
                else
                    throw new RuntimeException("Unrecognised parameter has been found: " + arg);
            }
        }

        if (!hadFileEncoding)
            jvmOptions = (jvmOptions.isEmpty() ? "" : (jvmOptions + " ")) + "-Dfile.encoding=UTF-8";
    }

    /**
     * Check if a JVM option is supported. Unsupported options are those who
     * need double quotes in the declaration. It's a limitation of a current Workaround
     *
     * @param jvmOpt JVM option to check.
     * @return true if option is supported false otherwise.
     */
    private boolean checkJVMOptionIsSupported(String jvmOpt) {
        return !(jvmOpt.contains("-XX:OnError") || jvmOpt.contains("-XX:OnOutOfMemoryError"));
    }
}
