/*
 * This file is part of Jstacs.
 * 
 * Jstacs is free software: you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 * 
 * Jstacs 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * Jstacs. If not, see <http://www.gnu.org/licenses/>.
 * 
 * For more information on Jstacs, visit http://www.jstacs.de
 */

package de.jstacs.classifier.scoringFunctionBased;

import java.io.OutputStream;

import de.jstacs.NonParsableException;
import de.jstacs.NotTrainedException;
import de.jstacs.algorithms.optimization.ConstantStartDistance;
import de.jstacs.algorithms.optimization.StartDistanceForecaster;
import de.jstacs.classifier.AbstractScoreBasedClassifier;
import de.jstacs.classifier.ClassDimensionException;
import de.jstacs.classifier.scoringFunctionBased.OptimizableFunction.KindOfParameter;
import de.jstacs.data.AlphabetContainer;
import de.jstacs.data.Sample;
import de.jstacs.data.Sequence;
import de.jstacs.data.Sample.WeightedSampleFactory;
import de.jstacs.data.Sample.WeightedSampleFactory.SortOperation;
import de.jstacs.io.ArrayHandler;
import de.jstacs.io.XMLParser;
import de.jstacs.motifDiscovery.MutableMotifDiscovererToolbox;
import de.jstacs.motifDiscovery.history.History;
import de.jstacs.results.CategoricalResult;
import de.jstacs.results.NumericalResult;
import de.jstacs.results.NumericalResultSet;
import de.jstacs.scoringFunctions.AbstractNormalizableScoringFunction;
import de.jstacs.scoringFunctions.ScoringFunction;
import de.jstacs.utils.SafeOutputStream;

/**
 * This abstract class implements the main functionality of a {@link ScoringFunction} based classifier.
 * 
 * @author Jens Keilwagen, Jan Grau
 */
public abstract class ScoreClassifier extends AbstractScoreBasedClassifier {

	/**
	 * The internally used scoring functions.
	 */
	protected ScoringFunction[] score;

	/**
	 * The parameter set for the classifier.
	 */
	protected ScoreClassifierParameterSet params;

	/**
	 * This boolean indicates whether the classifier has been optimized with the
	 * method {@link de.jstacs.classifier.AbstractClassifier#train(Sample[])} or
	 * the weighted version.
	 */
	protected boolean hasBeenOptimized;

	/**
	 * This value is the score of the last optimization. 
	 */
	private double lastScore;

	/**
	 * This stream is used for comments, e.g. during the training, ... .
	 */
	protected SafeOutputStream sostream;

	/**
	 * This value should be used in {@link ScoreClassifier#getLastScore()} if the classifier is not trained. 
	 */
	public static final double NOT_TRAINED_VALUE = Double.NaN;

	protected History template = null;//TODO new NoRevertHistory();
	
	/**
	 * Creates a new {@link ScoreClassifier} from a given
	 * {@link ScoreClassifierParameterSet} and {@link ScoringFunction}s .
	 * 
	 * @param params
	 *            the parameter set for the classifier
	 * @param lastScore
	 *            the score of the last optimization, if no such value exists the
	 *            programmer should use {@link ScoreClassifier#NOT_TRAINED_VALUE}
	 * @param score
	 *            the {@link ScoringFunction}s for the classes
	 * 
	 * @throws CloneNotSupportedException
	 *             if at least one {@link ScoringFunction} could not be cloned
	 * 
	 * @see AbstractScoreBasedClassifier#AbstractScoreBasedClassifier(AlphabetContainer,
	 *      int, int)
	 */
	public ScoreClassifier( ScoreClassifierParameterSet params, double lastScore, ScoringFunction... score ) throws CloneNotSupportedException {
		super( params.getAlphabetContainer(), params.getLength(), score.length );
		int i = 0, l, len = getLength();
		AlphabetContainer con = getAlphabetContainer();
		while( i < score.length ) {
			l = score[i].getLength();
			if( ( l == 0 || l == len ) && con.checkConsistency( score[i].getAlphabetContainer() ) ) {
				//everything is okay
				i++;
			} else {
				throw new IllegalArgumentException( "Please check the length (" + l + " vs. " + len + ")" +
						" and the AlphabetContainer of the ScoringFunction with index " + i + "." );
			}
		}
		this.score = ArrayHandler.clone( score );
		hasBeenOptimized = false;
		if( isTrained() ) {
			this.lastScore = lastScore;
		} else {
			this.lastScore = NOT_TRAINED_VALUE;
		}
		set( (ScoreClassifierParameterSet)params.clone() );
	}

