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

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.List;

import org.openstreetmap.osmosis.apidb.common.DatabaseContext;
import org.openstreetmap.osmosis.core.OsmosisRuntimeException;
import org.openstreetmap.osmosis.core.database.DatabaseLoginCredentials;
import org.openstreetmap.osmosis.core.database.ReleasableStatementContainer;
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.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.Completable;
import org.openstreetmap.osmosis.core.task.common.ChangeAction;
import org.openstreetmap.osmosis.core.util.FixedPrecisionCoordinateConvertor;
import org.openstreetmap.osmosis.core.util.TileCalculator;


/**
 * Writes changes to a database.
 * 
 * @author Brett Henderson
 */
public class ChangeWriter implements Completable {

    private static final String INSERT_SQL_NODE =
    	"INSERT INTO nodes (node_id, version, timestamp, visible, changeset_id, latitude, longitude, tile)"
            + " VALUES (?, ?, ?, ?, ?, ?, ?, ?)";

    private static final String UPDATE_SQL_NODE =
    	"UPDATE nodes SET timestamp = ?, visible = ?, changeset_id = ?, latitude = ?, longitude = ?, tile = ?"
            + " WHERE node_id = ? AND version = ?";

    private static final String SELECT_SQL_NODE_COUNT =
    	"SELECT Count(node_id) AS rowCount FROM nodes WHERE node_id = ? AND version = ?";

    private static final String INSERT_SQL_NODE_CURRENT =
    	"INSERT INTO current_nodes (id, version, timestamp, visible, changeset_id, latitude, longitude, tile)"
            + " VALUES (?, ?, ?, ?, ?, ?, ?, ?)";

    private static final String UPDATE_SQL_NODE_CURRENT =
    	"UPDATE current_nodes SET version = ?, timestamp = ?, visible = ?, changeset_id = ?, latitude = ?,"
            + " longitude = ?, tile = ? WHERE id = ?";

    private static final String SELECT_SQL_NODE_CURRENT_COUNT =
    	"SELECT Count(id) AS rowCount FROM current_nodes WHERE id = ?";

    private static final String INSERT_SQL_NODE_TAG =
	"INSERT INTO node_tags (node_id, version, k, v) VALUES (?, ?, ?, ?)";

    private static final String DELETE_SQL_NODE_TAG = "DELETE FROM node_tags WHERE node_id = ? AND version = ?";

    private static final String INSERT_SQL_NODE_TAG_CURRENT =
    	"INSERT INTO current_node_tags (node_id, k, v) VALUES (?, ?, ?)";

    private static final String DELETE_SQL_NODE_TAG_CURRENT = "DELETE FROM current_node_tags WHERE node_id = ?";

    private static final String INSERT_SQL_WAY =
    	"INSERT INTO ways (way_id, version, timestamp, visible, changeset_id) VALUES (?, ?, ?, ?, ?)";

    private static final String UPDATE_SQL_WAY =
    	"UPDATE ways SET timestamp = ?, visible = ?, changeset_id = ? WHERE way_id = ? AND version = ?";

    private static final String SELECT_SQL_WAY_COUNT =
    	"SELECT Count(way_id) AS rowCount FROM ways WHERE way_id = ? AND version = ?";

    private static final String INSERT_SQL_WAY_CURRENT =
    	"INSERT INTO current_ways (id, version, timestamp, visible, changeset_id) VALUES (?, ?, ?, ?, ?)";

    private static final String UPDATE_SQL_WAY_CURRENT =
    	"UPDATE current_ways SET version = ?, timestamp = ?, visible = ?, changeset_id = ? WHERE id = ?";

    private static final String SELECT_SQL_WAY_CURRENT_COUNT =
    	"SELECT Count(id) AS rowCount FROM current_ways WHERE id = ?";

    private static final String INSERT_SQL_WAY_TAG = "INSERT INTO way_tags (way_id, version, k, v) VALUES (?, ?, ?, ?)";

    private static final String DELETE_SQL_WAY_TAG = "DELETE FROM way_tags WHERE way_id = ? AND version = ?";

    private static final String INSERT_SQL_WAY_TAG_CURRENT =
	"INSERT INTO current_way_tags (way_id, k, v) VALUES (?, ?, ?)";

    private static final String DELETE_SQL_WAY_TAG_CURRENT = "DELETE FROM current_way_tags WHERE way_id = ?";

    private static final String INSERT_SQL_WAY_NODE =
    	"INSERT INTO way_nodes (way_id, version, node_id, sequence_id) VALUES (?, ?, ?, ?)";

    private static final String DELETE_SQL_WAY_NODE = "DELETE FROM way_nodes WHERE way_id = ? AND version = ?";

    private static final String INSERT_SQL_WAY_NODE_CURRENT =
    	"INSERT INTO current_way_nodes (way_id, node_id, sequence_id) VALUES (?, ?, ?)";

    private static final String DELETE_SQL_WAY_NODE_CURRENT = "DELETE FROM current_way_nodes WHERE way_id = ?";

    private static final String INSERT_SQL_RELATION =
    	"INSERT INTO relations (relation_id, version, timestamp, visible, changeset_id) VALUES (?, ?, ?, ?, ?)";

    private static final String UPDATE_SQL_RELATION =
    	"UPDATE relations SET timestamp = ?, visible = ?, changeset_id = ? WHERE relation_id = ? AND version = ?";

    private static final String SELECT_SQL_RELATION_COUNT =
    	"SELECT Count(id) AS rowCount FROM current_relations WHERE id = ? AND version = ?";

    private static final String INSERT_SQL_RELATION_CURRENT =
    	"INSERT INTO current_relations (id, version, timestamp, visible, changeset_id) VALUES (?, ?, ?, ?, ?)";

