package org.pmdreports.dao;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.TreeSet;

import org.apache.log4j.Logger;
import org.pmdreports.model.Component;
import org.pmdreports.model.Project;
import org.pmdreports.model.Ruleset;
import org.pmdreports.model.Statistics;
import org.pmdreports.util.DateUtil;
import org.pmdreports.util.PMDReportsException;
import org.pmdreports.util.StringUtil;

/**
 * Basic CRUD operations for the Statistics line. Note that this is not a very
 * efficient/elegant design for persisting data, but was implemented due to
 * ease/simplicity.
 * 
 * @author Ben Northrop Aug 22, 2007
 */
public final class StatisticsDAO {

	// DATA MEMBERS
	private final static String DELIMITER = "\t";

	private final Collection<Statistics> allStatistics = new ArrayList<Statistics>();

	private final String fileName;

	private final List<Ruleset> rulesets;

	private final List<Project> projects;

	private static final Logger LOG = Logger.getLogger(StatisticsDAO.class
			.getName());

	/**
	 * Default constructor, injecting the file name (so easier to test).
	 */
	public StatisticsDAO(String fileName, List<Ruleset> rulesets,
			List<Project> projects) {
		this.fileName = fileName;
		this.rulesets = rulesets;
		this.projects = projects;
		load();
	}

	/**
	 * Retrieve a list of all the components defined in the file.
	 */
	public Collection<Component> retrieveComponents() {
		Collection<Component> components = new ArrayList<Component>();
		for (Statistics stats : allStatistics) {
			if (!components.contains(stats.getComponent())) {
				components.add(stats.getComponent());
			}
		}
		return components;
	}

	/**
	 * Retrieve a list of all the components defined in the file.
	 */
	public Collection<Project> retrieveProjects() {
		Collection<Project> projects = new ArrayList<Project>();
		for (Statistics stats : allStatistics) {
			if (!projects.contains(stats.getComponent().getProject())) {
				projects.add(stats.getComponent().getProject());
			}
		}
		return projects;
	}

	/**
	 * Return a list of all statistics of a given component.
	 */
	public Collection<Statistics> retrieve(Component component) {
		Collection<Statistics> returnStats = new ArrayList<Statistics>();
		for (Statistics stats : allStatistics) {
			if (stats.getComponent().equals(component)) {
				returnStats.add(stats);
			}
		}
		return returnStats;
	}

	/**
	 * Retrieve a list of all statistics for a given date.
	 */
	public Collection<Statistics> retrieve(Date date) {
		Collection<Statistics> returnStats = new ArrayList<Statistics>();
		for (Statistics stats : allStatistics) {
			if (DateUtil.datesAreEqual(stats.getDateCalculated(), date)) {
				returnStats.add(stats);
			}
		}
		return returnStats;
	}

	/**
	 * Return a copy of the list of all statistics.
	 */
	public Collection<Statistics> retrieveAll() {
		return new ArrayList<Statistics>(allStatistics);
	}

	/**
	 * Find a statistics line with the given date and component.
	 */
	public Statistics find(Date date, Component component) {
		for (Statistics stats : allStatistics) {
			if (stats.getComponent().equals(component)
					&& DateUtil.datesAreEqual(date, stats.getDateCalculated())) {
				return stats;
			}
		}
		return null;
	}

	/**
	 * Insert an individual Statistics record for a component.
	 */
	public void insert(Statistics statistics) {
		Statistics sameDateStats = find(statistics.getDateCalculated(),
				statistics.getComponent());
		if (sameDateStats != null) {
			allStatistics.remove(sameDateStats);
			LOG.info("Removing stats from same day: "
					+ sameDateStats.getComponent().getName());
		}
		allStatistics.add(statistics);
		store();
	}

	/**
	 * Utility method to parse a double from the list.
	 */
	private Double getDouble(List<String> list, int index) {
		String value = list.get(index);
		if (value == null) {
			throw new PMDReportsException("value is null for index: " + index,
					null);
		}
		try {
			return Double.parseDouble(value);
		} catch (NumberFormatException nfe) {
			throw new PMDReportsException("value is not number: " + value
					+ " and index: " + index, nfe);
		}
	}

