package de.psychometrica.norming;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;

/**
 * This class takes raw values or relative ranks and computes the relative ranks
 * and empirical t values for a list of values based on group membership. It can
 * be used to generate norm tables or to retrieve normal values for data sets
 * that are not normally distributed.<br>
 * If no mode is specified, the Blom-algorithm is used. Its essential purpose is
 * to do a normal rank transformation. The algorithm is distribution free. The
 * data do not have to be normally distributed. Precompiled binaries and Javadoc
 * can be found at http://www.psychometrica.de/norming.html<br>
 * The code is licensed under the Lesser General Public License (LGPL 2.0)
 * 
 * @author PD Dr. Wolfgang Lenhard
 * 
 */
public class RankConverter {
	private final double[] empiricalTValue;
	private final double[] relativeRank;
	private final int[] group;
	private int[] rawValue = null;

	/**
	 * Constructor for calculating the empirical t values for a dataset with
	 * different samples. Please ensure, that both arrays have the same size.
	 * 
	 * @param group
	 *            an integer array for the group membership
	 * @param relativeRank
	 *            a double array for the relative ranks
	 */
	public RankConverter(int[] group, double[] relativeRank) {
		empiricalTValue = new double[relativeRank.length];
		this.group = group;
		this.relativeRank = relativeRank;
		computeEmpiricalTValue();
	}

	/**
	 * Constructor just for using this class for data storage. No computation is
	 * done.
	 * 
	 * @param group
	 *            an integer array for the group membership
	 * @param relativeRank
	 *            a double array for the relative ranks
	 * @param empiricalTValue
	 *            a double array with the empirical T Values
	 */
	public RankConverter(int[] group, double[] relativeRank,
			double[] empiricalTValue) {
		this.empiricalTValue = empiricalTValue;
		this.group = group;
		this.relativeRank = relativeRank;
	}

	/**
	 * Constructor for calculating relative ranks and the empirical t values
	 * based on raw values for a dataset with different samples. Please ensure,
	 * that both arrays have the same size.
	 * 
	 * @param group
	 *            an integer array for the group membership
	 * @param rawvalue
	 *            an integer array with the raw values
	 * @param mode
	 *            the ranking mode defined by Rank.java
	 */
	public RankConverter(int[] group, int[] rawvalue, int mode) {
		empiricalTValue = new double[rawvalue.length];
		relativeRank = new double[rawvalue.length];
		this.group = group;
		this.rawValue = rawvalue;
		computeRelativeRanks(group, rawvalue, mode);
		computeEmpiricalTValue();
	}

	/**
	 * Constructor for calculating relative ranks and the empirical t values
	 * based on raw values for a dataset with one sample.
	 * 
	 * @param rawvalue
	 *            an integer array with the raw values
	 * @param mode
	 *            the ranking mode defined by Rank.java
	 */
	public RankConverter(int[] rawvalue, int mode) {
		empiricalTValue = new double[rawvalue.length];
		relativeRank = new double[rawvalue.length];
		this.group = new int[rawvalue.length];
		for (int i = 0; i < group.length; i++)
			group[i] = 0;
		this.rawValue = rawvalue;
		computeRelativeRanks(group, rawvalue, mode);
		computeEmpiricalTValue();
	}

	/**
	 * Constructor for calculating relative ranks and the empirical t values
	 * based on raw values for a dataset with one sample. Blom (1959)-algorithm
	 * is used for calculating the relative ranks
	 * 
	 * @param rawvalue
	 *            an integer array with raw values
	 */
	public RankConverter(int[] rawvalue) {
		empiricalTValue = new double[rawvalue.length];
		relativeRank = new double[rawvalue.length];
		this.group = new int[rawvalue.length];
		for (int i = 0; i < group.length; i++)
			group[i] = 0;
		this.rawValue = rawvalue;
		computeRelativeRanks(group, rawvalue, Rank.BLOM);
		computeEmpiricalTValue();
	}

	/**
	 * Compute the t-Values for the relative ranks
	 */
	private void computeEmpiricalTValue() {
		HashMap<Double, Double> tMap = new HashMap<Double, Double>();
		for (int i = 0; i < relativeRank.length; i++) {
			if (!tMap.containsKey(relativeRank[i])) {
				double t = (double) NormalDistribution
						.getInverseCumulativeDistribution(relativeRank[i]) * 10f + 50f;
				tMap.put(relativeRank[i], t);
				empiricalTValue[i] = t;
			} else
				empiricalTValue[i] = tMap.get(relativeRank[i]);
		}
	}