    private static final String UPDATE_SQL_RELATION_CURRENT =
    	"UPDATE current_relations SET version = ?, timestamp = ?, visible = ?, changeset_id = ? WHERE id = ?";

    private static final String SELECT_SQL_RELATION_CURRENT_COUNT =
    	"SELECT Count(id) AS rowCount FROM current_relations WHERE id = ?";

    private static final String INSERT_SQL_RELATION_TAG =
    	"INSERT INTO relation_tags (relation_id, version, k, v) VALUES (?, ?, ?, ?)";

    private static final String DELETE_SQL_RELATION_TAG =
	"DELETE FROM relation_tags WHERE relation_id = ? AND version = ?";

    private static final String INSERT_SQL_RELATION_TAG_CURRENT =
    	"INSERT INTO current_relation_tags (relation_id, k, v) VALUES (?, ?, ?)";

    private static final String DELETE_SQL_RELATION_TAG_CURRENT =
    	"DELETE FROM current_relation_tags WHERE relation_id = ?";

    private static final String INSERT_SQL_RELATION_MEMBER_MYSQL =
    	"INSERT INTO relation_members (relation_id, version, member_type, member_id, member_role, sequence_id)"
            + " VALUES (?, ?, ?, ?, ?, ?)";

    private static final String INSERT_SQL_RELATION_MEMBER_PGSQL =
    	"INSERT INTO relation_members (relation_id, version, member_type, member_id, member_role, sequence_id)"
            + " VALUES (?, ?, ?::nwr_enum, ?, ?, ?)";

    private static final String DELETE_SQL_RELATION_MEMBER =
    	"DELETE FROM relation_members WHERE relation_id = ? AND version = ?";

    private static final String INSERT_SQL_RELATION_MEMBER_CURRENT_MYSQL =
    	"INSERT INTO current_relation_members (relation_id, member_type, member_id, member_role, sequence_id)"
            + " VALUES (?, ?, ?, ?, ?)";

    private static final String INSERT_SQL_RELATION_MEMBER_CURRENT_PGSQL =
    	"INSERT INTO current_relation_members (relation_id, member_type, member_id, member_role, sequence_id)"
            + " VALUES (?, ?::nwr_enum, ?, ?, ?)";

    private static final String DELETE_SQL_RELATION_MEMBER_CURRENT =
    	"DELETE FROM current_relation_members WHERE relation_id = ?";

    private final DatabaseContext dbCtx;
    private final UserManager userManager;
    private final ChangesetManager changesetManager;
    private final boolean populateCurrentTables;
    private final ReleasableStatementContainer statementContainer;
    private PreparedStatement insertNodeStatement;
    private PreparedStatement updateNodeStatement;
    private PreparedStatement selectNodeCountStatement;
    private PreparedStatement insertNodeCurrentStatement;
    private PreparedStatement updateNodeCurrentStatement;
    private PreparedStatement selectNodeCurrentCountStatement;
    private PreparedStatement insertNodeTagStatement;
    private PreparedStatement deleteNodeTagStatement;
    private PreparedStatement insertNodeTagCurrentStatement;
    private PreparedStatement deleteNodeTagCurrentStatement;
    private PreparedStatement insertWayStatement;
    private PreparedStatement updateWayStatement;
    private PreparedStatement selectWayCountStatement;
    private PreparedStatement insertWayCurrentStatement;
    private PreparedStatement updateWayCurrentStatement;
    private PreparedStatement selectWayCurrentCountStatement;
    private PreparedStatement insertWayTagStatement;
    private PreparedStatement deleteWayTagStatement;
    private PreparedStatement insertWayTagCurrentStatement;
    private PreparedStatement deleteWayTagCurrentStatement;
    private PreparedStatement insertWayNodeStatement;
    private PreparedStatement deleteWayNodeStatement;
    private PreparedStatement insertWayNodeCurrentStatement;
    private PreparedStatement deleteWayNodeCurrentStatement;
    private PreparedStatement insertRelationStatement;
    private PreparedStatement updateRelationStatement;
    private PreparedStatement selectRelationCountStatement;
    private PreparedStatement insertRelationCurrentStatement;
    private PreparedStatement updateRelationCurrentStatement;
    private PreparedStatement selectRelationCurrentCountStatement;
    private PreparedStatement insertRelationTagStatement;
    private PreparedStatement deleteRelationTagStatement;
    private PreparedStatement insertRelationTagCurrentStatement;
    private PreparedStatement deleteRelationTagCurrentStatement;
    private PreparedStatement insertRelationMemberStatement;
    private PreparedStatement deleteRelationMemberStatement;
    private PreparedStatement insertRelationMemberCurrentStatement;
    private PreparedStatement deleteRelationMemberCurrentStatement;
    private final MemberTypeRenderer memberTypeRenderer;
    private final TileCalculator tileCalculator;

    /**
     * Creates a new instance.
     * 
     * @param loginCredentials Contains all information required to connect to the database.
     * @param populateCurrentTables If true, the current tables will be populated as well as history
     *        tables.
     */
    public ChangeWriter(DatabaseLoginCredentials loginCredentials, boolean populateCurrentTables) {
        dbCtx = new DatabaseContext(loginCredentials);

        statementContainer = new ReleasableStatementContainer();
        userManager = new UserManager(dbCtx);
        changesetManager = new ChangesetManager(dbCtx);

        this.populateCurrentTables = populateCurrentTables;

        tileCalculator = new TileCalculator();
        memberTypeRenderer = new MemberTypeRenderer();
    }

