// This software is released into the Public Domain.  See copying.txt for details.
package org.openstreetmap.osmosis.set.v0_6;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import org.junit.Assert;
import org.junit.Test;
import org.openstreetmap.osmosis.core.container.v0_6.BoundContainer;
import org.openstreetmap.osmosis.core.container.v0_6.EntityContainer;
import org.openstreetmap.osmosis.core.container.v0_6.NodeContainer;
import org.openstreetmap.osmosis.core.domain.v0_6.Bound;
import org.openstreetmap.osmosis.core.domain.v0_6.CommonEntityData;
import org.openstreetmap.osmosis.core.domain.v0_6.EntityType;
import org.openstreetmap.osmosis.core.domain.v0_6.Node;
import org.openstreetmap.osmosis.core.domain.v0_6.OsmUser;
import org.openstreetmap.osmosis.core.merge.common.ConflictResolutionMethod;
import org.openstreetmap.osmosis.core.misc.v0_6.EmptyReader;
import org.openstreetmap.osmosis.core.task.v0_6.RunnableSource;
import org.openstreetmap.osmosis.core.task.v0_6.Sink;
import org.openstreetmap.osmosis.testutil.v0_6.RunTaskUtilities;
import org.openstreetmap.osmosis.testutil.v0_6.SinkEntityInspector;


/**
 * Tests bounding box processing in merge tasks.
 * 
 * @author Igor Podolskiy
 */
public class MergeBoundTest {

	/**
	 * A simple dummy ID generator for the helper source class.
	 */
	private static AtomicInteger idGenerator = new AtomicInteger(1000);
	
	/**
	 * Tests the proper working of the merge task if neither 
	 * source has a declared bound.
	 * 
	 * @throws Exception if something goes wrong
	 */
	@Test
	public void testNeitherHasBound() throws Exception {
		RunnableSource source0 = new BoundSource(new Bound(1, 2, 4, 3, "source0"), false);
		RunnableSource source1 = new BoundSource(new Bound(5, 6, 8, 7, "source1"), false);

		EntityMerger merger = new EntityMerger(ConflictResolutionMethod.LatestSource, 1,
				BoundRemovedAction.Ignore);
		
		SinkEntityInspector merged = RunTaskUtilities.run(merger, source0, source1);
		List<EntityContainer> mergedList = createList(merged.getProcessedEntities());

		Assert.assertEquals(2, mergedList.size());
		for (EntityContainer entityContainer : mergedList) {
			Assert.assertEquals(EntityType.Node, entityContainer.getEntity().getType());
		}
	}
	
	/**
	 * Tests whether merge will delete the declared bound if only source 0 
	 * has a declared bound.
	 * 
	 * @throws Exception if something goes wrong
	 */
	@Test
	public void testSource0HasBound() throws Exception {
		RunnableSource source0 = new BoundSource(new Bound(1, 2, 4, 3, "source0"), true);
		RunnableSource source1 = new BoundSource(new Bound(5, 6, 8, 7, "source1"), false);

		EntityMerger merger = new EntityMerger(ConflictResolutionMethod.LatestSource, 1,
				BoundRemovedAction.Ignore);
		
		SinkEntityInspector merged = RunTaskUtilities.run(merger, source0, source1);

		List<EntityContainer> mergedList = createList(merged.getProcessedEntities());
		Assert.assertEquals(2, mergedList.size());
		for (EntityContainer entityContainer : mergedList) {
			Assert.assertEquals(EntityType.Node, entityContainer.getEntity().getType());
		}
	}
	
	/**
	 * Tests whether merge will delete the declared bound if only source 1 
	 * has a declared bound.
	 * 
	 * @throws Exception if something goes wrong
	 */
	@Test
	public void testSource1HasBound() throws Exception {
		RunnableSource source0 = new BoundSource(new Bound(1, 2, 4, 3, "source0"), false);
		RunnableSource source1 = new BoundSource(new Bound(5, 6, 8, 7, "source1"), true);

		EntityMerger merger = new EntityMerger(ConflictResolutionMethod.LatestSource, 1,
				BoundRemovedAction.Ignore);
		
		SinkEntityInspector merged = RunTaskUtilities.run(merger, source0, source1);
		List<EntityContainer> mergedList = createList(merged.getProcessedEntities());
		
		Assert.assertEquals(2, mergedList.size());
		for (EntityContainer entityContainer : mergedList) {
			Assert.assertEquals(EntityType.Node, entityContainer.getEntity().getType());
		}
	}
	
