// Copyright (C) 2000 - 2012 Philip Aston
// All rights reserved.
//
// This file is part of The Grinder software distribution. Refer to
// the file LICENSE which is part of The Grinder distribution for
// licensing details. The Grinder distribution is available on the
// Internet at http://grinder.sourceforge.net/
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.
package net.grinder.console;

import net.grinder.communication.MessageDispatchRegistry;
import net.grinder.communication.MessageDispatchRegistry.AbstractHandler;
import net.grinder.console.common.ErrorQueue;
import net.grinder.console.common.Resources;
import net.grinder.console.communication.*;
import net.grinder.console.communication.server.DispatchClientCommands;
import net.grinder.console.distribution.FileDistributionImplementation;
import net.grinder.console.distribution.WireFileDistribution;
import net.grinder.console.model.*;
import net.grinder.console.synchronisation.WireDistributedBarriers;
import net.grinder.engine.communication.DistFilesDigestMessage;
import net.grinder.engine.console.ErrorHandlerImplementation;
import net.grinder.messages.console.RegisterExpressionViewMessage;
import net.grinder.messages.console.RegisterTestsMessage;
import net.grinder.messages.console.ReportStatisticsMessage;
import net.grinder.statistics.StatisticsServicesImplementation;
import net.grinder.util.ListenerSupport;
import net.grinder.util.StandardTimeAuthority;
import net.grinder.util.thread.Condition;
import org.apache.commons.lang.StringUtils;
import org.picocontainer.DefaultPicoContainer;
import org.picocontainer.MutablePicoContainer;
import org.picocontainer.behaviors.Caching;
import org.picocontainer.parameters.ComponentParameter;
import org.picocontainer.parameters.ConstantParameter;
import org.slf4j.Logger;

import java.util.Timer;

import static org.ngrinder.common.util.ExceptionUtils.processException;
import static org.ngrinder.common.util.NoOp.noOp;

/**
 * Extension of {@link ConsoleFoundation} for nGrinder use.
 *
 * @author Grinder Developers.
 * @author JunHo Yoon (modified for nGrinder)
 * @since 3.0
 */
public class ConsoleFoundationEx {
	private final MutablePicoContainer m_container;
	private final Timer m_timer;
	private boolean m_shutdown = false;
	private final Condition m_eventSyncCondition;
	private final ListenerSupport<AcceptDistFilesDigestListener> m_distFilesDigestAcceptListener = new ListenerSupport<>();

	/**
	 * Constructor. Allows properties to be specified.
	 *
	 * @param resources		Console resources
	 * @param logger		Logger.
	 * @param properties	The properties.
	 * @param eventSyncCondition	event synchronization condition.
	 */
	public ConsoleFoundationEx(Resources resources, Logger logger, ConsoleProperties properties,
							   ConsoleCommunicationSetting consoleCommunicationSetting,
							   Condition eventSyncCondition) {
		m_eventSyncCondition = eventSyncCondition;
		m_container = new DefaultPicoContainer(new Caching());
		m_container.addComponent(logger);
		m_container.addComponent(resources);
		m_container.addComponent(properties);
		m_container.addComponent(StatisticsServicesImplementation.getInstance());
		m_container.addComponent(new StandardTimeAuthority());
		m_container.addComponent(consoleCommunicationSetting);

		m_container.addComponent(SampleModelImplementationEx.class);
		m_container.addComponent(SampleModelViewsImplementation.class);
		m_container.addComponent(ConsoleCommunicationImplementationEx.class);
		m_container.addComponent(DistributionControlImplementation.class);
		m_container.addComponent(ProcessControlImplementation.class);
		m_timer = new Timer(true);
		m_container.addComponent(m_timer);

		m_container.addComponent(FileDistributionImplementation.class, FileDistributionImplementation.class,
			new ComponentParameter(DistributionControlImplementation.class),
			new ComponentParameter(ProcessControlImplementation.class),
			new ConstantParameter(properties.getDistributionDirectory()),
			new ConstantParameter(properties.getDistributionFileFilterPattern()));

		m_container.addComponent(DispatchClientCommands.class);
		m_container.addComponent(WireFileDistribution.class);
		m_container.addComponent(WireMessageDispatch.class, WireMessageDispatch.class,
			new ComponentParameter(ConsoleCommunicationImplementationEx.class),
			new ComponentParameter(SampleModelImplementationEx.class),
			new ComponentParameter(SampleModelViewsImplementation.class),
			new ComponentParameter(DispatchClientCommands.class),
			new ConstantParameter(m_distFilesDigestAcceptListener));
		m_container.addComponent(WireDistributedBarriers.class);
		m_container.addComponent(ErrorQueue.class);

		ErrorQueue errorQueue = m_container.getComponent(ErrorQueue.class);
		errorQueue.setErrorHandler(new ErrorHandlerImplementation(logger));
	}