    /**
     * Checks to see if the specified entity exists.
     * 
     * @param statement The statement used to perform the check.
     * @param id The entity identifier.
     * @return True if the entity exists, false otherwise.
     * @throws SQLException if an error occurs accessing the database.
     */
    private boolean checkIfEntityExists(PreparedStatement statement, long id) throws SQLException {
    	statement.setLong(1, id);
    	try (ResultSet resultSet = statement.executeQuery()) {
    		resultSet.next();
    		
    		return (resultSet.getInt("rowCount") != 0);
    	}
    }

    /**
     * Checks to see if the specified entity history item exists.
     * 
     * @param statement The statement used to perform the check.
     * @param id The entity identifier.
     * @param version The entity version.
     * @return True if the entity exists, false otherwise.
     * @throws SQLException if an error occurs accessing the database.
     */
    private boolean checkIfEntityHistoryExists(PreparedStatement statement, long id, int version) throws SQLException {
    	int prmIndex = 1;
        statement.setLong(prmIndex++, id);
        statement.setInt(prmIndex++, version);
        
        try (ResultSet resultSet = statement.executeQuery()) {
    		resultSet.next();
    		
    		return (resultSet.getInt("rowCount") != 0);
    	}
    }

    private void assertEntityHasTimestamp(Entity entity) {
        if (entity.getTimestamp() == null) {
            throw new OsmosisRuntimeException(
                    entity.getType().toString() + " " + entity.getId() + " does not have a timestamp set.");
        }
    }

