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

import java.io.InputStream;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.ignite.client.ClientException;
import org.apache.ignite.client.IgniteClient;
import org.apache.ignite.configuration.ClientConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.internal.IgnitionEx;
import org.apache.ignite.internal.client.thin.TcpIgniteClient;
import org.apache.ignite.internal.processors.security.sandbox.SandboxIgniteComponentProxy;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.thread.IgniteThread;
import org.jetbrains.annotations.Nullable;

import static org.apache.ignite.internal.processors.security.SecurityUtils.isInsideSandbox;

/**
 * This class defines a factory for the main Ignite API. It controls Grid life cycle
 * and allows listening for grid events.
 * <h1 class="header">Grid Loaders</h1>
 * Although user can apply grid factory directly to start and stop grid, grid is
 * often started and stopped by grid loaders. Grid loaders can be found in
 * {@link org.apache.ignite.startup} package, for example:
 * <ul>
 * <li>{@link org.apache.ignite.startup.cmdline.CommandLineStartup}</li>
 * <li>{@ignitelink org.apache.ignite.startup.servlet.ServletStartup}</li>
 * </ul>
 * <h1 class="header">Examples</h1>
 * Use {@link #start()} method to start grid with default configuration. You can also use
 * {@link org.apache.ignite.configuration.IgniteConfiguration} to override some default configuration. Below is an
 * example on how to start grid with custom configuration for <strong>URI deployment</strong>.
 * <pre name="code" class="java">
 * IgniteConfiguration cfg = new IgniteConfiguration();
 *
 * GridUriDeployment deploySpi = new GridUriDeployment();
 *
 * deploySpi.setUriList(Collections.singletonList("classes://tmp/output/classes"));
 *
 * cfg.setDeploymentSpi(deploySpi);
 *
 * Ignition.start(cfg);
 * </pre>
 * Here is how a grid instance can be configured from Spring XML configuration file. The
 * example below configures a grid instance with additional user attributes
 * (see {@link org.apache.ignite.cluster.ClusterNode#attributes()}) and specifies a Ignite instance name:
 * <pre name="code" class="xml">
 * &lt;bean id="grid.cfg" class="org.apache.ignite.configuration.IgniteConfiguration"&gt;
 *     ...
 *     &lt;property name="igniteInstanceName" value="grid"/&gt;
 *     &lt;property name="userAttributes"&gt;
 *         &lt;map&gt;
 *             &lt;entry key="group" value="worker"/&gt;
 *         &lt;/map&gt;
 *     &lt;/property&gt;
 *     ...
 * &lt;/bean&gt;
 * </pre>
 * A grid instance with Spring configuration above can be started as following. Note that
 * you do not need to pass path to Spring XML file if you are using
 * {@code IGNITE_HOME/config/default-config.xml}. Also note, that the path can be
 * absolute or relative to IGNITE_HOME.
 * <pre name="code" class="java">
 * ...
 * Ignition.start("/path/to/spring/xml/file.xml");
 * ...
 * </pre>
 * You can also instantiate grid directly from Spring without using {@code Ignition}.
 * For more information refer to {@ignitelink org.apache.ignite.IgniteSpringBean} documentation.
 */
public class Ignition {
    /**
     * This is restart code that can be used by external tools, like Shell scripts,
     * to auto-restart the Ignite JVM process. Note that there is no standard way
     * for a JVM to restart itself from Java application and therefore we rely on
     * external tools to provide that capability.
     * <p>
     * Note that standard <tt>ignite.{sh|bat}</tt> scripts support restarting when
     * JVM process exits with this code.
     */
    public static final int RESTART_EXIT_CODE = 250;

    /**
     * This is kill code that can be used by external tools, like Shell scripts,
     * to auto-stop the Ignite JVM process without restarting.
     */
    public static final int KILL_EXIT_CODE = 130;

    /**
     * Enforces singleton.
     */
    protected Ignition() {
        // No-op.
    }

    /**
     * Sets client mode thread-local flag.
     * <p>
     * This flag used when node is started if {@link IgniteConfiguration#isClientMode()}
     * is {@code null}. When {@link IgniteConfiguration#isClientMode()} is set this flag is ignored.
     *
     * @param clientMode Client mode flag.
     * @see IgniteConfiguration#isClientMode()
     */
    public static void setClientMode(boolean clientMode) {
        IgnitionEx.setClientMode(clientMode);
    }

