// 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.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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.task.v0_6.Sink;


/**
 * An OSM data sink that analyses the data sent to it and provides a simple
 * report.
 * 
 * @author Brett Henderson
 */
public class EntityReporter implements Sink {
	
	private static final int COLUMN_WIDTH_USER_NAME = 50;
	private static final int COLUMN_WIDTH_NODE_COUNT = 7;
	private static final int COLUMN_WIDTH_WAY_COUNT = 7;
	private static final int COLUMN_WIDTH_RELATION_COUNT = 7;
	
	private File file;
	private Map<String, UserStatistics> userMap;
	private UserStatistics anonymousUser;
	private UserStatistics totalUser;
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param file
	 *            The file to write.
	 */
	public EntityReporter(File file) {
		this.file = file;
		
		userMap = new HashMap<String, UserStatistics>();
		anonymousUser = new UserStatistics("anonymous");
		totalUser = new UserStatistics("Total");
	}


	/**
	 * {@inheritDoc}
	 */
	public void initialize(Map<String, Object> metaData) {
		// Do nothing.
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void process(EntityContainer entityContainer) {
		String userName;
		final UserStatistics user;
		
		// Obtain the user statistics object.
		userName = entityContainer.getEntity().getUser().getName();
		if (userName != null && userName.length() > 0) {
			if (userMap.containsKey(userName)) {
				user = userMap.get(userName);
			} else {
				user = new UserStatistics(userName);
				userMap.put(userName, user);
			}
		} else {
			user = anonymousUser;
		}
		
		// Increment the relevant user statistic.
		entityContainer.process(
			new EntityProcessor() {
				private UserStatistics processorUser = user;
				
				public void process(BoundContainer bound) {
					// Do nothing.
				}
				
				public void process(NodeContainer node) {
					processorUser.incrementNodeCount();
					totalUser.incrementNodeCount();
				}
				
				public void process(WayContainer way) {
					processorUser.incrementWayCount();
					totalUser.incrementWayCount();
				}
				
				public void process(RelationContainer relation) {
					processorUser.incrementRelationCount();
					totalUser.incrementRelationCount();
				}
			}
		);
	}
	
	
	/**
	 * Writes a single value and pads it out to the correct column width.
	 * 
	 * @param writer
	 *            The report destination.
	 * @param data
	 *            The data to be written.
	 * @param columnWidth
	 *            The width of the column.
	 */
	private void writeColumnValue(BufferedWriter writer, String data, int columnWidth) throws IOException {
		int padLength;
		
		// Calculate the required data padding. The total column width is the
		// specified column width plus one space.
		padLength = columnWidth - data.length() + 1;
		if (padLength < 1) {
			padLength = 1;
		}
		
		writer.write(data);
		for (int i = 0; i < padLength; i++) {
			writer.write(' ');
		}
	}
	
	
	/**
	 * Writes a single line summary of the user statistics.
	 * 
	 * @param writer
	 *            The report destination.
	 * @param userStatistics
	 *            The user to report on.
	 */
	private void writeUserLine(BufferedWriter writer, UserStatistics userStatistics) throws IOException {
		writeColumnValue(writer, userStatistics.getUserName(), COLUMN_WIDTH_USER_NAME);
		writeColumnValue(writer, Integer.toString(userStatistics.getNodeCount()), COLUMN_WIDTH_NODE_COUNT);
		writeColumnValue(writer, Integer.toString(userStatistics.getWayCount()), COLUMN_WIDTH_WAY_COUNT);
		writeColumnValue(writer, Integer.toString(userStatistics.getRelationCount()), COLUMN_WIDTH_RELATION_COUNT);
		writer.newLine();
	}
	
	
	/**
	 * Add user information to the report file.
	 * 
	 * @param writer
	 *            The report destination.
	 */
	private void writeUserReport(BufferedWriter writer) throws IOException {
		List<UserStatistics> userList;
		
		// Sort the user statistics by user id.
		userList = new ArrayList<UserStatistics>(userMap.values());
		Collections.sort(
			userList,
			new Comparator<UserStatistics>() {
				public int compare(UserStatistics o1, UserStatistics o2) {
					return o1.getUserName().compareTo(o2.getUserName());
				}
			}
		);
		
		writer.write("********** User Report **********");
		writer.newLine();
		writeColumnValue(writer, "USER NAME", COLUMN_WIDTH_USER_NAME);
		writeColumnValue(writer, "NODES", COLUMN_WIDTH_NODE_COUNT);
		writeColumnValue(writer, "WAYS", COLUMN_WIDTH_WAY_COUNT);
		writeColumnValue(writer, "RELNS", COLUMN_WIDTH_RELATION_COUNT);
		writer.newLine();
		writeUserLine(writer, anonymousUser);
		for (UserStatistics userStatistics : userList) {
			writeUserLine(writer, userStatistics);
		}
		writer.newLine();
		writeUserLine(writer, totalUser);
	}
	
	
	/**
	 * Flushes all changes to file.
	 */
	public void complete() {
		try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
			// Produce a report on the user statistics.
			writeUserReport(writer);

		} catch (IOException e) {
			throw new OsmosisRuntimeException("Unable to write report to file " + file + ".");
		}
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void close() {
	}
	
	
	/**
	 * A class holding the summary information for a single user.
	 */
	private static class UserStatistics {
		
		private String userName;
		private int nodeCount;
		private int wayCount;
		private int relationCount;
		
		
		/**
		 * Creates a new instance.
		 * 
		 * @param userName
		 *            The name of the user that this statistics record relates
		 *            to.
		 */
		UserStatistics(String userName) {
			this.userName = userName;
		}
		
		
		/**
		 * Increments the node count by one.
		 */
		public void incrementNodeCount() {
			nodeCount++;
		}
		
		
		/**
		 * Increments the way count by one.
		 */
		public void incrementWayCount() {
			wayCount++;
		}
		
		
		/**
		 * Increments the relation count by one.
		 */
		public void incrementRelationCount() {
			relationCount++;
		}
		
		
		/**
		 * Returns the name of the user for which this object contains data.
		 * 
		 * @return The user name.
		 */
		public String getUserName() {
			return userName;
		}
		
		
		/**
		 * Returns the node count.
		 * 
		 * @return The node count.
		 */
		public int getNodeCount() {
			return nodeCount;
		}
		
		
		/**
		 * Returns the way count.
		 * 
		 * @return The way count.
		 */
		public int getWayCount() {
			return wayCount;
		}
		
		
		/**
		 * Returns the relation count.
		 * 
		 * @return The relation count.
		 */
		public int getRelationCount() {
			return relationCount;
		}
	}
}
