/*
 * 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.testframework.junits.multijvm;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import javax.cache.CacheException;
import org.apache.ignite.DataRegionMetrics;
import org.apache.ignite.DataRegionMetricsAdapter;
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteAtomicLong;
import org.apache.ignite.IgniteAtomicReference;
import org.apache.ignite.IgniteAtomicSequence;
import org.apache.ignite.IgniteAtomicStamped;
import org.apache.ignite.IgniteBinary;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.IgniteCompute;
import org.apache.ignite.IgniteCountDownLatch;
import org.apache.ignite.IgniteDataStreamer;
import org.apache.ignite.IgniteEncryption;
import org.apache.ignite.IgniteEvents;
import org.apache.ignite.IgniteException;
import org.apache.ignite.IgniteIllegalStateException;
import org.apache.ignite.IgniteLock;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.IgniteMessaging;
import org.apache.ignite.IgniteQueue;
import org.apache.ignite.IgniteScheduler;
import org.apache.ignite.IgniteSemaphore;
import org.apache.ignite.IgniteServices;
import org.apache.ignite.IgniteSet;
import org.apache.ignite.IgniteSnapshot;
import org.apache.ignite.IgniteTransactions;
import org.apache.ignite.Ignition;
import org.apache.ignite.MemoryMetrics;
import org.apache.ignite.cache.affinity.Affinity;
import org.apache.ignite.cluster.ClusterGroup;
import org.apache.ignite.cluster.ClusterNode;
import org.apache.ignite.configuration.AtomicConfiguration;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.configuration.CollectionConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.configuration.NearCacheConfiguration;
import org.apache.ignite.events.DiscoveryEvent;
import org.apache.ignite.events.Event;
import org.apache.ignite.events.EventType;
import org.apache.ignite.internal.GridKernalContext;
import org.apache.ignite.internal.IgniteEx;
import org.apache.ignite.internal.IgniteInterruptedCheckedException;
import org.apache.ignite.internal.binary.BinaryMarshaller;
import org.apache.ignite.internal.cluster.IgniteClusterEx;
import org.apache.ignite.internal.management.IgniteCommandRegistry;
import org.apache.ignite.internal.processors.cache.GridCacheUtilityKey;
import org.apache.ignite.internal.processors.cache.IgniteInternalCache;
import org.apache.ignite.internal.util.GridJavaProcess;
import org.apache.ignite.internal.util.lang.IgnitePredicateX;
import org.apache.ignite.internal.util.typedef.G;
import org.apache.ignite.internal.util.typedef.X;
import org.apache.ignite.internal.util.typedef.internal.A;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.lang.IgniteBiTuple;
import org.apache.ignite.lang.IgniteCallable;
import org.apache.ignite.lang.IgniteInClosure;
import org.apache.ignite.lang.IgnitePredicate;
import org.apache.ignite.lang.IgniteProductVersion;
import org.apache.ignite.lang.IgniteRunnable;
import org.apache.ignite.marshaller.Marshaller;
import org.apache.ignite.metric.IgniteMetrics;
import org.apache.ignite.plugin.IgnitePlugin;
import org.apache.ignite.plugin.PluginNotFoundException;
import org.apache.ignite.resources.IgniteInstanceResource;
import org.apache.ignite.spi.tracing.TracingConfigurationManager;
import org.apache.ignite.testframework.junits.IgniteTestResources;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import static org.junit.Assert.fail;

/**
 * Ignite proxy for ignite instance at another JVM.
 */
@SuppressWarnings("TransientFieldInNonSerializableClass")
public class IgniteProcessProxy implements IgniteEx {
    /** Grid proxies. */
    private static final transient ConcurrentMap<String, IgniteProcessProxy> gridProxies = new ConcurrentHashMap<>();

    /** Property that specify alternative {@code JAVA_HOME}. */
    private static final String TEST_MULTIJVM_JAVA_HOME = "test.multijvm.java.home";

    /** Waiting milliseconds of the left of a node to topology. */
    private static final long NODE_LEFT_TIMEOUT = 30_000L;

    /** Jvm process with ignite instance. */
    private final transient GridJavaProcess proc;

    /** Configuration. */
    private final transient IgniteConfiguration cfg;

    /** Local JVM grid. */
    private final transient Supplier<Ignite> locJvmGrid;

