/*
  This file is part of the BJP package.

  BJP package is free software: you can redistribute it and/or modify it
  under the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation, either version 3 of the License, or (at your
  option) any later version.

  BJP package is distributed in the hope that it will be useful, but
  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
  License for more details.

  You should have received a copy of the GNU Lesser General Public License
  along with BJP package.  If not, see <http://www.gnu.org/licenses/>.
 */
package dataset;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import dataset.adtree.ADNode;
import dataset.feature.BinaryFeature;
import dataset.feature.Feature;

public class Dataset {
	private List<Feature> list;
	private List<Feature> examples;
	private Map<Feature, Integer> map;
	private int numberOfVariables;
	private int[] card;
	private ADNode adTree;
	private boolean useADTree;
	private int numberOfExamples = 0;

	/**
	 * Class constructor
	 */
	public Dataset() {
		this.list = new ArrayList<Feature>();
		this.examples= new ArrayList<Feature>();
		this.card = null;
		this.numberOfVariables = 0;
		this.map = new HashMap<Feature, Integer>();
		this.adTree = null;
		this.useADTree = false;
	}

	/**
	 * Class constructor for specifying the cardinalities of the variables
	 * 
	 * @param cardinalities
	 *            Array specifying the cardinalities of the variables of the
	 *            domain.
	 */
	public Dataset(int[] cardinalities) {
		this();
		this.card = cardinalities;
		this.numberOfVariables = cardinalities.length;
	}

	/**
	 * Adds a data point to the dataset
	 * 
	 * @param example
	 *            The specific data point to add
	 */
	public void addExample(Feature example) {
		Integer value = this.map.get(example);
		examples.add(example);
		if (value == null) {
			this.map.put(example, 1);
			this.getList().add(example);
		} else {
			this.map.put(example, value + 1);
		}

		this.numberOfExamples++;
	}

	/**
	 * Adds a specific number of equal datapoints to the datase
	 * 
	 * @param example
	 *            The specific data point to add
	 * @param count
	 *            The number of times the data point has to be added
	 */
	private void addExample(Feature example, int count) {
		Integer value = this.map.get(example);

		for(int i=0;i<count;++i) examples.add(example);
		if (value == null) {
			this.map.put(example, count);
		} else {
			this.map.put(example, value + count);
		}

		this.getList().add(example);
		this.numberOfExamples += count;

	}

	/**
	 * Returns the number of times a data point occurs in the dataset
	 * 
	 * @param example
	 *            The specific data point to add
	 * @return The number of times a data point occurs in the dataset
	 */
	public int getCount(Feature example) {
		Integer count = this.map.get(example);
		return (count == null) ? 0 : count;
	}

	/**
	 * Returns the number of datapoints that match with the values of the
	 * variables given as input.
	 * 
	 * @param variables
	 *            List of variables to count.
	 * @param values
	 *            The values of the variables to count.
	 * @return
	 */
	public int getCounts(List<Integer> variables, int[] values) {
			int counts = 0;
			for (Feature example : examples) {
				boolean match = true;
				for (int i = 0; i < variables.size(); ++i) {
					int variable = variables.get(i);
					int value = values[i];
					if (example.getVal(variable) != value) {
						match = false;
					}
				}
				if (match){
					++counts;
				}
			}
			return counts;
	}	
	
	/**
	 * Returns the number of times a data point occurs in the dataset
	 * 
	 * @param index
	 *            The index of the specific data point in the dataset
	 * @return The number of times the data point occurs in the dataset
	 */
	public int getCountFromIndex(int index) { // TODO
		return this.getCount(this.list.get(index));
	}

	/**
	 * @return A list with all unique features.
	 */
	public List<Feature> getList() {
		return this.list;
	}

	/**
	 * @return
	 */
	public int getNumberOfVariables() {
		return this.numberOfVariables;
	}

	/**
	 * @param numberOfVariables
	 */
	public void setNumberOfVariables(int numberOfVariables) {
		this.numberOfVariables = numberOfVariables;
	}

	/**
	 * @param card
	 */
	public void setCard(int[] card) {
		this.card = card;
	}

	/**
	 * @param var
	 * @return
	 */
	public int getCardinalities(int var) {
		return this.card[var];
	}

	/**
	 * @return
	 */
	public int[] getCardinalities() {
		return this.card;
	}

	/**
	 * @return
	 */
	public Map<Feature, Integer> getMap() {
		return map;
	}

	/**
	 * @param vars
	 * @param context
	 * @param records
	 * @return
	 */
	public int[] computeSufficientStatistics(BitSet vars,
			Map<Integer, Integer> context, BitSet records) {
		return computeSufficientStatistics(vars, context, records, useADTree);
	}

	/**
	 * @param vars
	 * @param context
	 * @param records
	 * @param cacheFlag
	 * @return
	 */
	public int[] computeSufficientStatistics(BitSet vars,
			Map<Integer, Integer> context, BitSet records, boolean cacheFlag) {
		if (cacheFlag) {
			return adTree.makeContab(vars, context, this.card, this);
		} else {
			return (records != null) ? computeSufficientStatisticsFromDataset(
					vars, context, records)
					: computeSufficientStatisticsFromDataset(vars, context);
		}
	}

