/*********************************************************************
 * This file is part of the PRAPI library.
 *
 * Copyright (C) 2001 Topi Mäenpää
 * All rights reserved.
 *
 * This program is free software. You can redistribute and/or modify
 * it under the terms of the free software licence found in the
 * accompanying file "COPYING". The licence terms must always be
 * redistributed with this source file. The above copyright notice
 * must be reproduced in all modified and unmodified copies of this
 * source file.
 *
 * $Revision: 1.2 $
 *********************************************************************/

#ifndef _MULTICLASSIFIER_H
#define _MULTICLASSIFIER_H

#include "Classifier.h"

namespace prapi
{
	/**
	 * RankCombiner is used by MultiClassifier in combining rankings
	 * obtained from multiple classifiers. Its purpose is to convert a
	 * list of rankings to a single class index that is the
	 * classification result.
	 *
	 * @see MultiClassifier
	 **/
	class RankCombiner : virtual public Object
	{
	public:
		/**
		 * Combine rankings by different classifiers. Rankings by
		 * different classifiers are provided as a list in which each slot
		 * stores the rankings given by one of the classifiers (in the
		 * order the classifiers are given to a MultiClassifier). Rankings
		 * are represented as a list with as many slots as the number of
		 * classes. Slot 0 represents the rank of the first class, slot 1
		 * the rank of the second class etc. The maximum rank is
		 * classCount-1.
		 **/
		virtual int combineRanks(const util::List<util::List<int> >& rankings) const = 0;

		class BordaCount;

	protected:
		/**
		 * Create a new rank combiner for the given number of classes.
		 **/
		RankCombiner(int classCount) : _iClassCount(classCount) {}
		/**
		 * The number of classes.
		 **/
		int _iClassCount;
	};

	/**
	 * The Borda count is a simple method of combining classification
	 * ranks. For each class, it simply counts the number of classes
	 * that are ranked below it by each classifier, and sums them
	 * together. The higher the sum, the better the match. Ties are
	 * resolved indeterministically.
	 **/
	class RankCombiner::BordaCount : public RankCombiner
	{
	public:
		/**
		 * Create a new Borda count rank combiner for the given number of
		 * samples.
		 **/
		BordaCount(int classCount) : RankCombiner(classCount) {}

		virtual int combineRanks(const util::List<util::List<int> >& rankings) const;
	};


	/**
	 * MultiClassifier combines class rankings from different
	 * classifiers and produces an overall result using a number of
	 * combination schemes.<p>
	 *
	 * Example:
	 * <pre>
	 * kNNClassifier&lt;double&gt; knn(samples,new EuclideanDistance&lt;double&gt;,4,3);
	 * MahalanobisClassifier&lt;double&gt; maha(samples,4);
	 * List&lt;Classifier&lt;double&gt;*&gt; lst(2);
	 * lst += &knn; lst += &maha;
	 *
	 * MultiClassifier&lt;double&gt; classifier(lst, new RankCombiner::BordaCount(4));
	 * classifier.holdOut(testingSamples);
	 * </pre>
	 **/
	template <class T, class I=std::string> class MultiClassifier : public Classifier<T,I,int>
	{
	public:
		/**
		 * Create a new MultiClassifier. The classifier will combine
		 * rankings from different classifiers.
		 *
		 * @param classifiers the classifiers whose rankings are combined
		 * @param combiner a rank combiner that is able to produce a
		 * single class index out of multiple rankings. (automatically deleted)
		 **/
		MultiClassifier(const util::List<Classifier<T,I,int>* >& classifiers, RankCombiner* combiner) :
			_plstClassifiers(classifiers), _rankCombiner(combiner) {}

		/**
		 * Create a new MultiClassifier. The classifier will combine
		 * rankings from different classifiers.
		 *
		 * @param classifiers the classifiers whose rankings are combined
		 * @param combiner a rank combiner that is able to produce a
		 * single class index out of multiple rankings.
		 **/
		MultiClassifier(const util::List<Classifier<T,I,int>* >& classifiers, RankCombiner& combiner) :
			_plstClassifiers(classifiers), _rankCombiner(&combiner,false) {}
		
		int getClassification(Sample<T,I,int>& sample) throw (ClassificationException&);

		void leaveOneOut(void) throw (ClassificationException&);

	private:
		util::List<Classifier<T,I,int>* > _plstClassifiers;
		SmartPtr<RankCombiner> _rankCombiner;
	};

	template <class T, class I> int MultiClassifier<T,I>::getClassification(Sample<T,I,int>& sample) throw (ClassificationException&)
	{
		using namespace util;
		
		List<List<int> > rankings(_plstClassifiers.getLength());
		for (int i=0;i<_plstClassifiers.getLength();i++)
			rankings += _plstClassifiers[i]->getRanks(sample);

		return _rankCombiner->combineRanks(rankings);
	}

	template <class T, class I> void MultiClassifier<T,I>::leaveOneOut(void) throw (ClassificationException&)
	{
		using namespace util;
		
		int trainSetSize = -1;
		int classifierCount = _plstClassifiers.getLength();
		for (int i=0;i<classifierCount;i++)
			{
				if (!_plstClassifiers[i]->getTrainingSamples())
					throw ClassificationException("MultiClassifier<T,I,C>::leaveOneOut(): Classifier " + String::toString(i) +
																				" has no training samples.");
				int len = _plstClassifiers[i]->getTrainingSamples()->getLength();
				if (trainSetSize == -1)
					trainSetSize = len;
				else if (trainSetSize != len)
					throw ClassificationException("MultiClassifier<T,I,C>::leaveOneOut(): The number of training samples in each"
																				" classifier must be equal. (Classifier " + String::toString(i) + " has " +
																				String::toString(len) + ", but the previous ones had " + String::toString(trainSetSize) + ").");
			}
		if (trainSetSize < 2)
			throw ClassificationException("MultiClassifier<T,I,C>::leaveOneOut(): Cannot classify less than two samples.");

	
		for (int i=0;i<trainSetSize;i++)
			{
				List<List<int> > rankings(classifierCount);
				//Create rankings for each classifier
				for (int j=0;j<classifierCount;j++)
					{
						List<Sample<T,I,int> >& trainingSamples = *_plstClassifiers[j]->getTrainingSamples();
						//store the last sample.
						Sample<T,I,int> last(trainingSamples[trainSetSize-1]);
						Sample<T,I,int> tmp(trainingSamples[i]); //store the current sample to be classified
						trainingSamples[i] = last; //replace it with the last sample
						//truncate the list (exclude the last element from classification)
						trainingSamples.setLength(trainSetSize-1);

						rankings += _plstClassifiers[j]->getRanks(tmp); //obtain rankings

						trainingSamples += last; //restore the last element
						trainingSamples[i] = tmp; //restore the current sample
					}
				
				int classification = _rankCombiner->combineRanks(rankings);
				for (int j=0;j<classifierCount;j++)
					{
						_plstClassifiers[j]->getTrainingSamples()->elementAt(i).setClassification(classification);
						if (!j)
							fireEvent(new ClassificationEvent<T,I,int>(_plstClassifiers[j]->getTrainingSamples()->elementAt(i),i));
					}
			}
	}
}

#endif