    /** Logger. */
    private final transient IgniteLogger log;

    /** Grid id. */
    private final UUID id;

    /**
     * @param cfg Configuration.
     * @param log Logger.
     * @param locJvmGrid Local JVM grid.
     * @throws Exception On error.
     */
    public IgniteProcessProxy(IgniteConfiguration cfg, IgniteLogger log, Ignite locJvmGrid)
        throws Exception {
        this(cfg, log, locJvmGrid == null ? null : () -> locJvmGrid, true);
    }

    /**
     * @param cfg Configuration.
     * @param log Logger.
     * @param locJvmGrid Local JVM grid.
     * @throws Exception On error.
     */
    public IgniteProcessProxy(IgniteConfiguration cfg, IgniteLogger log, Supplier<Ignite> locJvmGrid, boolean discovery)
        throws Exception {
        this(cfg, log, locJvmGrid, discovery, Collections.emptyList());
    }

    /**
     * @param cfg Configuration.
     * @param log Logger.
     * @param locJvmGrid Local JVM grid.
     * @param resetDiscovery Reset DiscoverySpi at the configuration.
     * @throws Exception On error.
     */
    public IgniteProcessProxy(
        IgniteConfiguration cfg,
        IgniteLogger log,
        Supplier<Ignite> locJvmGrid,
        boolean resetDiscovery,
        List<String> additionalArgs
    )
        throws Exception {
        this.cfg = cfg;
        this.id = cfg.getNodeId() == null ? UUID.randomUUID() : cfg.getNodeId();
        this.locJvmGrid = locJvmGrid;
        this.log = logger(log, "jvm-" + id.toString().substring(0, id.toString().indexOf('-')));

        final String javaHome = System.getProperty(TEST_MULTIJVM_JAVA_HOME);
        validateRemoteJre(javaHome);

        String params = params(cfg, resetDiscovery);

        Collection<String> filteredJvmArgs = filteredJvmArgs();
        filteredJvmArgs.addAll(additionalArgs);

        final CountDownLatch rmtNodeStartedLatch = new CountDownLatch(1);

        if (locJvmGrid != null)
            locJvmGrid.get().events()
                .localListen(new NodeStartedListener(id, rmtNodeStartedLatch), EventType.EVT_NODE_JOINED);

        proc = GridJavaProcess.exec(
            igniteNodeRunnerClassName(),
            params,
            this.log,
            // Optional closure to be called each time wrapped process prints line to system.out or system.err.
            (IgniteInClosure<String>)this.log::info,
            null,
            javaHome,
            filteredJvmArgs,
            System.getProperty("surefire.test.class.path")
        );

        if (locJvmGrid != null)
            assert rmtNodeStartedLatch.await(30, TimeUnit.SECONDS) : "Remote node has not joined [id=" + id + ']';

        IgniteProcessProxy prevVal = gridProxies.putIfAbsent(cfg.getIgniteInstanceName(), this);

        if (prevVal != null) {
            remoteCompute().run(new StopGridTask(cfg.getIgniteInstanceName(), true));

            throw new IllegalStateException("There was found instance assotiated with " + cfg.getIgniteInstanceName() +
                ", instance= " + prevVal + ". New started node was stopped.");
        }
    }

    /**
     * Validates that the JRE corresponding to the given Java home is valid for use as a remote JVM.
     * This currently means only checking that its major version matches the major version of the JRE we run on.
     *
     * @param javaHome Java home.
     * @throws IOException          If I/O fails when interacting with 'java' process.
     * @throws InterruptedException If we get interrupted.
     */
    private static void validateRemoteJre(@Nullable String javaHome) throws IOException, InterruptedException {
        int remoteMajorVer = new JavaVersionCommand().majorVersion(javaHome);
        int locMajorVer = U.majorJavaVersion(System.getProperty("java.version"));

        if (locMajorVer != remoteMajorVer) {
            fail("Version of remote java with home at '" + javaHome + "' (" + remoteMajorVer +
                ") is different from local java version (" + locMajorVer + "). " +
                "Make sure test.multijvm.java.home property specifies a path to a correct Java installation");
        }
    }

