/*
 *  Copyright 2011 Eric F. Savage, code@efsavage.com
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package com.ajah.util.log;

import java.io.PrintStream;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;

import com.ajah.util.compare.EntryValueComparator;
import com.ajah.util.text.Strings;

/**
 * An atomic counter for keeping track of things, intended but not restricted to
 * tally against the values of an enum.
 * 
 * @author efsavage
 * @param <T>
 */
public class Tally<T> {

	protected final ConcurrentMap<T, Long> map = new ConcurrentHashMap<>();
	protected final AtomicLong errors = new AtomicLong();
	protected final AtomicLong successes = new AtomicLong();
	protected PrintStream out;

	/**
	 * Public constructor.
	 * 
	 * @param out
	 *            The output for reporting.
	 */
	public Tally(final PrintStream out) {
		this.out = out;
	}

	/**
	 * Increment the error count.
	 */
	public void error() {
		this.errors.incrementAndGet();
	}

	/**
	 * Return the error count.
	 * 
	 * @return The error count.
	 */
	public long getErrors() {
		return this.errors.get();
	}

	/**
	 * Return the success count.
	 * 
	 * @return The success count.
	 */
	public long getSuccesses() {
		return this.successes.get();
	}

	/**
	 * Return the total count.
	 * 
	 * @return The total count.
	 */
	public long getTotal() {
		return this.errors.get() + this.successes.get();
	}

	/**
	 * Generates a report only if the {@link #getTotal()} evenly divides by the
	 * interval, and is greater than zero.
	 * 
	 * @param interval
	 *            The interval to space reports by
	 * @see #report()
	 */
	public void intervalReport(final int interval) {
		if (getTotal() > 0 && (getTotal() % interval) == 0) {
			report();
		}
	}

	/**
	 * Write a report with totals to the configured output.
	 */
	public void report() {
		report(0);
	}

	/**
	 * Write a report with totals to the configured output.
	 * 
	 * @param threshold
	 *            Only print totals of at least this value.
	 */
	public void report(final long threshold) {
		this.out.println(Strings.HYPEN35);
		this.out.println("Success/Error/Total: " + getSuccesses() + "/" + getErrors() + "/" + getTotal() + " - " + NumberFormat.getPercentInstance().format(1.0 * getSuccesses() / getTotal()));
		for (final T t : this.map.keySet()) {
			if (this.map.get(t).longValue() >= threshold) {
				this.out.println(t.toString() + ": " + this.map.get(t));
			}
		}
	}

	public void reportTop(final int number) {
		this.out.println(Strings.HYPEN35);
		this.out.println("Success/Error/Total: " + getSuccesses() + "/" + getErrors() + "/" + getTotal() + " - " + NumberFormat.getPercentInstance().format(1.0 * getSuccesses() / getTotal()));
		List<Entry<T, Long>> top = new ArrayList<>(this.map.entrySet());
		Collections.sort(top, new EntryValueComparator<T, Long>());
		if (top.size() > number) {
			top = top.subList(0, number);
		}
		for (final Entry<T, Long> entry : top) {
			this.out.println(entry.getKey().toString() + ": " + entry.getValue());
		}
	}

	/**
	 * Increment the success count.
	 */
	public void success() {
		this.successes.incrementAndGet();
	}

	/**
	 * Tally an object with an increment of 1. This works best with enums.
	 * 
	 * @param tallyObject
	 *            The object to tally by.
	 */
	public void tally(final T tallyObject) {
		tally(tallyObject, 1);
	}

	/**
	 * Tally an object with a specific increment. This works best with enums.
	 * 
	 * @param tallyObject
	 *            The object to tally by.
	 * @param increment
	 *            The number to increment the tally by.
	 */
	public void tally(final T tallyObject, final long increment) {
		final Long oldValue = this.map.get(tallyObject);
		if (oldValue == null) {
			this.map.putIfAbsent(tallyObject, Long.valueOf(increment));
		} else {
			final Long newValue = Long.valueOf(oldValue.intValue() + increment);
			this.map.replace(tallyObject, oldValue, newValue);
		}
	}

}

@@ -4020,36 +4020,36 @@
 y.%0A%09 */%0A%09public 
-void
+long
  tally(final T t
@@ -4064,16 +4064,23 @@
 ct) %7B%0A%09%09
+return 
 tally(ta
@@ -4094,24 +4094,337 @@
 ct, 1);%0A%09%7D%0A%0A
+%09/**%0A%09 * Get but do not increment the tally for an object.%0A%09 * %0A%09 * @param tallyObject%0A%09 *            The object to get the current count for.%0A%09 */%0A%09public long getValue(final T tallyObject) %7B%0A%09%09final Long value = this.map.get(tallyObject);%0A%09%09if (value == null) %7B%0A%09%09%09return 0;%0A%09%09%7D%0A%09%09return value.longValue();%0A%09%7D%0A%0A
 %09/**%0A%09 * Tal
@@ -4624,32 +4624,44 @@
 t the tally by.%0A
+%09 * @return%0A
 %09 */%0A%09public voi
@@ -4657,20 +4657,20 @@
 %09public 
-void
+long
  tally(f
@@ -4990,18 +4990,66 @@
 Value);%0A%09%09%7D%0A
+%09%09return this.map.get(tallyObject).longValue();%0A
 %09%7D%0A%0A%7D%0A

Return the value instead of void