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

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

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.Entity;
import org.openstreetmap.osmosis.core.domain.v0_6.Node;
import org.openstreetmap.osmosis.core.domain.v0_6.OsmUser;
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.Tag;
import org.openstreetmap.osmosis.core.domain.v0_6.Way;
import org.openstreetmap.osmosis.core.domain.v0_6.WayNode;
import org.openstreetmap.osmosis.core.lifecycle.CompletableContainer;
import org.openstreetmap.osmosis.pgsnapshot.common.CopyFileWriter;
import org.openstreetmap.osmosis.pgsnapshot.common.NodeLocationStoreType;
import org.openstreetmap.osmosis.pgsnapshot.common.PointBuilder;
import org.openstreetmap.osmosis.core.task.v0_6.Sink;
import org.openstreetmap.osmosis.hstore.PGHStore;


/**
 * An OSM data sink for storing all data to a set of database dump files. These
 * files can be used for populating an empty database.
 * 
 * @author Brett Henderson
 */
public class CopyFilesetBuilder implements Sink, EntityProcessor {
	
	private boolean enableBboxBuilder;
	private boolean enableLinestringBuilder;
	private boolean enableKeepPartialLinestring;
	private boolean keepInvalidWays;
	private WayGeometryBuilder wayGeometryBuilder;
	private CompletableContainer writerContainer;
	private MemberTypeValueMapper memberTypeValueMapper;
	private CopyFileWriter userWriter;
	private CopyFileWriter nodeWriter;
	private CopyFileWriter wayWriter;
	private CopyFileWriter wayNodeWriter;
	private CopyFileWriter relationWriter;
	private CopyFileWriter relationMemberWriter;
	private PointBuilder pointBuilder;
	private Set<Integer> userSet;
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param copyFileset
	 *            The set of COPY files to be populated.
	 * @param enableBboxBuilder
	 *            If true, the way bbox geometry is built during processing
	 *            instead of relying on the database to build them after import.
	 *            This increases processing but is faster than relying on the
	 *            database.
	 * @param enableLinestringBuilder
	 *            If true, the way linestring geometry is built during
	 *            processing instead of relying on the database to build them
	 *            after import. This increases processing but is faster than
	 *            relying on the database.
	 * @param enableKeepPartialLinestring
	 *            If true, the way linestring is build even on invalid or missing
	 *            nodes.
	 * @param storeType
	 *            The node location storage type used by the geometry builders.
	 * @param keepInvalidWays
	 *            If true, zero and single node ways are kept. Otherwise they are
	 *            silently dropped to avoid putting invalid geometries into the 
	 *            database which can cause problems with postgis functions.
	 */
	public CopyFilesetBuilder(
			CopyFileset copyFileset, boolean enableBboxBuilder,
			boolean enableLinestringBuilder, boolean enableKeepPartialLinestring,
			NodeLocationStoreType storeType, boolean keepInvalidWays) {
		this.enableBboxBuilder = enableBboxBuilder;
		this.enableLinestringBuilder = enableLinestringBuilder;
		this.enableKeepPartialLinestring = enableKeepPartialLinestring;
		this.keepInvalidWays = keepInvalidWays;
		
		writerContainer = new CompletableContainer();
		
		userWriter = writerContainer.add(new CopyFileWriter(copyFileset.getUserFile()));
		nodeWriter = writerContainer.add(new CopyFileWriter(copyFileset.getNodeFile()));
		wayWriter = writerContainer.add(new CopyFileWriter(copyFileset.getWayFile()));
		wayNodeWriter = writerContainer.add(new CopyFileWriter(copyFileset.getWayNodeFile()));
		relationWriter = writerContainer.add(new CopyFileWriter(copyFileset.getRelationFile()));
		relationMemberWriter = writerContainer.add(new CopyFileWriter(copyFileset.getRelationMemberFile()));
		
		pointBuilder = new PointBuilder();
		wayGeometryBuilder = new WayGeometryBuilder(storeType);
		memberTypeValueMapper = new MemberTypeValueMapper();
		memberTypeValueMapper = new MemberTypeValueMapper();
		
		userSet = new HashSet<Integer>();
	}
    
    
    /**
     * {@inheritDoc}
     */
    public void initialize(Map<String, Object> metaData) {
		// Do nothing.
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void process(EntityContainer entityContainer) {
		OsmUser user;
		
		// Write a user entry if the user doesn't already exist.
		user = entityContainer.getEntity().getUser();
		if (!user.equals(OsmUser.NONE)) {
			if (!userSet.contains(user.getId())) {
				userWriter.writeField(user.getId());
				userWriter.writeField(user.getName());
				userWriter.endRecord();
				
				userSet.add(user.getId());
			}
		}
		
		// Process the entity itself.
		entityContainer.process(this);
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void process(BoundContainer boundContainer) {
		// Do nothing.
	}
	
	
	private PGHStore buildTags(Entity entity) {
		PGHStore tags;
		
		tags = new PGHStore();
		for (Tag tag : entity.getTags()) {
			tags.put(tag.getKey(), tag.getValue());
		}
		
		return tags;
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void process(NodeContainer nodeContainer) {
		Node node;
		
		node = nodeContainer.getEntity();
		
		nodeWriter.writeField(node.getId());
		nodeWriter.writeField(node.getVersion());
		nodeWriter.writeField(node.getUser().getId());
		nodeWriter.writeField(node.getTimestamp());
		nodeWriter.writeField(node.getChangesetId());
		nodeWriter.writeField(buildTags(node));
		nodeWriter.writeField(pointBuilder.createPoint(node.getLatitude(), node.getLongitude()));
		nodeWriter.endRecord();
		
		if (enableBboxBuilder || enableLinestringBuilder) {
			wayGeometryBuilder.addNodeLocation(node);
		}
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void process(WayContainer wayContainer) {
		Way way;
		int sequenceId;
		List<Long> nodeIds;
		
		way = wayContainer.getEntity();
		
		nodeIds = new ArrayList<Long>(way.getWayNodes().size());
		for (WayNode wayNode : way.getWayNodes()) {
			nodeIds.add(wayNode.getNodeId());
		}
		
		// Keep invalid ways out of the database if desired by the user
		if (way.getWayNodes().size() > 1 || keepInvalidWays) {
			wayWriter.writeField(way.getId());
			wayWriter.writeField(way.getVersion());
			wayWriter.writeField(way.getUser().getId());
			wayWriter.writeField(way.getTimestamp());
			wayWriter.writeField(way.getChangesetId());
			wayWriter.writeField(buildTags(way));
			wayWriter.writeField(nodeIds);
			if (enableBboxBuilder) {
				wayWriter.writeField(wayGeometryBuilder.createWayBbox(way));
			}
			if (enableLinestringBuilder) {
				wayWriter.writeField(wayGeometryBuilder.createWayLinestring(way, enableKeepPartialLinestring));
			}
			wayWriter.endRecord();
			
			sequenceId = 0;
			for (WayNode wayNode : way.getWayNodes()) {
				wayNodeWriter.writeField(way.getId());
				wayNodeWriter.writeField(wayNode.getNodeId());
				wayNodeWriter.writeField(sequenceId++);
				wayNodeWriter.endRecord();
			}
		}
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void process(RelationContainer relationContainer) {
		Relation relation;
		int memberSequenceId;
		
		relation = relationContainer.getEntity();
		
		relationWriter.writeField(relation.getId());
		relationWriter.writeField(relation.getVersion());
		relationWriter.writeField(relation.getUser().getId());
		relationWriter.writeField(relation.getTimestamp());
		relationWriter.writeField(relation.getChangesetId());
		relationWriter.writeField(buildTags(relation));
		relationWriter.endRecord();
		
		memberSequenceId = 0;
		for (RelationMember member : relation.getMembers()) {
			relationMemberWriter.writeField(relation.getId());
			relationMemberWriter.writeField(member.getMemberId());
			relationMemberWriter.writeField(memberTypeValueMapper.getMemberType(member.getMemberType()));
			relationMemberWriter.writeField(member.getMemberRole());
			relationMemberWriter.writeField(memberSequenceId++);
			relationMemberWriter.endRecord();
		}
	}
	
	
	/**
	 * Writes any buffered data to the database and commits. 
	 */
	public void complete() {
		writerContainer.complete();
	}
	
	
	/**
	 * Releases all resources.
	 */
	public void close() {
		writerContainer.close();
		wayGeometryBuilder.close();
	}
}