    /**
     * Gets client mode thread-local flag.
     * <p>
     * This flag used when node is started if {@link IgniteConfiguration#isClientMode()}
     * is {@code null}. When {@link IgniteConfiguration#isClientMode()} is set this flag is ignored.
     *
     * @return Client mode flag.
     * @see IgniteConfiguration#isClientMode()
     */
    public static boolean isClientMode() {
        return IgnitionEx.isClientMode();
    }

    /**
     * Gets state of grid default grid.
     *
     * @return Default grid state.
     */
    public static IgniteState state() {
        return IgnitionEx.state();
    }

    /**
     * Gets states of named Ignite instance. If name is {@code null}, then state of
     * default no-name Ignite instance is returned.
     *
     * @param name Ignite instance name. If name is {@code null}, then state of
     *      default no-name Ignite instance is returned.
     * @return Ignite instance state.
     */
    public static IgniteState state(@Nullable String name) {
        return IgnitionEx.state(name);
    }

    /**
     * Stops default grid. This method is identical to {@code G.stop(null, cancel)} apply.
     * Note that method does not wait for all tasks to be completed.
     *
     * @param cancel If {@code true} then all jobs currently executing on
     *      default grid will be cancelled by calling {@link org.apache.ignite.compute.ComputeJob#cancel()}
     *      method. Note that just like with {@link Thread#interrupt()}, it is
     *      up to the actual job to exit from execution
     * @return {@code true} if default grid instance was indeed stopped,
     *      {@code false} otherwise (if it was not started).
     */
    public static boolean stop(boolean cancel) {
        return IgnitionEx.stop(cancel, null);
    }

    /**
     * Stops named Ignite instance. If {@code cancel} flag is set to {@code true} then
     * all jobs currently executing on local node will be interrupted. If
     * Ignite instance name is {@code null}, then default no-name Ignite instance will be stopped.
     * If wait parameter is set to {@code true} then Ignite instance will wait for all
     * tasks to be finished.
     *
     * @param name Ignite instance name. If {@code null}, then default no-name Ignite instance will
     *      be stopped.
     * @param cancel If {@code true} then all jobs currently will be cancelled
     *      by calling {@link org.apache.ignite.compute.ComputeJob#cancel()} method. Note that just like with
     *      {@link Thread#interrupt()}, it is up to the actual job to exit from
     *      execution. If {@code false}, then jobs currently running will not be
     *      canceled. In either case, grid node will wait for completion of all
     *      jobs running on it before stopping.
     * @return {@code true} if named Ignite instance was indeed found and stopped,
     *      {@code false} otherwise (the instance with given {@code name} was
     *      not found).
     */
    public static boolean stop(String name, boolean cancel) {
        return IgnitionEx.stop(name, cancel, null, false);
    }

    /**
     * Stops <b>all</b> started grids in current JVM. If {@code cancel} flag is set to {@code true} then
     * all jobs currently executing on local node will be interrupted.
     * If wait parameter is set to {@code true} then grid will wait for all
     * tasks to be finished.
     * <p>
     * <b>Note:</b> it is usually safer and more appropriate to stop grid instances individually
     * instead of blanket operation. In most cases, the party that started the grid instance
     * should be responsible for stopping it.
     *
     * @param cancel If {@code true} then all jobs currently executing on
     *      all grids will be cancelled by calling {@link org.apache.ignite.compute.ComputeJob#cancel()}
     *      method. Note that just like with {@link Thread#interrupt()}, it is
     *      up to the actual job to exit from execution
     */
    public static void stopAll(boolean cancel) {
        IgnitionEx.stopAll(cancel, null);
    }

    /**
     * Restarts <b>all</b> started grids. If {@code cancel} flag is set to {@code true} then
     * all jobs currently executing on the local node will be interrupted.
     * If {@code wait} parameter is set to {@code true} then grid will wait for all
     * tasks to be finished.
     * <p>
     * <b>Note:</b> it is usually safer and more appropriate to stop grid instances individually
     * instead of blanket operation. In most cases, the party that started the grid instance
     * should be responsible for stopping it.
     * <p>
     * Note also that restarting functionality only works with the tools that specifically
     * support Ignite's protocol for restarting. Currently only standard <tt>ignite.{sh|bat}</tt>
     * scripts support restarting of JVM Ignite's process.
     *
     * @param cancel If {@code true} then all jobs currently executing on
     *      all grids will be cancelled by calling {@link org.apache.ignite.compute.ComputeJob#cancel()}
     *      method. Note that just like with {@link Thread#interrupt()}, it is
     *      up to the actual job to exit from execution.
     * @see #RESTART_EXIT_CODE
     */
    public static void restart(boolean cancel) {
        IgnitionEx.restart(cancel);
    }