	/**
	 * Load all statistics from the file.
	 */
	private void load() {

		try {
			FileReader fileReader = new FileReader(fileName);
			BufferedReader reader = new BufferedReader(fileReader);
			String in = "";

			while ((in = reader.readLine()) != null) {
				List<String> list = StringUtil.getSplitList(in, DELIMITER);
				String projectName = list.get(0);
				String componentName = list.get(1);
				Component component = findComponent(projectName, componentName);
				if (component != null) {
					String dateStr = list.get(2);
					Date date = new Date(Long.parseLong(dateStr)); // NOPMD
					Statistics stats = new Statistics(component); // NOPMD
					stats.setDateCalculated(date);
					stats.setLinesOfCode(Integer.parseInt(list.get(3)));
					stats.setNumberOfJavaFiles(Integer.parseInt(list.get(4)));
					for (int i = 5; i < list.size(); i++) {
						String rulesetName = list.get(i);
						Ruleset ruleset = findRuleset(rulesetName);
						stats.addStatistic(ruleset, getDouble(list, ++i));
					}

					allStatistics.add(stats);
				}
			}
			reader.close();
			fileReader.close();
		} catch (FileNotFoundException fnfe) {
			LOG.info(fileName + " not found.  May not exist yet.");
		} catch (IOException ioe) {
			throw new PMDReportsException("Exception reading: " + fileName, ioe);
		}

	}

	/**
	 * Store all statistics anew. This means overwriting the existing file,
	 * which of course is not very efficient (or safe). But it sure is easy!
	 */
	private void store() {
		try {
			FileOutputStream fos = new FileOutputStream(fileName);
			PrintWriter pw = new PrintWriter(fos);
			for (Statistics stats : allStatistics) {
				if (stats != null) {
					pw.println(buildStatisticsString(stats));
				}
			}

			pw.close();
			fos.close();
		} catch (IOException ioe) {
			throw new PMDReportsException("IOException when storing!", ioe);
		}
	}

	/**
	 * Build the string of the Statistics line.
	 */
	private String buildStatisticsString(Statistics stats) {
		if (stats == null) {
			throw new PMDReportsException("Stats are null!", null);
		}
		if (stats.getComponent() == null) {
			throw new PMDReportsException("Component is null!", null);
		}

		StringBuffer str = new StringBuffer(200);
		str.append(stats.getComponent().getProject().getName());
		str.append(DELIMITER);
		str.append(stats.getComponent().getName());
		str.append(DELIMITER);
		str.append(stats.getDateCalculated().getTime());
		str.append(DELIMITER);
		str.append(stats.getLinesOfCode());
		str.append(DELIMITER);
		str.append(stats.getNumberOfFiles());
		for (Ruleset ruleset : stats.getRulesets()) {
			str.append(DELIMITER);
			str.append(ruleset.getName());
			str.append(DELIMITER);
			str.append(stats.getStatistic(ruleset, false));
		}
		return str.toString();
	}

	/**
	 * Helper method for finding a ruleset from the master list.
	 */
	private Ruleset findRuleset(String name) {
		if (StringUtil.isEmpty(name)) {
			throw new InvalidParameterException("Ruleset name is empty!");
		}
		for (Ruleset ruleset : rulesets) {
			if (ruleset.getName().equals(name)) {
				return ruleset;
			}
		}
		throw new PMDReportsException("No ruleset found for name: " + name);
	}

	/**
	 * Get the project and component for a given project name and component
	 * name.
	 */
	private Component findComponent(String projectName, String componentName) {
		for (Project project : projects) {
			if (project.getComponents() != null) {
				for (Component component : project.getComponents()) {
					if (component.getName().equals(componentName)
							&& project.getName().equals(projectName)) {
						return component;
					}
				}
			}
		}
		LOG.warn("No component found for project: " + projectName
				+ " and component: " + componentName);
		return null;
	}

	/**
	 * Return a collection of all dates that statistics were gathered.
	 */
	public Collection<Date> retrieveDates() {
		Collection<Date> dates = new TreeSet<Date>(new Comparator<Date>() {
			public int compare(Date d1, Date d2) {
				return d1.compareTo(d2);
			}
		});
		for (Statistics stats : this.allStatistics) {
			boolean isInList = false;
			{
				for (Date date : dates) {
					if (DateUtil.datesAreEqual(date, stats.getDateCalculated())) {
						isInList = true;
					}
				}
			}
			if (!isInList) {
				dates.add(stats.getDateCalculated());
			}
		}
		return dates;
	}
}
