/*
 * 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.request.mapper;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import org.apache.wicket.request.IRequestHandler;
import org.apache.wicket.request.IRequestMapper;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.Url;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Thread safe compound {@link IRequestMapper}. The mappers are searched depending on their
 * compatibility score and the orders they were registered. If two or more {@link IRequestMapper}s
 * have the same compatibility score, the last registered mapper has highest priority.
 * 
 * @author igor.vaynberg
 * @author Matej Knopp
 */
public class CompoundRequestMapper implements ICompoundRequestMapper
{
	private static final Logger LOG = LoggerFactory.getLogger(CompoundRequestMapper.class);

	static class MapperWithScore implements Comparable<MapperWithScore>
	{
		private final IRequestMapper mapper;
		private final int compatibilityScore;

		public MapperWithScore(final IRequestMapper mapper, final int compatibilityScore)
		{
			this.mapper = mapper;
			this.compatibilityScore = compatibilityScore;
		}

		@Override
		public int compareTo(final MapperWithScore o)
		{
			return (compatibilityScore < o.compatibilityScore ? 1
				: (compatibilityScore > o.compatibilityScore ? -1 : 0));
		}

		public IRequestMapper getMapper()
		{
			return mapper;
		}

		@Override
		public boolean equals(Object o)
		{
			if (this == o)
				return true;
			if (!(o instanceof MapperWithScore))
				return false;

			MapperWithScore that = (MapperWithScore)o;

			if (compatibilityScore != that.compatibilityScore)
				return false;
			return mapper.equals(that.mapper);
		}

		@Override
		public int hashCode()
		{
			int result = mapper.hashCode();
			result = 31 * result + compatibilityScore;
			return result;
		}

		@Override
		public String toString()
		{
			return "Mapper: " + mapper.getClass().getName() + "; Score: " + compatibilityScore;
		}
	}

	private final List<IRequestMapper> mappers = new CopyOnWriteArrayList<>();

	@Override
	public CompoundRequestMapper add(final IRequestMapper mapper)
	{
		mappers.add(0, mapper);
		return this;
	}

	@Override
	public CompoundRequestMapper remove(final IRequestMapper mapper)
	{
		mappers.remove(mapper);
		return this;
	}

	/**
	 * Searches the registered {@link IRequestMapper}s to find one that can map the {@link Request}.
	 * Each registered {@link IRequestMapper} is asked to provide its compatibility score. Then the
	 * mappers are asked to map the request in order depending on the provided compatibility
	 * score.
	 * <p>
	 * The mapper with highest compatibility score which can map the request is returned.
	 * 
	 * @param request
	 * @return RequestHandler for the request or <code>null</code> if no mapper for the request is
	 *         found.
	 */
	@Override
	public IRequestHandler mapRequest(final Request request)
	{
		List<MapperWithScore> list = new ArrayList<>(mappers.size());

		for (IRequestMapper mapper : this)
		{
			int score = mapper.getCompatibilityScore(request);
			list.add(new MapperWithScore(mapper, score));
		}

		Collections.sort(list);

		if (LOG.isDebugEnabled())
		{
			logMappers(list, request.getUrl().toString());
		}

		for (MapperWithScore mapperWithScore : list)
		{
			IRequestMapper mapper = mapperWithScore.getMapper();
			IRequestHandler handler = mapper.mapRequest(request);
			if (handler != null)
			{
				return handler;
			}
		}

		return null;
	}

	/**
	 * Logs all mappers with a positive compatibility score
	 *
	 * @param mappersWithScores
	 *      the list of all mappers
	 * @param url
	 *      the url to match by these mappers
	 */
	private void logMappers(final List<MapperWithScore> mappersWithScores, final String url)
	{
		final List<MapperWithScore> compatibleMappers = new ArrayList<>();
		for (MapperWithScore mapperWithScore : mappersWithScores)
		{
			if (mapperWithScore.compatibilityScore > 0)
			{
				compatibleMappers.add(mapperWithScore);
			}
		}
		if (compatibleMappers.size() == 0)
		{
			LOG.debug("No compatible mapper found for URL '{}'", url);
		}
		else if (compatibleMappers.size() == 1)
		{
			LOG.debug("One compatible mapper found for URL '{}' -> '{}'", url, compatibleMappers.get(0));
		}
		else
		{
			LOG.debug("Multiple compatible mappers found for URL '{}'", url);
			for (MapperWithScore compatibleMapper : compatibleMappers)
			{
		        LOG.debug(" * {}", compatibleMapper);
			}
		}
	}

	/**
	 * Searches the registered {@link IRequestMapper}s to find one that can map the
	 * {@link IRequestHandler}. Each registered {@link IRequestMapper} is asked to map the
	 * {@link IRequestHandler} until a mapper which can map the {@link IRequestHandler} is found or
	 * no more mappers are left.
	 * <p>
	 * The mappers are searched in reverse order as they have been registered. More recently
	 * registered mappers have bigger priority.
	 * 
	 * @param handler
	 * @return Url for the handler or <code>null</code> if no mapper for the handler is found.
	 */
	@Override
	public Url mapHandler(final IRequestHandler handler)
	{
		for (IRequestMapper mapper : this)
		{
			Url url = mapper.mapHandler(handler);
			if (url != null)
			{
				return url;
			}
		}
		return null;
	}

	/**
	 * The scope of the compound mapper is the highest score of the registered mappers.
	 * 
	 * {@inheritDoc}
	 */
	@Override
	public int getCompatibilityScore(final Request request)
	{
		int score = Integer.MIN_VALUE;
		for (IRequestMapper mapper : this)
		{
			score = Math.max(score, mapper.getCompatibilityScore(request));
		}
		return score;
	}

	@Override
	public Iterator<IRequestMapper> iterator()
	{
		return mappers.iterator();
	}
}
