/*
 * Copyright 2014 the original author or authors.
 *
 * Licensed 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.gradle.launcher.daemon.client;

import org.gradle.api.GradleException;
import org.gradle.api.internal.specs.ExplainingSpec;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.internal.time.CountdownTimer;
import org.gradle.internal.time.TimeProvider;
import org.gradle.internal.time.Timers;
import org.gradle.internal.time.TrueTimeProvider;
import org.gradle.internal.id.IdGenerator;
import org.gradle.launcher.daemon.context.DaemonConnectDetails;
import org.gradle.launcher.daemon.context.DaemonContext;
import org.gradle.launcher.daemon.logging.DaemonMessages;
import org.gradle.launcher.daemon.protocol.Stop;
import org.gradle.launcher.daemon.protocol.StopWhenIdle;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * <p>To stop a daemon:</p>
 *
 * <ul>
 * <li>The client creates a connection to daemon.</li>
 * <li>The client sends exactly one {@link org.gradle.launcher.daemon.protocol.Stop} message.</li>
 * <li>The daemon sends exactly one {@link org.gradle.launcher.daemon.protocol.Result} message. It may no longer send any messages.</li>
 * <li>The client sends a {@link org.gradle.launcher.daemon.protocol.Finished} message once it has received the {@link org.gradle.launcher.daemon.protocol.Result} message.
 *     It may no longer send any messages.</li>
 * <li>The client closes the connection.</li>
 * <li>The daemon closes the connection once it has received the {@link org.gradle.launcher.daemon.protocol.Finished} message.</li>
 * </ul>
 */
public class DaemonStopClient {
    private static final Logger LOGGER = Logging.getLogger(DaemonClient.class);
    private static final int STOP_TIMEOUT_SECONDS = 30;
    private final DaemonConnector connector;
    private final IdGenerator<?> idGenerator;
    private final StopDispatcher stopDispatcher;
    private final TimeProvider timeProvider = new TrueTimeProvider();

    public DaemonStopClient(DaemonConnector connector, IdGenerator<?> idGenerator) {
        this.connector = connector;
        this.idGenerator = idGenerator;
        this.stopDispatcher = new StopDispatcher();
    }

    /**
     * Requests that the given daemons stop when idle. Does not block and returns before the daemons have all stopped.
     */
    public void gracefulStop(Collection<DaemonConnectDetails> daemons) {
        for (DaemonConnectDetails daemon : daemons) {
            DaemonClientConnection connection = connector.maybeConnect(daemon);
            if (connection == null) {
                continue;
            }
            try {
                LOGGER.debug("Requesting daemon {} stop when idle", daemon);
                stopDispatcher.dispatch(connection, new StopWhenIdle(idGenerator.generateId(), connection.getDaemon().getToken()));
                LOGGER.lifecycle("Gradle daemon stopped.");
            } finally {
                connection.stop();
            }
        }
    }

    /**
     * Stops all daemons, blocking until all have completed.
     */
    public void stop() {
        CountdownTimer timer = Timers.startTimer(STOP_TIMEOUT_SECONDS, TimeUnit.SECONDS);
        final Set<String> seen = new HashSet<String>();

        ExplainingSpec<DaemonContext> spec = new ExplainingSpec<DaemonContext>() {
            @Override
            public String whyUnsatisfied(DaemonContext element) {
                return "already seen";
            }

            @Override
            public boolean isSatisfiedBy(DaemonContext element) {
                return !seen.contains(element.getUid());
            }
        };

        DaemonClientConnection connection = connector.maybeConnect(spec);
        if (connection == null) {
            LOGGER.lifecycle(DaemonMessages.NO_DAEMONS_RUNNING);
            return;
        }

        LOGGER.lifecycle("Stopping Daemon(s)");

        //iterate and stop all daemons
        int numStopped = 0;
        while (connection != null && !timer.hasExpired()) {
            try {
                seen.add(connection.getDaemon().getUid());
                LOGGER.debug("Requesting daemon {} stop now", connection.getDaemon());
                boolean stopped = stopDispatcher.dispatch(connection, new Stop(idGenerator.generateId(), connection.getDaemon().getToken()));
                if (stopped) {
                    numStopped++;
                }
            } finally {
                connection.stop();
            }
            connection = connector.maybeConnect(spec);
        }

        if (numStopped > 0) {
            LOGGER.lifecycle(numStopped + " Daemon" + ((numStopped > 1) ? "s" : "") + " stopped");
        }

        if (connection != null) {
            throw new GradleException(String.format("Timeout waiting for all daemons to stop. Waited %s.", timer.getElapsed()));
        }
    }
}