	/**
	 * Compute the relative ranks for the raw values in the different groups
	 * 
	 * @param group
	 *            array of group indices for single values
	 * @param rawValues
	 *            array of raw values
	 * @param mode
	 *            rank computation; see constants in Rank.java
	 */
	private void computeRelativeRanks(int[] group, int[] rawValues, int mode) {
		HashMap<Integer, Double> rankMap = new HashMap<Integer, Double>();

		ArrayList<Integer> groupList = new ArrayList<Integer>();
		for (int i = 0; i < rawValues.length; i++) {
			if (!groupList.contains(group[i]))
				groupList.add(group[i]);
		}

		Collections.sort(groupList);
		for (int i = 0; i < groupList.size(); i++) {
			ArrayList<Integer> valueList = new ArrayList<Integer>();

			// build valueList for group
			for (int j = 0; j < rawValues.length; j++) {
				if (group[j] == groupList.get(i)) {
					valueList.add(rawValues[j]);
				}
			}

			// retrieve relative rank for value x in this group (if not yet
			// present), store it and
			// write it to relativeRank-Array
			for (int j = 0; j < rawValues.length; j++) {
				if ((group[j] == groupList.get(i))
						&& !rankMap.containsKey(rawValues[j])) {
					rankMap.put(rawValues[j],
							Rank.getRank(valueList, rawValues[j], mode));
				}

				if (rankMap.get(rawValues[j]) != null)
					relativeRank[j] = rankMap.get(rawValues[j]);
			}
		}
	}

	/**
	 * @return group indices
	 */
	public int[] getGroup() {
		return group;
	}

	/**
	 * @return the empirical T values: normal distributed values with m = 50.0
	 *         and sd = 10.0
	 */
	public double[] getEmpiricalTValue() {
		return empiricalTValue;
	}

	/**
	 * @return the relative ranks for the raw values depending on the groups,
	 *         varying between 0 (lowest rank) and 1.0 (highest rank)
	 */
	public double[] getRelativeRank() {
		return relativeRank;
	}

	/**
	 * Retrieve rank for group
	 * 
	 * @param group
	 *            Group membership
	 * @param raw
	 *            the raw value
	 * @return the relative rank; search for next lower value if rank is not
	 *         existant; return Double.NaN if no raw values are present and
	 *         0.0d, if no according rank could be found
	 */
	public double getRelativeRank(int group, int raw){
		if(rawValue==null)
			return Double.NaN;
		
		HashMap<Integer, Double> rankMap = new HashMap<Integer, Double>();
		for (int i = 0; i < rawValue.length; i++) {
			if (this.group[i] == group && !rankMap.containsKey(raw)) {
				rankMap.put(raw, relativeRank[i]);
			}
		}
		
		double rank = 0;
		if (rankMap.containsKey(raw))
			rank = rankMap.get(raw);
		else {
			// retrieve next lower rank
			int r1 = raw - 1;
			while (r1 > 0) {
				if (rankMap.containsKey(r1))
					rank = rankMap.get(r1);
				else
					r1--;
			}
		}

		return rank;
	}

	/**
	 * Retrieve rank for raw value; assuming, that there is only one sample in
	 * the data
	 * 
	 * @param raw
	 *            the raw value
	 * @return the relative rank; search for next lower value if rank is not
	 *         existent; return Double.NaN if no raw values are present or if
	 *         there is more than one group and 0.0d, if no according rank could
	 *         be found
	 */
	public double getRelativeRank(int raw) {
		if (rawValue == null)
			return Double.NaN;

		// count groups
		ArrayList<Integer> groupList = new ArrayList<Integer>();
		for (int i = 0; i < group.length; i++) {
			if (!groupList.contains(group[i]))
				groupList.add(group[i]);
		}

		if (groupList.size() > 1)
			return Double.NaN;

		HashMap<Integer, Double> rankMap = new HashMap<Integer, Double>();
		for (int i = 0; i < rawValue.length; i++) {
			rankMap.put(raw, relativeRank[i]);
		}

		double rank = 0;
		if (rankMap.containsKey(raw))
			rank = rankMap.get(raw);
		else {
			// retrieve next lower rank
			int r1 = raw - 1;
			while (r1 > 0) {
				if (rankMap.containsKey(r1))
					rank = rankMap.get(r1);
				else
					r1--;
			}
		}

		return rank;
	}

	/**
	 * Small demo for using the library
	 * 
	 * @param args
	 *            No arguments necessary
	 */
	public static void main(String[] args) {
		System.out
				.println("This function serves as a demo for the usage of the small library.");

		// easiest case: One sample and raw data: you need the raw values
		int[] rawValues = { 1, 2, 3, 2, 3, 4, 4, 5, 5, 4, 5, 6, 1, 4, 3, 2, 3,
				4, 5, 6, 7, 8, 6, 7, 5, 6, 7, 8, 9, 10 };
		RankConverter rc = new RankConverter(rawValues);

		double[] tValues = rc.getEmpiricalTValue();
		double[] ranks = rc.getRelativeRank();

		System.out.println("Raw Values\tRelative Ranks\tT Values");
		for (int i = 0; i < rawValues.length; i++) {
			System.out.println(rawValues[i] + "\t" + ranks[i] + "\t"
					+ tValues[i]);
		}

	}
}