    /**
     * 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) {
        boolean visible;
        boolean exists;
        int prmIndex;

        assertEntityHasTimestamp(node);

        // Add or update the user in the database.
        userManager.addOrUpdateUser(node.getUser());
        
        // Create the changeset in the database.
        changesetManager.addChangesetIfRequired(node.getChangesetId(), node.getUser());

        // If this is a deletion, the entity is not visible.
        visible = !action.equals(ChangeAction.Delete);

        // Create the prepared statements for node creation if necessary.
        if (insertNodeStatement == null) {
            insertNodeStatement = statementContainer.add(dbCtx.prepareStatement(INSERT_SQL_NODE));
            updateNodeStatement = statementContainer.add(dbCtx.prepareStatement(UPDATE_SQL_NODE));
            selectNodeCountStatement = statementContainer.add(dbCtx.prepareStatement(SELECT_SQL_NODE_COUNT));
            insertNodeCurrentStatement = statementContainer.add(dbCtx.prepareStatement(INSERT_SQL_NODE_CURRENT));
            updateNodeCurrentStatement = statementContainer.add(dbCtx.prepareStatement(UPDATE_SQL_NODE_CURRENT));
            selectNodeCurrentCountStatement = statementContainer.add(dbCtx
                    .prepareStatement(SELECT_SQL_NODE_CURRENT_COUNT));
            insertNodeTagStatement = statementContainer.add(dbCtx.prepareStatement(INSERT_SQL_NODE_TAG));
            deleteNodeTagStatement = statementContainer.add(dbCtx.prepareStatement(DELETE_SQL_NODE_TAG));
            insertNodeTagCurrentStatement = statementContainer.add(dbCtx.prepareStatement(INSERT_SQL_NODE_TAG_CURRENT));
            deleteNodeTagCurrentStatement = statementContainer.add(dbCtx.prepareStatement(DELETE_SQL_NODE_TAG_CURRENT));
        }

        // Remove the existing tags of the node history item.
        try {
            prmIndex = 1;
            deleteNodeTagStatement.setLong(prmIndex++, node.getId());
            deleteNodeTagStatement.setInt(prmIndex++, node.getVersion());

            deleteNodeTagStatement.execute();

        } catch (SQLException e) {
            throw new OsmosisRuntimeException("Unable to delete node history tags for node with id=" + node.getId()
                    + ".", e);
        }

        // Update the node if it already exists in the history table, otherwise insert it.
        try {
            exists = checkIfEntityHistoryExists(selectNodeCountStatement, node.getId(), node.getVersion());

        } catch (SQLException e) {
            throw new OsmosisRuntimeException(
            		"Unable to check if current node with id=" + node.getId() + " exists.", e);
        }
        if (exists) {
            // Update the node in the history table.
            try {
                prmIndex = 1;
                updateNodeStatement.setTimestamp(prmIndex++, new Timestamp(node.getTimestamp().getTime()));
                updateNodeStatement.setBoolean(prmIndex++, visible);
                updateNodeStatement.setLong(prmIndex++, node.getChangesetId());
                updateNodeStatement.setInt(prmIndex++, FixedPrecisionCoordinateConvertor.convertToFixed(node
                        .getLatitude()));
                updateNodeStatement.setInt(prmIndex++, FixedPrecisionCoordinateConvertor.convertToFixed(node
                        .getLongitude()));
                updateNodeStatement.setLong(prmIndex++, tileCalculator.calculateTile(node.getLatitude(), node
                        .getLongitude()));
                updateNodeStatement.setLong(prmIndex++, node.getId());
                updateNodeStatement.setInt(prmIndex++, node.getVersion());

                updateNodeStatement.execute();

            } catch (SQLException e) {
                throw new OsmosisRuntimeException("Unable to update history node with id=" + node.getId() + ".", e);
            }
        } else {
            // Insert the new node into the history table.
            try {
                prmIndex = 1;
                insertNodeStatement.setLong(prmIndex++, node.getId());
                insertNodeStatement.setInt(prmIndex++, node.getVersion());
                insertNodeStatement.setTimestamp(prmIndex++, new Timestamp(node.getTimestamp().getTime()));
                insertNodeStatement.setBoolean(prmIndex++, visible);
                insertNodeStatement.setLong(prmIndex++, node.getChangesetId());
                insertNodeStatement.setInt(prmIndex++, FixedPrecisionCoordinateConvertor.convertToFixed(node
                        .getLatitude()));
                insertNodeStatement.setInt(prmIndex++, FixedPrecisionCoordinateConvertor.convertToFixed(node
                        .getLongitude()));
                insertNodeStatement.setLong(prmIndex++, tileCalculator.calculateTile(node.getLatitude(), node
                        .getLongitude()));

                insertNodeStatement.execute();

            } catch (SQLException e) {
                throw new OsmosisRuntimeException("Unable to insert history node with id=" + node.getId() + ".", e);
            }
        }

        // Insert the tags of the new node into the history table.
        for (Tag tag : node.getTags()) {
            try {
                prmIndex = 1;
                insertNodeTagStatement.setLong(prmIndex++, node.getId());
                insertNodeTagStatement.setInt(prmIndex++, node.getVersion());
                insertNodeTagStatement.setString(prmIndex++, tag.getKey());
                insertNodeTagStatement.setString(prmIndex++, tag.getValue());

                insertNodeTagStatement.execute();

            } catch (SQLException e) {
                throw new OsmosisRuntimeException("Unable to insert history node tag with id=" + node.getId()
                        + " and key=(" + tag.getKey() + ").", e);
            }
        }

        if (populateCurrentTables) {
            // Delete the existing node tags from the current table.
            try {
                deleteNodeTagCurrentStatement.setLong(1, node.getId());

                deleteNodeTagCurrentStatement.execute();

            } catch (SQLException e) {
                throw new OsmosisRuntimeException(
                		"Unable to delete current node tags with id=" + node.getId() + ".", e);
            }

            // Update the node if it already exists in the current table, otherwise insert it.
            try {
                exists = checkIfEntityExists(selectNodeCurrentCountStatement, node.getId());

            } catch (SQLException e) {
                throw new OsmosisRuntimeException("Unable to check if current node with id=" + node.getId()
                        + " exists.", e);
            }
            if (exists) {
                // Update the node in the current table.
                try {
                    prmIndex = 1;
                    updateNodeCurrentStatement.setInt(prmIndex++, node.getVersion());
                    updateNodeCurrentStatement.setTimestamp(prmIndex++, new Timestamp(node.getTimestamp().getTime()));
                    updateNodeCurrentStatement.setBoolean(prmIndex++, visible);
                    updateNodeCurrentStatement.setLong(prmIndex++, node.getChangesetId());
                    updateNodeCurrentStatement.setInt(prmIndex++, FixedPrecisionCoordinateConvertor.convertToFixed(node
                            .getLatitude()));
                    updateNodeCurrentStatement.setInt(prmIndex++, FixedPrecisionCoordinateConvertor.convertToFixed(node
                            .getLongitude()));
                    updateNodeCurrentStatement.setLong(prmIndex++, tileCalculator.calculateTile(node.getLatitude(),
                            node.getLongitude()));
                    updateNodeCurrentStatement.setLong(prmIndex++, node.getId());

                    updateNodeCurrentStatement.execute();

                } catch (SQLException e) {
                    throw new OsmosisRuntimeException("Unable to update current node with id=" + node.getId() + ".", e);
                }
            } else {
                // Insert the new node into the current table.
                try {
                    prmIndex = 1;
                    insertNodeCurrentStatement.setLong(prmIndex++, node.getId());
                    insertNodeCurrentStatement.setInt(prmIndex++, node.getVersion());
                    insertNodeCurrentStatement.setTimestamp(prmIndex++, new Timestamp(node.getTimestamp().getTime()));
                    insertNodeCurrentStatement.setBoolean(prmIndex++, visible);
                    insertNodeCurrentStatement.setLong(prmIndex++, node.getChangesetId());
                    insertNodeCurrentStatement.setInt(prmIndex++, FixedPrecisionCoordinateConvertor.convertToFixed(node
                            .getLatitude()));
                    insertNodeCurrentStatement.setInt(prmIndex++, FixedPrecisionCoordinateConvertor.convertToFixed(node
                            .getLongitude()));
                    insertNodeCurrentStatement.setLong(prmIndex++, tileCalculator.calculateTile(node.getLatitude(),
                            node.getLongitude()));

                    insertNodeCurrentStatement.execute();

                } catch (SQLException e) {
                    throw new OsmosisRuntimeException("Unable to insert current node with id=" + node.getId() + ".", e);
                }
            }

            // Insert the tags of the new node into the current table.
            for (Tag tag : node.getTags()) {
                try {
                    prmIndex = 1;
                    insertNodeTagCurrentStatement.setLong(prmIndex++, node.getId());
                    insertNodeTagCurrentStatement.setString(prmIndex++, tag.getKey());
                    insertNodeTagCurrentStatement.setString(prmIndex++, tag.getValue());

                    insertNodeTagCurrentStatement.execute();

                } catch (SQLException e) {
                    throw new OsmosisRuntimeException("Unable to insert current node tag with id=" + node.getId()
                            + " and key=(" + tag.getKey() + ").", e);
                }
            }
        }
    }

    /**
     * Writes the specified way change to the database.
     * 
     * @param way The way to be written.
     * @param action The change to be applied.
     */
    public void write(Way way, ChangeAction action) {
        boolean visible;
        boolean exists;
        int prmIndex;
        List<WayNode> nodeReferenceList;

        assertEntityHasTimestamp(way);

        // Add or update the user in the database.
        userManager.addOrUpdateUser(way.getUser());
        
        // Create the changeset in the database.
        changesetManager.addChangesetIfRequired(way.getChangesetId(), way.getUser());

        nodeReferenceList = way.getWayNodes();

        // If this is a deletion, the entity is not visible.
        visible = !action.equals(ChangeAction.Delete);

        // Create the prepared statements for way creation if necessary.
        if (insertWayStatement == null) {
            insertWayStatement = statementContainer.add(dbCtx.prepareStatement(INSERT_SQL_WAY));
            updateWayStatement = statementContainer.add(dbCtx.prepareStatement(UPDATE_SQL_WAY));
            selectWayCountStatement = statementContainer.add(dbCtx.prepareStatement(SELECT_SQL_WAY_COUNT));
            insertWayCurrentStatement = statementContainer.add(dbCtx.prepareStatement(INSERT_SQL_WAY_CURRENT));
            updateWayCurrentStatement = statementContainer.add(dbCtx.prepareStatement(UPDATE_SQL_WAY_CURRENT));
            selectWayCurrentCountStatement = statementContainer.add(dbCtx
                    .prepareStatement(SELECT_SQL_WAY_CURRENT_COUNT));
            insertWayTagStatement = statementContainer.add(dbCtx.prepareStatement(INSERT_SQL_WAY_TAG));
            deleteWayTagStatement = statementContainer.add(dbCtx.prepareStatement(DELETE_SQL_WAY_TAG));
            insertWayTagCurrentStatement = statementContainer.add(dbCtx.prepareStatement(INSERT_SQL_WAY_TAG_CURRENT));
            deleteWayTagCurrentStatement = statementContainer.add(dbCtx.prepareStatement(DELETE_SQL_WAY_TAG_CURRENT));
            insertWayNodeStatement = statementContainer.add(dbCtx.prepareStatement(INSERT_SQL_WAY_NODE));
            deleteWayNodeStatement = statementContainer.add(dbCtx.prepareStatement(DELETE_SQL_WAY_NODE));
            insertWayNodeCurrentStatement = statementContainer.add(dbCtx.prepareStatement(INSERT_SQL_WAY_NODE_CURRENT));
            deleteWayNodeCurrentStatement = statementContainer.add(dbCtx.prepareStatement(DELETE_SQL_WAY_NODE_CURRENT));
        }

        // Remove the existing tags of the way history item.
        try {
            prmIndex = 1;
            deleteWayTagStatement.setLong(prmIndex++, way.getId());
            deleteWayTagStatement.setInt(prmIndex++, way.getVersion());

            deleteWayTagStatement.execute();

        } catch (SQLException e) {
            throw new OsmosisRuntimeException("Unable to delete way history tags for way with id=" + way.getId() + ".",
                    e);
        }

        // Remove the existing way nodes of the way history item.
        try {
            prmIndex = 1;
            deleteWayNodeStatement.setLong(prmIndex++, way.getId());
            deleteWayNodeStatement.setInt(prmIndex++, way.getVersion());

            deleteWayNodeStatement.execute();

        } catch (SQLException e) {
            throw new OsmosisRuntimeException(
                    "Unable to delete way history nodes for way with id=" + way.getId() + ".", e);
        }

        // Update the way if it already exists in the history table, otherwise insert it.
        try {
            exists = checkIfEntityHistoryExists(selectWayCountStatement, way.getId(), way.getVersion());

        } catch (SQLException e) {
            throw new OsmosisRuntimeException("Unable to check if current way with id=" + way.getId() + " exists.", e);
        }
        if (exists) {
            // Update the way in the history table.
            try {
                prmIndex = 1;
                updateWayStatement.setTimestamp(prmIndex++, new Timestamp(way.getTimestamp().getTime()));
                updateWayStatement.setBoolean(prmIndex++, visible);
                updateWayStatement.setLong(prmIndex++, way.getChangesetId());
                updateWayStatement.setLong(prmIndex++, way.getId());
                updateWayStatement.setInt(prmIndex++, way.getVersion());

                updateWayStatement.execute();

            } catch (SQLException e) {
                throw new OsmosisRuntimeException("Unable to update history way with id=" + way.getId() + ".", e);
            }
        } else {
            // Insert the new way into the history table.
            try {
                prmIndex = 1;
                insertWayStatement.setLong(prmIndex++, way.getId());
                insertWayStatement.setInt(prmIndex++, way.getVersion());
                insertWayStatement.setTimestamp(prmIndex++, new Timestamp(way.getTimestamp().getTime()));
                insertWayStatement.setBoolean(prmIndex++, visible);
                insertWayStatement.setLong(prmIndex++, way.getChangesetId());

                insertWayStatement.execute();

            } catch (SQLException e) {
                throw new OsmosisRuntimeException("Unable to insert history way with id=" + way.getId() + ".", e);
            }
        }

        // Insert the tags of the new way into the history table.
        for (Tag tag : way.getTags()) {
            try {
                prmIndex = 1;
                insertWayTagStatement.setLong(prmIndex++, way.getId());
                insertWayTagStatement.setInt(prmIndex++, way.getVersion());
                insertWayTagStatement.setString(prmIndex++, tag.getKey());
                insertWayTagStatement.setString(prmIndex++, tag.getValue());

                insertWayTagStatement.execute();

            } catch (SQLException e) {
                throw new OsmosisRuntimeException("Unable to insert history way tag with id=" + way.getId()
                        + " and key=(" + tag.getKey() + ").", e);
            }
        }

        // Insert the nodes of the new way into the history table.
        for (int i = 0; i < nodeReferenceList.size(); i++) {
            WayNode nodeReference;

            nodeReference = nodeReferenceList.get(i);

            try {
                prmIndex = 1;
                insertWayNodeStatement.setLong(prmIndex++, way.getId());
                insertWayNodeStatement.setInt(prmIndex++, way.getVersion());
                insertWayNodeStatement.setLong(prmIndex++, nodeReference.getNodeId());
                insertWayNodeStatement.setLong(prmIndex++, i + 1);

                insertWayNodeStatement.execute();

            } catch (SQLException e) {
                throw new OsmosisRuntimeException("Unable to insert history way node with way id=" + way.getId()
                        + " and node id=" + nodeReference.getNodeId() + ".", e);
            }
        }

        if (populateCurrentTables) {
            // Delete the existing way tags from the current table.
            try {
                deleteWayTagCurrentStatement.setLong(1, way.getId());

                deleteWayTagCurrentStatement.execute();

            } catch (SQLException e) {
                throw new OsmosisRuntimeException("Unable to delete current way tags with id=" + way.getId() + ".", e);
            }
            // Delete the existing way nodes from the current table.
            try {
                deleteWayNodeCurrentStatement.setLong(1, way.getId());

                deleteWayNodeCurrentStatement.execute();

            } catch (SQLException e) {
                throw new OsmosisRuntimeException("Unable to delete current way nodes with id=" + way.getId() + ".", e);
            }

            // Update the node if it already exists in the current table, otherwise insert it.
            try {
                exists = checkIfEntityExists(selectWayCurrentCountStatement, way.getId());

            } catch (SQLException e) {
                throw new OsmosisRuntimeException("Unable to check if current way with id=" + way.getId() + " exists.",
                        e);
            }
            if (exists) {
                // Update the way in the current table.
                try {
                    prmIndex = 1;
                    updateWayCurrentStatement.setInt(prmIndex++, way.getVersion());
                    updateWayCurrentStatement.setTimestamp(prmIndex++, new Timestamp(way.getTimestamp().getTime()));
                    updateWayCurrentStatement.setBoolean(prmIndex++, visible);
                    updateWayCurrentStatement.setLong(prmIndex++, way.getChangesetId());
                    updateWayCurrentStatement.setLong(prmIndex++, way.getId());

                    updateWayCurrentStatement.execute();

                } catch (SQLException e) {
                    throw new OsmosisRuntimeException("Unable to update current way with id=" + way.getId() + ".", e);
                }
            } else {
                // Insert the new way into the current table.
                try {
                    prmIndex = 1;
                    insertWayCurrentStatement.setLong(prmIndex++, way.getId());
                    insertWayCurrentStatement.setInt(prmIndex++, way.getVersion());
                    insertWayCurrentStatement.setTimestamp(prmIndex++, new Timestamp(way.getTimestamp().getTime()));
                    insertWayCurrentStatement.setBoolean(prmIndex++, visible);
                    insertWayCurrentStatement.setLong(prmIndex++, way.getChangesetId());

                    insertWayCurrentStatement.execute();

                } catch (SQLException e) {
                    throw new OsmosisRuntimeException("Unable to insert current way with id=" + way.getId() + ".", e);
                }
            }

            // Insert the tags of the new way into the current table.
            for (Tag tag : way.getTags()) {
                try {
                    prmIndex = 1;
                    insertWayTagCurrentStatement.setLong(prmIndex++, way.getId());
                    insertWayTagCurrentStatement.setString(prmIndex++, tag.getKey());
                    insertWayTagCurrentStatement.setString(prmIndex++, tag.getValue());

                    insertWayTagCurrentStatement.execute();

                } catch (SQLException e) {
                    throw new OsmosisRuntimeException("Unable to insert current way tag with id=" + way.getId()
                            + " and key=(" + tag.getKey() + ").", e);
                }
            }

            // Insert the nodes of the new way into the current table.
            for (int i = 0; i < nodeReferenceList.size(); i++) {
                WayNode nodeReference;

                nodeReference = nodeReferenceList.get(i);

                try {
                    prmIndex = 1;
                    insertWayNodeCurrentStatement.setLong(prmIndex++, way.getId());
                    insertWayNodeCurrentStatement.setLong(prmIndex++, nodeReference.getNodeId());
                    insertWayNodeCurrentStatement.setLong(prmIndex++, i);

                    insertWayNodeCurrentStatement.execute();

                } catch (SQLException e) {
                    throw new OsmosisRuntimeException("Unable to insert current way node with way id=" + way.getId()
                            + " and node id=" + nodeReference.getNodeId() + ".", e);
                }
            }
        }
    }

