/*
 * 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.internal.managers;

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;
import javax.cache.expiry.Duration;
import javax.cache.expiry.ExpiryPolicy;
import javax.cache.expiry.TouchedExpiryPolicy;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteException;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.cluster.ClusterNode;
import org.apache.ignite.events.Event;
import org.apache.ignite.internal.GridComponent;
import org.apache.ignite.internal.GridKernalContext;
import org.apache.ignite.internal.IgniteInternalFuture;
import org.apache.ignite.internal.managers.communication.GridMessageListener;
import org.apache.ignite.internal.managers.eventstorage.GridLocalEventListener;
import org.apache.ignite.internal.processors.timeout.GridSpiTimeoutObject;
import org.apache.ignite.internal.util.tostring.GridToStringExclude;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.A;
import org.apache.ignite.internal.util.typedef.internal.CU;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.lang.IgniteBiPredicate;
import org.apache.ignite.lang.IgniteFuture;
import org.apache.ignite.plugin.extensions.communication.Message;
import org.apache.ignite.plugin.extensions.communication.MessageFactory;
import org.apache.ignite.plugin.extensions.communication.MessageFormatter;
import org.apache.ignite.plugin.security.SecuritySubject;
import org.apache.ignite.spi.IgniteNodeValidationResult;
import org.apache.ignite.spi.IgnitePortProtocol;
import org.apache.ignite.spi.IgniteSpi;
import org.apache.ignite.spi.IgniteSpiAdapter;
import org.apache.ignite.spi.IgniteSpiContext;
import org.apache.ignite.spi.IgniteSpiException;
import org.apache.ignite.spi.IgniteSpiNoop;
import org.apache.ignite.spi.IgniteSpiTimeoutObject;
import org.apache.ignite.spi.discovery.DiscoveryDataBag;
import org.apache.ignite.spi.discovery.DiscoveryDataBag.GridDiscoveryData;
import org.apache.ignite.spi.discovery.DiscoveryDataBag.JoiningNodeDiscoveryData;
import org.apache.ignite.spi.metric.ReadOnlyMetricRegistry;
import org.jetbrains.annotations.Nullable;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.apache.ignite.internal.managers.communication.GridIoPolicy.SYSTEM_POOL;

/**
 * Convenience adapter for grid managers.
 *
 * @param <T> SPI wrapped by this manager.
 *
 */
public abstract class GridManagerAdapter<T extends IgniteSpi> implements GridManager {
    /** Kernal context. */
    @GridToStringExclude
    protected final GridKernalContext ctx;

    /** Logger. */
    @GridToStringExclude
    protected final IgniteLogger log;

    /** Set of SPIs for this manager. */
    @GridToStringExclude
    private final T[] spis;

    /** Checks is SPI implementation is {@code NO-OP} or not. */
    private final boolean enabled;

    /** */
    private final Map<IgniteSpi, Boolean> spiMap = new IdentityHashMap<>();

    /** */
    @GridToStringExclude
    private boolean injected;

    /**
     * @param ctx Kernal context.
     * @param spis Specific SPI instance.
     */
    protected GridManagerAdapter(GridKernalContext ctx, T... spis) {
        assert spis != null;
        assert spis.length > 0;
        assert ctx != null;

        this.ctx = ctx;
        this.spis = spis;

        boolean enabled = false;

        for (int i = 0; i < spis.length; i++) {
            if (!U.hasAnnotation(this.spis[i].getClass(), IgniteSpiNoop.class))
                enabled = true;
        }

        this.enabled = enabled;

        log = ctx.log(getClass());
    }

    /**
     * Gets wrapped SPI.
     *
     * @return Wrapped SPI.
     */
    protected final T getSpi() {
        return spis[0];
    }

    /**
     * @param name SPI name
     * @return SPI for given name. If {@code null} or empty, then 1st SPI on the list
     *      is returned.
     */
    protected final T getSpi(@Nullable String name) {
        if (F.isEmpty(name))
            return spis[0];

        // Loop through SPI's, not proxies, because
        // proxy.getName() is more expensive than spi.getName().
        for (T t : spis) {
            if (t.getName().equals(name))
                return t;
        }

        throw new IgniteException("Failed to find SPI for name: " + name);
    }

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

    /**
     * @return Configured SPI's.
     */
    protected final T[] getSpis() {
        return spis;
    }

