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

import java.util.NoSuchElementException;

import org.openstreetmap.osmosis.core.database.DbFeature;
import org.openstreetmap.osmosis.core.domain.v0_6.Entity;
import org.openstreetmap.osmosis.core.domain.v0_6.Tag;
import org.openstreetmap.osmosis.core.lifecycle.ReleasableIterator;
import org.openstreetmap.osmosis.core.store.PeekableIterator;
import org.openstreetmap.osmosis.core.store.PersistentIterator;
import org.openstreetmap.osmosis.core.store.SingleClassObjectSerializationFactory;
import org.openstreetmap.osmosis.pgsimple.common.DatabaseContext;


/**
 * Reads instances of an entity type from a database ordered by the identifier.
 * It combines the output of the entity feature table readers to produce fully
 * configured entity objects.
 * 
 * @author Brett Henderson
 * @param <T>
 *            The entity type to be supported.
 */
public class EntityReader<T extends Entity> implements ReleasableIterator<T> {
	
	private ReleasableIterator<T> entityReader;
	private PeekableIterator<DbFeature<Tag>> entityTagReader;
	private T nextValue;
	private boolean nextValueLoaded;
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param dbCtx
	 *            The database context to use for accessing the database.
	 * @param entityMapper
	 *            The database mapper for the entity type.
	 */
	public EntityReader(DatabaseContext dbCtx, EntityMapper<T> entityMapper) {
		// The postgres jdbc driver doesn't appear to allow concurrent result
		// sets on the same connection so only the last opened result set may be
		// streamed. The rest of the result sets must be persisted first.
		entityReader = new PersistentIterator<T>(
			new SingleClassObjectSerializationFactory(entityMapper.getEntityClass()),
			new EntityTableReader<T>(dbCtx, entityMapper),
			"ent",
			true
		);
		entityTagReader = new PeekableIterator<DbFeature<Tag>>(
			new PersistentIterator<DbFeature<Tag>>(
				new SingleClassObjectSerializationFactory(DbFeature.class),
				new EntityFeatureTableReader<Tag, DbFeature<Tag>>(dbCtx, new TagMapper(entityMapper.getEntityName())),
				"enttag",
				true
			)
		);
	}
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param dbCtx
	 *            The database context to use for accessing the database.
	 * @param entityMapper
	 *            The database mapper for the entity type.
	 * @param constraintTable
	 *            The table containing a column named id defining the list of
	 *            entities to be returned.
	 */
	public EntityReader(DatabaseContext dbCtx, EntityMapper<T> entityMapper, String constraintTable) {
		// The postgres jdbc driver doesn't appear to allow concurrent result
		// sets on the same connection so only the last opened result set may be
		// streamed. The rest of the result sets must be persisted first.
		entityReader = new PersistentIterator<T>(
			new SingleClassObjectSerializationFactory(entityMapper.getEntityClass()),
			new EntityTableReader<T>(dbCtx, entityMapper, constraintTable),
			"nod",
			true
		);
		entityTagReader = new PeekableIterator<DbFeature<Tag>>(
			new PersistentIterator<DbFeature<Tag>>(
				new SingleClassObjectSerializationFactory(DbFeature.class),
				new EntityFeatureTableReader<Tag, DbFeature<Tag>>(
						dbCtx, new TagMapper(entityMapper.getEntityName()), constraintTable),
				"enttag",
				true
			)
		);
	}
	
	
	/**
	 * Populates the entity with the its features. These features will be read
	 * from related tables.
	 * 
	 * @param entity
	 *            The entity to be populated.
	 */
	protected void populateEntityFeatures(T entity) {
		long entityId;
		
		entityId = entity.getId();
		
		// Skip all tags that are from a lower entity.
		while (entityTagReader.hasNext()) {
			DbFeature<Tag> dbTag;
			
			dbTag = entityTagReader.peekNext();
			
			if (dbTag.getEntityId() < entityId) {
				entityTagReader.next();
			} else {
				break;
			}
		}
		
		// Load all tags matching this version of the node.
		while (entityTagReader.hasNext() && entityTagReader.peekNext().getEntityId() == entityId) {
			entity.getTags().add(entityTagReader.next().getFeature());
		}
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public boolean hasNext() {
		if (!nextValueLoaded && entityReader.hasNext()) {
			T entity;
			
			entity = entityReader.next();
			
			populateEntityFeatures(entity);
			
			nextValue = entity;
			nextValueLoaded = true;
		}
		
		return nextValueLoaded;
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public T next() {
		T result;
		
		if (!hasNext()) {
			throw new NoSuchElementException();
		}
		
		result = nextValue;
		nextValueLoaded = false;
		
		return result;
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void remove() {
		throw new UnsupportedOperationException();
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void close() {
		entityReader.close();
		entityTagReader.close();
	}
}