    /**
     * Stops <b>all</b> started grids. If {@code cancel} flag is set to {@code true} then
     * all jobs currently executing on the local node will be interrupted.
     * If {@code wait} parameter is set to {@code true} then grid will wait for all
     * tasks to be finished.
     * <p>
     * <b>Note:</b> it is usually safer and more appropriate to stop grid instances individually
     * instead of blanket operation. In most cases, the party that started the grid instance
     * should be responsible for stopping it.
     * <p>
     * Note that upon completion of this method, the JVM with forcefully exist with
     * exit code {@link #KILL_EXIT_CODE}.
     *
     * @param cancel If {@code true} then all jobs currently executing on
     *      all grids will be cancelled by calling {@link org.apache.ignite.compute.ComputeJob#cancel()}
     *      method. Note that just like with {@link Thread#interrupt()}, it is
     *      up to the actual job to exit from execution.
     * @see #KILL_EXIT_CODE
     */
    public static void kill(boolean cancel) {
        IgnitionEx.kill(cancel);
    }

    /**
     * Starts grid with default configuration. By default this method will
     * use grid configuration defined in {@code IGNITE_HOME/config/default-config.xml}
     * configuration file. If such file is not found, then all system defaults will be used.
     *
     * @return Started grid.
     * @throws IgniteException If default grid could not be started. This exception will be thrown
     *      also if default grid has already been started.
     */
    public static Ignite start() throws IgniteException {
        try {
            return wrapToProxyIfNeeded(IgnitionEx.start());
        }
        catch (IgniteCheckedException e) {
            throw U.convertException(e);
        }
    }

    /**
     * Starts grid with given configuration. Note that this method will throw an exception if grid with the name
     * provided in given configuration is already started.
     *
     * @param cfg Grid configuration. This cannot be {@code null}.
     * @return Started grid.
     * @throws IgniteException If grid could not be started. This exception will be thrown
     *      also if named grid has already been started.
     */
    public static Ignite start(IgniteConfiguration cfg) throws IgniteException {
        try {
            return wrapToProxyIfNeeded(IgnitionEx.start(cfg));
        }
        catch (IgniteCheckedException e) {
            throw U.convertException(e);
        }
    }

    /**
     * Starts all grids specified within given Spring XML configuration file. If grid with given name
     * is already started, then exception is thrown. In this case all instances that may
     * have been started so far will be stopped too.
     * <p>
     * Usually Spring XML configuration file will contain only one Grid definition. Note that
     * Grid configuration bean(s) is retrieved form configuration file by type, so the name of
     * the Grid configuration bean is ignored.
     *
     * @param springCfgPath Spring XML configuration file path or URL.
     * @return Started grid. If Spring configuration contains multiple grid instances,
     *      then the 1st found instance is returned.
     * @throws IgniteException If grid could not be started or configuration
     *      read. This exception will be thrown also if grid with given name has already
     *      been started or Spring XML configuration file is invalid.
     */
    public static Ignite start(String springCfgPath) throws IgniteException {
        try {
            return wrapToProxyIfNeeded(IgnitionEx.start(springCfgPath));
        }
        catch (IgniteCheckedException e) {
            throw U.convertException(e);
        }
    }

    /**
     * Starts all grids specified within given Spring XML configuration file URL. If grid with given name
     * is already started, then exception is thrown. In this case all instances that may
     * have been started so far will be stopped too.
     * <p>
     * Usually Spring XML configuration file will contain only one Grid definition. Note that
     * Grid configuration bean(s) is retrieved form configuration file by type, so the name of
     * the Grid configuration bean is ignored.
     *
     * @param springCfgUrl Spring XML configuration file URL. This cannot be {@code null}.
     * @return Started grid. If Spring configuration contains multiple grid instances,
     *      then the 1st found instance is returned.
     * @throws IgniteException If grid could not be started or configuration
     *      read. This exception will be thrown also if grid with given name has already
     *      been started or Spring XML configuration file is invalid.
     */
    public static Ignite start(URL springCfgUrl) throws IgniteException {
        try {
            return wrapToProxyIfNeeded(IgnitionEx.start(springCfgUrl));
        }
        catch (IgniteCheckedException e) {
            throw U.convertException(e);
        }
    }