	/**
	 * The standard constructor for the interface {@link de.jstacs.Storable}.
	 * Creates a new {@link ScoreClassifier} out of its XML representation.
	 * 
	 * @param xml
	 *            the XML representation as {@link StringBuffer}
	 * 
	 * @throws NonParsableException
	 *             if the {@link ScoreClassifier} could not be reconstructed out
	 *             of the XML representation (the {@link StringBuffer} could not
	 *             be parsed)
	 * 
	 * @see AbstractScoreBasedClassifier#AbstractScoreBasedClassifier(StringBuffer)
	 * @see de.jstacs.Storable
	 */
	public ScoreClassifier( StringBuffer xml ) throws NonParsableException {
		super( xml );
	}

	/* (non-Javadoc)
	 * @see de.jstacs.classifier.AbstractScoreBasedClassifier#clone()
	 */
	@Override
	public ScoreClassifier clone() throws CloneNotSupportedException {
		ScoreClassifier clone = (ScoreClassifier)super.clone();
		clone.params = (ScoreClassifierParameterSet)params.clone();
		clone.score = ArrayHandler.clone( score );
		clone.setOutputStream( this.sostream.doesNothing() ? null : SafeOutputStream.DEFAULT_STREAM );
		return clone;
	}

	/* (non-Javadoc)
	 * @see de.jstacs.classifier.AbstractClassifier#getInstanceName()
	 */
	@Override
	public String getInstanceName() {
		return getClass().getSimpleName();
	}

	/* (non-Javadoc)
	 * @see de.jstacs.classifier.AbstractClassifier#getClassifierAnnotation()
	 */
	@Override
	public CategoricalResult[] getClassifierAnnotation() {
		CategoricalResult[] res = new CategoricalResult[score.length + 1];
		res[0] = new CategoricalResult( "classifier", "a <b>short</b> description of the classifier", getInstanceName() );
		int i = 0;
		while( i < score.length ) {
			res[i + 1] = new CategoricalResult( "class info " + i, "some information about the class", score[i++].getInstanceName() );
		}
		return res;
	}

	/* (non-Javadoc)
	 * @see de.jstacs.classifier.AbstractClassifier#getNumericalCharacteristics()
	 */
	@Override
	public NumericalResultSet getNumericalCharacteristics() throws Exception {

		NumericalResult[] pars = new NumericalResult[score.length + ( hasBeenOptimized ? 1 : 0 )];
		if( hasBeenOptimized ) {
			pars[0] = new NumericalResult( "Last score", "The final score after the optimization", lastScore );
		}
		for( int i = 0; i < score.length; i++ ) {
			pars[i + ( hasBeenOptimized ? 1 : 0 )] = new NumericalResult( "Number of parameters " + ( i + 1 ),
					"The number of parameters for scoring function " + ( i + 1 ) + ", -1 indicates unknown number of parameters.",
					score[i].getNumberOfParameters() );
		}
		return new NumericalResultSet( pars );
	}

	/* (non-Javadoc)
	 * @see de.jstacs.classifier.AbstractClassifier#isTrained()
	 */
	@Override
	public boolean isTrained() {
		int i = 0;
		while( i < score.length && score[i].isInitialized() ) {
			i++;
		}
		return i == score.length;
	}