	/**
	 * @param vars
	 * @param context
	 * @return
	 */
	public int[] computeSufficientStatisticsFromDataset(BitSet vars,
			Map<Integer, Integer> context) {
		BitSet valuedVars = new BitSet(this.getNumberOfVariables());
		BitSet nonValuedVars = new BitSet(this.getNumberOfVariables());

		int numberOfCells = 1;
		for (int i = vars.nextSetBit(0); i >= 0; i = vars.nextSetBit(i + 1))
			if (context.containsKey(i)) {
				valuedVars.set(i, true);

			} else {
				nonValuedVars.set(i, true);
				numberOfCells *= this.getCardinalities(i);
			}

		int[] counts = new int[numberOfCells];

		int idx, val, k;
		// loop over examples in dataset
		for (Feature example : this.list) {
			idx = 0;
			k = 0;

			// check compatibility with context
			boolean flag = true;
			for (int i = valuedVars.nextSetBit(0); i >= 0; i = valuedVars
					.nextSetBit(i + 1)) {
				val = example.getVal(i);

				if (context.containsKey(i) && (context.get(i) != val))
					flag = false;
			}

			if (!flag)
				continue;

			for (int i = nonValuedVars.nextSetBit(0); i >= 0; i = nonValuedVars
					.nextSetBit(i + 1)) {
				val = example.getVal(i);

				if (val != 0)
					idx += (int) Math.pow(2, nonValuedVars.cardinality() - 1
							- k);
				k++;
			}

			counts[idx] += this.getCount(example);
		}

		return counts;
	}

	/**
	 * @param vars
	 * @param context
	 * @param records
	 * @return
	 */
	public int[] computeSufficientStatisticsFromDataset(BitSet vars,
			Map<Integer, Integer> context, BitSet records) {
		BitSet valuedVars = new BitSet(this.getNumberOfVariables());
		BitSet nonValuedVars = new BitSet(this.getNumberOfVariables());

		int numberOfCells = 1;
		for (int i = vars.nextSetBit(0); i >= 0; i = vars.nextSetBit(i + 1))
			if (context.containsKey(i)) {
				valuedVars.set(i, true);

			} else {
				nonValuedVars.set(i, true);
				numberOfCells *= this.getCardinalities(i);
			}

		int[] counts = new int[numberOfCells];

		int idx, val, k;
		// loop over examples in dataset

		Feature example;
		for (int j = records.nextSetBit(0); j >= 0; j = records
				.nextSetBit(j + 1)) {
			example = this.list.get(j);

			idx = 0;
			k = 0;

			// check compatibility with context
			boolean flag = true;
			for (int i = valuedVars.nextSetBit(0); i >= 0; i = valuedVars
					.nextSetBit(i + 1)) {
				val = example.getVal(i);

				if (context.containsKey(i) && (context.get(i) != val))
					flag = false;
			}

			if (!flag)
				continue;

			for (int i = nonValuedVars.nextSetBit(0); i >= 0; i = nonValuedVars
					.nextSetBit(i + 1)) {
				val = example.getVal(i);

				if (val != 0)
					idx += (int) Math.pow(2, nonValuedVars.cardinality() - 1
							- k);
				k++;
			}

			counts[idx] += this.getCount(example);
		}

		return counts;
	}

	/**
	 * Initializes the cache
	 * 
	 * @param threshold
	 */
	public void createCache(int threshold) {
		// check upper and lower bounds
		ADNode.MIN_RECORD_SIZE = (threshold < 0) ? 0 : threshold;
		ADNode.MIN_RECORD_SIZE = (threshold > this.getList().size()) ? this
				.getList().size() : threshold;

		// make the root node
		this.adTree = ADNode.makeADNode(this);
	}

	/**
	 * Gets a portion of the current dataset, corresponding to the @param slice.
	 * 
	 * @param z
	 *            List of variables of the slice.
	 * @param slice
	 *            List of the values of the variables in the slice.
	 * @return An instance of the dataset with the datapoints that match whit
	 *         the input slice.
	 */
	public Dataset getSubdataset(Feature z) {
		if (z == null || z.getLength() < 1) {
			return this;
		} else {
			Dataset subdataset = new Dataset();
			subdataset.setCard(card);
			subdataset.setNumberOfVariables(numberOfVariables);

			for (Feature example : this.map.keySet()) {
				if (z.isSatisfied(example)) {
					subdataset.addExample(example, this.getCount(example));
				}
			}
			return subdataset;
		}

	}

	// /**
	// * Returns the number of datapoints that match with the values of the
	// * variables given as input.
	// *
	// * @param variables
	// * List of variables to count.
	// * @param values
	// * The values of the variables to count.
	// * @return
	// */
	// public int getCount(List<Integer> variables, int[] values) {
	// int counts = 0;
	// for (Feature example : this.map.keySet()) {
	// boolean match = true;
	// for (int i = 0; i < variables.size(); ++i) {
	// int variable = variables.get(i);
	// int value = values[i];
	// if (example.getVal(variable) != value) {
	// match = false;
	// }
	// }
	// if (match)
	// counts += this.getCount(example);
	// }
	// return counts;
	// }

