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

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.openstreetmap.osmosis.core.OsmosisRuntimeException;
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.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.DynamicIdTracker;
import org.openstreetmap.osmosis.core.filter.common.IdTracker;
import org.openstreetmap.osmosis.core.task.v0_6.Sink;


/**
 * A sink that verifies the referential integrity of all data passing through
 * it.
 * 
 * @author Brett Henderson
 */
public class IntegrityReporter implements Sink, EntityProcessor {
	
	private static final Logger LOG = Logger.getLogger(IntegrityReporter.class.getName());
	
	private File file;
	private boolean initialized;
	private BufferedWriter writer;
	private IdTracker nodeBitSet;
	private IdTracker wayBitSet;
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param file
	 *            The file to write.
	 */
	public IntegrityReporter(File file) {
		this.file = file;
		
		initialized = false;
		nodeBitSet = new DynamicIdTracker();
		wayBitSet = new DynamicIdTracker();
	}
	
	
	/**
	 * Writes data to the output file.
	 * 
	 * @param data
	 *            The data to be written.
	 */
	private void write(String data) {
		try {
			writer.write(data);
			
		} catch (IOException e) {
			throw new OsmosisRuntimeException("Unable to write data.", e);
		}
	}
	
	
	/**
	 * Writes a new line in the output file.
	 */
	private void writeNewLine() {
		try {
			writer.newLine();
			
		} catch (IOException e) {
			throw new OsmosisRuntimeException("Unable to write data.", e);
		}
	}
	
	
	/**
	 * Initialises the output file for writing. This must be called by
	 * sub-classes before any writing is performed. This method may be called
	 * multiple times without adverse affect allowing sub-classes to invoke it
	 * every time they perform processing.
	 */
	protected void initialize() {
		if (!initialized) {
			OutputStream outStream = null;
			
			try {
				outStream = new FileOutputStream(file);
				
				writer = new BufferedWriter(new OutputStreamWriter(outStream, "UTF-8"));
				
				outStream = null;
				
			} catch (IOException e) {
				throw new OsmosisRuntimeException("Unable to open file " + file + " for writing.", e);
			} finally {
				if (outStream != null) {
					try {
						outStream.close();
					} catch (Exception e) {
						LOG.log(Level.SEVERE, "Unable to close output stream for file " + file + ".", e);
					}
					outStream = null;
				}
			}
			
			initialized = true;
			
			write("Entity Type, Entity Id, Referred Type, Referred Id");
			writeNewLine();
		}
	}


	/**
	 * {@inheritDoc}
	 */
	public void initialize(Map<String, Object> metaData) {
		// Do nothing.
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void process(EntityContainer entityContainer) {
		entityContainer.process(this);
	}
	
	
	/**
	 * {@inheritDoc}
	 */
    public void process(BoundContainer bound) {
	    // Do nothing.
    }


    /**
	 * {@inheritDoc}
	 */
	public void process(NodeContainer node) {
		nodeBitSet.set(node.getEntity().getId());
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void process(WayContainer wayContainer) {
		Way way;
		
		way = wayContainer.getEntity();
		
		wayBitSet.set(way.getId());
		
		for (WayNode wayNode : way.getWayNodes()) {
			if (!nodeBitSet.get(wayNode.getNodeId())) {
				initialize();
				
				write("Way," + way.getId() + ",Node," + wayNode.getNodeId());
				writeNewLine();
			}
		}
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void process(RelationContainer relationContainer) {
		Relation relation;
		
		relation = relationContainer.getEntity();
		
		for (RelationMember relationMember : relation.getMembers()) {
			EntityType memberType;
			
			memberType = relationMember.getMemberType();
			
			if (EntityType.Node.equals(memberType)) {
				if (!nodeBitSet.get(relationMember.getMemberId())) {
					initialize();
					
					write("Relation," + relation.getId() + ",Node," + relationMember.getMemberId());
					writeNewLine();
				}
			} else if (EntityType.Way.equals(memberType)) {
				if (!wayBitSet.get(relationMember.getMemberId())) {
					initialize();
					
					write("Relation," + relation.getId() + ",Way," + relationMember.getMemberId());
					writeNewLine();
				}
			}
		}
	}
	
	
	/**
	 * Flushes all changes to file.
	 */
	public void complete() {
		try {
			if (writer != null) {
				writer.close();
			}
			
		} catch (IOException e) {
			throw new OsmosisRuntimeException("Unable to complete writing to the file " + file + ".", e);
		} finally {
			initialized = false;
			writer = null;
		}
	}
	
	
	/**
	 * Cleans up any open file handles.
	 */
	public void close() {
		try {
			try {
				if (writer != null) {
					writer.close();
				}
			} catch (IOException e) {
				LOG.log(Level.SEVERE, "Unable to close writer.", e);
			}
		} finally {
			initialized = false;
			writer = null;
		}
	}
}