    /**
     * Starts all grids specified within given Spring XML configuration input stream. If grid with given name
     * is already started, then exception is thrown. In this case all instances that may
     * have been started so far will be stopped too.
     * <p>
     * Usually Spring XML configuration input stream will contain only one Grid definition. Note that
     * Grid configuration bean(s) is retrieved form configuration input stream by type, so the name of
     * the Grid configuration bean is ignored.
     *
     * @param springCfgStream Input stream containing Spring XML configuration. This cannot be {@code null}.
     * @return Started grid. If Spring configuration contains multiple grid instances,
     *      then the 1st found instance is returned.
     * @throws IgniteException If grid could not be started or configuration
     *      read. This exception will be thrown also if grid with given name has already
     *      been started or Spring XML configuration file is invalid.
     */
    public static Ignite start(InputStream springCfgStream) throws IgniteException {
        try {
            return wrapToProxyIfNeeded(IgnitionEx.start(springCfgStream));
        }
        catch (IgniteCheckedException e) {
            throw U.convertException(e);
        }
    }

    /**
     * Gets or starts new grid instance if it hasn't been started yet.
     *
     * @param cfg Grid configuration. This cannot be {@code null}.
     * @return Grid instance.
     * @throws IgniteException If grid could not be started.
     */
    public static Ignite getOrStart(IgniteConfiguration cfg) throws IgniteException {
        try {
            return wrapToProxyIfNeeded(IgnitionEx.start(cfg, false));
        }
        catch (IgniteCheckedException e) {
            throw U.convertException(e);
        }
    }

    /**
     * Loads Spring bean by its name from given Spring XML configuration file. If bean
     * with such name doesn't exist, exception is thrown.
     *
     * @param springXmlPath Spring XML configuration file path (cannot be {@code null}).
     * @param beanName Bean name (cannot be {@code null}).
     * @param <T> Type of the loaded bean.
     * @return Loaded bean instance.
     * @throws IgniteException If bean with provided name was not found or in case any other error.
     */
    public static <T> T loadSpringBean(String springXmlPath, String beanName) throws IgniteException {
        try {
            return IgnitionEx.loadSpringBean(springXmlPath, beanName);
        }
        catch (IgniteCheckedException e) {
            throw U.convertException(e);
        }
    }

    /**
     * Loads Spring bean by its name from given Spring XML configuration file. If bean
     * with such name doesn't exist, exception is thrown.
     *
     * @param springXmlUrl Spring XML configuration file URL (cannot be {@code null}).
     * @param beanName Bean name (cannot be {@code null}).
     * @param <T> Type of the loaded bean.
     * @return Loaded bean instance.
     * @throws IgniteException If bean with provided name was not found or in case any other error.
     */
    public static <T> T loadSpringBean(URL springXmlUrl, String beanName) throws IgniteException {
        try {
            return IgnitionEx.loadSpringBean(springXmlUrl, beanName);
        }
        catch (IgniteCheckedException e) {
            throw U.convertException(e);
        }
    }

    /**
     * Loads Spring bean by its name from given Spring XML configuration file. If bean
     * with such name doesn't exist, exception is thrown.
     *
     * @param springXmlStream Input stream containing Spring XML configuration (cannot be {@code null}).
     * @param beanName Bean name (cannot be {@code null}).
     * @param <T> Type of the loaded bean.
     * @return Loaded bean instance.
     * @throws IgniteException If bean with provided name was not found or in case any other error.
     */
    public static <T> T loadSpringBean(InputStream springXmlStream, String beanName) throws IgniteException {
        try {
            return IgnitionEx.loadSpringBean(springXmlStream, beanName);
        }
        catch (IgniteCheckedException e) {
            throw U.convertException(e);
        }
    }

    /**
     * Gets an instance of default no-name grid. Note that
     * caller of this method should not assume that it will return the same
     * instance every time.
     *
     * @return An instance of default no-name grid. This method never returns
     *      {@code null}.
     * @throws IgniteIllegalStateException Thrown if default grid was not properly
     *      initialized or grid instance was stopped or was not started.
     */
    public static Ignite ignite() throws IgniteIllegalStateException {
        return wrapToProxyIfNeeded(IgnitionEx.grid());
    }

