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

import java.util.Collections;

import org.openstreetmap.osmosis.core.OsmosisRuntimeException;
import org.openstreetmap.osmosis.core.change.v0_6.impl.TimestampSetter;
import org.openstreetmap.osmosis.core.container.v0_6.ChangeContainer;
import org.openstreetmap.osmosis.core.container.v0_6.EntityContainer;
import org.openstreetmap.osmosis.core.sort.v0_6.EntityByTypeThenIdComparator;
import org.openstreetmap.osmosis.core.sort.v0_6.EntityContainerComparator;
import org.openstreetmap.osmosis.core.store.DataPostbox;
import org.openstreetmap.osmosis.core.task.common.ChangeAction;
import org.openstreetmap.osmosis.core.task.v0_6.ChangeSink;
import org.openstreetmap.osmosis.core.task.v0_6.MultiSinkRunnableChangeSource;
import org.openstreetmap.osmosis.core.task.v0_6.Sink;
import org.openstreetmap.osmosis.set.v0_6.impl.DataPostboxSink;


/**
 * Compares two different data sources and produces a set of differences.
 * 
 * @author Brett Henderson
 */
public class ChangeDeriver implements MultiSinkRunnableChangeSource {

	private ChangeSink changeSink;
	private DataPostbox<EntityContainer> fromPostbox;
	private DataPostboxSink fromSink;
	private DataPostbox<EntityContainer> toPostbox;
	private DataPostboxSink toSink;
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param inputBufferCapacity
	 *            The size of the buffers to use for input sources.
	 */
	public ChangeDeriver(int inputBufferCapacity) {
		fromPostbox = new DataPostbox<EntityContainer>(inputBufferCapacity);
		fromSink = new DataPostboxSink(fromPostbox);
		toPostbox = new DataPostbox<EntityContainer>(inputBufferCapacity);
		toSink = new DataPostboxSink(toPostbox);
	}


	/**
	 * {@inheritDoc}
	 */
	public Sink getSink(int instance) {
		switch (instance) {
		case 0:
			return fromSink;
		case 1:
			return toSink;
		default:
			throw new OsmosisRuntimeException("Sink instance " + instance
					+ " is not valid.");
		}
	}


	/**
	 * This implementation always returns 2.
	 * 
	 * @return 2
	 */
	public int getSinkCount() {
		return 2;
	}


	/**
	 * {@inheritDoc}
	 */
	public void setChangeSink(ChangeSink changeSink) {
		this.changeSink = changeSink;
	}
	
	
	/**
	 * Processes the input sources and sends the changes to the change sink.
	 */
	public void run() {
		try {
			EntityContainerComparator comparator;
			EntityContainer fromEntityContainer = null;
			EntityContainer toEntityContainer = null;
			TimestampSetter timestampSetter;
			
			// Create a comparator for comparing two entities by type and identifier.
			comparator = new EntityContainerComparator(new EntityByTypeThenIdComparator());
			
			// Create an object for setting the current timestamp on entities being deleted.
			timestampSetter = new TimestampSetter();
			
			// We can't get meaningful data from the initialize data on the
			// input streams, so pass empty meta data to the sink and discard
			// the input meta data.
			fromPostbox.outputInitialize();
			toPostbox.outputInitialize();
			changeSink.initialize(Collections.<String, Object>emptyMap());
			
			// We continue in the comparison loop while both sources still have data.
			while (
					(fromEntityContainer != null || fromPostbox.hasNext())
					&& (toEntityContainer != null || toPostbox.hasNext())) {
				int comparisonResult;
				
				// Get the next input data where required.
				if (fromEntityContainer == null) {
					fromEntityContainer = fromPostbox.getNext();
				}
				if (toEntityContainer == null) {
					toEntityContainer = toPostbox.getNext();
				}
				
				// Compare the two sources.
				comparisonResult = comparator.compare(fromEntityContainer, toEntityContainer);
				
				if (comparisonResult < 0) {
					// The from entity doesn't exist on the to source therefore
					// has been deleted. We don't know when the entity was
					// deleted so set the delete time to the current time.
					changeSink.process(
							new ChangeContainer(
									timestampSetter.updateTimestamp(fromEntityContainer),
									ChangeAction.Delete));
					fromEntityContainer = null;
				} else if (comparisonResult > 0) {
					// The to entity doesn't exist on the from source therefore has
					// been created.
					changeSink.process(new ChangeContainer(toEntityContainer, ChangeAction.Create));
					toEntityContainer = null;
				} else {
					// The entity exists on both sources, therefore we must
					// compare
					// the entities directly. If there is a difference, the
					// entity has been modified.
					if (!fromEntityContainer.getEntity().equals(toEntityContainer.getEntity())) {
						changeSink.process(new ChangeContainer(toEntityContainer, ChangeAction.Modify));
					}
					fromEntityContainer = null;
					toEntityContainer = null;
				}
			}
			
			// Any remaining "from" entities are deletes.
			while (fromEntityContainer != null || fromPostbox.hasNext()) {
				if (fromEntityContainer == null) {
					fromEntityContainer = fromPostbox.getNext();
				}

				// The from entity doesn't exist on the to source therefore
				// has been deleted. We don't know when the entity was
				// deleted so set the delete time to the current time.
				changeSink.process(
						new ChangeContainer(
								timestampSetter.updateTimestamp(fromEntityContainer),
								ChangeAction.Delete));
				fromEntityContainer = null;
			}
			// Any remaining "to" entities are creates.
			while (toEntityContainer != null || toPostbox.hasNext()) {
				if (toEntityContainer == null) {
					toEntityContainer = toPostbox.getNext();
				}
				changeSink.process(new ChangeContainer(toEntityContainer, ChangeAction.Create));
				toEntityContainer = null;
			}
			
			changeSink.complete();
			fromPostbox.outputComplete();
			toPostbox.outputComplete();
			
		} finally {
			changeSink.close();
			
			fromPostbox.outputRelease();
			toPostbox.outputRelease();
		}
	}
}
