/*
 * 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.processors.query.schema;

import org.apache.ignite.IgniteLogger;
import org.apache.ignite.cluster.ClusterNode;
import org.apache.ignite.internal.GridKernalContext;
import org.apache.ignite.internal.IgniteInternalFuture;
import org.apache.ignite.internal.processors.query.GridQueryProcessor;
import org.apache.ignite.internal.processors.query.QueryUtils;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.lang.IgniteInClosure;
import org.jetbrains.annotations.Nullable;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.UUID;

/**
 * Schema operation manager.
 */
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
public class SchemaOperationManager {
    /** Kernal context. */
    private final GridKernalContext ctx;

    /** Query processor. */
    private final GridQueryProcessor qryProc;

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

    /** Operation handler. */
    private final SchemaOperationWorker worker;

    /** Mutex for concurrency control. */
    private final Object mux = new Object();

    /** Participants. */
    private Collection<UUID> nodeIds;

    /** Node results. */
    private Map<UUID, SchemaOperationException> nodeRess;

    /** Current coordinator node. */
    private ClusterNode crd;

    /** Whether coordinator state is mapped. */
    private boolean crdMapped;

    /** Coordinator finished flag. */
    private boolean crdFinished;

    /**
     * Constructor.
     *
     * @param ctx Context.
     * @param qryProc Query processor.
     * @param worker Operation handler.
     * @param crd Coordinator node.
     */
    public SchemaOperationManager(GridKernalContext ctx, GridQueryProcessor qryProc, SchemaOperationWorker worker,
        @Nullable ClusterNode crd) {
        assert !ctx.clientNode() || crd == null;

        this.ctx = ctx;

        log = ctx.log(SchemaOperationManager.class);

        this.qryProc = qryProc;
        this.worker = worker;

        synchronized (mux) {
            this.crd = crd;

            prepareCoordinator();
        }
    }

    /**
     * Map operation handling.
     */
    @SuppressWarnings("unchecked")
    public void start() {
        worker.start();

        synchronized (mux) {
            worker.future().listen(new IgniteInClosure<IgniteInternalFuture>() {
                @Override public void apply(IgniteInternalFuture fut) {
                    onLocalNodeFinished(fut);
                }
            });
        }
    }

    /**
     * Handle local node finish.
     *
     * @param fut Future.
     */
    private void onLocalNodeFinished(IgniteInternalFuture fut) {
        assert fut.isDone();

        if (ctx.clientNode())
            return;

        SchemaOperationException err;

        try {
            fut.get();

            err = null;
        }
        catch (Exception e) {
            err = QueryUtils.wrapIfNeeded(e);
        }

        synchronized (mux) {
            if (isLocalCoordinator())
                onNodeFinished(ctx.localNodeId(), err);
            else
                qryProc.sendStatusMessage(crd.id(), operationId(), err);
        }
    }

    /**
     * Handle node finish.
     *
     * @param nodeId Node ID.
     * @param err Error.
     */
    public void onNodeFinished(UUID nodeId, @Nullable SchemaOperationException err) {
        synchronized (mux) {
            assert isLocalCoordinator();

            if (nodeRess.containsKey(nodeId)) {
                if (log.isDebugEnabled())
                    log.debug("Received duplicate result [opId=" + operationId() + ", nodeId=" + nodeId +
                        ", err=" + err + ']');

                return;
            }

            if (nodeIds.contains(nodeId)) {
                if (log.isDebugEnabled())
                    log.debug("Received result [opId=" + operationId() + ", nodeId=" + nodeId + ", err=" + err + ']');

                nodeRess.put(nodeId, err);
            }
            else {
                if (log.isDebugEnabled())
                    log.debug("Received result from non-tracked node (joined after operation started, will ignore) " +
                        "[opId=" + operationId() + ", nodeId=" + nodeId + ", err=" + err + ']');
            }

            checkFinished();
        }
    }

    /**
     * Handle node leave event.
     *
     * @param nodeId ID of the node that has left the grid.
     * @param curCrd Current coordinator node.
     */
    public void onNodeLeave(UUID nodeId, ClusterNode curCrd) {
        synchronized (mux) {
            assert crd != null;

            if (F.eq(nodeId, crd.id())) {
                // Coordinator has left!
                crd = curCrd;

                prepareCoordinator();
            }
            else if (isLocalCoordinator()) {
                // Other node has left, remove it from the coordinator's wait set.
                // Handle this as success.
                if (nodeIds.remove(nodeId))
                    nodeRess.remove(nodeId);
            }

            IgniteInternalFuture fut = worker().future();

            if (fut.isDone())
                onLocalNodeFinished(fut);

            checkFinished();
        }
    }

    /**
     * Check if operation finished.
     */
    private void checkFinished() {
        assert Thread.holdsLock(mux);

        if (isLocalCoordinator()) {
            if (crdFinished)
                return;

            if (nodeIds.size() == nodeRess.size()) {
                // Initiate finish request.
                SchemaOperationException err = null;

                for (Map.Entry<UUID, SchemaOperationException> nodeRes : nodeRess.entrySet()) {
                    if (nodeRes.getValue() != null) {
                        err = nodeRes.getValue();

                        break;
                    }
                }

                if (log.isDebugEnabled())
                    log.debug("Collected all results, about to send finish message [opId=" + operationId() +
                        ", err=" + err + ']');

                crdFinished = true;

                qryProc.onCoordinatorFinished(worker.operation(), err);
            }
        }
    }

    /**
     * Prepare topology state in case local node is coordinator.
     *
     * @return {@code True} if state was changed by this call.
     */
    private boolean prepareCoordinator() {
        if (isLocalCoordinator() && !crdMapped) {
            // Initialize local structures.
            nodeIds = new HashSet<>();
            nodeRess = new HashMap<>();

            for (ClusterNode alive : ctx.discovery().aliveServerNodes())
                nodeIds.add(alive.id());

            if (log.isDebugEnabled())
                log.debug("Mapped participating nodes on coordinator [opId=" + operationId() +
                    ", crdNodeId=" + ctx.localNodeId() + ", nodes=" + nodeIds + ']');

            crdMapped = true;

            return true;
        }

        return false;
    }

    /**
     * Check if current node is local coordinator.
     *
     * @return {@code True} if coordinator.
     */
    private boolean isLocalCoordinator() {
        assert Thread.holdsLock(mux);

        return crd != null && crd.isLocal();
    }

    /**
     * @return Worker.
     */
    public SchemaOperationWorker worker() {
        return worker;
    }

    /**
     * @return Operation ID.
     */
    private UUID operationId() {
        return worker.operation().id();
    }
}