	/**
	 * @return The number of examples present in the dataset
	 */
	public int getNumberOfExamples() {
		return numberOfExamples;
	}

	/**
	 * Returns a vector with dimension @param numberOfVariables, containing the
	 * domain size specified by @parm domainSize
	 * 
	 * @param numberOfVariables
	 *            Number of components of the array to return
	 * @param domainSize
	 *            Domain size of the variables
	 * @return A vector with dimension @param numberOfVariables, containing the
	 *         domain size specified by @parm domainSize
	 */
	public static int[] getUniformDomainSizes(int numberOfVariables,
			int domainSize) {
		int[] domain = new int[numberOfVariables];
		for (int i = 0; i < numberOfVariables; ++i) {
			domain[i] = domainSize;
		}
		return domain;
	}

	/**
	 * Reads a tabular dataset (train set) from disk.
	 * 
	 * @param inputFile
	 *            Complete path of the file containing the training examples.
	 * @param numberOfExamples
	 *            Number specifying how many rows must be read from the file
	 *            that contains the training examples.
	 * @param skipFirstExamples
	 *            Number of rows to skip in the top of the file, before reading
	 *            the datapoints.
	 * @return An instance of the Dataset class.
	 * @throws IOException
	 */
	public Dataset readDataset(File inputFile, int numberOfExamples,
			int skipFirstExamples) throws IOException {
		Dataset dataset = new Dataset(this.card);
		FileReader fileReader = new FileReader(inputFile);
		BufferedReader bufferedReader = new BufferedReader(fileReader);
		int numberOfVariables = 0;
		String line = null;
		for (int i = 0; i < skipFirstExamples; ++i)
			bufferedReader.readLine();

		while ((line = bufferedReader.readLine()) != null
				&& dataset.getNumberOfExamples() < numberOfExamples) {
			String[] items = line.trim().split(",");
			numberOfVariables = items.length;
			Feature example = null;

			example = new BinaryFeature(numberOfVariables);

			for (int i = 0; i < numberOfVariables; i++) {
				example.addVal(i, Integer.parseInt(items[i]));
			}

			dataset.addExample(example);
		}
		bufferedReader.close();

		dataset.setNumberOfVariables(numberOfVariables);
		return dataset;
	}

	/**
	 * Returns the number of columns in the file specified in @param inputFile
	 * 
	 * @param inputFile
	 *            The file that contains the dataset
	 * @return The number of columns in the file specified in @param inputFile
	 * @throws IOException
	 */
	public static int getNumberOfColumns(File inputFile) throws IOException {
		FileReader fileReader = new FileReader(inputFile);
		BufferedReader bufferedReader = new BufferedReader(fileReader);
		int numberOfVariables = 0;
		String line = null;
		line = bufferedReader.readLine();
		String[] items = line.trim().split(",");
		numberOfVariables = items.length;
		bufferedReader.close();
		return numberOfVariables;
	}

	/**
	 * Returns the cardinalities of the variables in @param vars
	 * 
	 * @param vars
	 *            The variables to know its cardinalities
	 * @return The cardinalities of the variables in @param vars
	 */
	public int[] getCardinalities(BitSet vars) {
		int[] cards = new int[vars.cardinality()];
		int counter = 0;
		for (int i = vars.nextSetBit(0); i >= 0; i = vars.nextSetBit(i + 1)) {
			cards[counter++] = this.getCardinalities(i);
		}
		return cards;
	}

	

	/**
	 * Gets a portion of the current dataset, corresponding to the @param slice.
	 * 
	 * @param z
	 *            List of variables of the slice.
	 * @param slice
	 *            List of the values of the variables in the slice.
	 * @return An instance of the dataset with the datapoints that match whit
	 *         the input slice.
	 */
	public Dataset getSubdataset(List<Integer> z, int[] slice) {
		List<Feature> matchingExamples = new ArrayList<Feature>();
		for (Feature example : examples) {
			boolean match = true;
			for (int i = 0; i < z.size(); ++i) {
				int variable = z.get(i);
				int value = slice[i];
				if (example.getVal(variable) != value) {
					match = false;
				}
			}
			if (match) {
				matchingExamples.add(example);
			}
		}
		Dataset datasetOfSlice = new Dataset(getCardinalities());
		datasetOfSlice.list = matchingExamples;
		datasetOfSlice.examples= matchingExamples;
		return datasetOfSlice;
	}
	
	/**
	 * Returns a list with the domain variables
	 * 
	 * @return A list with the domain variables
	 */
	public List<Integer> getDomainVariables() {
		List<Integer> domain = new ArrayList<Integer>(numberOfVariables);
		for (int i = 0; i < numberOfVariables; ++i) {
			domain.add(i);
		}
		return domain;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {

		String string = "";
		for (Feature f : this.list) {
			string += f.toString() + "\n";
		}

		return string;
	}

}