    /**
     * Creates new logger instance based on given logger and given category.
     *
     * @param log Base logger.
     * @param ctgr Category.
     * @return Initiated logger.
     * @throws Exception In case of an error.
     */
    protected IgniteLogger logger(IgniteLogger log, Object ctgr) throws Exception {
        return log.getLogger(ctgr);
    }

    /**
     * Gets Ignite node runner class name.
     *
     * @return Node runner class name.
     * @throws Exception In case of an error.
     */
    protected String igniteNodeRunnerClassName() throws Exception {
        return IgniteNodeRunner.class.getCanonicalName();
    }

    /**
     * Creates parameters which will be passed to new Ignite Process as command line arguments.
     *
     * @param cfg Configuration.
     * @param resetDiscovery Reset DiscoverySpi at the configuration.
     * @return Params to be passed to new Ignite process.
     * @throws Exception In case of an error.
     */
    protected String params(IgniteConfiguration cfg, boolean resetDiscovery) throws Exception {
        return IgniteNodeRunner.storeToFile(cfg.setNodeId(id), resetDiscovery);
    }

    /**
     * Creates list of JVM arguments to be used to start new Ignite process in separate JVM.
     *
     * @return JVM arguments.
     * @throws Exception In case of an error.
     */
    protected Collection<String> filteredJvmArgs() throws Exception {
        Collection<String> filteredJvmArgs = new ArrayList<>();

        filteredJvmArgs.add("-ea");

        Marshaller marsh = cfg.getMarshaller();

        if (marsh != null)
            filteredJvmArgs.add("-D" + IgniteTestResources.MARSH_CLASS_NAME + "=" + marsh.getClass().getName());
        else
            filteredJvmArgs.add("-D" + IgniteTestResources.MARSH_CLASS_NAME + "=" + BinaryMarshaller.class.getName());

        for (String arg : U.jvmArgs()) {
            if (arg.startsWith("-Xmx") || arg.startsWith("-Xms") ||
                arg.startsWith("-cp") || arg.startsWith("-classpath") ||
                (marsh != null && arg.startsWith("-D" + IgniteTestResources.MARSH_CLASS_NAME)) ||
                arg.startsWith("--add-opens") || arg.startsWith("--add-exports") || arg.startsWith("--add-modules") ||
                arg.startsWith("--patch-module") || arg.startsWith("--add-reads") ||
                arg.startsWith("-XX:+IgnoreUnrecognizedVMOptions"))
                filteredJvmArgs.add(arg);
        }

        return filteredJvmArgs;
    }

    /**
     */
    private static class NodeStartedListener extends IgnitePredicateX<Event> {
        /** Id. */
        private final UUID id;

        /** Remote node started latch. */
        private final CountDownLatch rmtNodeStartedLatch;

        /**
         * @param id Id.
         * @param rmtNodeStartedLatch Remote node started latch.
         */
        NodeStartedListener(UUID id, CountDownLatch rmtNodeStartedLatch) {
            this.id = id;
            this.rmtNodeStartedLatch = rmtNodeStartedLatch;
        }

        /** {@inheritDoc} */
        @Override public boolean applyx(Event e) {
            if (((DiscoveryEvent)e).eventNode().id().equals(id)) {
                rmtNodeStartedLatch.countDown();

                return false;
            }

            return true;
        }
    }

    /**
     * @param igniteInstanceName Ignite instance name.
     * @return Instance by name or exception wiil be thrown.
     */
    public static IgniteProcessProxy ignite(String igniteInstanceName) {
        IgniteProcessProxy res = gridProxies.get(igniteInstanceName);

        if (res == null)
            throw new IgniteIllegalStateException("Grid instance was not properly started " +
                "or was already stopped: " + igniteInstanceName + ". All known grid instances: " +
                gridProxies.keySet());

        return res;
    }