	/**
	 * This method indicates if the classifier has been optimized by a
	 * <code>train</code>-method.
	 * 
	 * @return <code>true</code> if the classifier has been optimized by a
	 *         <code>train</code>-method, <code>false</code> otherwise
	 */
	public boolean hasBeenOptimized() {
		return hasBeenOptimized;
	}

	/**
	 * Sets the {@link OutputStream} that is used e.g. for writing information
	 * during training. It is possible to set <code>o=null</code>, then nothing
	 * will be written.
	 * 
	 * @param o
	 *            the {@link OutputStream}
	 */
	public void setOutputStream( OutputStream o ) {
		sostream = new SafeOutputStream( o );
	}

	/* (non-Javadoc)
	 * @see de.jstacs.classifier.AbstractClassifier#train(de.jstacs.data.Sample[], double[][])
	 */
	@Override
	public void train( Sample[] data, double[][] weights ) throws Exception {
		hasBeenOptimized = false;
		// check
		if( weights != null && data.length != weights.length ) {
			throw new IllegalArgumentException( "data and weights do not match" );
		}
		if( score.length != data.length ) {
			throw new ClassDimensionException();
		}
		if( weights == null ) {
			weights = new double[data.length][];
		}
		WeightedSampleFactory wsf;
		Sample[] reduced = new Sample[data.length];
		double[][] newWeights = new double[data.length][];
		AlphabetContainer abc = getAlphabetContainer();
		for( int l = getLength(), i = 0; i < score.length; i++ ) {
			if( weights[i] != null && data[i].getNumberOfElements() != weights[i].length ) {
				throw new IllegalArgumentException( "At least for one sample: The dimension of the sample and the weight do not match." );
			}
			if( !abc.checkConsistency( data[i].getAlphabetContainer() ) ) {
				throw new IllegalArgumentException( "At least one sample is not defined over the correct alphabets." );
			}
			if( data[i].getElementLength() != l ) {
				// throw new IllegalArgumentException( "At least one sample has not the correct length." );
				wsf = new WeightedSampleFactory( SortOperation.NO_SORT, data[i], weights[i], l );
			} else {
				wsf = new WeightedSampleFactory( SortOperation.NO_SORT, data[i], weights[i] );
			}
			reduced[i] = wsf.getSample();
			newWeights[i] = wsf.getWeights();
		}
		lastScore = doOptimization( reduced, newWeights );
	}