    /**
     * Gets a list of all grids started so far.
     *
     * @return List of all grids started so far.
     */
    public static List<Ignite> allGrids() {
        List<Ignite> res = IgnitionEx.allGrids();

        if (F.isEmpty(res))
            return res;

        if (isInsideSandbox())
            return res.stream().map(Ignition::wrapToProxy).collect(Collectors.toList());

        return res;
    }

    /**
     * Gets a grid instance for given local node ID. Note that grid instance and local node have
     * one-to-one relationship where node has ID and instance has name of the grid to which
     * both grid instance and its node belong. Note also that caller of this method
     * should not assume that it will return the same instance every time.
     *
     * @param locNodeId ID of local node the requested grid instance is managing.
     * @return An instance of named grid. This method never returns
     *      {@code null}.
     * @throws IgniteIllegalStateException Thrown if grid was not properly
     *      initialized or grid instance was stopped or was not started.
     */
    public static Ignite ignite(UUID locNodeId) throws IgniteIllegalStateException {
        return wrapToProxyIfNeeded(IgnitionEx.grid(locNodeId));
    }

    /**
     * Gets a named Ignite instance. If Ignite instance name is {@code null} or empty string,
     * then default no-name Ignite instance will be returned. Note that caller of this method
     * should not assume that it will return the same instance every time.
     * <p>
     * The name allows having multiple Ignite instances with different names within the same Java VM.
     *
     * @param name Ignite instance name. If {@code null}, then a default no-name
     *      Ignite instance will be returned.
     * @return A named Ignite instance. This method never returns {@code null}.
     * @throws IgniteIllegalStateException Thrown if default Ignite instance was not properly
     *      initialized or Ignite instance was stopped or was not started.
     */
    public static Ignite ignite(@Nullable String name) throws IgniteIllegalStateException {
        return wrapToProxyIfNeeded(IgnitionEx.grid(name));
    }

    /**
     * This method is used to address a local {@link Ignite} instance, principally from closure.
     * <p>
     * According to contract this method has to be called only under {@link IgniteThread}.
     * An {@link IllegalArgumentException} will be thrown otherwise.
     *
     * @return A current {@link Ignite} instance to address from closure.
     * @throws IgniteIllegalStateException Thrown if grid was not properly
     *      initialized or grid instance was stopped or was not started
     * @throws IllegalArgumentException Thrown if current thread is not an {@link IgniteThread}.
     */
    public static Ignite localIgnite() throws IgniteIllegalStateException, IllegalArgumentException {
        return wrapToProxyIfNeeded(IgnitionEx.localIgnite());
    }

    /**
     * @param ignite Ignite.
     * @return Ignite component proxy.
     */
    private static Ignite wrapToProxy(Ignite ignite) {
        return AccessController.doPrivileged((PrivilegedAction<Ignite>)
            () -> SandboxIgniteComponentProxy.igniteProxy(ignite)
        );
    }

    /**
     * @param ignite Ignite.
     * @return Ignite component proxy if the Ignite Sandbox is enabled.
     */
    private static Ignite wrapToProxyIfNeeded(Ignite ignite) {
        return isInsideSandbox() ? wrapToProxy(ignite) : ignite;
    }

    /**
     * Adds a lsnr for grid life cycle events.
     * <p>
     * Note that unlike other listeners in Ignite this listener will be
     * notified from the same thread that triggers the state change. Because of
     * that it is the responsibility of the user to make sure that listener logic
     * is light-weight and properly handles (catches) any runtime exceptions, if any
     * are expected.
     *
     * @param lsnr Listener for grid life cycle events. If this listener was already added
     *      this method is no-op.
     */
    public static void addListener(IgnitionListener lsnr) {
        IgnitionEx.addListener(lsnr);
    }

    /**
     * Removes lsnr added by {@link #addListener(IgnitionListener)} method.
     *
     * @param lsnr Listener to remove.
     * @return {@code true} if lsnr was added before, {@code false} otherwise.
     */
    public static boolean removeListener(IgnitionListener lsnr) {
        return IgnitionEx.removeListener(lsnr);
    }

    /**
     * Initializes new instance of {@link IgniteClient}.
     *
     * @param cfg Thin client configuration.
     * @return Client with successfully opened thin client connection.
     */
    public static IgniteClient startClient(ClientConfiguration cfg) throws ClientException {
        Objects.requireNonNull(cfg, "cfg");

        return TcpIgniteClient.start(cfg);
    }
}