    /**
     * Gracefully shut down the Grid.
     *
     * @param igniteInstanceName Ignite instance name.
     * @param cancel If {@code true} then all jobs currently will be cancelled.
     * @throws Exception In case of the node stopping error.
     */
    public static void stop(String igniteInstanceName, boolean cancel) throws Exception {
        final IgniteProcessProxy proxy = gridProxies.get(igniteInstanceName);

        if (proxy != null) {
            final CountDownLatch rmtNodeStoppedLatch = new CountDownLatch(1);
            final UUID rmNodeId = proxy.getId();

            proxy.localJvmGrid().events().localListen(new IgnitePredicateX<Event>() {
                @Override public boolean applyx(Event e) {
                    if (((DiscoveryEvent)e).eventNode().id().equals(rmNodeId)) {
                        rmtNodeStoppedLatch.countDown();

                        return false;
                    }

                    return true;
                }
            }, EventType.EVT_NODE_LEFT);

            try {
                proxy.remoteCompute().runAsync(new StopGridTask(igniteInstanceName, cancel));

                if (!rmtNodeStoppedLatch.await(NODE_LEFT_TIMEOUT, TimeUnit.MILLISECONDS))
                    throw new IllegalStateException("Remote node has not stopped [id=" + rmNodeId + ']');
            }
            catch (Throwable t) {
                proxy.log().error("Failed to stop grid [igniteInstanceName=" + igniteInstanceName +
                    ", cancel=" + cancel + ']', t);

                throw t;
            }

            proxy.getProcess().kill();

            gridProxies.remove(igniteInstanceName, proxy);
        }
    }

    /**
     * Forcefully shut down the Grid.
     *
     * @param igniteInstanceName Ignite instance name.
     */
    public static void kill(String igniteInstanceName) {
        A.notNull(igniteInstanceName, "igniteInstanceName");

        IgniteProcessProxy proxy = gridProxies.get(igniteInstanceName);

        if (proxy == null)
            return;

        if (proxy == null)
            return;

        try {
            proxy.getProcess().kill();
        }
        catch (Exception e) {
            U.error(proxy.log, "Exception while killing " + igniteInstanceName, e);
        }

        gridProxies.remove(igniteInstanceName, proxy);
    }

    /**
     * @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) {
        A.notNull(locNodeId, "locNodeId");

        for (IgniteProcessProxy ignite : gridProxies.values()) {
            if (ignite.getId().equals(locNodeId))
                return ignite;
        }

        throw new IgniteIllegalStateException("Grid instance with given local node ID was not properly " +
            "started or was stopped: " + locNodeId);
    }

    /**
     * Kill all running processes.
     */
    public static void killAll() {
        for (IgniteProcessProxy ignite : gridProxies.values()) {
            try {
                ignite.getProcess().kill();
            }
            catch (Exception e) {
                U.error(ignite.log, "Killing failed.", e);
            }
        }

        gridProxies.clear();
    }

    /**
     * @return Local JVM grid instance.
     */
    public Ignite localJvmGrid() {
        return locJvmGrid.get();
    }

    /**
     * @return Grid id.
     */
    public UUID getId() {
        return id;
    }

    /**
     * @throws Exception If failed to kill.
     */
    public void kill() throws Exception {
        getProcess().kill();

        gridProxies.remove(cfg.getGridName(), this);
    }

    /** {@inheritDoc} */
    @Override public String name() {
        return cfg.getIgniteInstanceName();
    }

    /** {@inheritDoc} */
    @Override public IgniteLogger log() {
        return log;
    }

    /** {@inheritDoc} */
    @Override public IgniteConfiguration configuration() {
        return cfg;
    }

    /** {@inheritDoc} */
    @Override public <K extends GridCacheUtilityKey, V> IgniteInternalCache<K, V> utilityCache() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Nullable @Override public <K, V> IgniteInternalCache<K, V> cachex(@Nullable String name) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public Collection<IgniteInternalCache<?, ?>> cachesx(
        @Nullable IgnitePredicate<? super IgniteInternalCache<?, ?>>... p) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public boolean eventUserRecordable(int type) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public boolean allEventsUserRecordable(int[] types) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public boolean isJmxRemoteEnabled() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public boolean isRestartEnabled() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteClusterEx cluster() {
        return new IgniteClusterProcessProxy(this);
    }

    /** {@inheritDoc} */
    @Nullable @Override public String latestVersion() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public ClusterNode localNode() {
        return remoteCompute().call(new NodeTask());
    }

    /** {@inheritDoc} */
    @Override public GridKernalContext context() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public boolean isRebalanceEnabled() {
        return true;
    }

