/*
 * 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.wicket.protocol.ws.api.registry;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;

import org.apache.wicket.Application;
import org.apache.wicket.MetaDataKey;
import org.apache.wicket.protocol.ws.api.IWebSocketConnection;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.lang.Generics;

/**
 * A registry that keeps all currently opened web socket connections in
 * maps in Application's meta data.
 *
 * @since 6.0
 */
public class SimpleWebSocketConnectionRegistry implements IWebSocketConnectionRegistry
{
	private static final MetaDataKey<ConcurrentMap<String, ConcurrentMap<IKey, IWebSocketConnection>>> KEY =
			new MetaDataKey<>()
	{
	};

	@Override
	public IWebSocketConnection getConnection(Application application, String sessionId, IKey key)
	{
		Args.notNull(application, "application");
		Args.notNull(sessionId, "sessionId");
		Args.notNull(key, "key");

		IWebSocketConnection connection = null;
		ConcurrentMap<String, ConcurrentMap<IKey, IWebSocketConnection>> connectionsBySession = application.getMetaData(KEY);
		if (connectionsBySession != null)
		{
			ConcurrentMap<IKey, IWebSocketConnection> connectionsByKey = connectionsBySession.get(sessionId);
			if (connectionsByKey != null)
			{
				connection = connectionsByKey.get(key);
			}
		}
		return connection;
	}

	@Override
	public Collection<IWebSocketConnection> getConnections(Application application, String sessionId)
	{
		Args.notNull(application, "application");
		Args.notNull(sessionId, "sessionId");

		Collection<IWebSocketConnection> connections = Collections.emptyList();
		ConcurrentMap<String, ConcurrentMap<IKey, IWebSocketConnection>> connectionsBySession = application.getMetaData(KEY);
		if (connectionsBySession != null)
		{
			ConcurrentMap<IKey, IWebSocketConnection> connectionsByPage = connectionsBySession.get(sessionId);
			if (connectionsByPage != null)
			{
				connections = connectionsByPage.values();
			}
		}
		return connections;
	}

	@Override
	public Collection<IWebSocketConnection> getConnections(Application application, IConnectionsFilter connectionsFilter)
	{
		Args.notNull(application, "application");
		Args.notNull(connectionsFilter, "connectionsFilter");

		Collection<IWebSocketConnection> connections = new ArrayList<>();
		ConcurrentMap<String, ConcurrentMap<IKey, IWebSocketConnection>> connectionsBySession = application.getMetaData(KEY);
		if (connectionsBySession != null)
		{
			for (Map.Entry<String, ConcurrentMap<IKey, IWebSocketConnection>> connectionsByPage : connectionsBySession.entrySet())
			{
				for (Map.Entry<IKey, IWebSocketConnection> connectionEntry: connectionsByPage.getValue().entrySet())
				{
					if (connectionsFilter.accept(connectionsByPage.getKey(), connectionEntry.getKey()))
					{
						connections.add(connectionEntry.getValue());
					}
				}

			}
		}
		return connections;
	}

	/**
	 * Returns a collection of currently active websockets. The connections might close at any time.
	 *
	 * @param application
	 *          The application
	 * @return a collection of currently active websockets
	 */
	@Override
	public Collection<IWebSocketConnection> getConnections(Application application)
	{
		Args.notNull(application, "application");

		Collection<IWebSocketConnection> connections = new ArrayList<>();
		ConcurrentMap<String, ConcurrentMap<IKey, IWebSocketConnection>> connectionsBySession = application.getMetaData(KEY);
		if (connectionsBySession != null)
		{
			for (ConcurrentMap<IKey, IWebSocketConnection> connectionsByPage : connectionsBySession.values())
			{
				connections.addAll(connectionsByPage.values());
			}
		}
		return connections;
	}

	@Override
	public void setConnection(Application application, String sessionId, IKey key, IWebSocketConnection connection)
	{
		Args.notNull(application, "application");
		Args.notNull(sessionId, "sessionId");
		Args.notNull(key, "key");

		ConcurrentMap<String, ConcurrentMap<IKey, IWebSocketConnection>> connectionsBySession = application.getMetaData(KEY);
		if (connectionsBySession == null)
		{
			synchronized (KEY)
			{
				connectionsBySession = application.getMetaData(KEY);
				if (connectionsBySession == null)
				{
					connectionsBySession = Generics.newConcurrentHashMap();
					application.setMetaData(KEY, connectionsBySession);
				}
			}
		}

		ConcurrentMap<IKey, IWebSocketConnection> connectionsByPage = connectionsBySession.get(sessionId);
		if (connectionsByPage == null && connection != null)
		{
			connectionsByPage = connectionsBySession.get(sessionId);
			if (connectionsByPage == null)
			{
				connectionsByPage = Generics.newConcurrentHashMap();
				ConcurrentMap<IKey, IWebSocketConnection> old = connectionsBySession.putIfAbsent(sessionId, connectionsByPage);
				if (old != null)
				{
					connectionsByPage = old;
				}
			}
		}

		if (connection != null)
		{
			connectionsByPage.put(key, connection);
		}
		else if (connectionsByPage != null)
		{
			connectionsByPage.remove(key);
			if (connectionsByPage.isEmpty())
			{
				connectionsBySession.remove(sessionId);
			}
		}
	}

	@Override
	public void removeConnection(Application application, String sessionId, IKey key)
	{
		setConnection(application, sessionId, key, null);
	}
}