    /**
     * 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) {
        boolean visible;
        boolean exists;
        int prmIndex;
        List<RelationMember> relationMemberList;

        assertEntityHasTimestamp(relation);

        // Add or update the user in the database.
        userManager.addOrUpdateUser(relation.getUser());
        
        // Create the changeset in the database.
        changesetManager.addChangesetIfRequired(relation.getChangesetId(), relation.getUser());

        relationMemberList = relation.getMembers();

        // If this is a deletion, the entity is not visible.
        visible = !action.equals(ChangeAction.Delete);

        // Create the prepared statements for relation creation if necessary.
        if (insertRelationStatement == null) {
            insertRelationStatement = statementContainer.add(dbCtx.prepareStatement(INSERT_SQL_RELATION));
            updateRelationStatement = statementContainer.add(dbCtx.prepareStatement(UPDATE_SQL_RELATION));
            selectRelationCountStatement = statementContainer.add(dbCtx.prepareStatement(SELECT_SQL_RELATION_COUNT));
            insertRelationCurrentStatement = statementContainer
                    .add(dbCtx.prepareStatement(INSERT_SQL_RELATION_CURRENT));
            updateRelationCurrentStatement = statementContainer
                    .add(dbCtx.prepareStatement(UPDATE_SQL_RELATION_CURRENT));
            selectRelationCurrentCountStatement = statementContainer.add(dbCtx
                    .prepareStatement(SELECT_SQL_RELATION_CURRENT_COUNT));
            insertRelationTagStatement = statementContainer.add(dbCtx.prepareStatement(INSERT_SQL_RELATION_TAG));
            deleteRelationTagStatement = statementContainer.add(dbCtx.prepareStatement(DELETE_SQL_RELATION_TAG));
            insertRelationTagCurrentStatement = statementContainer.add(dbCtx
                    .prepareStatement(INSERT_SQL_RELATION_TAG_CURRENT));
            deleteRelationTagCurrentStatement = statementContainer.add(dbCtx
                    .prepareStatement(DELETE_SQL_RELATION_TAG_CURRENT));
            switch (dbCtx.getDatabaseType()) {
            case POSTGRESQL:
            	insertRelationMemberStatement =
            		statementContainer.add(dbCtx.prepareStatement(INSERT_SQL_RELATION_MEMBER_PGSQL));
                break;
            case MYSQL:
            	insertRelationMemberStatement =
            		statementContainer.add(dbCtx.prepareStatement(INSERT_SQL_RELATION_MEMBER_MYSQL));
                break;
            default:
                throw new OsmosisRuntimeException("Unknown database type " + dbCtx.getDatabaseType() + ".");
            }
            deleteRelationMemberStatement = statementContainer.add(dbCtx.prepareStatement(DELETE_SQL_RELATION_MEMBER));
            switch (dbCtx.getDatabaseType()) {
            case POSTGRESQL:
            	insertRelationMemberCurrentStatement =
            		statementContainer.add(dbCtx.prepareStatement(INSERT_SQL_RELATION_MEMBER_CURRENT_PGSQL));
                break;
            case MYSQL:
            	insertRelationMemberCurrentStatement =
            		statementContainer.add(dbCtx.prepareStatement(INSERT_SQL_RELATION_MEMBER_CURRENT_MYSQL));
                break;
            default:
                throw new OsmosisRuntimeException("Unknown database type " + dbCtx.getDatabaseType() + ".");
            }
            deleteRelationMemberCurrentStatement = statementContainer.add(dbCtx
                    .prepareStatement(DELETE_SQL_RELATION_MEMBER_CURRENT));
        }

        // Remove the existing tags of the relation history item.
        try {
            prmIndex = 1;
            deleteRelationTagStatement.setLong(prmIndex++, relation.getId());
            deleteRelationTagStatement.setInt(prmIndex++, relation.getVersion());

            deleteRelationTagStatement.execute();

        } catch (SQLException e) {
            throw new OsmosisRuntimeException("Unable to delete relation history tags for relation with id="
                    + relation.getId() + ".", e);
        }

        // Remove the existing relation members of the relation history item.
        try {
            prmIndex = 1;
            deleteRelationMemberStatement.setLong(prmIndex++, relation.getId());
            deleteRelationMemberStatement.setInt(prmIndex++, relation.getVersion());

            deleteRelationMemberStatement.execute();

        } catch (SQLException e) {
            throw new OsmosisRuntimeException("Unable to delete relation history members for relation with id="
                    + relation.getId() + ".", e);
        }

        // Update the relation if it already exists in the history table, otherwise insert it.
        try {
            exists = checkIfEntityHistoryExists(selectRelationCountStatement, relation.getId(), relation.getVersion());

        } catch (SQLException e) {
            throw new OsmosisRuntimeException("Unable to check if current relation with id=" + relation.getId()
                    + " exists.", e);
        }
        if (exists) {
            // Update the relation in the history table.
            try {
                prmIndex = 1;
                updateRelationStatement.setTimestamp(prmIndex++, new Timestamp(relation.getTimestamp().getTime()));
                updateRelationStatement.setBoolean(prmIndex++, visible);
                updateRelationStatement.setLong(prmIndex++, relation.getChangesetId());
                updateRelationStatement.setLong(prmIndex++, relation.getId());
                updateRelationStatement.setInt(prmIndex++, relation.getVersion());

                updateRelationStatement.execute();

            } catch (SQLException e) {
                throw new OsmosisRuntimeException(
                        "Unable to update history relation with id=" + relation.getId() + ".", e);
            }
        } else {
            // Insert the new relation into the history table.
            try {
                prmIndex = 1;
                insertRelationStatement.setLong(prmIndex++, relation.getId());
                insertRelationStatement.setInt(prmIndex++, relation.getVersion());
                insertRelationStatement.setTimestamp(prmIndex++, new Timestamp(relation.getTimestamp().getTime()));
                insertRelationStatement.setBoolean(prmIndex++, visible);
                insertRelationStatement.setLong(prmIndex++, relation.getChangesetId());

                insertRelationStatement.execute();

            } catch (SQLException e) {
                throw new OsmosisRuntimeException(
                        "Unable to insert history relation with id=" + relation.getId() + ".", e);
            }
        }

        // Insert the tags of the new relation into the history table.
        for (Tag tag : relation.getTags()) {
            try {
                prmIndex = 1;
                insertRelationTagStatement.setLong(prmIndex++, relation.getId());
                insertRelationTagStatement.setInt(prmIndex++, relation.getVersion());
                insertRelationTagStatement.setString(prmIndex++, tag.getKey());
                insertRelationTagStatement.setString(prmIndex++, tag.getValue());

                insertRelationTagStatement.execute();

            } catch (SQLException e) {
                throw new OsmosisRuntimeException("Unable to insert history relation tag with id=" + relation.getId()
                        + " and key=(" + tag.getKey() + ").", e);
            }
        }

        // Insert the members of the new relation into the history table.
        for (int i = 0; i < relationMemberList.size(); i++) {
            RelationMember relationMember;

            relationMember = relationMemberList.get(i);

            try {
                prmIndex = 1;
                insertRelationMemberStatement.setLong(prmIndex++, relation.getId());
                insertRelationMemberStatement.setInt(prmIndex++, relation.getVersion());
                insertRelationMemberStatement.setString(prmIndex++, memberTypeRenderer.render(relationMember
                        .getMemberType()));
                insertRelationMemberStatement.setLong(prmIndex++, relationMember.getMemberId());
                insertRelationMemberStatement.setString(prmIndex++, relationMember.getMemberRole());
                insertRelationMemberStatement.setInt(prmIndex++, i + 1);

                insertRelationMemberStatement.execute();

            } catch (SQLException e) {
                throw new OsmosisRuntimeException("Unable to insert history relation member with relation id="
                        + relation.getId() + ", member type=" + relationMember.getMemberId() + " and member id="
                        + relationMember.getMemberId() + ".", e);
            }
        }

        if (populateCurrentTables) {
            // Delete the existing relation tags from the current table.
            try {
                deleteRelationTagCurrentStatement.setLong(1, relation.getId());

                deleteRelationTagCurrentStatement.execute();

            } catch (SQLException e) {
                throw new OsmosisRuntimeException("Unable to delete current relation tags with id=" + relation.getId()
                        + ".", e);
            }
            // Delete the existing relation members from the current table.
            try {
                deleteRelationMemberCurrentStatement.setLong(1, relation.getId());

                deleteRelationMemberCurrentStatement.execute();

            } catch (SQLException e) {
                throw new OsmosisRuntimeException("Unable to delete current relation members with id="
                        + relation.getId() + ".", e);
            }

            // Update the relation if it already exists in the current table, otherwise insert it.
            try {
                exists = checkIfEntityExists(selectRelationCurrentCountStatement, relation.getId());

            } catch (SQLException e) {
                throw new OsmosisRuntimeException("Unable to check if current relation with id=" + relation.getId()
                        + " exists.", e);
            }
            if (exists) {
                // Update the relation in the current table.
                try {
                    prmIndex = 1;
                    updateRelationCurrentStatement.setInt(prmIndex++, relation.getVersion());
                    updateRelationCurrentStatement.setTimestamp(prmIndex++, new Timestamp(relation.getTimestamp()
                            .getTime()));
                    updateRelationCurrentStatement.setBoolean(prmIndex++, visible);
                    updateRelationCurrentStatement.setLong(prmIndex++, relation.getChangesetId());
                    updateRelationCurrentStatement.setLong(prmIndex++, relation.getId());

                    updateRelationCurrentStatement.execute();

                } catch (SQLException e) {
                    throw new OsmosisRuntimeException("Unable to update current relation with id=" + relation.getId()
                            + ".", e);
                }
            } else {
                // Insert the new node into the current table.
                try {
                    prmIndex = 1;
                    insertRelationCurrentStatement.setLong(prmIndex++, relation.getId());
                    insertRelationCurrentStatement.setInt(prmIndex++, relation.getVersion());
                    insertRelationCurrentStatement.setTimestamp(prmIndex++, new Timestamp(relation.getTimestamp()
                            .getTime()));
                    insertRelationCurrentStatement.setBoolean(prmIndex++, visible);
                    insertRelationCurrentStatement.setLong(prmIndex++, relation.getChangesetId());

                    insertRelationCurrentStatement.execute();

                } catch (SQLException e) {
                    throw new OsmosisRuntimeException("Unable to insert current relation with id=" + relation.getId()
                            + ".", e);
                }
            }

            // Insert the tags of the new relation into the current table.
            for (Tag tag : relation.getTags()) {
                try {
                    prmIndex = 1;
                    insertRelationTagCurrentStatement.setLong(prmIndex++, relation.getId());
                    insertRelationTagCurrentStatement.setString(prmIndex++, tag.getKey());
                    insertRelationTagCurrentStatement.setString(prmIndex++, tag.getValue());

                    insertRelationTagCurrentStatement.execute();

                } catch (SQLException e) {
                    throw new OsmosisRuntimeException("Unable to insert current relation tag with id="
                            + relation.getId() + " and key=(" + tag.getKey() + ").", e);
                }
            }

            // Insert the members of the new relation into the current table.
            for (int i = 0; i < relationMemberList.size(); i++) {
                RelationMember relationMember;

                relationMember = relationMemberList.get(i);

                try {
                    prmIndex = 1;
                    insertRelationMemberCurrentStatement.setLong(prmIndex++, relation.getId());
                    insertRelationMemberCurrentStatement.setString(prmIndex++, memberTypeRenderer.render(relationMember
                            .getMemberType()));
                    insertRelationMemberCurrentStatement.setLong(prmIndex++, relationMember.getMemberId());
                    insertRelationMemberCurrentStatement.setString(prmIndex++, relationMember.getMemberRole());
                    insertRelationMemberCurrentStatement.setInt(prmIndex++, i + 1);

                    insertRelationMemberCurrentStatement.execute();

                } catch (SQLException e) {
                    throw new OsmosisRuntimeException("Unable to insert current relation member with relation id="
                            + relation.getId() + ", member type=" + relationMember.getMemberId() + " and member id="
                            + relationMember.getMemberId() + ".", e);
                }
            }
        }
    }

    /**
     * Flushes all changes to the database.
     */
    public void complete() {
        dbCtx.commit();
    }

    /**
     * Releases all database resources.
     */
    public void close() {
        statementContainer.close();
        userManager.close();
        changesetManager.close();

        dbCtx.close();
    }
}
