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

import java.util.Iterator;
import java.util.Map;

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.EntityProcessor;
import org.openstreetmap.osmosis.core.container.v0_6.NodeContainer;
import org.openstreetmap.osmosis.core.container.v0_6.RelationContainer;
import org.openstreetmap.osmosis.core.container.v0_6.WayContainer;
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.Relation;
import org.openstreetmap.osmosis.core.domain.v0_6.RelationMember;
import org.openstreetmap.osmosis.core.domain.v0_6.Way;
import org.openstreetmap.osmosis.core.domain.v0_6.WayNode;
import org.openstreetmap.osmosis.core.filter.common.IdTracker;
import org.openstreetmap.osmosis.core.filter.common.IdTrackerFactory;
import org.openstreetmap.osmosis.core.filter.common.IdTrackerType;
import org.openstreetmap.osmosis.core.lifecycle.ReleasableIterator;
import org.openstreetmap.osmosis.core.store.SimpleObjectStore;
import org.openstreetmap.osmosis.core.store.SingleClassObjectSerializationFactory;
import org.openstreetmap.osmosis.core.task.v0_6.Sink;
import org.openstreetmap.osmosis.core.task.v0_6.SinkSource;


/**
 * A base class for all tasks filter entities within an area.
 * 
 * @author Brett Henderson
 * @author Karl Newman
 */
public abstract class AreaFilter implements SinkSource, EntityProcessor {
	private Sink sink;
	private IdTracker availableNodes; // Nodes within the area.
	private IdTracker requiredNodes; // Nodes needed to complete referencing entities.
	private IdTracker availableWays; // Ways within the area.
	private IdTracker requiredWays; // Ways needed to complete referencing relations.
	private IdTracker availableRelations; // Relations within the area.
	private IdTracker requiredRelations; // Relations needed to complete referencing relations.
	private boolean clipIncompleteEntities;
	private boolean completeWays;
	private boolean completeRelations;
	private boolean storeEntities;
    private boolean cascadingRelations;
	private SimpleObjectStore<WayContainer> allWays;
	private SimpleObjectStore<NodeContainer> allNodes;
    // this duplicates as a container for held-back relations in the cascadingRelations case:
	private SimpleObjectStore<RelationContainer> allRelations; 
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param idTrackerType
	 *            Defines the id tracker implementation to use.
	 * @param clipIncompleteEntities
	 *            If true, entities referring to non-existent entities will be
	 *            modified to ensure referential integrity. For example, ways
	 *            will be modified to only include nodes inside the area.
	 * @param completeWays
	 *            Include all nodes for ways which have at least one node inside
	 *            the filtered area.
	 * @param completeRelations
	 *            Include all relations referenced by other relations which have
	 *            members inside the filtered area.
	 * @param cascadingRelations
	 *            Make sure that a relation referencing a relation which is included
	 *            will also be included.
	 */
	public AreaFilter(
			IdTrackerType idTrackerType, boolean clipIncompleteEntities, boolean completeWays,
			boolean completeRelations, boolean cascadingRelations) {
		this.clipIncompleteEntities = clipIncompleteEntities;
		// Allowing complete relations without complete ways is very difficult and not allowed for
		// now.
		this.completeWays = completeWays || completeRelations;
		this.completeRelations = completeRelations;
        // cascadingRelations is included for free with any of the complete options so you don't
        // need it if those are set.
        this.cascadingRelations = cascadingRelations && !completeRelations && !completeWays;
		
		availableNodes = IdTrackerFactory.createInstance(idTrackerType);
		requiredNodes = IdTrackerFactory.createInstance(idTrackerType);
		availableWays = IdTrackerFactory.createInstance(idTrackerType);
		requiredWays = IdTrackerFactory.createInstance(idTrackerType);
		availableRelations = IdTrackerFactory.createInstance(idTrackerType);
		requiredRelations = IdTrackerFactory.createInstance(idTrackerType);
		
		// If either complete ways or complete relations are required, then all data must be stored
		// during processing.
		storeEntities = completeWays || completeRelations;
		if (storeEntities) {
			allNodes = new SimpleObjectStore<NodeContainer>(
					new SingleClassObjectSerializationFactory(NodeContainer.class), "afn", true);
			allWays = new SimpleObjectStore<WayContainer>(
					new SingleClassObjectSerializationFactory(WayContainer.class), "afw", true);
			allRelations =
				new SimpleObjectStore<RelationContainer>(
						new SingleClassObjectSerializationFactory(RelationContainer.class), "afr", true);
		} else if (cascadingRelations) {
            allRelations = 
				new SimpleObjectStore<RelationContainer>(
						new SingleClassObjectSerializationFactory(RelationContainer.class), "afr", true);
        }
	}


