// 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.container.v0_6.ChangeContainer;
import org.openstreetmap.osmosis.core.merge.common.ConflictResolutionMethod;
import org.openstreetmap.osmosis.core.sort.v0_6.EntityByTypeThenIdThenVersionComparator;
import org.openstreetmap.osmosis.core.sort.v0_6.EntityContainerComparator;
import org.openstreetmap.osmosis.core.sort.v0_6.SortedHistoryChangePipeValidator;
import org.openstreetmap.osmosis.core.store.DataPostbox;
import org.openstreetmap.osmosis.core.task.v0_6.ChangeSink;
import org.openstreetmap.osmosis.core.task.v0_6.MultiChangeSinkRunnableChangeSource;
import org.openstreetmap.osmosis.set.v0_6.impl.DataPostboxChangeSink;


/**
 * Merges two change sources into a single data set. Conflicting elements are
 * resolved by using either the latest timestamp (default) or always selecting
 * the second source.
 * 
 * @author Brett Henderson
 */
public class ChangeMerger implements MultiChangeSinkRunnableChangeSource {
	
	private ChangeSink changeSink;
	private DataPostbox<ChangeContainer> postbox0;
	private SortedHistoryChangePipeValidator sortedChangeValidator0;
	private DataPostbox<ChangeContainer> postbox1;
	private SortedHistoryChangePipeValidator sortedChangeValidator1;
	private ConflictResolutionMethod conflictResolutionMethod;
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param conflictResolutionMethod
	 *            The method to used to resolve conflict when two sources
	 *            contain the same entity.
	 * @param inputBufferCapacity
	 *            The size of the buffers to use for input sources.
	 */
	public ChangeMerger(ConflictResolutionMethod conflictResolutionMethod, int inputBufferCapacity) {
		this.conflictResolutionMethod = conflictResolutionMethod;
		
		postbox0 = new DataPostbox<ChangeContainer>(inputBufferCapacity);
		sortedChangeValidator0 = new SortedHistoryChangePipeValidator();
		sortedChangeValidator0.setChangeSink(new DataPostboxChangeSink(postbox0));
		
		postbox1 = new DataPostbox<ChangeContainer>(inputBufferCapacity);
		sortedChangeValidator1 = new SortedHistoryChangePipeValidator();
		sortedChangeValidator1.setChangeSink(new DataPostboxChangeSink(postbox1));
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public ChangeSink getChangeSink(int instance) {
		// Determine which postbox should be written to.
		switch (instance) {
		case 0:
			return sortedChangeValidator0;
		case 1:
			return sortedChangeValidator1;
		default:
			throw new OsmosisRuntimeException("Sink instance " + instance + " is not valid.");
		}
	}


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


	/**
	 * {@inheritDoc}
	 */
	public void setChangeSink(ChangeSink changeSink) {
		this.changeSink = changeSink;
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void run() {
		try {
			EntityContainerComparator comparator;
			ChangeContainer changeContainer0 = null;
			ChangeContainer changeContainer1 = null;
			
			// Create a comparator for comparing two entities by type and identifier.
			comparator = new EntityContainerComparator(new EntityByTypeThenIdThenVersionComparator());
			
			// 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.
			postbox0.outputInitialize();
			postbox1.outputInitialize();
			changeSink.initialize(Collections.<String, Object>emptyMap());
			
			// We continue in the comparison loop while both sources still have data.
			while (
					(changeContainer0 != null || postbox0.hasNext())
					&& (changeContainer1 != null || postbox1.hasNext())) {
				long comparisonResult;
				
				// Get the next input data where required.
				if (changeContainer0 == null) {
					changeContainer0 = postbox0.getNext();
				}
				if (changeContainer1 == null) {
					changeContainer1 = postbox1.getNext();
				}
				
				// Compare the two entities.
				comparisonResult =
					comparator.compare(changeContainer0.getEntityContainer(), changeContainer1.getEntityContainer());
				
				if (comparisonResult < 0) {
					// Entity 0 doesn't exist on the other source and can be
					// sent straight through.
					changeSink.process(changeContainer0);
					changeContainer0 = null;
				} else if (comparisonResult > 0) {
					// Entity 1 doesn't exist on the other source and can be
					// sent straight through.
					changeSink.process(changeContainer1);
					changeContainer1 = null;
				} else {
					// The entity exists on both sources so we must resolve the conflict.
					if (conflictResolutionMethod.equals(ConflictResolutionMethod.Timestamp)) {
						int timestampComparisonResult;
						
						timestampComparisonResult =
							changeContainer0.getEntityContainer().getEntity().getTimestamp()
							.compareTo(changeContainer1.getEntityContainer().getEntity().getTimestamp());
						
						if (timestampComparisonResult < 0) {
							changeSink.process(changeContainer1);
						} else if (timestampComparisonResult > 0) {
							changeSink.process(changeContainer0);
						} else {
							// If both have identical timestamps, use the second source.
							changeSink.process(changeContainer1);
						}
						
					} else if (conflictResolutionMethod.equals(ConflictResolutionMethod.LatestSource)) {
						changeSink.process(changeContainer1);
					} else if (conflictResolutionMethod.equals(ConflictResolutionMethod.Version)) {
						int version0 = changeContainer0.getEntityContainer().getEntity().getVersion();
						int version1 = changeContainer1.getEntityContainer().getEntity().getVersion();
						if (version0 < version1) {
							changeSink.process(changeContainer1);
						} else if (version0 > version1) {
							changeSink.process(changeContainer0);
						} else {
							// If both have identical versions, use the second source.
							changeSink.process(changeContainer1);
						}

					} else {
						throw new OsmosisRuntimeException(
								"Conflict resolution method " + conflictResolutionMethod + " is not recognized.");
					}
					
					changeContainer0 = null;
					changeContainer1 = null;
				}
			}
			
			// Any remaining entities on either source can be sent straight through.
			while (changeContainer0 != null || postbox0.hasNext()) {
				if (changeContainer0 == null) {
					changeContainer0 = postbox0.getNext();
				}
				changeSink.process(changeContainer0);
				changeContainer0 = null;
			}
			while (changeContainer1 != null || postbox1.hasNext()) {
				if (changeContainer1 == null) {
					changeContainer1 = postbox1.getNext();
				}
				changeSink.process(changeContainer1);
				changeContainer1 = null;
			}
			
			changeSink.complete();
			
			postbox0.outputComplete();
			postbox1.outputComplete();
			
		} finally {
			changeSink.close();
			
			postbox0.outputRelease();
			postbox1.outputRelease();
		}
	}
}
