/*
 * 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;

import java.io.File;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.internal.util.typedef.G;
import org.apache.ignite.testframework.GridTestUtils;
import org.apache.log4j.Appender;
import org.apache.log4j.Logger;
import org.apache.log4j.varia.NullAppender;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;

import static org.apache.ignite.IgniteSystemProperties.IGNITE_UPDATE_NOTIFIER;

/**
 * This class
 *
 */
public final class GridVmNodesStarter {
    /** Name of the system property defining name of command line program. */
    private static final String IGNITE_PROG_NAME = "IGNITE_PROG_NAME";

    /** */
    private static final String IGNITE_INSTANCE_NAME_PREF = "gg-vm-grid-";

    /** */
    private static final int DFLT_NODES_COUNT = 20;

    /** */
    private static final String OPTION_CFG = "cfg";

    /** */
    private static final String OPTION_N = "n";

    /** */
    private static final AtomicInteger gridCnt = new AtomicInteger();

    /**
     * Enforces singleton.
     */
    private GridVmNodesStarter() {
        // No-op.
    }

    /**
     * Echos the given messages.
     *
     * @param msg Message to echo.
     */
    private static void echo(String msg) {
        assert msg != null;

        System.out.println(msg);
    }

    /**
     * Echos exception stack trace.
     *
     * @param e Exception to print.
     */
    private static void echo(IgniteCheckedException e) {
        assert e != null;

        System.err.println(e);
    }

    /**
     * Exists with optional error message, usage show and exit code.
     *
     * @param errMsg Optional error message.
     * @param options Command line options to show usage information.
     * @param exitCode Exit code.
     */
    private static void exit(String errMsg, Options options, int exitCode) {
        if (errMsg != null)
            echo("ERROR: " + errMsg);

        String runner = System.getProperty(IGNITE_PROG_NAME, "randignite.{sh|bat}");

        int space = runner.indexOf(' ');

        runner = runner.substring(0, space == -1 ? runner.length() : space);

        if (options != null) {
            HelpFormatter formatter = new HelpFormatter();

            formatter.printHelp(runner, options);
        }

        System.exit(exitCode);
    }

    /**
     * Main entry point.
     *
     * @param args Command line arguments.
     * @throws IgniteCheckedException If failed.
     */
    public static void main(String[] args) throws IgniteCheckedException {
        System.setProperty(IGNITE_UPDATE_NOTIFIER, "false");

        Options options = createOptions();

        // Create the command line parser.
        CommandLineParser parser = new PosixParser();

        String cfgPath = null;

        Integer nodesCnt = null;

        try {
            CommandLine cmd = parser.parse(options, args);

            if (cmd.hasOption(OPTION_CFG))
                cfgPath = cmd.getOptionValue(OPTION_CFG);

            if (cmd.hasOption(OPTION_N))
                try {
                    nodesCnt = Integer.parseInt(cmd.getOptionValue(OPTION_N));
                }
                catch (NumberFormatException ignored) {
                    // No-op.
                }

            if (nodesCnt == null)
                nodesCnt = DFLT_NODES_COUNT;
        }
        catch (ParseException e) {
            exit(e.getMessage(), options, -1);
        }

        System.out.println();
        System.out.println(">>> VM Nodes Starter parameters:");
        System.out.println("  Nodes Count: " + nodesCnt);
        System.out.println("  Config Path: " + cfgPath);
        System.out.println();

        final IgniteConfiguration[] cfgs = new IgniteConfiguration[nodesCnt];

        for (int i = 0; i < nodesCnt; i++)
            cfgs[i] = getConfigurations(cfgPath).iterator().next();

        final AtomicInteger cfgIdx = new AtomicInteger(0);

        GridTestUtils.runMultiThreadedAsync(new Callable<Object>() {
            @Override public Object call() throws Exception {
                G.start(cfgs[cfgIdx.getAndIncrement()]);

                return null;
            }
        }, nodesCnt, "test-node-starter");
    }

    /**
     * Initializes configurations.
     *
     *
     * @param springCfgPath Configuration file path.
     * @return List of configurations.
     * @throws IgniteCheckedException If an error occurs.
     */
    private static Iterable<IgniteConfiguration> getConfigurations(String springCfgPath)
        throws IgniteCheckedException {
        File path = GridTestUtils.resolveIgnitePath(springCfgPath);

        if (path == null)
            throw new IgniteCheckedException("Spring XML configuration file path is invalid: " + new File(springCfgPath) +
                ". Note that this path should be either absolute path or a relative path to IGNITE_HOME.");

        if (!path.isFile())
            throw new IgniteCheckedException("Provided file path is not a file: " + path);

        // Add no-op logger to remove no-appender warning.
        Appender app = new NullAppender();

        Logger.getRootLogger().addAppender(app);

        ApplicationContext springCtx;

        try {
            springCtx = new FileSystemXmlApplicationContext(path.toURI().toURL().toString());
        }
        catch (BeansException | MalformedURLException e) {
            throw new IgniteCheckedException("Failed to instantiate Spring XML application context: " + e.getMessage(), e);
        }

        Map cfgMap;

        try {
            // Note: Spring is not generics-friendly.
            cfgMap = springCtx.getBeansOfType(IgniteConfiguration.class);
        }
        catch (BeansException e) {
            throw new IgniteCheckedException("Failed to instantiate bean [type=" + IgniteConfiguration.class + ", err=" +
                e.getMessage() + ']', e);
        }

        if (cfgMap == null)
            throw new IgniteCheckedException("Failed to find a single grid factory configuration in: " + path);

        // Remove previously added no-op logger.
        Logger.getRootLogger().removeAppender(app);

        if (cfgMap.isEmpty())
            throw new IgniteCheckedException("Can't find grid factory configuration in: " + path);

        Collection<IgniteConfiguration> res = new ArrayList<>();

        for (IgniteConfiguration cfg : (Collection<IgniteConfiguration>)cfgMap.values()) {
            res.add(cfg);

            cfg.setIgniteInstanceName(IGNITE_INSTANCE_NAME_PREF + gridCnt.incrementAndGet());
        }

        return res;
    }

    /**
     * Creates cli options.
     *
     * @return Command line options
     */
    private static Options createOptions() {
        Options options = new Options();

        OptionGroup grp = new OptionGroup();

        grp.setRequired(true);

        Option cfg = new Option(OPTION_CFG, null, true, "path to Spring XML configuration file.");

        cfg.setArgName("file");

        Option n = new Option(null, OPTION_N, true, "nodes count.");

        n.setValueSeparator('=');
        n.setType(Integer.class);

        grp.addOption(cfg);
        grp.addOption(n);

        options.addOptionGroup(grp);

        return options;
    }
}