	/**
	 * Test the proper computation of the union bound iff both sources
	 * have bounds.
	 * 
	 * @throws Exception if something goes wrong
	 */
	@Test
	public void testBothHaveBounds() throws Exception {
		Bound bound0 = new Bound(1, 2, 4, 3, "source1");
		RunnableSource source0 = new BoundSource(bound0, true);

		Bound bound1 = new Bound(5, 6, 8, 7, "source2");
		RunnableSource source1 = new BoundSource(bound1, true);

		EntityMerger merger = new EntityMerger(ConflictResolutionMethod.LatestSource, 1,
				BoundRemovedAction.Ignore);
		
		SinkEntityInspector merged = RunTaskUtilities.run(merger, source0, source1);
		List<EntityContainer> mergedList = createList(merged.getProcessedEntities());
		Assert.assertEquals(3, mergedList.size());
		Assert.assertEquals(EntityType.Bound, mergedList.get(0).getEntity().getType());
		
		// Check the bound
		Bound bound01 = (Bound) mergedList.get(0).getEntity();
		Assert.assertEquals(bound0.union(bound1), bound01);

		for (int i = 1; i < mergedList.size(); i++) {
			Assert.assertEquals(EntityType.Node, mergedList.get(i).getEntity().getType());
		}
	}
	
	/**
	 * Tests the proper working of the merge task if both sources are
	 * empty.
	 * 
	 * @throws Exception if something goes wrong
	 */
	@Test
	public void testBothEmpty() throws Exception {
		RunnableSource source0 = new EmptyReader();
		RunnableSource source1 = new EmptyReader();

		EntityMerger merger = new EntityMerger(ConflictResolutionMethod.LatestSource, 1,
				BoundRemovedAction.Ignore);
		
		SinkEntityInspector merged = RunTaskUtilities.run(merger, source0, source1);
		Assert.assertTrue("Expected empty result set but got some data", merged.getLastEntityContainer() == null);
	}
	
	/**
	 * Tests the proper working of the merge task if exactly one source is
	 * empty with respect to the declared bound.
	 * 
	 * @throws Exception if something goes wrong
	 */
	@Test
	public void testOneSourceEmpty() throws Exception {
		RunnableSource source0 = new EmptyReader();

		Bound bound1 = new Bound(5, 6, 8, 7, "source2");
		RunnableSource source1 = new BoundSource(bound1, true);
		
		EntityMerger merger = new EntityMerger(ConflictResolutionMethod.LatestSource, 1,
				BoundRemovedAction.Ignore);
		
		SinkEntityInspector merged = RunTaskUtilities.run(merger, source0, source1);
		List<EntityContainer> mergedList = createList(merged.getProcessedEntities());
		
		Assert.assertEquals(2, mergedList.size());
		Assert.assertEquals(bound1, mergedList.get(0).getEntity());
		Assert.assertEquals(EntityType.Node, mergedList.get(1).getEntity().getType());
	}

	private static <T> List<T> createList(Iterable<T> t) {
		List<T> list = new ArrayList<T>();
		for (T elem : t) {
			list.add(elem);
		}
		return list;
	}

	/**
	 * A simple source which provides a single node in the center of a given
	 * bounding box, and optionally, a declared bounding box.
	 */
	private static class BoundSource implements RunnableSource {

		private Sink sink;
		private Bound bound;
		private boolean publishBound;

		BoundSource(Bound bound, boolean publishBound) {
			if (bound == null) {
				throw new IllegalArgumentException("bound must not be null");
			}
			this.publishBound = publishBound;
			this.bound = bound;
		}

		@Override
		public void setSink(Sink sink) {
			this.sink = sink;
		}


		@Override
		public void run() {
			try {
				sink.initialize(Collections.<String, Object>emptyMap());
				if (publishBound) {
					sink.process(new BoundContainer(bound));
				}
				sink.process(new NodeContainer(createNode()));
				sink.complete();
			} finally {
				sink.close();
			}
		}
		
		private Node createNode() {
			double lon = (bound.getRight() - bound.getLeft()) / 2;
			double lat = (bound.getTop() - bound.getBottom()) / 2;
			return new Node(
					new CommonEntityData(idGenerator.incrementAndGet(), 1, new Date(), OsmUser.NONE, 1),
					lat, lon);
		}
	}
}