	/**
	 * Get the component of the given type.
	 *
	 * @param <T>	component type
	 * @param componentType	component type class
	 * @return component
	 */
	public <T> T getComponent(Class<T> componentType) {
		return m_container.getComponent(componentType);
	}

	/**
	 * Shut down the console.
	 *
	 */
	public void shutdown() {
		m_shutdown = true;
		m_container.getComponent(ConsoleCommunication.class).shutdown();
		try {
			m_timer.cancel();
		} catch (Exception e) {
			noOp();
		}
		if (m_container.getLifecycleState().isStarted()) {
			m_container.stop();
		}
	}

	public void addDistFilesDigestAcceptListener(AcceptDistFilesDigestListener acceptDistFilesDigestListener) {
		 m_distFilesDigestAcceptListener.add(acceptDistFilesDigestListener);
	}

	private String getConsoleInfo() {
		ConsoleProperties consoleProperties = m_container.getComponent(ConsoleProperties.class);
		return StringUtils.defaultIfBlank(consoleProperties.getConsoleHost(), "localhost") + ":"
						+ consoleProperties.getConsolePort();
	}

	/**
	 * Console message event loop.
	 *
	 * Dispatches communication messages appropriately. Blocks until we are {@link #shutdown()}.
	 */
	public void run() {
		if (m_shutdown) {
			throw processException("console can not run because it's shutdowned");
		}
		m_container.start();
		ConsoleCommunication communication = m_container.getComponent(ConsoleCommunication.class);
		// Need to request components, or they won't be instantiated.
		m_container.getComponent(WireMessageDispatch.class);
		m_container.getComponent(WireFileDistribution.class);
		m_container.getComponent(WireDistributedBarriers.class);
		m_container.getComponent(Logger.class).info("console {} has been started", getConsoleInfo());
		synchronized (m_eventSyncCondition) {
			m_eventSyncCondition.notifyAll();
		}
		while (communication.processOneMessage()) {
			// Process until communication is shut down.
			noOp();
		}

	}

	/**
	 * @return the m_container
	 */
	public MutablePicoContainer getContainer() {
		return m_container;
	}

	/**
	 * Factory that wires up the message dispatch.
	 *
	 * <p>
	 * Must be public for PicoContainer.
	 * </p>
	 *
	 * @see WireFileDistribution
	 */
	public static class WireMessageDispatch {

		/**
		 * Constructor.
		 *
		 * @param communication	Console communication.
		 * @param model			Console sample model.
		 * @param sampleModelViews	Console sample model views
		 * @param dispatchClientCommands	Client command dispatcher.
		 */
		public WireMessageDispatch(ConsoleCommunication communication, final SampleModel model,
						final SampleModelViews sampleModelViews,DispatchClientCommands dispatchClientCommands,
								   ListenerSupport<AcceptDistFilesDigestListener> distFilesDigestAcceptListener) {

			final MessageDispatchRegistry messageDispatchRegistry = communication.getMessageDispatchRegistry();

			messageDispatchRegistry.set(RegisterTestsMessage.class, new AbstractHandler<RegisterTestsMessage>() {
				public void handle(RegisterTestsMessage message) {
					model.registerTests(message.getTests());
				}
			});

			messageDispatchRegistry.set(ReportStatisticsMessage.class, new AbstractHandler<ReportStatisticsMessage>() {
				public void handle(ReportStatisticsMessage message) {
					model.addTestReport(message.getStatisticsDelta());
				}
			});

			messageDispatchRegistry.set(RegisterExpressionViewMessage.class, new AbstractHandler<RegisterExpressionViewMessage>() {
				public void handle(RegisterExpressionViewMessage message) {
					sampleModelViews.registerStatisticExpression(message.getExpressionView());
				}
			});

			messageDispatchRegistry.set(DistFilesDigestMessage.class, new AbstractHandler<DistFilesDigestMessage>() {
				public void handle(DistFilesDigestMessage message) {
					distFilesDigestAcceptListener.apply(new ListenerSupport.Informer<AcceptDistFilesDigestListener>() {
						@Override
						public void inform(AcceptDistFilesDigestListener listener) {
							listener.onAcceptDistFilesDigestListener(message.getDistFilesDigest());
						}
					});
				}
			});

			dispatchClientCommands.registerMessageHandlers(messageDispatchRegistry);
		}
	}
}