    /**
     * @param spi SPI whose internal objects need to be injected.
     * @throws IgniteCheckedException If injection failed.
     */
    private void inject(IgniteSpi spi) throws IgniteCheckedException {
        if (spi instanceof IgniteSpiAdapter) {
            Collection<Object> injectables = ((IgniteSpiAdapter)spi).injectables();

            if (!F.isEmpty(injectables))
                for (Object o : injectables)
                    ctx.resource().injectGeneric(o);
        }
    }

    /**
     * @param spi SPI whose internal objects need to be injected.
     * @throws IgniteCheckedException If injection failed.
     */
    private void cleanup(IgniteSpi spi) throws IgniteCheckedException {
        if (spi instanceof IgniteSpiAdapter) {
            Collection<Object> injectables = ((IgniteSpiAdapter)spi).injectables();

            if (!F.isEmpty(injectables))
                for (Object o : injectables)
                    ctx.resource().cleanupGeneric(o);
        }
    }

    /** {@inheritDoc} */
    @Override public void onBeforeSpiStart() {
        // No-op.
    }

    /** {@inheritDoc} */
    @Override public void onAfterSpiStart() {
        // No-op.
    }

    /** {@inheritDoc} */
    @Override public void onDisconnected(IgniteFuture<?> reconnectFut) throws IgniteCheckedException {
        for (T t : spis)
            t.onClientDisconnected(reconnectFut);
    }

    /** {@inheritDoc} */
    @Override public IgniteInternalFuture<?> onReconnected(boolean clusterRestarted) throws IgniteCheckedException {
        for (T t : spis)
            t.onClientReconnected(clusterRestarted);

        return null;
    }

    /**
     * Injects resources to SPI.
     *
     * @throws IgniteCheckedException If failed.
     */
    protected void inject() throws IgniteCheckedException {
        if (injected)
            return;

        for (T spi : spis) {
            // Inject all spi resources.
            ctx.resource().inject(spi);

            // Inject SPI internal objects.
            inject(spi);
        }

        injected = true;
    }

    /**
     * Starts wrapped SPI.
     *
     * @throws IgniteCheckedException If wrapped SPI could not be started.
     */
    protected final void startSpi() throws IgniteCheckedException {
        Collection<String> names = U.newHashSet(spis.length);

        for (T spi : spis) {
            if (spi instanceof IgniteSpiAdapter)
                ((IgniteSpiAdapter)spi).onBeforeStart();

            // Save SPI to map to make sure to stop it properly.
            Boolean res = spiMap.put(spi, Boolean.TRUE);

            assert res == null;

            if (!injected) {
                // Inject all spi resources.
                ctx.resource().inject(spi);

                // Inject SPI internal objects.
                inject(spi);
            }

            // Print-out all SPI parameters only in DEBUG mode.
            if (log.isDebugEnabled())
                log.debug("Starting SPI: " + spi);

            if (names.contains(spi.getName()))
                throw new IgniteCheckedException("Duplicate SPI name (need to explicitly configure 'setName()' property): " +
                    spi.getName());

            names.add(spi.getName());

            if (log.isDebugEnabled())
                log.debug("Starting SPI implementation: " + spi.getClass().getName());

            onBeforeSpiStart();

            try {
                spi.spiStart(ctx.igniteInstanceName());
            }
            catch (IgniteSpiException e) {
                throw new IgniteCheckedException("Failed to start SPI: " + spi, e);
            }

            onAfterSpiStart();

            parseNodeAttributes(spi);

            if (log.isDebugEnabled())
                log.debug("SPI module started OK: " + spi.getClass().getName());
        }

        injected = true;
    }

    /**
     * Stops wrapped SPI.
     *
     * @throws IgniteCheckedException If underlying SPI could not be stopped.
     */
    protected final void stopSpi() throws IgniteCheckedException {
        for (T spi : spis) {
            if (spiMap.remove(spi) == null) {
                if (log.isDebugEnabled())
                    log.debug("Will not stop SPI since it has not been started by this manager: " + spi);

                continue;
            }

            if (log.isDebugEnabled())
                log.debug("Stopping SPI: " + spi);

            try {
                spi.spiStop();

                if (log.isDebugEnabled())
                    log.debug("SPI module stopped OK: " + spi.getClass().getName());
            }
            catch (IgniteSpiException e) {
                throw new IgniteCheckedException("Failed to stop SPI: " + spi, e);
            }

            try {
                cleanup(spi);

                ctx.resource().cleanup(spi);
            }
            catch (IgniteCheckedException e) {
                U.error(log, "Failed to remove injected resources from SPI (ignoring): " + spi, e);
            }
        }
    }

    /**
     * @return Uniformly formatted ack string.
     */
    protected final String startInfo() {
        return "Manager started ok: " + getClass().getName();
    }

