/*
 * 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.qpid.disttest.controller;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.apache.qpid.disttest.DistributedTestException;
import org.apache.qpid.disttest.controller.config.QueueConfig;
import org.apache.qpid.disttest.controller.config.TestInstance;
import org.apache.qpid.disttest.jms.ControllerJmsDelegate;
import org.apache.qpid.disttest.message.Command;
import org.apache.qpid.disttest.message.CommandType;
import org.apache.qpid.disttest.message.ParticipantResult;
import org.apache.qpid.disttest.message.Response;
import org.apache.qpid.disttest.message.StartTestCommand;
import org.apache.qpid.disttest.message.TearDownTestCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TestRunner
{
    private static final Logger LOGGER = LoggerFactory.getLogger(TestRunner.class);

    private static final long PARTICIPANT_RESULTS_LOG_INTERVAL = 60000;
    public static final long WAIT_FOREVER = -1;

    private final long _commandResponseTimeout;

    private final Set<CommandType> _setOfResponsesToExpect = Collections.synchronizedSet(new HashSet<CommandType>());

    private final ParticipatingClients _participatingClients;

    private final TestInstance _testInstance;
    private ControllerJmsDelegate _jmsDelegate;

    private volatile CountDownLatch _commandResponseLatch = null;
    private final CountDownLatch _testResultsLatch;
    private final TestResult _testResult;

    /** Length of time to await test results or {@value #WAIT_FOREVER} */
    private final long _testResultTimeout;


    public TestRunner(ParticipatingClients participatingClients, TestInstance testInstance, ControllerJmsDelegate jmsDelegate, long commandResponseTimeout, long testResultTimeout)
    {
        _participatingClients = participatingClients;
        _testInstance = testInstance;
        _jmsDelegate = jmsDelegate;
        _commandResponseTimeout = commandResponseTimeout;
        _testResultsLatch = new CountDownLatch(testInstance.getTotalNumberOfParticipants());
        _testResultTimeout = testResultTimeout;
        _testResult = new TestResult(testInstance.getName());
    }

    public TestResult run()
    {
        final ParticipantResultListener participantResultListener = new ParticipantResultListener();
        TestCommandResponseListener testCommandResponseListener = new TestCommandResponseListener();

        try
        {
            _jmsDelegate.addCommandListener(testCommandResponseListener);
            _jmsDelegate.addCommandListener(participantResultListener);

            runParts();

            return _testResult;
        }
        finally
        {
            _jmsDelegate.removeCommandListener(participantResultListener);
            _jmsDelegate.removeCommandListener(testCommandResponseListener);
        }
    }

    private void runParts()
    {
        boolean queuesCreated = false;
        try
        {
            createQueues();
            queuesCreated = true;
            sendTestSetupCommands();
            awaitCommandResponses();
            sendCommandToParticipatingClients(new StartTestCommand());
            awaitCommandResponses();

            awaitTestResults();

            sendCommandToParticipatingClients(new TearDownTestCommand());
            awaitCommandResponses();
        }
        finally
        {
            if (queuesCreated)
            {
                deleteQueues();
            }
        }
    }

    void createQueues()
    {
        List<QueueConfig> queues = _testInstance.getQueues();
        if (!queues.isEmpty())
        {
            _jmsDelegate.createQueues(queues);
        }
    }

    void sendTestSetupCommands()
    {
        List<CommandForClient> commandsForAllClients = _testInstance.createCommands();
        _commandResponseLatch = new CountDownLatch(commandsForAllClients.size());
        for (CommandForClient commandForClient : commandsForAllClients)
        {
            String configuredClientName = commandForClient.getClientName();
            String registeredClientName = _participatingClients.getRegisteredNameFromConfiguredName(configuredClientName);

            Command command = commandForClient.getCommand();
            sendCommandInternal(registeredClientName, command);
        }
    }

    void awaitCommandResponses()
    {
        awaitLatch(_commandResponseLatch, _commandResponseTimeout, "Timed out waiting for command responses. Expecting %d more responses.");
    }


    void processCommandResponse(final Response response)
    {
        if (LOGGER.isDebugEnabled())
        {
            LOGGER.debug("Received response for command " + response);
        }

        _commandResponseLatch.countDown();
        checkForResponseError(response);
    }


    void awaitTestResults()
    {
        long timeout = _testResultTimeout;
        DistributedTestException lastException = null;

        boolean waitForever = _testResultTimeout == WAIT_FOREVER;
        final long interval = waitForever ? PARTICIPANT_RESULTS_LOG_INTERVAL : Math.min(PARTICIPANT_RESULTS_LOG_INTERVAL, _testResultTimeout);

        while(_testResultsLatch.getCount() > 0 && (waitForever || timeout > 0))
        {
            try
            {
                awaitLatch(_testResultsLatch, interval, "Waiting for participant results... Expecting %d more responses.");
            }
            catch (DistributedTestException e)
            {
                lastException = e;
                LOGGER.info(e.getMessage());
            }

            if (!waitForever)
            {
                timeout =- interval;
            }
        }

        if (_testResultsLatch.getCount() > 0)
        {
            throw lastException;
        }
    }

    void deleteQueues()
    {
        List<QueueConfig> queues = _testInstance.getQueues();
        if (!queues.isEmpty())
        {
            _jmsDelegate.deleteQueues(queues);
        }
    }

    void sendCommandToParticipatingClients(final Command command)
    {
        Collection<String> participatingRegisteredClients = _participatingClients.getRegisteredNames();
        _commandResponseLatch = new CountDownLatch(participatingRegisteredClients.size());
        for (final String clientName : participatingRegisteredClients)
        {
            sendCommandInternal(clientName, command);
        }
    }

    public void processParticipantResult(ParticipantResult result)
    {
        setOriginalTestDetailsOn(result);

        _testResult.addParticipantResult(result);
        LOGGER.info("Received result " + result);

        _testResultsLatch.countDown();
        checkForResponseError(result);
    }

    private void setOriginalTestDetailsOn(ParticipantResult result)
    {
        // Client knows neither the configured client name nor test name
        String registeredClientName = result.getRegisteredClientName();
        String configuredClient = _participatingClients.getConfiguredNameFromRegisteredName(registeredClientName);

        result.setConfiguredClientName(configuredClient);
        result.setTestName(_testInstance.getName());
        result.setIterationNumber(_testInstance.getIterationNumber());
    }

    private void sendCommandInternal(String registeredClientName, Command command)
    {
        _setOfResponsesToExpect.add(command.getType());
        _jmsDelegate.sendCommandToClient(registeredClientName, command);
    }

    private void awaitLatch(CountDownLatch latch, long timeout, String messageWithOneDecimalPlaceholder)
    {
        try
        {
            final boolean countedDownOK = latch.await(timeout, TimeUnit.MILLISECONDS);
            if (!countedDownOK)
            {
                final long latchCount = latch.getCount();
                String formattedMessage = String.format(messageWithOneDecimalPlaceholder, latchCount);
                throw new DistributedTestException(formattedMessage);
            }
        }
        catch (final InterruptedException e)
        {
            Thread.currentThread().interrupt();
        }
    }

    private void checkForResponseError(final Response response)
    {
        if (response.hasError())
        {
            LOGGER.error("Client " + response.getRegisteredClientName() + " reported error " + response);
        }
    }

    final class ParticipantResultListener implements CommandListener
    {
        @Override
        public boolean supports(Command command)
        {
            return command instanceof ParticipantResult;
        }

        @Override
        public void processCommand(Command command)
        {
            processParticipantResult((ParticipantResult) command);

        }
    }

    final class TestCommandResponseListener implements CommandListener
    {
        @Override
        public void processCommand(Command command)
        {
            processCommandResponse((Response)command);
        }

        @Override
        public boolean supports(Command command)
        {
            CommandType type = command.getType();
            if (type == CommandType.RESPONSE)
            {
                Response response = (Response)command;
                return _setOfResponsesToExpect.contains(response.getInReplyToCommandType());
            }
            return false;
        }
    }

}