	/**
	 * This method does the optimization of the <code>train</code>-method
	 * 
	 * @param reduced
	 *            the samples
	 * @param newWeights
	 *            the weights
	 * 
	 * @return the value of the optimization
	 * 
	 * @throws Exception
	 *             if something went wrong during the optimization
	 */
	protected double doOptimization( Sample[] reduced, double[][] newWeights ) throws Exception {
		// train
		byte algo = (Byte) params.getParameterAt( 0 ).getValue();
		double eps = (Double) params.getParameterAt( 1 ).getValue(),
			linEps = (Double) params.getParameterAt( 2 ).getValue(),
			startDist = (Double) params.getParameterAt( 3 ).getValue();
		KindOfParameter plugIn;

		double[] best = null;
		double[][] res = new double[2][];		
		double max = Double.NEGATIVE_INFINITY, current;
		
		int iterations = AbstractNormalizableScoringFunction.getNumberOfStarts( score );
		sostream.writeln( getInstanceName() );
		ScoringFunction[] bestSF = new ScoringFunction[score.length], secure;
		if( iterations > 1 ) {
			secure = ArrayHandler.clone( score );
		} else {
			secure = null;
		}
		History[][] hist = MutableMotifDiscovererToolbox.createHistoryArray( score, template );
		int[][] minimalNewLength = MutableMotifDiscovererToolbox.createMinimalNewLengthArray( score );
		StartDistanceForecaster sd = new ConstantStartDistance( startDist );
		
		SFBasedOptimizableFunction f = getFunction( reduced, newWeights );
		//old DifferentiableFunction g = new NegativeDifferentiableFunction( f );
		for( int i = 0; i < iterations; ) {
			// create structure
			createStructure( reduced, newWeights );
			f.reset( score );
			
			if( i == 0 ) {
				sostream.writeln( "optimizing " + f.getDimensionOfScope() + " parameters" );
			}
			sostream.writeln( "start " + ++i + ":" );

			//pre-optimize
			plugIn = preoptimize(f);	
			
			//new
			MutableMotifDiscovererToolbox.clearHistoryArray( hist );
			sd.reset();
			res = MutableMotifDiscovererToolbox.optimize( score, f, algo, eps, linEps, sd, sostream, false, hist, minimalNewLength, plugIn, true );//TODO
			current = res[0][0];

			/*
			//old
			res[1]= f.getParameters( plugIn );
			Optimizer.optimize( algo,
					g,
					res[1],
					Optimizer.TerminationCondition.SMALL_DIFFERENCE_OF_FUNCTION_EVALUATIONS,
					eps,
					linEps,
					new ConstantStartDistance( startDist ),
					sostream );
			current = f.evaluateFunction( res[1] );
			*/
			if( current > max ) {
				System.arraycopy( score, 0, bestSF, 0, score.length );
				//bestSF = score;
				best = res[1];
				max = current;
				System.gc();
			}
			if( iterations > 1 ) {
				score = ArrayHandler.clone( secure );
				if( sostream.doesNothing() ) {
					System.out.println( "start " + i + ": " + res[0][0] );
				}
			}
		}
		sostream.writeln( "best = " + max );

		score = bestSF;
		setClassWeights( false, best );
		hasBeenOptimized = true;
		
		if( f instanceof AbstractMultiThreadedOptimizableFunction ) {
			((AbstractMultiThreadedOptimizableFunction)f).stopThreads();
		}
		return max;
	}
	
	/**
	 * This method allows to preoptimize the parameter before the real optimization. It might be used useful, for instance,
	 * to find initial parameters on a part of the complete data.
	 * 
	 * @param f the function to be optimized
	 * 
	 * @return enumeration value that indicates how to retrieve the parameters from the function
	 * 
	 * @throws Exception
	 */
	protected KindOfParameter preoptimize( OptimizableFunction f ) throws Exception {
		return (KindOfParameter) params.getParameterAt( 5 ).getValue();
	}	
	
	/**
	 * Creates the structure that will be used in the optimization.
	 * 
	 * @param data
	 *            the data
	 * @param weights
	 *            the weights of the data
	 * @param initRandomly
	 * 			  initialize the functions randomly
	 * 
	 * @throws Exception
	 *             if something went wrong
	 */
	protected void createStructure( Sample[] data, double[][] weights, boolean initRandomly ) throws Exception {
		boolean freeParams = params.useOnlyFreeParameter();
		for( int i = 0; i < score.length; i++ ) {
			if(initRandomly){
				score[i].initializeFunctionRandomly( freeParams );
			}else{
				score[i].initializeFunction( i, freeParams, data, weights );
			}
		}
	}
	
	/**
	 * Creates the structure that will be used in the optimization.
	 * 
	 * @param data
	 *            the data
	 * @param weights
	 *            the weights of the data	 * 
	 * @throws Exception
	 *             if something went wrong
	 */
	protected void createStructure( Sample[] data, double[][] weights) throws Exception{
		createStructure( data, weights, false );
	}

	/* (non-Javadoc)
	 * @see de.jstacs.classifier.AbstractScoreBasedClassifier#extractFurtherClassifierInfosFromXML(java.lang.StringBuffer)
	 */
	@Override
	protected void extractFurtherClassifierInfosFromXML( StringBuffer xml ) throws NonParsableException {
		super.extractFurtherClassifierInfosFromXML( xml );
		set( (ScoreClassifierParameterSet) XMLParser.extractObjectForTags( xml, "params" ) );
		hasBeenOptimized = XMLParser.extractObjectForTags( xml, "hasBeenOptimized", boolean.class );
		lastScore = XMLParser.extractObjectForTags( xml, "lastScore", double.class );
		score = XMLParser.extractObjectForTags( xml, "score", ScoringFunction[].class );
	}
	
