/**
 * Copyright (C) 2001-2017 by RapidMiner and the contributors
 * 
 * Complete list of developers available at our web site:
 * 
 * http://rapidminer.com
 * 
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Affero General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 * 
 * This program 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
 * Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License along with this program.
 * If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.example.table;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import com.rapidminer.example.AttributeTypeException;
import com.rapidminer.example.Example;
import com.rapidminer.tools.Tools;


/**
 * This is an implementation of {@link NominalMapping} which can be used for nominal attributes with
 * an arbitrary number of different values.
 *
 * @author Ingo Mierswa
 */
public class PolynominalMapping implements NominalMapping {

	private static final long serialVersionUID = 5021638750496191771L;

	/** The map between symbolic values and their indices. */
	private final Map<String, Integer> symbolToIndexMap = new LinkedHashMap<>();

	/** The map between indices of nominal values and the actual nominal value. */
	private final List<String> indexToSymbolMap = new ArrayList<>();

	public PolynominalMapping() {}

	public PolynominalMapping(Map<Integer, String> map) {
		this.symbolToIndexMap.clear();
		this.indexToSymbolMap.clear();
		for (Map.Entry<Integer, String> entry : map.entrySet()) {
			int index = entry.getKey();
			String value = entry.getValue();
			this.symbolToIndexMap.put(value, index);
			while (this.indexToSymbolMap.size() <= index) {
				this.indexToSymbolMap.add(null);
			}
			this.indexToSymbolMap.set(index, value);
		}
	}

	/* pp */ PolynominalMapping(NominalMapping mapping) {
		this.symbolToIndexMap.clear();
		this.indexToSymbolMap.clear();
		for (int i = 0; i < mapping.size(); i++) {
			int index = i;
			String value = mapping.mapIndex(index);
			this.symbolToIndexMap.put(value, index);
			this.indexToSymbolMap.add(value);
		}
	}

	@Override
	public Object clone() {
		return new PolynominalMapping(this);
	}

	@Override
	public boolean equals(NominalMapping mapping) {
		if (mapping.size() != size()) {
			return false;
		}
		for (String value : mapping.getValues()) {
			if (!symbolToIndexMap.containsKey(value)) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Returns the index for the nominal attribute value <code>str</code>. If the string is unknown,
	 * a new index value is assigned. Returns -1, if str is null.
	 */
	@Override
	public int mapString(String str) {
		if (str == null) {
			return -1;
		}
		// lookup string in hashtable
		int index = getIndex(str);
		// if string is not yet in the map, add it
		if (index < 0) {
			indexToSymbolMap.add(str);
			index = indexToSymbolMap.size() - 1;
			symbolToIndexMap.put(str, index);
		}
		return index;
	}

	/**
	 * Returns the index of the given nominal value or -1 if this value was not mapped before by
	 * invoking the method {@link #mapIndex(int)}.
	 */
	@Override
	public int getIndex(String str) {
		Integer index = symbolToIndexMap.get(str);
		if (index == null) {
			return -1;
		} else {
			return index.intValue();
		}
	}

	/**
	 * Returns the attribute value, that is associated with this index. Index counting starts with
	 * 0. <b>WARNING:</b> In order to iterate over all values please use the collection returned by
	 * {@link #getValues()}.
	 */
	@Override
	public String mapIndex(int index) {
		if (index < 0 || index >= indexToSymbolMap.size()) {
			throw new AttributeTypeException(
					"Cannot map index of nominal attribute to nominal value: index " + index + " is out of bounds!");
		}
		return indexToSymbolMap.get(index);
	}

	/**
	 * Sets the given mapping. Please note that this will overwrite existing mappings and might
	 * cause data changes in this way.
	 */
	@Override
	public void setMapping(String nominalValue, int index) {
		String oldValue = indexToSymbolMap.get(index);
		indexToSymbolMap.set(index, nominalValue);
		symbolToIndexMap.remove(oldValue);
		symbolToIndexMap.put(nominalValue, index);
	}

	/**
	 * Returns the index of the first value if this attribute is a classification attribute, i.e. if
	 * it is binominal.
	 */
	@Override
	public int getNegativeIndex() {
		ensureClassification();
		if (mapIndex(0) == null) {
			throw new AttributeTypeException("Attribute: Cannot use FIRST_CLASS_INDEX for negative class!");
		}
		return 0;
	}

	/**
	 * Returns the index of the second value if this attribute is a classification attribute. Works
	 * for all binominal attributes.
	 */
	@Override
	public int getPositiveIndex() {
		ensureClassification();
		if (mapIndex(0) == null) {
			throw new AttributeTypeException("Attribute: Cannot use FIRST_CLASS_INDEX for negative class!");
		}
		Iterator<Integer> i = symbolToIndexMap.values().iterator();
		while (i.hasNext()) {
			int index = i.next();
			if (index != 0) {
				return index;
			}
		}
		throw new AttributeTypeException("Attribute: No other class than FIRST_CLASS_INDEX found!");
	}

	@Override
	public String getNegativeString() {
		return mapIndex(getNegativeIndex());
	}

	@Override
	public String getPositiveString() {
		return mapIndex(getPositiveIndex());
	}

	/** Returns the values of the attribute as an enumeration of strings. */
	@Override
	public List<String> getValues() {
		return indexToSymbolMap;
	}

	/** Returns the number of different nominal values. */
	@Override
	public int size() {
		return indexToSymbolMap.size();
	}

	/**
	 * This method rearranges the string to number mappings such that they are in alphabetical
	 * order. <br>
	 * <b>VERY IMPORTANT NOTE:</b> Do not call this method when this attribute is already associated
	 * with an {@link ExampleTable} and it already contains {@link Example}s. All examples will be
	 * messed up since the indices will not be replaced in the data table.
	 */
	@Override
	public void sortMappings() {
		List<String> allStrings = new LinkedList<>(symbolToIndexMap.keySet());
		Collections.sort(allStrings);
		symbolToIndexMap.clear();
		indexToSymbolMap.clear();
		Iterator<String> i = allStrings.iterator();
		while (i.hasNext()) {
			mapString(i.next());
		}
	}

	/** Clears all mappings for nominal values. */
	@Override
	public void clear() {
		symbolToIndexMap.clear();
		indexToSymbolMap.clear();
	}

	/**
	 * Throws a runtime exception if this attribute is not a classification attribute.
	 */
	private void ensureClassification() {
		if (size() != 2) {
			throw new AttributeTypeException("Attribute " + this.toString() + " is not a classification attribute!");
		}
	}

	@Override
	public String toString() {
		return indexToSymbolMap.toString() + Tools.getLineSeparator() + symbolToIndexMap.toString();
	}
}