    /**
     * @return Uniformly formatted ack string.
     */
    protected final String stopInfo() {
        return "Manager stopped ok: " + getClass().getName();
    }

    /** {@inheritDoc} */
    @Override public final void onKernalStart(boolean active) throws IgniteCheckedException {
        for (final IgniteSpi spi : spis) {
            try {
                spi.onContextInitialized(new IgniteSpiContext() {
                    @Override public boolean isStopping() {
                        return ctx.isStopping();
                    }

                    @Override public Collection<ClusterNode> remoteNodes() {
                        return ctx.discovery().remoteNodes();
                    }

                    @Override public Collection<ClusterNode> nodes() {
                        return ctx.discovery().allNodes();
                    }

                    @Override public ClusterNode localNode() {
                        return ctx.discovery().localNode();
                    }

                    @Nullable @Override public ClusterNode node(UUID nodeId) {
                        A.notNull(nodeId, "nodeId");

                        return ctx.discovery().node(nodeId);
                    }

                    @Override public boolean pingNode(UUID nodeId) {
                        A.notNull(nodeId, "nodeId");

                        try {
                            return ctx.discovery().pingNode(nodeId);
                        }
                        catch (IgniteCheckedException e) {
                            throw U.convertException(e);
                        }
                    }

                    @Override public void send(ClusterNode node, Serializable msg, String topic)
                        throws IgniteSpiException {
                        A.notNull(node, "node");
                        A.notNull(msg, "msg");
                        A.notNull(topic, "topic");

                        try {
                            if (msg instanceof Message)
                                ctx.io().sendToCustomTopic(node, topic, (Message)msg, SYSTEM_POOL);
                            else
                                ctx.io().sendUserMessage(Collections.singletonList(node), msg, topic, false, 0, false);
                        }
                        catch (IgniteCheckedException e) {
                            throw unwrapException(e);
                        }
                    }

                    @Override public void addLocalMessageListener(Object topic, IgniteBiPredicate<UUID, ?> p) {
                        A.notNull(topic, "topic");
                        A.notNull(p, "p");

                        ctx.io().addUserMessageListener(topic, p);
                    }

                    @Override public void removeLocalMessageListener(Object topic, IgniteBiPredicate<UUID, ?> p) {
                        A.notNull(topic, "topic");
                        A.notNull(topic, "p");

                        ctx.io().removeUserMessageListener(topic, p);
                    }

                    @Override public void addMessageListener(GridMessageListener lsnr, String topic) {
                        A.notNull(lsnr, "lsnr");
                        A.notNull(topic, "topic");

                        ctx.io().addMessageListener(topic, lsnr);
                    }

                    @Override public boolean removeMessageListener(GridMessageListener lsnr, String topic) {
                        A.notNull(lsnr, "lsnr");
                        A.notNull(topic, "topic");

                        return ctx.io().removeMessageListener(topic, lsnr);
                    }

                    @Override public void addLocalEventListener(GridLocalEventListener lsnr, int... types) {
                        A.notNull(lsnr, "lsnr");

                        ctx.event().addLocalEventListener(lsnr, types);
                    }

                    @Override public boolean removeLocalEventListener(GridLocalEventListener lsnr) {
                        A.notNull(lsnr, "lsnr");

                        return ctx.event().removeLocalEventListener(lsnr);
                    }

                    @Override public boolean isEventRecordable(int... types) {
                        for (int t : types)
                            if (!ctx.event().isRecordable(t))
                                return false;

                        return true;
                    }

                    @Override public void recordEvent(Event evt) {
                        A.notNull(evt, "evt");

                        if (ctx.event().isRecordable(evt.type()))
                            ctx.event().record(evt);
                    }

                    @Override public void registerPort(int port, IgnitePortProtocol proto) {
                        ctx.ports().registerPort(port, proto, spi.getClass());
                    }

                    @Override public void deregisterPort(int port, IgnitePortProtocol proto) {
                        ctx.ports().deregisterPort(port, proto, spi.getClass());
                    }

                    @Override public void deregisterPorts() {
                        ctx.ports().deregisterPorts(spi.getClass());
                    }

                    @Nullable @Override public <K, V> V get(String cacheName, K key) {
                        return ctx.cache().<K, V>jcache(cacheName).get(key);
                    }

                    @Nullable @Override public <K, V> V put(String cacheName, K key, V val, long ttl) {
                        try {
                            if (ttl > 0) {
                                ExpiryPolicy plc = new TouchedExpiryPolicy(new Duration(MILLISECONDS, ttl));

                                IgniteCache<K, V> cache = ctx.cache().<K, V>publicJCache(cacheName).withExpiryPolicy(plc);

                                return cache.getAndPut(key, val);
                            }
                            else
                                return ctx.cache().<K, V>jcache(cacheName).getAndPut(key, val);
                        }
                        catch (IgniteCheckedException e) {
                            throw CU.convertToCacheException(e);
                        }
                    }

                    @Nullable @Override public <K, V> V putIfAbsent(String cacheName, K key, V val, long ttl) {
                        try {
                            if (ttl > 0) {
                                ExpiryPolicy plc = new TouchedExpiryPolicy(new Duration(MILLISECONDS, ttl));

                                IgniteCache<K, V> cache = ctx.cache().<K, V>publicJCache(cacheName).withExpiryPolicy(plc);

                                return cache.getAndPutIfAbsent(key, val);
                            }
                            else
                                return ctx.cache().<K, V>jcache(cacheName).getAndPutIfAbsent(key, val);
                        }
                        catch (IgniteCheckedException e) {
                            throw CU.convertToCacheException(e);
                        }
                    }

                    @Nullable @Override public <K, V> V remove(String cacheName, K key) {
                        return ctx.cache().<K, V>jcache(cacheName).getAndRemove(key);
                    }

                    @Override public <K> boolean containsKey(String cacheName, K key) {
                        return ctx.cache().cache(cacheName).containsKey(key);
                    }

                    @Override public int partition(String cacheName, Object key) {
                        return ctx.cache().cache(cacheName).affinity().partition(key);
                    }

                    @Override public IgniteNodeValidationResult validateNode(ClusterNode node) {
                        for (GridComponent comp : ctx) {
                            IgniteNodeValidationResult err = comp.validateNode(node);

                            if (err != null)
                                return err;
                        }

                        return null;
                    }

                    @Nullable @Override public IgniteNodeValidationResult validateNode(ClusterNode node, DiscoveryDataBag discoData) {
                        for (GridComponent comp : ctx) {
                            if (comp.discoveryDataType() == null)
                                continue;

                            IgniteNodeValidationResult err =
                                comp.validateNode(node, discoData.newJoinerDiscoveryData(comp.discoveryDataType().ordinal()));

                            if (err != null)
                                return err;
                        }

                        return null;
                    }

                    @Override public Collection<SecuritySubject> authenticatedSubjects() {
                        try {
                            return ctx.security().authenticatedSubjects();
                        }
                        catch (IgniteCheckedException e) {
                            throw U.convertException(e);
                        }
                    }

                    @Override public SecuritySubject authenticatedSubject(UUID subjId) {
                        try {
                            return ctx.security().authenticatedSubject(subjId);
                        }
                        catch (IgniteCheckedException e) {
                            throw U.convertException(e);
                        }
                    }

                    @Override public MessageFormatter messageFormatter() {
                        return ctx.io().formatter();
                    }

                    @Override public MessageFactory messageFactory() {
                        return ctx.io().messageFactory();
                    }

                    @Override public boolean tryFailNode(UUID nodeId, @Nullable String warning) {
                        return ctx.discovery().tryFailNode(nodeId, warning);
                    }

                    @Override public void failNode(UUID nodeId, @Nullable String warning) {
                        ctx.discovery().failNode(nodeId, warning);
                    }

                    @Override public void addTimeoutObject(IgniteSpiTimeoutObject obj) {
                        ctx.timeout().addTimeoutObject(new GridSpiTimeoutObject(obj));
                    }

                    @Override public void removeTimeoutObject(IgniteSpiTimeoutObject obj) {
                        ctx.timeout().removeTimeoutObject(new GridSpiTimeoutObject(obj));
                    }

                    @Override public Map<String, Object> nodeAttributes() {
                        return ctx.nodeAttributes();
                    }

                    @Override public boolean communicationFailureResolveSupported() {
                        return ctx.discovery().communicationErrorResolveSupported();
                    }

                    @Override public void resolveCommunicationFailure(ClusterNode node, Exception err) {
                        ctx.discovery().resolveCommunicationError(node, err);
                    }

                    @Override public ReadOnlyMetricRegistry getOrCreateMetricRegistry(String name) {
                        return ctx.metric().registry(name);
                    }

                    @Override public void removeMetricRegistry(String name) {
                        ctx.metric().remove(name);
                    }

                    @Override public Iterable<ReadOnlyMetricRegistry> metricRegistries() {
                        return ctx.metric();
                    }

                    @Override public void addMetricRegistryCreationListener(Consumer<ReadOnlyMetricRegistry> lsnr) {
                        ctx.metric().addMetricRegistryCreationListener(lsnr);
                    }

                    /**
                     * @param e Exception to handle.
                     * @return GridSpiException Converted exception.
                     */
                    private IgniteSpiException unwrapException(IgniteCheckedException e) {
                        // Avoid double-wrapping.
                        if (e.getCause() instanceof IgniteSpiException)
                            return (IgniteSpiException)e.getCause();

                        return new IgniteSpiException("Failed to execute SPI context method.", e);
                    }
                });
            }
            catch (IgniteSpiException e) {
                throw new IgniteCheckedException("Failed to initialize SPI context.", e);
            }
        }

        onKernalStart0();
    }