    /**
     * {@inheritDoc}
     */
    public void initialize(Map<String, Object> metaData) {
		sink.initialize(metaData);
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void process(EntityContainer entityContainer) {
		// Ask the entity container to invoke the appropriate processing method
		// for the entity type.
		entityContainer.process(this);
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void process(BoundContainer boundContainer) {
		// By default, pass it on unchanged
		sink.process(boundContainer);
	}

	/**
	 * Indicates if the node lies within the area required.
	 * 
	 * @param node
	 *            The node to be checked.
	 * @return True if the node lies within the area.
	 */
	protected abstract boolean isNodeWithinArea(Node node);
	
	
	/**
	 * {@inheritDoc}
	 */
	public void process(NodeContainer container) {
		Node node;
		
		node = container.getEntity();
		
		// Check if we're storing entities for later.
		if (storeEntities) {
			allNodes.add(container);
		}
		
		// Only add the node if it lies within the box boundaries.
		if (isNodeWithinArea(node)) {
			availableNodes.set(node.getId());
			
			// If we're not storing entities, we pass it on immediately.
			if (!storeEntities) {
				emitNode(container);
			}
		}
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void process(WayContainer container) {
		Way way;
		boolean inArea;
		
		way = container.getEntity();

		// Check if we're storing entities for later.
		if (storeEntities) {
			allWays.add(container);
		}
		
		// First look through all the nodes to see if any are within the filtered area
		inArea = false;
		for (WayNode nodeReference : way.getWayNodes()) {
			if (availableNodes.get(nodeReference.getNodeId())) {
				inArea = true;
				break;
			}
		}
		
		// If the way has at least one node in the filtered area.
		if (inArea) {
			availableWays.set(way.getId());
			
			// If complete ways are desired, mark any unavailable nodes as required.
			if (completeWays) {
				for (WayNode nodeReference : way.getWayNodes()) {
					long nodeId = nodeReference.getNodeId();
					
					if (!availableNodes.get(nodeId)) {
						requiredNodes.set(nodeId);
					}
				}
			}
			
			// If we're not storing entities, we pass it on immediately.
			if (!storeEntities) {
				emitWay(container);
			}
		}
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void process(RelationContainer container) {
		Relation relation;
		boolean inArea;
        boolean holdBackRelation;
		
		relation = container.getEntity();
		
		// First look through all the node and way members to see if any are within the filtered area
		inArea = false;
        holdBackRelation = false;

		for (RelationMember member : relation.getMembers()) {
			switch (member.getMemberType()) {
			case Node:
				inArea = availableNodes.get(member.getMemberId());
				break;
			case Way:
				inArea = availableWays.get(member.getMemberId());
				break;
			case Relation:
				inArea = availableRelations.get(member.getMemberId());
				break;
			default:
				break;
			}
			
			if (inArea) {
				break;
			}
		}

        if (cascadingRelations) { //  && referencesOtherRelation && (!inArea || clipIncompleteEntities)) {
            holdBackRelation = true;
        }

		// Check if we're storing entities for later.
		if (storeEntities || holdBackRelation) {
			allRelations.add(container);
        }
		
		// If the relation has at least one member in the filtered area.
		if (inArea) {
			availableRelations.set(relation.getId());
			
			// If we're not storing entities, we pass it on immediately.
			if (!storeEntities && !holdBackRelation) {
				emitRelation(container);
			}
		}
	}


	/**
	 * Sends a node to the sink. This will perform any necessary transformations on the node before
	 * sending it.
	 * 
	 * @param nodeContainer
	 *            Node to be sent.
	 */
	private void emitNode(NodeContainer nodeContainer) {
		sink.process(nodeContainer);
	}


	/**
	 * Sends a way to the sink. This will perform any necessary transformations on the way before
	 * sending it.
	 * 
	 * @param wayContainer
	 *            Way to be sent.
	 */
	private void emitWay(WayContainer wayContainer) {
		if (clipIncompleteEntities) {
			WayContainer filteredWayContainer;
			Way filteredWay;
			
			filteredWayContainer = wayContainer.getWriteableInstance();
			filteredWay = filteredWayContainer.getEntity();
			
			// Remove node references for nodes that are unavailable.
			for (Iterator<WayNode> i = filteredWay.getWayNodes().iterator(); i.hasNext();) {
				WayNode nodeReference = i.next();
				
				if (!availableNodes.get(nodeReference.getNodeId())) {
					i.remove();
				}
			}
			
			// Only add ways that contain nodes.
			if (filteredWay.getWayNodes().size() > 0) {
				sink.process(filteredWayContainer);
			}
			
		} else {
			sink.process(wayContainer);
		}
	}


	/**
	 * Sends a relation to the sink. This will perform any necessary transformations on the way before
	 * sending it.
	 * 
	 * @param relationContainer
	 *            Relation to be sent.
	 */
	private void emitRelation(RelationContainer relationContainer) {
    	if (clipIncompleteEntities) {
    		RelationContainer filteredRelationContainer;
    		Relation filteredRelation;
    		
		    filteredRelationContainer = relationContainer.getWriteableInstance();
		    filteredRelation = filteredRelationContainer.getEntity();
		    
		    // Remove members for entities that are unavailable.
		    for (Iterator<RelationMember> i = filteredRelation.getMembers().iterator(); i.hasNext();) {
		    	RelationMember member = i.next();
		    	EntityType memberType;
		    	long memberId;
		    	
		    	memberType = member.getMemberType();
		    	memberId = member.getMemberId();
		    	
		    	switch (memberType) {
		    	case Node:
		    		if (!availableNodes.get(memberId)) {
						i.remove();
					}
		    		break;
		    	case Way:
		    		if (!availableWays.get(memberId)) {
						i.remove();
					}
		    		break;
		    	case Relation:
		    		if (!availableRelations.get(memberId)) {
						i.remove();
					}
		    		break;
		    	default:
		    			break;
		    	}
		    }
			
			// Only add relations that contain entities.
			if (filteredRelation.getMembers().size() > 0) {
				sink.process(filteredRelationContainer);
			}
			
    	} else {
    		sink.process(relationContainer);
    	}
	}
	
	
	private boolean selectParentRelationsPass() {
		try (ReleasableIterator<RelationContainer> i = allRelations.iterate()) {
			int selectionCount;
			
			selectionCount = 0;
			
			while (i.hasNext()) {
				Relation relation = i.next().getEntity();
				long relationId = relation.getId();
				
				// Ignore relations that have already been selected.
				if (!availableRelations.get(relationId)) {
					
					// This relation becomes an available relation if one of its member
					// relations is also available.
					for (RelationMember member : relation.getMembers()) {
						if (member.getMemberType().equals(EntityType.Relation)) {
							if (availableRelations.get(member.getMemberId())) {
								availableRelations.set(relationId);
								selectionCount++;
							}
						}
					}
				}
			}
			
			return selectionCount > 0;
			
		}
	}


	/**
	 * Walk up the relation tree. This means iterating through relations until all parent relations
	 * of existing relations are marked in the available list. We may have to do this multiple times
	 * depending on the nesting level of relations.
	 */
	private void selectParentRelations() {
		boolean selectionsMade;
		
		do {
			selectionsMade = selectParentRelationsPass();
		} while (selectionsMade);
	}


	/**
	 * Select all relation members of type relation for existing selected relations. This may need
	 * to be called several times until all children are selected.
	 * 
	 * @return True if additional selections were made an another pass is needed.
	 */
	private boolean selectChildRelationsPass() {
		try (ReleasableIterator<RelationContainer> i = allRelations.iterate()) {
			int selectionCount;
			
			selectionCount = 0;
			
			while (i.hasNext()) {
				Relation relation = i.next().getEntity();
				long relationId = relation.getId();
				
				// Only examine available relations.
				if (availableRelations.get(relationId)) {
					// Select the child if it hasn't already been selected.
					for (RelationMember member : relation.getMembers()) {
						if (member.getMemberType().equals(EntityType.Relation)) {
							long memberId = member.getMemberId();
							
							if (!availableRelations.get(memberId)) {
								availableRelations.set(memberId);
								selectionCount++;
							}
						}
					}
				}
			}
			
			return selectionCount > 0;
		}
	}
	
	
	/**
	 * Select all relation members of type node or way for existing selected relations.
	 */
	private void selectChildNonRelationsPass() {
		try (ReleasableIterator<RelationContainer> i = allRelations.iterate()) {
			while (i.hasNext()) {
				Relation relation = i.next().getEntity();
				long relationId = relation.getId();
				
				// Only examine available relations.
				if (availableRelations.get(relationId)) {
					// Select the member if it hasn't already been selected.
					for (RelationMember member : relation.getMembers()) {
						switch (member.getMemberType()) {
						case Node:
							availableNodes.set(member.getMemberId());
							break;
						case Way:
							availableWays.set(member.getMemberId());
							break;
						default:
							break;
						}
					}
				}
			}
		}
	}
	
	
	/**
	 * Select all nodes within already selected ways.
	 */
	private void selectWayNodes() {
		try (ReleasableIterator<WayContainer> i = allWays.iterate()) {
			while (i.hasNext()) {
				Way way = i.next().getEntity();
				long wayId = way.getId();
				
				// Only examine available relations.
				if (availableWays.get(wayId)) {
					// Select all nodes within the way.
					for (WayNode wayNode : way.getWayNodes()) {
						availableNodes.set(wayNode.getNodeId());
					}
				}
			}
		}
	}
	
	
	private void buildCompleteRelations() {
		boolean selectionsMade;
		
		// Select all child relation members of type relation. 
		do {
			selectionsMade = selectChildRelationsPass();
		} while (selectionsMade);
		
		// Select all child relation members of type way or node.
		selectChildNonRelationsPass();
		
		// Select all way nodes of existing nodes.
		selectWayNodes();
	}
    
    
    private void pumpNodesToSink() {
    	try (ReleasableIterator<NodeContainer> i = allNodes.iterate()) {
    		while (i.hasNext()) {
				NodeContainer nodeContainer = i.next();
				if (availableNodes.get(nodeContainer.getEntity().getId())) {
					emitNode(nodeContainer);
				}
			}
    	}
    }
    
    
    private void pumpWaysToSink() {
    	try (ReleasableIterator<WayContainer> i = allWays.iterate()) {
    		while (i.hasNext()) {
				WayContainer wayContainer = i.next();
				if (availableWays.get(wayContainer.getEntity().getId())) {
					emitWay(wayContainer);
				}
			}
    	}
    }
    
    
    private void pumpRelationsToSink() {
    	try (ReleasableIterator<RelationContainer> i = allRelations.iterate()) {
    		while (i.hasNext()) {
				RelationContainer relationContainer = i.next();
				if (availableRelations.get(relationContainer.getEntity().getId())) {
					emitRelation(relationContainer);
				}
			}
    	}
    }
	
	
	/**
	 * {@inheritDoc}
	 */
	public void complete() {
		// If we've stored entities temporarily, we now need to forward the selected ones to the output.
		if (storeEntities) {
			// Select all parents of current relations.
			selectParentRelations();
			
			// Merge required ids into available ids.
			availableNodes.setAll(requiredNodes);
			availableWays.setAll(requiredWays);
			availableRelations.setAll(requiredRelations);
			requiredNodes = null;
			requiredWays = null;
			requiredRelations = null;
			
			if (completeRelations) {
				buildCompleteRelations();
			}
			
			// Send the selected entities to the output.
			pumpNodesToSink();
			pumpWaysToSink();
			pumpRelationsToSink();
		} else if (cascadingRelations) {
			// Select all parents of current relations.
			selectParentRelations();
			availableRelations.setAll(requiredRelations);
			
			// nodes, ways, and relations *not* referencing other relations will already have
            // been written in this mode. we only pump the remaining ones, relations that 
            // reference other relations. this may result in an un-ordered relation stream.
			pumpRelationsToSink();
        }
		
		sink.complete();
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void close() {
		if (allNodes != null) {
			allNodes.close();
		}
		if (allWays != null) {
			allWays.close();			
		}
		if (allRelations != null) {
			allRelations.close();
		}
		sink.close();
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void setSink(Sink sink) {
		this.sink = sink;
	}
}