	/**
	 * Returns the function that should be optimized.
	 * 
	 * @param data
	 *            the samples
	 * @param weights
	 *            the weights of the sequences of the samples
	 * 
	 * @return the function that should be optimized
	 * 
	 * @throws Exception
	 *             if something went wrong
	 */
	protected abstract SFBasedOptimizableFunction getFunction( Sample[] data, double[][] weights ) throws Exception;

	/* (non-Javadoc)
	 * @see de.jstacs.classifier.AbstractScoreBasedClassifier#getFurtherClassifierInfos()
	 */
	@Override
	protected StringBuffer getFurtherClassifierInfos() {
		StringBuffer xml = super.getFurtherClassifierInfos();
		XMLParser.appendObjectWithTags( xml, params, "params" );
		XMLParser.appendObjectWithTags( xml, hasBeenOptimized, "hasBeenOptimized" );
		XMLParser.appendObjectWithTags( xml, lastScore, "lastScore" );
		XMLParser.appendObjectWithTags( xml, score, "score" );
		return xml;
	}

	/* (non-Javadoc)
	 * @see de.jstacs.classifier.AbstractScoreBasedClassifier#getScore(de.jstacs.data.Sequence, int, boolean)
	 */
	@Override
	protected double getScore( Sequence seq, int i, boolean check ) throws IllegalArgumentException, NotTrainedException, Exception {
		if( check ) {
			check( seq );
		}
		return getClassWeight( i ) + score[i].getLogScore( seq, 0 );
	}

	/**
	 * Returns the score that was computed in the last optimization of the
	 * parameters.
	 * 
	 * @return score from the last parameter optimization
	 */
	public double getLastScore() {
		return lastScore;
	}

	/**
	 * Returns the internally used {@link ScoringFunction} with index
	 * <code>i</code>.
	 * 
	 * @param i
	 *            the internal index of the {@link ScoringFunction}
	 * 
	 * @return the internally used {@link ScoringFunction} with index
	 *         <code>i</code>
	 * 
	 * @throws CloneNotSupportedException
	 *             if the {@link ScoringFunction} could not be cloned
	 */
	public ScoringFunction getScoringFunction( int i ) throws CloneNotSupportedException {
		return score[i].clone();
	}

	/**
	 * Returns all internally used {@link ScoringFunction}s in the internal
	 * order.
	 * 
	 * @return the internally used {@link ScoringFunction}s in the internal
	 *         order
	 * 
	 * @throws CloneNotSupportedException
	 *             if a {@link ScoringFunction} could not be cloned
	 */
	public ScoringFunction[] getScoringFunctions() throws CloneNotSupportedException {
		return ArrayHandler.clone( score );
	}

	/* (non-Javadoc)
	 * @see de.jstacs.classifier.AbstractClassifier#getXMLTag()
	 */
	@Override
	protected abstract String getXMLTag();

	private void set( ScoreClassifierParameterSet params ) {
		this.params = params;
		setOutputStream( SafeOutputStream.DEFAULT_STREAM );
	}
	
	/**
	 * This method returns the current {@link de.jstacs.parameters.ParameterSet} of the classifier.
	 * @return the current {@link de.jstacs.parameters.ParameterSet} of the classifier.
	 * @throws CloneNotSupportedException if the {@link de.jstacs.parameters.ParameterSet} could not be cloned
	 */
	public ScoreClassifierParameterSet getCurrentParameterSet() throws CloneNotSupportedException {
		return (ScoreClassifierParameterSet) params.clone();
	}
}