    /** {@inheritDoc} */
    @Override public final void onKernalStop(boolean cancel) {
        onKernalStop0(cancel);

        for (IgniteSpi spi : spis)
            spi.onContextDestroyed();
    }

    /** {@inheritDoc} */
    @Nullable @Override public DiscoveryDataExchangeType discoveryDataType() {
        return null;
    }

    /** {@inheritDoc} */
    @Override public void collectJoiningNodeData(DiscoveryDataBag dataBag) {
        // No-op.
    }

    /** {@inheritDoc} */
    @Override public void collectGridNodeData(DiscoveryDataBag dataBag) {
        // No-op.
    }

    /** {@inheritDoc} */
    @Override public void onGridDataReceived(GridDiscoveryData data) {
        // No-op.
    }

    /** {@inheritDoc} */
    @Override public void onJoiningNodeDataReceived(JoiningNodeDiscoveryData data) {
        // No-op.
    }

    /**
     * @throws IgniteCheckedException If failed.
     */
    protected void onKernalStart0() throws IgniteCheckedException {
        // No-op.
    }

    /**
     * @param cancel Cancel flag.
     */
    protected void onKernalStop0(boolean cancel) {
        // No-op.
    }

    /**
     * Throws exception with uniform error message if given parameter's assertion condition
     * is {@code false}.
     *
     * @param cond Assertion condition to check.
     * @param condDesc Description of failed condition. Note that this description should include
     *      JavaBean name of the property (<b>not</b> a variable name) as well condition in
     *      Java syntax like, for example:
     *      <pre name="code" class="java">
     *      ...
     *      assertParameter(dirPath != null, "dirPath != null");
     *      ...
     *      </pre>
     *      Note that in case when variable name is the same as JavaBean property you
     *      can just copy Java condition expression into description as a string.
     * @throws IgniteCheckedException Thrown if given condition is {@code false}
     */
    protected final void assertParameter(boolean cond, String condDesc) throws IgniteCheckedException {
        if (!cond)
            throw new IgniteCheckedException("Grid configuration parameter failed condition check: " + condDesc);
    }