    /** {@inheritDoc} */
    @Override public void rebalanceEnabled(boolean rebalanceEnabled) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <T> IgniteSet<T> set(String name, int cacheId, boolean collocated, boolean separated) throws IgniteException {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteCommandRegistry commandsRegistry() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteCompute compute() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteMetrics metrics() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteCompute compute(ClusterGroup grp) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteMessaging message() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteMessaging message(ClusterGroup grp) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteEvents events() {
        return new IgniteEventsProcessProxy(this);
    }

    /** {@inheritDoc} */
    @Override public IgniteEvents events(ClusterGroup grp) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteServices services() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteServices services(ClusterGroup grp) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public ExecutorService executorService() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public ExecutorService executorService(ClusterGroup grp) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteProductVersion version() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteScheduler scheduler() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <K, V> IgniteCache<K, V> createCache(CacheConfiguration<K, V> cacheCfg) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public Collection<IgniteCache> createCaches(Collection<CacheConfiguration> cacheCfgs) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <K, V> IgniteCache<K, V> createCache(String cacheName) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <K, V> IgniteCache<K, V> getOrCreateCache(CacheConfiguration<K, V> cacheCfg) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <K, V> IgniteCache<K, V> getOrCreateCache(String cacheName) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public Collection<IgniteCache> getOrCreateCaches(Collection<CacheConfiguration> cacheCfgs) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <K, V> void addCacheConfiguration(CacheConfiguration<K, V> cacheCfg) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <K, V> IgniteCache<K, V> createCache(CacheConfiguration<K, V> cacheCfg,
        NearCacheConfiguration<K, V> nearCfg) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <K, V> IgniteCache<K, V> getOrCreateCache(CacheConfiguration<K, V> cacheCfg,
        NearCacheConfiguration<K, V> nearCfg) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <K, V> IgniteCache<K, V> createNearCache(
        @Nullable String cacheName,
        NearCacheConfiguration<K, V> nearCfg) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <K, V> IgniteCache<K, V> getOrCreateNearCache(@Nullable String cacheName,
        NearCacheConfiguration<K, V> nearCfg) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <K, V> IgniteBiTuple<IgniteCache<K, V>, Boolean> getOrCreateCache0(
        CacheConfiguration<K, V> cacheCfg, boolean sql) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public void destroyCache(String cacheName) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public boolean destroyCache0(String cacheName, boolean sql) throws CacheException {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public void destroyCaches(Collection<String> cacheNames) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <K, V> IgniteCache<K, V> cache(@Nullable final String name) {
        return new IgniteCacheProcessProxy<>(name, this);
    }

    /** {@inheritDoc} */
    @Override public Collection<String> cacheNames() {
        return localJvmGrid().cacheNames();
    }

    /** {@inheritDoc} */
    @Override public IgniteTransactions transactions() {
        throw new UnsupportedOperationException("Transactions can't be supported automatically in multi JVM mode.");
    }

    /** {@inheritDoc} */
    @Override public <K, V> IgniteDataStreamer<K, V> dataStreamer(@Nullable String cacheName) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteAtomicSequence atomicSequence(String name, long initVal, boolean create)
        throws IgniteException {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteAtomicSequence atomicSequence(String name, AtomicConfiguration cfg, long initVal,
        boolean create) throws IgniteException {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteAtomicLong atomicLong(String name, long initVal, boolean create) throws IgniteException {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteAtomicLong atomicLong(String name, AtomicConfiguration cfg, long initVal,
        boolean create) throws IgniteException {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <T> IgniteAtomicReference<T> atomicReference(String name, @Nullable T initVal,
        boolean create) throws IgniteException {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <T> IgniteAtomicReference<T> atomicReference(String name, AtomicConfiguration cfg,
        @Nullable T initVal, boolean create) throws IgniteException {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <T, S> IgniteAtomicStamped<T, S> atomicStamped(
        String name,
        @Nullable T initVal,
        @Nullable S initStamp,
        boolean create) throws IgniteException {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <T, S> IgniteAtomicStamped<T, S> atomicStamped(String name, AtomicConfiguration cfg,
        @Nullable T initVal, @Nullable S initStamp, boolean create) throws IgniteException {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteCountDownLatch countDownLatch(String name, int cnt, boolean autoDel,
        boolean create) throws IgniteException {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteSemaphore semaphore(String name, int cnt, boolean failoverSafe,
        boolean create) throws IgniteException {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteLock reentrantLock(String name, boolean failoverSafe,
        boolean fair, boolean create) throws IgniteException {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <T> IgniteQueue<T> queue(String name, int cap,
        @Nullable CollectionConfiguration cfg) throws IgniteException {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <T> IgniteSet<T> set(String name, @Nullable CollectionConfiguration cfg) throws IgniteException {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public <T extends IgnitePlugin> T plugin(String name) throws PluginNotFoundException {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteBinary binary() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public void resetLostPartitions(Collection<String> cacheNames) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public Collection<DataRegionMetrics> dataRegionMetrics() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Nullable @Override public DataRegionMetrics dataRegionMetrics(String memPlcName) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteEncryption encryption() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public IgniteSnapshot snapshot() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public Collection<MemoryMetrics> memoryMetrics() {
        return DataRegionMetricsAdapter.collectionOf(dataRegionMetrics());
    }

    /** {@inheritDoc} */
    @Nullable @Override public MemoryMetrics memoryMetrics(String memPlcName) {
        return DataRegionMetricsAdapter.valueOf(dataRegionMetrics(memPlcName));
    }

    /** {@inheritDoc} */
    @Override public @NotNull TracingConfigurationManager tracingConfiguration() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public void close() throws IgniteException {
        if (localJvmGrid() != null) {
            final CountDownLatch rmtNodeStoppedLatch = new CountDownLatch(1);

            localJvmGrid().events().localListen(new IgnitePredicateX<Event>() {
                @Override public boolean applyx(Event e) {
                    if (((DiscoveryEvent)e).eventNode().id().equals(id)) {
                        rmtNodeStoppedLatch.countDown();

                        return false;
                    }

                    return true;
                }
            }, EventType.EVT_NODE_LEFT, EventType.EVT_NODE_FAILED);

            compute().run(new StopGridTask(localJvmGrid().name(), true));

            try {
                assert U.await(rmtNodeStoppedLatch, 15, TimeUnit.SECONDS) : "NodeId=" + id;
            }
            catch (IgniteInterruptedCheckedException e) {
                throw new IgniteException(e);
            }
        }

        try {
            getProcess().kill();
        }
        catch (Exception e) {
            X.printerr("Could not kill process after close.", e);
        }
    }

    /** {@inheritDoc} */
    @Override public <K> Affinity<K> affinity(String cacheName) {
        return new AffinityProcessProxy<>(cacheName, this);
    }

    /** {@inheritDoc} */
    @Override public boolean active() {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /** {@inheritDoc} */
    @Override public void active(boolean active) {
        throw new UnsupportedOperationException("Operation isn't supported yet.");
    }

    /**
     * @return Jvm process in which grid node started.
     */
    public GridJavaProcess getProcess() {
        return proc;
    }

    /**
     * @return {@link IgniteCompute} instance to communicate with remote node.
     */
    public IgniteCompute remoteCompute() {
        Ignite locJvmGrid = localJvmGrid();

        if (locJvmGrid == null)
            return null;

        ClusterGroup grp = locJvmGrid.cluster().forNodeId(id);

        if (grp.nodes().isEmpty())
            throw new IllegalStateException("Could not found node with id=" + id + ".");

        return locJvmGrid.compute(grp);
    }

    /**
     * Executes {@link Ignition#stop(String, boolean)} with given arguments in a separated thread, doesn't wait up the
     * fulfillment.
     */
    private static class StopGridTask implements IgniteRunnable {
        /** Ignite instance name. */
        private final String igniteInstanceName;

        /** Cancel. */
        private final boolean cancel;

        /**
         * @param igniteInstanceName Ignite instance name.
         * @param cancel Cancel.
         */
        public StopGridTask(String igniteInstanceName, boolean cancel) {
            this.igniteInstanceName = igniteInstanceName;
            this.cancel = cancel;
        }

        /** {@inheritDoc} */
        @Override public void run() {
            CompletableFuture.runAsync(() -> G.stop(igniteInstanceName, cancel));
        }
    }

    /**
     *
     */
    private static class NodeTask implements IgniteCallable<ClusterNode> {
        /** Ignite. */
        @IgniteInstanceResource
        private Ignite ignite;

        /** {@inheritDoc} */
        @Override public ClusterNode call() throws Exception {
            return ((IgniteEx)ignite).localNode();
        }
    }
}
