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

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

import org.openstreetmap.osmosis.core.OsmosisRuntimeException;
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.Way;
import org.openstreetmap.osmosis.core.task.common.ChangeAction;
import org.openstreetmap.osmosis.pgsnapshot.common.DatabaseContext;
import org.openstreetmap.osmosis.pgsnapshot.common.NoSuchRecordException;
import org.springframework.jdbc.core.CallableStatementCreator;
import org.springframework.jdbc.core.SqlParameter;


/**
 * Writes changes to a database.
 * 
 * @author Brett Henderson
 */
public class ChangeWriter {
	
	private DatabaseContext dbCtx;
	private ActionDao actionDao;
	private UserDao userDao;
	private NodeDao nodeDao;
	private WayDao wayDao;
	private RelationDao relationDao;
	private Set<Integer> userSet;
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param dbCtx
	 *            The database context to use for accessing the database.
	 * @param logging
	 * 			  Verbose logging directly to the database
	 */
	public ChangeWriter(DatabaseContext dbCtx, boolean logging) {
		this.dbCtx = dbCtx;
		
		actionDao = new ActionDao(dbCtx);
		userDao = new UserDao(dbCtx, actionDao);
		nodeDao = new NodeDao(dbCtx, actionDao, logging);
		wayDao = new WayDao(dbCtx, actionDao, logging);
		relationDao = new RelationDao(dbCtx, actionDao, logging);
		
		userSet = new HashSet<Integer>();
	}


	/**
	 * Writes the specified user to the database.
	 * 
	 * @param user
	 *            The user to write.
	 */
	private void writeUser(OsmUser user) {
		// Entities without a user assigned should not be written.
		if (!OsmUser.NONE.equals(user)) {
			// Users will only be updated in the database once per changeset
			// run.
			if (!userSet.contains(user.getId())) {
				int userId;
				OsmUser existingUser;

				userId = user.getId();

				try {
					existingUser = userDao.getUser(userId);

					if (!user.equals(existingUser)) {
						userDao.updateUser(user);
					}

				} catch (NoSuchRecordException e) {
					userDao.addUser(user);
				}

				userSet.add(user.getId());
			}
		}
	}


	/**
	 * Performs any validation and pre-processing required for all entity types.
	 */
	private void processEntityPrerequisites(Entity entity) {
		// We can't write an entity with a null timestamp.
		if (entity.getTimestamp() == null) {
			throw new OsmosisRuntimeException("Entity(" + entity.getType()
					+ ") " + entity.getId() + " does not have a timestamp set.");
		}
		
		// Process the user data.
		writeUser(entity.getUser());
	}


	/**
	 * Writes the specified node change to the database.
	 * 
	 * @param node
	 *            The node to be written.
	 * @param action
	 *            The change to be applied.
	 */
	public void write(Node node, ChangeAction action) {
		processEntityPrerequisites(node);

		// If this is a create or modify, we must create or modify the records
		// in the database. Note that we don't use the input source to
		// distinguish between create and modify, we make this determination
		// based on our current data set.
		if (ChangeAction.Create.equals(action)
				|| ChangeAction.Modify.equals(action)) {
			if (nodeDao.exists(node.getId())) {
				nodeDao.modifyEntity(node);
			} else {
				nodeDao.addEntity(node);
			}

		} else {
			// Remove the node from the database.
			nodeDao.removeEntity(node.getId());
		}
	}


	/**
	 * Writes the specified way change to the database.
	 * 
	 * @param way
	 *            The way to be written.
	 * @param action
	 *            The change to be applied.
	 * @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 void write(Way way, ChangeAction action, boolean keepInvalidWays) {
		processEntityPrerequisites(way);
		
		// If this is a create or modify, we must create or modify the records
		// in the database. Note that we don't use the input source to
		// distinguish between create and modify, we make this determination
		// based on our current data set.
		if (ChangeAction.Create.equals(action) || ChangeAction.Modify.equals(action)) {
			if (wayDao.exists(way.getId())) {
				if (way.getWayNodes().size() >= 2 || keepInvalidWays) {
					wayDao.modifyEntity(way);
				} else {
					wayDao.removeEntity(way.getId());
				}
				
			} else {
				if (way.getWayNodes().size() >= 2 || keepInvalidWays) {
					wayDao.addEntity(way);
				}
			}

		} else {
			// Remove the way from the database.
			wayDao.removeEntity(way.getId());
		}
	}


	/**
	 * Writes the specified relation change to the database.
	 * 
	 * @param relation
	 *            The relation to be written.
	 * @param action
	 *            The change to be applied.
	 */
	public void write(Relation relation, ChangeAction action) {
		processEntityPrerequisites(relation);

		// If this is a create or modify, we must create or modify the records
		// in the database. Note that we don't use the input source to
		// distinguish between create and modify, we make this determination
		// based on our current data set.
		if (ChangeAction.Create.equals(action)
				|| ChangeAction.Modify.equals(action)) {
			if (relationDao.exists(relation.getId())) {
				relationDao.modifyEntity(relation);
			} else {
				relationDao.addEntity(relation);
			}

		} else {
			// Remove the relation from the database.
			relationDao.removeEntity(relation.getId());
		}
	}


	/**
	 * Performs post-change database updates.
	 */
	public void complete() {
		dbCtx.getJdbcTemplate().call(
				new CallableStatementCreator() {
					@Override
					public CallableStatement createCallableStatement(Connection con) throws SQLException {
						return con.prepareCall("{call osmosisUpdate()}");
					}
				}, new ArrayList<SqlParameter>());
		
		// Clear all action records.
		actionDao.truncate();
	}


	/**
	 * Releases all resources.
	 */
	public void release() {
		// Nothing to do.
	}
}