    /** {@inheritDoc} */
    @Override public void printMemoryStats() {
        // No-op.
    }

    /** {@inheritDoc} */
    @Nullable @Override public IgniteNodeValidationResult validateNode(ClusterNode node) {
        return null;
    }

    /** {@inheritDoc} */
    @Nullable @Override public IgniteNodeValidationResult validateNode(
        ClusterNode node,
        DiscoveryDataBag.JoiningNodeDiscoveryData discoData
    ) {
        return null;
    }

    /** {@inheritDoc} */
    @Override public final String toString() {
        return S.toString(GridManagerAdapter.class, this, "name", getClass().getName());
    }

    /**
     * Read attributes from Spi and set to node attributes.
     *
     * @param spi Spi which provide the attributes.
     */
    private void parseNodeAttributes(T spi) throws IgniteCheckedException {
        try {
            Map<String, Object> retval = spi.getNodeAttributes();

            if (retval != null) {
                for (Map.Entry<String, Object> e : retval.entrySet()) {
                    if (ctx.hasNodeAttribute(e.getKey()))
                        throw new IgniteCheckedException("SPI attribute collision for attribute [spi=" + spi +
                            ", attr=" + e.getKey() + ']' +
                            ". Attribute set by one SPI implementation has the same name (name collision) as " +
                            "attribute set by other SPI implementation. Such overriding is not allowed. " +
                            "Please check your Ignite configuration and/or SPI implementation to avoid " +
                            "attribute name collisions.");

                    ctx.addNodeAttribute(e.getKey(), e.getValue());
                }
            }
        }
        catch (IgniteSpiException e) {
            throw new IgniteCheckedException("Failed to get SPI attributes.", e);
        }
    }
}
