import {
  getTemplate,
  writeFile,
  readFile,
  removeFile,
} from './utils';
import { existsSync } from 'fs';
import { join } from 'path';
import assert from 'assert';
import j from 'jscodeshift';
import { addModel } from './entry';
import Model from '../collections/Model';

Model.register();

export function create(payload) {
  assert(payload.namespace, 'api/models/create: payload should have namespace');
  const template = getTemplate('models.create');
  const source = template(payload);
  const filePath = join(payload.sourcePath, payload.filePath);
  assert(!existsSync(filePath), 'api/models/create: file exists');
  writeFile(filePath, source);

  // Add model to entry
  if (payload.entry && payload.modelPath) {
    addModel({
      sourcePath: payload.sourcePath,
      filePath: payload.entry,
      modelPath: payload.modelPath,
    });
  }
}

export function remove(payload) {
  const filePath = join(payload.sourcePath, payload.filePath);
  removeFile(filePath);
}

export function updateNamespace(payload) {
  _action('updateNamespace', payload, ['newNamespace']);
}

export function updateState(payload) {
  _action('updateState', payload, ['source']);
}

export function addReducer(payload) {
  _action('addReducer', payload, ['name', 'source'], ['source']);
}

export function addEffect(payload) {
  _action('addEffect', payload, ['name', 'source'], ['source']);
}

export function addSubscription(payload) {
  _action('addSubscription', payload, ['name', 'source'], ['source']);
}

export function updateReducer(payload) {
  _action('updateReducer', payload, ['name', 'source']);
}

export function updateEffect(payload) {
  _action('updateEffect', payload, ['name', 'source']);
}

export function updateSubscription(payload) {
  _action('updateSubscription', payload, ['name', 'source']);
}

export function removeReducer(payload) {
  _action('removeReducer', payload, ['name']);
}

export function removeEffect(payload) {
  _action('removeEffect', payload, ['name']);
}

export function removeSubscription(payload) {
  _action('removeSubscription', payload, ['name']);
}

/**
 * private
 */
function _action(type, payload, checklist, optional = []) {
  for (let checkitem of ['namespace', ...checklist]) {
    if (optional.indexOf(checkitem) === -1) {
      assert(payload[checkitem], `api/models/${type}: payload should have ${checkitem}`);
    }
  }

  const filePath = join(payload.sourcePath, payload.filePath);
  const source = readFile(filePath);
  const root = j(source);
  const models = root.findModels(payload.namespace);
  const args = checklist.map(checkitem => payload[checkitem]);
  models[type].apply(models, args);
  writeFile(filePath, root.toSource());
}


/**
 * Small utility for writing a dataset in SVM-light format.
 *
 * @author Erel Segal-Halevi
 * @since 2013-09
 */

/**
 * convert a single dataset to compact JSON format.
 * @param dataset an array of samples in the format {input: [value1, value2, ...], output: (0|1)}
 * @param bias if nonzero, add it at the beginning of the vector.
 * @param binarize if true, change output to -1/1. If false, leave output as it is
 */
exports.toSvmLight = function(dataset, bias, binarize, firstFeatureNumber) {
	var lines = "";
	for (var i=0; i<dataset.length; ++i) {
		var line = (i>0? "\n": "") +
			(binarize? (dataset[i].output>0? "1": "-1"): dataset[i].output) +  // in svm-light, the output comes first:
			featureArrayToFeatureString(dataset[i].input, bias, firstFeatureNumber)
			;
		lines += line;
	};
	lines += "\n";
	return lines;
}

/**
 * convert an array of features to a single line in SVM-light format. The line starts with a space.
 */
function featureArrayToFeatureString(features, bias, firstFeatureNumber) {
	if (!Array.isArray(features))
		throw new Error("Expected an array, but got "+JSON.stringify(features))
	var line = (bias? " "+firstFeatureNumber+":"+bias: "");
	for (var feature=0; feature<features.length; ++feature) {
		var value = features[feature];
		if (value)
			line += (" "+(feature+firstFeatureNumber+(bias?1:0))+":"+value.toPrecision(5));
	}
	return line;
}


/**
 * FeatureLookupTable - a table for converting features to numbers and vice versa
 */

var FeatureLookupTable = function() {
	this.featureIndexToFeatureName = [undefined];
	this.featureNameToFeatureIndex = {undefined: 0};
}

FeatureLookupTable.prototype = {

	// add a single feature, if it does not exist
	addFeature: function(feature) {
		if (!(feature in this.featureNameToFeatureIndex)) {
			var newIndex = this.featureIndexToFeatureName.length;
			this.featureIndexToFeatureName.push(feature);
			this.featureNameToFeatureIndex[feature] = newIndex;
		}
	},

	// add all features in the given hash or array
	addFeatures: function(hash) {
		if (hash instanceof Array) {
			for (var index in hash)
				this.addFeature(hash[index]);
		} else if (hash instanceof Object) {
			for (var feature in hash)
				this.addFeature(feature);
		}
		else throw new Error("FeatureLookupTable.addFeatures expects a hash or an array, but got: "+JSON.stringify(hash));
	},

	// add all features in all hashes in the given array
	addFeaturess: function(hashes) {
		for (var i=0; i<hashes.length; ++i)
			this.addFeatures(hashes[i]);
	},

	/**
	 * Convert the given feature to a numeric index.
	 */
	featureToNumber: function(feature) {
		this.addFeature(feature);
		return this.featureNameToFeatureIndex[feature];
	},

	numberToFeature: function(number) {
		return this.featureIndexToFeatureName[number];
	},

	/**
	 * Convert the given hash of features to a numeric array, using 0 for padding.
	 * If some features in the hash do not exist - they will be added.
	 * @param hash any hash, for example, {a: 111, b: 222, c: 333}
	 * @return a matching array, based on the current feature table. For example: [0, 111, 222, 0, 333]
	 * @note some code borrowed from Heather Arthur: https://github.com/harthur/brain/blob/master/lib/lookup.js
	 */
	hashToArray: function(hash) {
		this.addFeatures(hash);
		var array = [];
		for (var featureIndex=0; featureIndex<this.featureIndexToFeatureName.length; ++featureIndex)
			array[featureIndex]=0;
		if (hash instanceof Array) {
			for (var i in hash)
				array[this.featureNameToFeatureIndex[hash[i]]] = true;
		} else if (hash instanceof Object) {
			for (var feature in hash)
				array[this.featureNameToFeatureIndex[feature]] = hash[feature];
		}
		else throw new Error("Unsupported type: "+JSON.stringify(hash));
		return array;
	},

	/**
	 * Convert all the given hashes of features to numeric arrays, using 0 for padding.
	 * If some features in some of the hashes do not exist - they will be added.
	 * @param hashes an array of hashes, for example, [{a: 111, b: 222}, {a: 11, c: 33}, ...]
	 * @return an array of matching arrays, based on the current feature table. For example: [[111, 222], [11, 0, 33]]
	 */
	hashesToArrays: function(hashes) {
		this.addFeaturess(hashes);

		var arrays = [];
		for (var i=0; i<hashes.length; ++i) {
			arrays[i] = [];
			for (var feature in this.featureNameToFeatureIndex)
				arrays[i][this.featureNameToFeatureIndex[feature]] = hashes[i][feature] || 0;
		}
		return arrays;
	},

	/**
	 * Convert the given numeric array to a hash of features, ignoring zero values.
	 * @note some code borrowed from Heather Arthur: https://github.com/harthur/brain/blob/master/lib/lookup.js
	 */
	arrayToHash: function(array) {
		var hash = {};
		for (var feature in this.featureNameToFeatureIndex) {
			if (array[this.featureNameToFeatureIndex[feature]])
				hash[feature] = array[this.featureNameToFeatureIndex[feature]];
		}
		return hash;
	},

	/**
	 * Convert the given numeric arrays to array of hashes of features, ignoring zero values.
	 */
	arraysToHashes: function(arrays) {
		var hashes = [];
		for (var i=0; i<arrays.length; ++i)
			hashes[i] = this.arrayToHash(arrays[i]);
		return hashes;
	},

	toJSON: function() {
		return {
			featureIndexToFeatureName: this.featureIndexToFeatureName,
			featureNameToFeatureIndex: this.featureNameToFeatureIndex,
		}
	},

	fromJSON: function(json) {
		this.featureIndexToFeatureName = json.featureIndexToFeatureName;
		this.featureNameToFeatureIndex = json.featureNameToFeatureIndex;
	},
}

module.exports = FeatureLookupTable;


var hash = require("../../utils/hash");
var sprintf = require("sprintf").sprintf;
var _ = require("underscore")._;
var util = require('util');
var multilabelutils = require('./multilabelutils');

/**
 * HOMER - Hierarchy Of Multilabel classifiERs. See:
 *
 * Tsoumakas Grigorios, Katakis Ioannis, Vlahavas Ioannis. Effective and Efficient Multilabel Classification in Domains with Large Number of Labels in Proc. ECML/PKDD 2008 Workshop on Mining Multidimensional Data (MMD'08):XX 2008.
 * http://www.citeulike.org/user/erelsegal-halevi/tag/homer
 *
 * @param opts
 *            multilabelClassifierType (mandatory) - the type of the multilabel classifier used in each level of the hierarchy.
 *            splitLabel (optional) - a function that splits a label to a array of sub-labels, from root to leaves. DEFAULT: split around the "@" char.
 *            joinLabel (optional) - a function that joins an array of sub-labels, from root to leaves, to create a full label. DEFAULT: join with the "@" char.
 *
 * @note The original HOMER paper used a clustering algorithm to create a hierarchy of labels.
 * This clustering algorithm is not implemented here.
 * Instead, we use a custom function that converts a label to a path in the hierarchy, and another custom function that converts a path back to a label.
 */
var Homer = function(opts) {
	opts = opts || {};
	if (!opts.multilabelClassifierType) {
		console.dir(opts);
		throw new Error("opts.multilabelClassifierType is null");
	}
	this.multilabelClassifierType = opts.multilabelClassifierType;

	this.splitLabel = opts.splitLabel || function(label)      {return label.split(/@/);}
	this.joinLabel  = opts.joinLabel  || function(superlabel) {return superlabel.join("@");}

	this.root = {
		superlabelClassifier: this.newMultilabelClassifier(),
		mapSuperlabelToBranch: {}
	}

	this.allClasses = {};
}

Homer.prototype = {

	/**
	 * Tell the classifier that the given sample belongs to the given classes.
	 *
	 * @param sample
	 *            a document.
	 * @param classes
	 *            an object whose KEYS are classes, or an array whose VALUES are classes.
	 */
	trainOnline: function(sample, labels) {
		labels = multilabelutils.normalizeOutputLabels(labels);

		for (var i in labels)
			this.allClasses[labels[i]]=true;

		return this.trainOnlineRecursive(
				sample,
				labels.map(this.splitLabel),
				this.root);
	},

	/**
	 *  Recursive internal subroutine of trainOnline.
	 *  @param splitLabels an array of arrays: each internal array represents the parts of a single label.
	 */
	trainOnlineRecursive: function(sample, splitLabels, treeNode) {
		var superlabels = {}; // the first parts of each of the splitLabels
		var mapSuperlabelToRest = {};   // each value is a list of continuations of the key.
		for (var i in splitLabels) {
			var splitLabel = splitLabels[i];
			var superlabel = splitLabel[0];
			superlabels[superlabel] = true;
			if (splitLabel.length>1) {
				if (!mapSuperlabelToRest[superlabel])
					mapSuperlabelToRest[superlabel] = [];
				mapSuperlabelToRest[superlabel].push(splitLabel.slice(1));
			}
		}

		treeNode.superlabelClassifier.trainOnline(sample, Object.keys(superlabels));
		for (var superlabel in mapSuperlabelToRest) {
			if (!(superlabel in treeNode.mapSuperlabelToBranch)) {
				treeNode.mapSuperlabelToBranch[superlabel] = {
					superlabelClassifier: this.newMultilabelClassifier(),
					mapSuperlabelToBranch: {}
				}
			}
			this.trainOnlineRecursive(sample, mapSuperlabelToRest[superlabel], treeNode.mapSuperlabelToBranch[superlabel]);
		}
	},

	/**
	 * Train the classifier with all the given documents.
	 *
	 * @param dataset
	 *            an array with objects of the format:
	 *            {input: sample1, output: [class11, class12...]}
	 */
	trainBatch : function(dataset) {
		dataset = dataset.map(function(datum) {
			var normalizedLabels = multilabelutils.normalizeOutputLabels(datum.output);
			for (var i in normalizedLabels)
				this.allClasses[normalizedLabels[i]]=true;
			return {
				input: datum.input,
				output: normalizedLabels.map(this.splitLabel)
			}
		}, this);

		// [ [ 'Offer', 'Leased Car', 'Without leased car' ], [ 'Offer', 'Working Hours', '9 hours' ] ]

		return this.trainBatchRecursive(dataset, this.root);
	},

	/**
	 *  Recursive internal subroutine of trainBatch.
	 */
	trainBatchRecursive: function(dataset, treeNode) {
		var superlabelsDataset = [];
		var mapSuperlabelToRestDataset = {};
		dataset.forEach(function(datum) {
			var splitLabels = datum.output;	// [ [ 'Offer', 'Leased Car', 'Without leased car' ], [ 'Offer', 'Working Hours', '9 hours' ] ]
			var superlabels = {};           // the first parts of each of the splitLabels
			var mapSuperlabelToRest = {};   // each value is a list of continuations of the key.
			for (var i in splitLabels) {
				var splitLabel = splitLabels[i];//[ 'Offer', 'Leased Car', 'Without leased car' ]
				var superlabel = splitLabel[0];
				superlabels[superlabel] = true; //superlabels['Offer'] = true
				if (splitLabel.length>1) { 		// if it have more than one label (superlabel)
					if (!mapSuperlabelToRest[superlabel])
						mapSuperlabelToRest[superlabel] = [];
					mapSuperlabelToRest[superlabel].push(splitLabel.slice(1));//['Leased Car', 'Without leased car']
				}
			}

/*			Sample of mapSuperlabelToRest
			{ Offer:
			[ [ 'Leased Car', 'Without leased car' ],
   			  [ 'Working Hours', '9 hours' ] ] }

			Sample of superlabelsDataset, initial dataset with superlabel instead of entire output
			'. [end]': 0.965080896043587 },
			output: [ 'Offer' ] } ]
*/
			superlabelsDataset.push({
				input: datum.input,
				output: Object.keys(superlabels)
			});

			for (var superlabel in mapSuperlabelToRest) {
				if (!(superlabel in mapSuperlabelToRestDataset))
					mapSuperlabelToRestDataset[superlabel] = [];
				mapSuperlabelToRestDataset[superlabel].push({
					input: datum.input,
					output: mapSuperlabelToRest[superlabel]
				});
			}
		}, this);

/*		Sample of mapSuperlabelToRestDataset
		{ Offer: [ { input: [Object], output: [["Leased Car","Without leased car"],["Working Hours","9 hours"]] } ] }
*/

		// train the classifier only on superlabels
		treeNode.superlabelClassifier.trainBatch(superlabelsDataset);

		for (var superlabel in mapSuperlabelToRestDataset) {
			if (!(superlabel in treeNode.mapSuperlabelToBranch)) {
				treeNode.mapSuperlabelToBranch[superlabel] = {
					superlabelClassifier: this.newMultilabelClassifier(),
					mapSuperlabelToBranch: {}
				}
			}
/*			train the next level classifier for a give superlabel classifier superlabel (from loop)
			with the dataset from new structure mapSuperlabelToRestDataset (see above)
*/			this.trainBatchRecursive(mapSuperlabelToRestDataset[superlabel], treeNode.mapSuperlabelToBranch[superlabel]);
		}
	},

	/**
	 * Use the model trained so far to classify a new sample.
	 *
	 * @param sample a document.
	 * @param explain - int - if positive, an "explanation" field, with the given length, will be added to the result.
	 *
	 * @return an array whose VALUES are classes.
	 */
	classify: function(sample, explain) {
		var splitLabels = this.classifyRecursive(sample, explain, this.root);
		//console.log("splitLabels:"+JSON.stringify(splitLabels));
		if (explain>0) {
			splitLabels.classes = splitLabels.classes.map(this.joinLabel);
		} else {
			splitLabels = splitLabels.map(this.joinLabel);
		}
		return splitLabels;
	},

	/**
	 *  Recursive internal subroutine of classify.
	 *  @return an array of arrays, where each internal array represents a split label.
	 */
	classifyRecursive: function(sample, explain, treeNode, depth) {
		if (!depth) depth = 1;
		// classify the superlabel
		var superlabelsWithExplain = treeNode.superlabelClassifier.classify(sample, explain);
		var superlabels = (explain>0? superlabelsWithExplain.classes: superlabelsWithExplain);

		var splitLabels = [];
		if (explain>0) {
			var explanations = ["depth="+depth+": "+superlabels, superlabelsWithExplain.explanation];
		}

		// for all superlabels that were classified, may be there are more than one that were classified with it
		for (var i in superlabels) {
			var superlabel = superlabels[i];
			var splitLabel = [superlabel];

			// classifier of [Offer] types / second level / classifies Offer's parameters
			var branch = treeNode.mapSuperlabelToBranch[superlabel];

			if (branch) {

				// [ [ 'Without leased car' ] ]
				var branchLabelsWithExplain = this.classifyRecursive(sample, explain, branch, depth+1);

				var branchLabels = (explain>0? branchLabelsWithExplain.classes: branchLabelsWithExplain);

				for (var j in branchLabels)
					splitLabels.push(splitLabel.concat(branchLabels[j]));
				if (explain>0)
					explanations = explanations.concat(branchLabelsWithExplain.explanation);
			} else {
				splitLabels.push(splitLabel);
			}
		}
		return (explain>0?
				{classes: splitLabels, explanation: explanations}:
				splitLabels);
	},

	toJSON: function() {
		var json = this.toJSONRecursive(this.root);
		json.allClasses = this.allClasses;
		return json;
	},

	toJSONRecursive: function(treeNode) {
		var treeNodeJson = {
			superlabelClassifier: treeNode.superlabelClassifier.toJSON(),
			mapSuperlabelToBranch: {}
		};
		for (var superlabel in treeNode.mapSuperlabelToBranch) {
			treeNodeJson.mapSuperlabelToBranch[superlabel] = this.toJSONRecursive(treeNode.mapSuperlabelToBranch[superlabel]);
		}
		return treeNodeJson;
	},

	fromJSON: function(json) {
		this.allClasses = json.allClasses;
		this.root = this.fromJSONRecursive(json);
	},

	fromJSONRecursive: function(treeNodeJson) {
		var treeNode = {
			mapSuperlabelToBranch: {}
		};
		treeNode.superlabelClassifier =  this.newMultilabelClassifier();
		treeNode.superlabelClassifier.fromJSON(treeNodeJson.superlabelClassifier);
		for (var superlabel in treeNodeJson.mapSuperlabelToBranch)
			treeNode.mapSuperlabelToBranch[superlabel] =
				this.fromJSONRecursive(treeNodeJson.mapSuperlabelToBranch[superlabel]);
		return treeNode;
	},

	getAllClasses: function() {
		return Object.keys(this.allClasses);
	},

	/**
	 * Link to a FeatureLookupTable from a higher level in the hierarchy (typically from an EnhancedClassifier), used ONLY for generating meaningful explanations.
	 */
	setFeatureLookupTableRecursive: function(featureLookupTable, treeNode) {
		if (treeNode.superlabelClassifier && treeNode.superlabelClassifier.setFeatureLookupTable)
			treeNode.superlabelClassifier.setFeatureLookupTable(featureLookupTable);
		for (var superlabel in treeNode.mapSuperlabelToBranch)
			this.setFeatureLookupTableRecursive(featureLookupTable, treeNode.mapSuperlabelToBranch[superlabel]);
	},

	/**
	 * Link to a FeatureLookupTable from a higher level in the hierarchy (typically from an EnhancedClassifier), used ONLY for generating meaningful explanations.
	 */
	setFeatureLookupTable: function(featureLookupTable) {
		//console.log("HOMER setFeatureLookupTable "+featureLookupTable);
		this.featureLookupTable = featureLookupTable;
		this.setFeatureLookupTableRecursive(featureLookupTable, this.root);
	},

	newMultilabelClassifier: function() {
		var classifier = new this.multilabelClassifierType();
		if (this.featureLookupTable && classifier.setFeatureLookupTable)
			classifier.setFeatureLookupTable(this.featureLookupTable);
		return classifier;
	}
}

/*
 * UTILS
 */

module.exports = Homer;


var hash = require("../../utils/hash");
var sprintf = require("sprintf").sprintf;
var _ = require("underscore")._;
var ftrs = require('../../features');

/**
 * BinarySegmentation - Multi-label text classifier, based on a segmentation scheme using base binary classifiers.
 *
 * Inspired by:
 *
 * Morbini Fabrizio, Sagae Kenji. Joint Identification and Segmentation of Domain-Specific Dialogue Acts for Conversational Dialogue Systems. ACL-HLT 2011
 * http://www.citeulike.org/user/erelsegal-halevi/article/10259046
 *
 * @author Erel Segal-haLevi
 *
 * @param opts
 *            binaryClassifierType (mandatory) - the type of the base binary classifier.
 *            featureExtractor (optional) - a single feature-extractor (see the "features" folder), or an array of extractors, for extracting features from the text segments during classification.
 */
var BinarySegmentation = function(opts) {
	if (!('binaryClassifierType' in opts)) {
		console.dir(opts);
		throw new Error("opts must contain binaryClassifierType");
	}
	if (!opts.binaryClassifierType) {
		console.dir(opts);
		throw new Error("opts.binaryClassifierType is null");
	}

	this.binaryClassifierType = opts.binaryClassifierType;
	this.classifier = new this.binaryClassifierType();

	switch (opts.segmentSplitStrategy) {
	case 'shortestSegment': this.segmentSplitStrategy = this.shortestSegmentSplitStrategy; break;
	case 'longestSegment':  this.segmentSplitStrategy = this.longestSegmentSplitStrategy;  break;
	case 'cheapestSegment':  this.segmentSplitStrategy = this.cheapestSegmentSplitStrategy;  break;
	default: this.segmentSplitStrategy = null;
	}

	this.mapClassnameToClassifier = {};
}

BinarySegmentation.prototype = {

	/* Tell the classifier that the given sample belongs to the given classes.
	 *
	 * @param sample
	 *            a document.
	 * @param classes
	 *            an object whose KEYS are classes, or an array whose VALUES are classes.
	 */
	trainOnline: function(sample, classes) {
		sample = this.sampleToFeatures(sample, this.featureExtractors);
		classes = hash.normalized(classes);
		for (var positiveClass in classes) {
			this.makeSureClassifierExists(positiveClass);
			this.mapClassnameToClassifier[positiveClass].trainOnline(sample, 1);
		}
		for (var negativeClass in this.mapClassnameToClassifier) {
			if (!classes[negativeClass])
				this.mapClassnameToClassifier[negativeClass].trainOnline(sample, 0);
		}
	},

	/**
	 * Train the classifier with all the given documents.
	 *
	 * @param dataset
	 *            an array with objects of the format:
	 *            {input: sample1, output: [class11, class12...]}
	 */
	trainBatch : function(dataset) {
		// add ['start'] and ['end'] as a try to resolve Append:previous FP
		_.map(dataset, function(num){
			num['input'] = "['start'] "+ num['input'] + " ['end']"
			return num });

		this.classifier.trainBatch(dataset)
	},

	/**
	 * Internal function - use the model trained so far to classify a single segment of a sentence.
	 *
	 * @param segment a part of a text sentence.
	 * @param explain - int - if positive, an "explanation" field, with the given length, will be added to the result.
	 *
	 * @return an array whose VALUES are classes.
	 */

	classifySegment: function(segment, explain) {
			var classification = this.classifier.classify(segment, explain, true);
			// console.log(classification)
			// if (classification.classes.length != 0)
			{
				// HERE
				// console.log(segment)
				// console.log(classification['classes'])
				// console.log(classification['scores'])
				// console.log(classification['explanation']['positive'])
				// console.log(classification['explanation']['negative'])
				// // console.log()
				// process.exit(0)
				// console.log(classification['explanation']['negative']['Query'])
				// console.log(classification['explanation']['negative']['10%'])
			}

			// if ((segment == "Leased car")&&('With leased car' in classification['explanation']['negative']))
				{
				// console.log(classification['explanation']['negative']['With leased car'])
				// console.log("______________________________________")
				// process.exit(0)

				}
			return classification
	},

	/**
	 * Internal function - use the model trained so far to classify a single segment of a sentence.
	 *
	 * @param segment a part of a text sentence.
	 * @param explain - int - if positive, an "explanation" field, with the given length, will be added to the result.
	 *
	 * @return an array [the_best_class, and_its_probability].
	 */
	bestClassOfSegment: function(segment, explain) {
		var classes = this.classifySegment(segment, explain);
		if (classes.classes.length==0) {
			// FEATURES
			return ['', 0, ''];
			// return ['', 0];
		} else {
			// HERE
			// console.log([classes.classes[0], classes.scores[classes.classes[0]]])
			// FEATURES
			return [classes.classes[0], classes.scores[classes.classes[0]], classes['explanation']['positive']];
			// return [classes.classes[0], classes.scores[classes.classes[0]]];
		}
	},

	/**
	 * protected function:
	 * Strategy of finding the cheapest segmentation (- most probable segmentation), using a dynamic programming algorithm.
	 * Based on:
	 * Morbini Fabrizio, Sagae Kenji. Joint Identification and Segmentation of Domain-Specific Dialogue Acts for Conversational Dialogue Systems. ACL-HLT 2011
 	 * http://www.citeulike.org/user/erelsegal-halevi/article/10259046
	 */
	cheapestSegmentSplitStrategy: function(words, accumulatedClasses, explain, explanations) {

		//for (var start=0; start<=words.length; ++start) {
		//	for (var end=start+1; end<=words.length; ++end) {
		//		var segment = words.slice(start,end).join(" ");

		//		var bestClassAndProbability = this.bestClassOfSegment(segment, explain);
		//		if (bestClassAndProbability[1] != Infinity)
		//		{
		//			var bestClass = bestClassAndProbability[0];
		//			var bestClassProbability = bestClassAndProbability[1];
			//		digraph.add(start, end, -bestClassProbability);
		//		}
		//	}
		//}

		var cheapest_paths = require("graph-paths").cheapest_paths;

                var mini = Infinity
                _(words.length).times(function(nn){
                   cheapestSegmentClassificationCosts = cheapest_paths(segmentClassificationCosts, nn);
                       _.each(cheapestSegmentClassificationCosts, function(value, key, list){
                             if (value.cost<mini)
                               {
                                mini = value.cost
	                            cheapestSentenceClassificationCost = value
                                 }
                         }, this)
                 }, this)

     cheapestSegmentClassificationCosts = cheapest_paths(segmentClassificationCosts, 0);
     cheapestSentenceClassificationCost = cheapestSegmentClassificationCosts[words.length];

        var path = cheapestSentenceClassificationCost.path;

		for (var i=0; i<path.length-1; ++i) {
			// var segment = words.slice(cheapestClassificationPath[i],cheapestClassificationPath[i+1]).join(" ");
			var segment = words.slice(path[i],path[i+1]).join(" ");
			//HERE
			var segmentClassesWithExplain = this.classifySegment(segment, explain);
			var segmentClasses = (segmentClassesWithExplain.classes? segmentClassesWithExplain.classes: segmentClassesWithExplain);

			if (segmentClasses.length>0)
				accumulatedClasses[segmentClasses[0]] = true;

			// explanations = []
			if (explain>0) {
					if (segmentClasses.length>0)
						explanations.push([segmentClasses[0], segment, [path[i], path[i+1]],segmentClassesWithExplain['explanation']['positive']])

			};

		}
	},

	/**
	 * protected function:
	 * Strategy of classifying the shortest segments with a single class.
	 */
	shortestSegmentSplitStrategy: function(words, accumulatedClasses, explain, explanations) {
		var currentStart = 0;
		for (var currentEnd=1; currentEnd<=words.length; ++currentEnd) {
			var segment = words.slice(currentStart,currentEnd).join(" ");
			var segmentClassesWithExplain = this.classifySegment(segment, explain);
			var segmentClasses = (segmentClassesWithExplain.classes? segmentClassesWithExplain.classes: segmentClassesWithExplain);

			if (segmentClasses.length==1) {
				// greedy algorithm: found a section with a single class - cut it and go on
				accumulatedClasses[segmentClasses[0]]=true;
				currentStart = currentEnd;
				if (explain>0) {
					explanations.push(segment);
					explanations.push(segmentClassesWithExplain.explanation);
				};
			}
		}
	},

	/**
	 * protected function:
	 * Strategy of classifying the longest segments with a single class.
	 */
	longestSegmentSplitStrategy: function(words, accumulatedClasses, explain, explanations) {
		var currentStart = 0;
		var segment = null;
		var segmentClassesWithExplain = null;
		var segmentClasses = null;
		for (var currentEnd=1; currentEnd<=words.length; ++currentEnd) {
			var nextSegment = words.slice(currentStart,currentEnd).join(" ");
			var nextSegmentClassesWithExplain = this.classifySegment(nextSegment, explain);
			var nextSegmentClasses = (nextSegmentClassesWithExplain.classes? nextSegmentClassesWithExplain.classes: nextSegmentClassesWithExplain);
			//console.log("\t"+JSON.stringify(nextSegment) +" -> "+nextSegmentClasses)
			nextSegmentClasses.sort();

			if (segmentClasses && segmentClasses.length==1 && (nextSegmentClasses.length>1 || !_(nextSegmentClasses).isEqual(segmentClasses))) {
				// greedy algorithm: found a section with a single class - cut it and go on
				accumulatedClasses[segmentClasses[0]]=true;
				currentStart = currentEnd-1;
				if (explain>0) {
					explanations.push(segment);
					explanations.push(segmentClassesWithExplain.explanation);
				};
			}

			segment = nextSegment;
			segmentClassesWithExplain = nextSegmentClassesWithExplain;
			segmentClasses = nextSegmentClasses;
		}

		// add the classes of the last section:
		for (var i in segmentClasses)
			accumulatedClasses[segmentClasses[i]]=true;
		if (explain>0) {
			explanations.push(segment);
			explanations.push(segmentClassesWithExplain.explanation);
		};
		/*if (words.length>20)  {
			console.dir(explanations);
			process.exit(1);
		}*/
	},

	/**
	 * Use the model trained so far to classify a new sample.
	 *
	 * @param segment a part of a text sentence.
	 * @param explain - int - if positive, an "explanation" field, with the given length, will be added to the result.
	 *
	 * @return an array whose VALUES are classes.
	 */
	classify: function(sentence, explain) {

		// sentence = "['start'] " + sentence + " ['end']"
		var minWordsToSplit = 2;
		var words = sentence.split(/ /);
		// var words = tokenizer.tokenize(sentence);
		if (this.segmentSplitStrategy && words.length>=minWordsToSplit) {
			var accumulatedClasses = {};
			var explanations = [];
			this.segmentSplitStrategy(words, accumulatedClasses, explain, explanations);

			var classes = Object.keys(accumulatedClasses);

			return (explain>0?	{
				classes: classes,
				explanation: explanations
			}:
			classes);
		} else {
			classification = this.bestClassOfSegment(sentence, explain)
			// classification = this.classifySegment(sentence, explain);
			//HERER

			// console.log(sentence)
			// console.log(classification)
			// process.exit(0)
			// process.exit(0)

			return (explain>0?	{
				classes: classification[0],
				// FEATURES
				explanation: [[classification[0], sentence, [0,sentence.length-1], classification[2]]]
				// explanation: [[classification[0], sentence, [0,sentence.length-1]]]

			}:
			classification[0]);
			// return {classes: classification[0],
					// explanation: [[classification[0], sentence, [0,sentence.length-1]]]}
			// return {classes: classification.classes[0]}
		}
	},

	toJSON : function(callback) {
		var result = {};
		for ( var aClass in this.mapClassnameToClassifier) {
			var binaryClassifier = this.mapClassnameToClassifier[aClass];
			if (!binaryClassifier.toJSON) {
				console.dir(binaryClassifier);
				console.log("prototype: ");
				console.dir(binaryClassifier.__proto__);
				throw new Error("this binary classifier does not have a toJSON function");
			}
			result[aClass] = binaryClassifier.toJSON(callback);
		}
		return result;
	},

	fromJSON : function(json, callback) {
		for ( var aClass in json) {
			this.mapClassnameToClassifier[aClass] = new this.binaryClassifierType();
			this.mapClassnameToClassifier[aClass].fromJSON(json[aClass]);
		}
	},

	setFeatureLookupTable: function(featureLookupTable) {
		if (featureLookupTable)
			// this.featureLookupTable = featureLookupTable
			if (this.classifier.setFeatureLookupTable)
				this.classifier.setFeatureLookupTable(featureLookupTable);  // for generating clearer explanations only
		// }
 	},
}

module.exports = BinarySegmentation;


var hash = require("../../utils/hash");
var sprintf = require("sprintf").sprintf;
var _ = require("underscore")._;
var util = require("util");
var multilabelutils = require('./multilabelutils');

/**
 * Multilabel online classifier based on Perceptron and Passive-Aggressive.
 *
 * features and categories are strings; samples are hashes.
 *
 * Based on Python code Copyright (c) 2013 Joseph Keshet.
 *
 * Conversion to Node.js started by Erel Segal-haLevi, but not finished yet.
 *
 * @since 2013-08
 *
 * @param opts
 *			Constant (optional) - tradeoff constant (default=5.0)
 *			retrain_count (optional) - number of epoches to run in trainBatch mode (default=10)
 */
var MultiLabelPassiveAggressive = function(opts) {
	this.retrain_count = opts.retrain_count || 10;
	this.Constant = opts.Constant || 5.0;
	this.weights = {
		//DUMMY_CLASS:{}
	};
	this.weights_sum = {
		//DUMMY_CLASS:{}
	};
	this.seenFeatures = {};
	this.num_iterations = 0
}

MultiLabelPassiveAggressive.prototype = {

	/**
	 * @param sample a hash of feature-values (string features)
	 * @param averaging boolean
	 * @return an array of pairs [category,score], sorted by decreasing score
	 */
	predict: function(features, averaging, explain) {
		var weights_for_classification = (averaging? this.weights_sum: this.weights);
		var scores = {};
		if (explain>0) var explanations = [];

//		for (var feature in features) {
//			if (feature in weights_for_classification) {
//				var weight = weights_for_classification[feature];
//				var value = features[feature];
//				var relevance = value * weight;
//				score += relevance;
//				if (explain>0) explanations.push(this.detailed_explanations?
//						{
//							feature: feature,
//							value: value,
//							weight: weight,
//							relevance: relevance,
//						}:
//						sprintf("%s%+1.2f*%+1.2f=%+1.2f", feature, value, weight, relevance);
//				);
//			}
//		}

		scores = hash.inner_product_matrix(features, weights_for_classification); // scores is a map: category=>score
		var scoresVector = _.pairs(scores); // scoresVector is an array of pairs [category,score]
		scoresVector.sort(function(a,b) {return b[1]-a[1]}); // sort by decreasing score
		return scoresVector;
	},

	/**
	 * Tell the classifier that the given sample belongs to the given classes.
	 *
	 * @param sample a hash of feature-values (string features)
	 * @param classes an array whose VALUES are classes.
	 */
	update: function(sample, classes) {
		var classesSet = hash.normalized(classes);

		var ranks = this.predict(sample, /*averaging=*/false);  // pairs of [class,score] sorted by decreasing score

		// find the lowest ranked relevant label r:
		var r = 0
		var r_score = Number.MAX_VALUE
		ranks.forEach(function(labelAndScore) {
			var label = labelAndScore[0];
			var score = labelAndScore[1];
			if ((label in classesSet) && score < r_score) {
				r = label
				r_score = score
			}
		});

		// find the highest ranked irrelevant label s
		var s = 0
		var s_score = -Number.MAX_VALUE
		ranks.reverse();
		ranks.forEach(function(labelAndScore) {
			var label = labelAndScore[0];
			var score = labelAndScore[1];
			if (!(label in classesSet) && score > s_score) {
				s = label;
				s_score = score;
			}
		});

		var loss = Math.max(1.0 - r_score, 0.0) + Math.max(1.0 + s_score, 0.0);
		if (loss > 0) {
			var sample_norm2 = hash.sum_of_square_values(sample);
			var tau = Math.min(this.Constant, loss / sample_norm2);

			if (r_score < Number.MAX_VALUE)
				hash.addtimes(this.weights[r], tau, sample);  // weights[r] += tau*sample
			if (s_score > -Number.MAX_VALUE)
				hash.addtimes(this.weights[s], -tau, sample); // weights[s] -= tau*sample
		}
		// this.weights_sum = (this.weights + this.weights_sum);
		for (category in this.weights)
			hash.add(this.weights_sum[category], this.weights[category]);

		hash.add(this.seenFeatures, sample);
		this.num_iterations += 1;
	},

	/**
	 * Train the classifier with all the given documents.
	 *
	 * @param dataset
	 *			an array with objects of the format:
	 *			{input: sample1, output: [class11, class12...]}
	 */
	trainBatch : function(dataset) {
		// preprocessing: add all the classes in the dataset to the weights vector;
		dataset.forEach(function(datum) {
			this.addClasses(datum.output);
			this.editFeatureValues(datum.input, /*remove_unknown_features=*/false);
		}, this);

		for (var i=0; i<this.retrain_count; ++i)
			dataset.forEach(function(datum) {
				this.update(datum.input, datum.output);
			}, this);
	},

	trainOnline: function(features, classes) {
		this.addClasses(classes);
		this.editFeatureValues(features, /*remove_unknown_features=*/false);
		this.update(features, classes);
	},

	/**
	 * Use the model trained so far to classify a new sample.
	 *
	 * @param sample a hash {feature1: value1, feature2: value2, ...}.
	 * @param explain - int - if positive, an "explanation" field, with the given length, should be added to the result.
	 * @param withScores - boolean - if true, return an array of [class,score], ordered by decreasing order of score.
	 *
	 * @return an array whose VALUES are classes.
	 */
	classify : function(features, explain, withScores) {
		this.editFeatureValues(features, /*remove_unknown_features=*/true);
		var scoresVector = this.predict(features, /*averaging=*/true, explain);
		return multilabelutils.mapScoresVectorToMultilabelResult(scoresVector, explain, withScores, /*threshold=*/0);
	},

	/**
	 * Copied from Modified Balanced Winnow (see winnow/winnow_hash.js).
	 * Commented out, because it is unuseful here.
	 */
	editFeatureValues: function (features, remove_unknown_features) {
//		console.log("before: "+util.inspect(features));
//		if (!('bias' in features))
//			features['bias'] = 1.0;
//		if (remove_unknown_features) {
//			for (var feature in features)
//				if (!(feature in this.seenFeatures))
//					delete features[feature];
//		}
//		hash.normalize_sum_of_values_to_1(features);
//		console.log("after: "+util.inspect(features));
	},

	/**
	 * Tell the classifier that the given classes will be used for the following
	 * samples, so that it will know to add negative samples to classes that do
	 * not appear.
	 *
	 * @param classes an object whose KEYS are classes, or an array whose VALUES are classes.
	 */
	addClasses: function(classes) {
		classes = hash.normalized(classes);
		for (var aClass in classes) {
			if (!(aClass in this.weights)) {
				this.weights[aClass]={};
				this.weights_sum[aClass]={};
			}
		}
	},

	getAllClasses: function() {
		return Object.keys(this.weights);
	},

	toJSON : function(callback) {
		return {
			weights_sum: this.weights_sum,
			weights: this.weights,
			num_iterations: this.num_iterations,
		}
	},

	fromJSON : function(json, callback) {
		this.weights_sum = json.weights_sum;
		this.weights = json.weights;
		this.num_iterations = json.num_iterations;
	},
}

module.exports = MultiLabelPassiveAggressive;


var sprintf = require("sprintf").sprintf;
var _ = require("underscore")._;
var multilabelutils = require('./multilabelutils');

/**
 *  PartialClassification is a test classifier that learns and classifies the components
 * of the labels separately according to the splitLabel routine. One of the examples could be
 * classifying intent, attribute, value separately by three different classifiers.
 * When performing test by trainAndTest module, there is a check for toFormat routine, if it exists
 * then pretest format converting occurs.
 *
 * @author Vasily Konovalov
 * @since March 2014
 */

var PartialClassification = function(opts) {

	opts = opts || {};
	if (!opts.multilabelClassifierType) {
		console.dir(opts);
		throw new Error("opts.multilabelClassifierType is null");
	}

	if (!opts.numberofclassifiers) {
		console.dir(opts);
		throw new Error("opts.numberofclassifiers is null");
	}

	// this.splitLabel = opts.splitLabel || function(label)      {return label.split(/@/);}
	this.classifier = this.intializeClassifiers(opts.numberofclassifiers, opts.multilabelClassifierType)
}

PartialClassification.prototype = {

	intializeClassifiers: function(numberofclassifiers, multilabelClassifierType) {
		classifier = []
	_(numberofclassifiers).times(function(n){
		classif = new multilabelClassifierType;
		classifier.push(classif);
 	});
 	return classifier
	},

	trainOnline: function(sample, labels) {
		throw new Error("PartialClassification does not support online training");
	},

	trainBatch : function(dataset) {
		num_of_classifiers = 0

		_.each(dataset, function(value, key, list){
			num_of_classifiers =  Math.max(num_of_classifiers, (value['output']).length)
		}, this);

		_(num_of_classifiers).times(function(n){
			data = []
			_.each(dataset, function(value, key, list){
				if (value.output.length - 1 >= n)
					{
						value1 = _.clone(value)
						value1['output'] = value.output[n]
						data.push(value1)
					}

			 },this)

			// classifier = new this.multilabelClassifierType();
			this.classifier[n].trainBatch(data)
			// classifier.trainBatch(data)
			// this.classifier.push(classifier)
			}, this)

	},

	classify: function(sample, explain, continuous_output) {

		var labels = []
		var explanation = []
		var scores = {}

	 	_.each(this.classifier, function(classif, key, list){
	 		var value = classif.classify(sample, explain, continuous_output)
	 	 	if (explain>0)
	 	 		labels.push(value.classes)
	 	 	else
 				labels.push(value)
	 	 	explanation.push(value.explanation)
	 	 	scores = _.extend(scores, value.scores)
	 	})

		if (explain>0)
			{
				var positive = {}
				var negative = {}

				_.each(_.pluck(explanation, 'positive'), function(value, key, list){
					positive = _.extend(positive, value)
					}, this)

				_.each(_.pluck(explanation, 'negative'), function(value, key, list){
					negative = _.extend(negative, value)
					}, this)

			if (_.keys(negative)!=0)
				explanation = {
					positive: positive,
					negative: negative,
				}
			}

		return (explain>0?
			{
				classes: labels,
				scores: scores,
				explanation: explanation
			}:
			labels);

		// console.log(JSON.stringify(explanation, null, 4))
		// return (explain>0?
		// 	{
		// 		classes: labels,
		// 		scores: scores,
		// 		explanation: explanation
		// 	}:
		// 	labels);

 	},

 	setFeatureLookupTable: function(featureLookupTable) {
 		_.each(this.classifier, function(classif, key, list){
	 		classif.setFeatureLookupTable(featureLookupTable)
	 	})
 	},

	getAllClasses: function() {
		throw new Error("No implementation in PartialClassification");
	},

	toJSON : function() {
		throw new Error("No implementation in PartialClassification");
	},

	fromJSON : function(json) {
		throw new Error("No implementation in PartialClassification");
	},

}

module.exports = PartialClassification;


var hash = require("./hash");
var sprintf = require('sprintf').sprintf;
var _ = require('underscore')._;

/**
 * PrecisionRecall - an object for tracking results of experiments: precision, recall, f1, and execution time.
 *
 * @author Erel Segal-haLevi
 * @since 2013-06
 *
 * The present version is from https://github.com/erelsgl/limdu/blob/d61166c91a81daee62e3d67d5fff2b06cee8191f/utils/PrecisionRecall.js
 * @author Vasily Konovalov
 * @since 2015-10-15
 */

 https://github.com/erelsgl/limdu/blob/d61166c91a81daee62e3d67d5fff2b06cee8191f/utils/PrecisionRecall.js

var PrecisionRecall = function() {
	this.count = 0;
	this.TP = 0;
	this.TN = 0;
	this.FP = 0;
	this.FN = 0;
	this.TRUE = 0;
	this.startTime = new Date();
	this.labels = {}
	this.dep = {}
	this.confusion = {}
}

PrecisionRecall.prototype = {

	/**
	 * Record the result of a new binary experiment.
	 *
	 * @param expected - the expected result (true/false).
	 * @param actual   - the actual   result (true/false).
	 */
	addCase: function(expected, actual) {
		this.count++;
		if (expected && actual) this.TP++;
		if (!expected && actual) this.FP++;
		if (expected && !actual) this.FN++;
		if (!expected && !actual) this.TN++;
		if (expected==actual) this.TRUE++;
	},

	/**
	 * Record the result of a new classes experiment per labels.
	 *
	 * @param expectedClasses - the expected set of classes (as an array or a hash).
	 * @param actualClasses   - the actual   set of classes (as an array or a hash).
	 * @return an array of explanations "FALSE POSITIVE", "FALSE NEGATIVE", and maybe also "TRUE POSITIVE"
	 */

	addCasesLabels: function (expectedClasses, actualClasses ) {
		var explanations = [];

		actualClasses = hash.normalized(actualClasses);
		expectedClasses = hash.normalized(expectedClasses);

		var allTrue = true;

		if (!(Object.keys(expectedClasses)[0] in this.confusion))
			this.confusion[Object.keys(expectedClasses)[0]] = {}

		if (!(Object.keys(actualClasses)[0] in this.confusion[Object.keys(expectedClasses)[0]]))
			this.confusion[Object.keys(expectedClasses)[0]][Object.keys(actualClasses)[0]] = 0

		this.confusion[Object.keys(expectedClasses)[0]][Object.keys(actualClasses)[0]] += 1

		for (var actualClass in actualClasses) {

			if (!(actualClass in this.confusion))
				this.confusion[actualClass]={}

			if (!(actualClass in this.labels)) {
				this.labels[actualClass]={}
				this.labels[actualClass]['TP']=0
				this.labels[actualClass]['FP']=0
				this.labels[actualClass]['FN']=0
				}

			if (actualClass in expectedClasses) {
				this.labels[actualClass]['TP'] += 1

			} else {
				this.labels[actualClass]['FP'] += 1
			}
		}
		for (var expectedClass in expectedClasses) {

			if (!(expectedClass in this.labels)) {
				this.labels[expectedClass]={}
				this.labels[expectedClass]['TP']=0
				this.labels[expectedClass]['FP']=0
				this.labels[expectedClass]['FN']=0
				}

			if (!(expectedClass in actualClasses)) {
				this.labels[expectedClass]['FN'] += 1
			}
		}
	},

	/* intented to calculate macro and micro average */
	addPredicition: function(expected, actual)
	{
		this.addCasesHash(expected, actual, 1)
		this.addCasesLabels(expected, actual)
	},

	/**
	 * Record the result of a new classes experiment.
	 *
	 * @param expectedClasses - the expected set of classes (as an array or a hash).
	 * @param actualClasses   - the actual   set of classes (as an array or a hash).
	 * @param logTruePositives- if true, log the true positives.
	 * @return an array of explanations "FALSE POSITIVE", "FALSE NEGATIVE", and maybe also "TRUE POSITIVE"
	 */
	addCases: function (expectedClasses, actualClasses, logTruePositives) {
		var explanations = [];
		actualClasses = hash.normalized(actualClasses);
		expectedClasses = hash.normalized(expectedClasses);

		var allTrue = true;
		for (var actualClass in actualClasses) {
			if (actualClass in expectedClasses) {
				if (logTruePositives) explanations.push("\t\t+++ TRUE POSITIVE: "+actualClass);
				this.TP++;
			} else {
				explanations.push("\t\t--- FALSE POSITIVE: "+actualClass);
				this.FP++;
				allTrue = false;
			}
		}
		for (var expectedClass in expectedClasses) {
			if (!(expectedClass in actualClasses)) {
				explanations.push("\t\t--- FALSE NEGATIVE: "+expectedClass);
				this.FN++;
				allTrue = false;
			}
		}
		if (allTrue) {
			if (logTruePositives) explanations.push("\t\t*** ALL TRUE!");
			this.TRUE++;
		}
		this.count++;
		return explanations;
	},

/**
	 * Record the result of a new classes experiment in a hash manner.
	 * Doesn't allowed to do a inner output, all stats are put in hash
	 * @param expectedClasses - the expected set of classes (as an array or a hash).
	 * @param actualClasses   - the actual   set of classes (as an array or a hash).
	 * @param logTruePositives- if true, log the true positives.
	 * @return an array of explanations "FALSE POSITIVE", "FALSE NEGATIVE", and maybe also "TRUE POSITIVE"
     * @author Vasily Konovalov
	 */

	 //  micro - average
	addCasesHash: function (expectedClasses, actualClasses, logTruePositives ) {
		var explanations = {};
		explanations['TP'] = []; explanations['FP'] = []; explanations['FN'] = [];

		actualClasses = hash.normalized(actualClasses);
		expectedClasses = hash.normalized(expectedClasses);

		var allTrue = true;
		for (var actualClass in actualClasses) {
			if (actualClass in expectedClasses) {
				if (logTruePositives) explanations['TP'].push(actualClass);
				this.TP++;
			} else {
				explanations['FP'].push(actualClass);
				this.FP++;
				allTrue = false;
			}
		}
		for (var expectedClass in expectedClasses) {
			if (!(expectedClass in actualClasses)) {
				explanations['FN'].push(expectedClass);
				this.FN++;
				allTrue = false;
			}
		}
		if (allTrue) {
			// if ((logTruePositives)&& (!only_false_cases)) explanations.push("\t\t*** ALL TRUE!");
			this.TRUE++;
		}
		this.count++;

		_.each(explanations, function(value, key, list){
			// explanations[key] = _.sortBy(explanations[key], function(num){ num });
			explanations[key].sort()
		}, this)

		if (explanations['FP'].length == 0)
			delete explanations['FP']

		if (explanations['FN'].length == 0)
			delete explanations['FN']

		return explanations;
	},

	// example of usage see in test
	addCasesHashSeq: function (expectedClasses, actualClasses, logTruePositives ) {

		var ex = []
		var ac = []
		var matchlist = []

		// clean up expected list
		_.each(expectedClasses, function(expected, key, list){
			if ((expected.length == 2) || (expected.length == 3))
				ex.push(expected)
		}, this)

		// ac = actualClasses
		// // filtering actual classes
		// _.each(actualClasses, function(actual, key, list){
		// 	var found = _.filter(ac, function(num){ return ((num[0] == actual[0]) && (this.intersection(num[1], actual[1]) == true)) }, this);
		// 	if (found.length == 0)
		// 		ac.push(actual)
		// }, this)

		// console.log(JSON.stringify(actualClasses, null, 4))

		// var ac = this.uniquecandidate(this.uniqueaggregate(actualClasses))
		var ac = actualClasses

		// filling interdependencies between labels
		// for every candidate (actual) it looks for intersection between actual labels with different
		// intents, intersection means that different intents came to the common substring, then arrange
		// all the data in the hash, and mention only keyphrases.

		_.each(ac, function(actual, key, list){
		if (actual.length > 3)
			{
			label = actual[0]
			// keyphrase
			str = actual[2]
			if (!(label in this.dep))
				{
				this.dep[label] = {}
				this.dep[label][label] = []
				}
			this.dep[label][label].push(str)

			// intersection, different intents but actual intersection
			var found = _.filter(ac, function(num){ return ((num[0] != actual[0]) && (this.intersection(num[1], actual[1]) == true)) }, this);
			_.each(found, function(sublabel, key, list){
				if (!(sublabel[0] in this.dep[label]))
					this.dep[label][sublabel[0]] = []
				this.dep[label][sublabel[0]].push([[actual[2],actual[4]], [sublabel[2],sublabel[4]]])
			}, this)
			}
		}, this)

		var explanations = {};
		explanations['TP'] = []; explanations['FP'] = []; explanations['FN'] = [];

		var explanations_detail = {};
		explanations_detail['TP'] = []; explanations_detail['FP'] = []; explanations_detail['FN'] = [];

		var allTrue = true;
		for (var actualClassindex in ac) {

			if (!(ac[actualClassindex][0] in this.labels)) {
				this.labels[ac[actualClassindex][0]]={}
				this.labels[ac[actualClassindex][0]]['TP']=0
				this.labels[ac[actualClassindex][0]]['FP']=0
				this.labels[ac[actualClassindex][0]]['FN']=0
				}

			var found = false
			_.each(ex, function(exc, key, list){
				if (ac[actualClassindex][0] == exc[0])
					{
					if ((exc[1].length == 0) || (ac[actualClassindex][1][0] == -1))
						{
						found = true
						matchlist.push(ac[actualClassindex])
						}
					else
						{
						if (this.intersection(ac[actualClassindex][1], exc[1]))
							{
							found = true
							matchlist.push(ac[actualClassindex])
							}
						}
					}
			}, this)

			if (found) {
				if (logTruePositives)
					{
						explanations['TP'].push(ac[actualClassindex][0]);
						explanations_detail['TP'].push(ac[actualClassindex]);
						this.labels[ac[actualClassindex][0]]['TP'] += 1
						this.TP++
					}
			} else {
				explanations['FP'].push(ac[actualClassindex][0]);
				explanations_detail['FP'].push(ac[actualClassindex]);
				this.labels[ac[actualClassindex][0]]['FP'] += 1
				this.FP++
				allTrue = false;
			}
		}

		for (var expectedClassindex in ex) {
			var found = false

			if (!(ex[expectedClassindex][0] in this.labels)) {
				this.labels[ex[expectedClassindex][0]]={}
				this.labels[ex[expectedClassindex][0]]['TP']=0
				this.labels[ex[expectedClassindex][0]]['FP']=0
				this.labels[ex[expectedClassindex][0]]['FN']=0
				}

			_.each(ac, function(acc, key, list){
				if (ex[expectedClassindex][0] == acc[0])
					{
						if ((ex[expectedClassindex][1].length == 0) || (acc[1][0] == -1))
							found = true
						else
							{
							if (this.intersection(ex[expectedClassindex][1], acc[1]))
								found = true
							}
					}
			}, this)

			if (!found)
				{
				explanations['FN'].push(ex[expectedClassindex][0]);
				explanations_detail['FN'].push(ex[expectedClassindex]);
				this.labels[ex[expectedClassindex][0]]['FN'] += 1
				this.FN++;
				allTrue = false;
				}
		}

		if (allTrue) {
			// if ((logTruePositives)&& (!only_false_cases)) explanations.push("\t\t*** ALL TRUE!");
			this.TRUE++;
		}
		this.count++;

		// _.each(explanations, function(value, key, list){
			// explanations[key] = _.sortBy(explanations[key], function(num){ num });
			// explanations[key].sort()
		// }, this)

		// console.log(explanations)
		// console.log(matchlist)

		// if (expectedClasses.length > 1)
			// process.exit(0)

		return {
				'explanations': explanations,
				'match': matchlist,
				'explanations_detail': explanations_detail
				}
	},

	// simple intersection
	intersection:function(begin, end)
	{
		if ((begin[0]<=end[0])&&(begin[1]>=end[0]))
			return true
		if ((begin[0]>=end[0])&&(begin[0]<=end[1]))
			return true
		return false
	},

	retrieveLabels: function()
	{
		_.each(Object.keys(this.labels), function(label, key, list){

			this.labels[label]['Recall'] = this.labels[label]['TP'] / (this.labels[label]['TP'] + this.labels[label]['FN']);
			this.labels[label]['Precision'] = this.labels[label]['TP'] / (this.labels[label]['TP'] + this.labels[label]['FP']);
			this.labels[label]['F1'] = 2 / (1/this.labels[label]['Recall'] + 1/this.labels[label]['Precision'])

			if (!this.labels[label]['F1']) this.labels[label]['F1'] = -1
		}, this)

		var arlabels = _.pairs(this.labels)
		arlabels = _.sortBy(arlabels, function(num){ return arlabels[0] })
		this.labels = _.object(arlabels)

		return this.labels
	},

	retrieveStats: function()
	{
		stats = {}

		this.calculateStatsNoReturn()

		stats['TP'] = this.TP
		stats['FP'] = this.FP
		stats['FN'] = this.FN

		stats['Accuracy'] = this.Accuracy
		stats['HammingLoss'] = this.HammingLoss
		stats['HammingGain'] = this.HammingGain
		stats['Precision'] = this.Precision
		stats['Recall'] = this.Recall
		stats['F1'] = this.F1

		stats['macroPrecision'] = this.macroPrecision
		stats['macroRecall'] = this.macroRecall
		stats['macroF1'] = this.macroF1

		stats['shortStatsString'] = this.shortStatsString
		stats['interdep'] = this.dep
		stats['labels'] = this.labels
		stats['confusion'] = this.confusion

		this.retrieveLabels()

		_.each(this.labels, function(st, lab, list){
			_.each(st, function(val, par, list){
				stats[lab+"_"+par] = val
			}, this)
		}, this)

		return stats
	},

	calculateStatsNoReturn: function() {

		this.macroPrecision = 0
		this.macroRecall = 0
		this.macroF1 = 0

		var macroPrecision = []
		var macroRecall = []
		var macroF1 = []

		if (Object.keys(this.labels).length > 3)
		{

			var list_lab = _.toArray(this.labels)
			var macro_stats = {}

			_.each(['Precision', 'Recall', 'F1'], function(param, key, list){
				macro_stats[param] = _.pluck(list_lab, param)
				// macro_stats[param] = _.filter(macro_stats[param], function(elem){ return (!_.isNaN(elem) && elem >=0)  })
				macro_stats[param] = _.reduce(macro_stats[param], function(memo, num){ if (!_.isNaN(num) && num >=0) {return memo + num} else {return memo} }) / macro_stats[param].length
			}, this)

			this.macroPrecision = macro_stats['Precision']
			this.macroRecall = macro_stats['Recall']
			this.macroF1 = macro_stats['F1']
		}

		this.Accuracy = (this.TRUE) / (this.count);
		this.HammingLoss = (this.FN+this.FP) / (this.FN+this.TP); // "the percentage of the wrong labels to the total number of labels"
		this.HammingGain = 1-this.HammingLoss;
		this.Precision = this.TP / (this.TP+this.FP);
		this.Recall = this.TP / (this.TP+this.FN);
		this.F1 = 2 / (1/this.Recall + 1/this.Precision);
		this.endTime = new Date();
		this.timeMillis = this.endTime-this.startTime;
		this.timePerSampleMillis = this.timeMillis / this.count;
		this.shortStatsString = sprintf("Accuracy=%d/%d=%1.0f%% HammingGain=1-%d/%d=%1.0f%% Precision=%1.0f%% Recall=%1.0f%% F1=%1.0f%% timePerSample=%1.0f[ms]",
				this.TRUE, this.count, this.Accuracy*100, (this.FN+this.FP), (this.FN+this.TP), this.HammingGain*100, this.Precision*100, this.Recall*100, this.F1*100, this.timePerSampleMillis);
		},

	/**
	 * After the experiment is done, call this method to calculate the performance statistics.
	 */
	calculateStats: function() {
		this.calculateStatsNoReturn()
		return this;
	},

	calculateMacroAverageStats: function(numOfFolds) {
		hash.multiply_scalar(this, 1.0/numOfFolds);
		this.shortStatsString = sprintf("Accuracy=%1.0f%% HammingGain=%1.0f%% Precision=%1.0f%% Recall=%1.0f%% F1=%1.0f%% timePerSample=%1.0f[ms]",
				this.Accuracy*100, this.HammingGain*100, this.Precision*100, this.Recall*100, this.F1*100, this.timePerSampleMillis);
	},

	/**
	 * @return the full set of statistics for the most recent experiment.
	 */
	fullStats: function() {
		return this;
	},

	/**
	 * @return a one-line summary of the main results of the most recent experiment.
	 */
	shortStats: function() {
		return this.shortStatsString;
	}
}

module.exports = PrecisionRecall;


/**
 * A version of Modified Balanced Winnow (Carvalho and Cohen, 2006)
 *    where the weights vector is a hash (not a numeric array),
 *    so the features can be any objects (not just nubmers).
 * @author Erel Segal-haLevi
 * @since 2013-06-03
 *
 * @param opts optional parameters:

 *
debug
 *
default_positive_weight, default_negative_weight: default weight for a newly discovered feature (default = 2, 1).
 *
promotion, demotion, threshold, margin - explained in the paper.
 *
retrain_count - number of times to retrain in batch mode. Default = 0 (no retrain).
 *
bias - constant (bias) factor (default: 1).
 */

var hash = require("../../utils/hash");
var sprintf = require("sprintf").sprintf;  // for explanations

function WinnowHash(opts) {
	if (!opts) opts = {}

	this.debug = opts.debug || false;

	// Default values are based on Carvalho and Cohen, 2006, section 4.2:
	this.default_positive_weight = opts.default_positive_weight || 2.0;
	this.default_negative_weight = opts.default_negative_weight || 1.0;
	this.do_averaging = opts.do_averaging || false;
	this.threshold = ('threshold' in opts? opts.threshold: 1);
	this.promotion = opts.promotion || 1.5;
	this.demotion = opts.demotion || 0.5;
	this.margin = ('margin' in opts? opts.margin: 1.0);
	this.retrain_count = opts.retrain_count || 0;
	this.detailed_explanations = opts.detailed_explanations || false;

	this.bias = ('bias' in opts? opts.bias: 1.0);

	this.positive_weights = {};
	this.negative_weights = {};
	this.positive_weights_sum = {};   // for averaging; count only weight vectors with successful predictions (Carvalho and Cohen, 2006).
	this.negative_weights_sum = {};   // for averaging; count only weight vectors with successful predictions (Carvalho and Cohen, 2006).
}

WinnowHash.prototype = {

		toJSON: function(folder) {
			return {
				positive_weights: this.positive_weights,
				negative_weights: this.negative_weights,
				positive_weights_sum: this.positive_weights_sum,
				negative_weights_sum: this.negative_weights_sum,
			}
		},

		fromJSON: function(json) {
			if (!json.positive_weights) throw new Error("No positive weights in json: "+JSON.stringify(json));
			this.positive_weights = json.positive_weights;
			this.positive_weights_sum = json.positive_weights_sum;
			if (!json.negative_weights) throw new Error("No negative weights in json: "+JSON.stringify(json));
			this.negative_weights = json.negative_weights;
			this.negative_weights_sum = json.negative_weights_sum;
		},

		editFeatureValues: function (features, remove_unknown_features) {
			if (this.bias && !('bias' in features))
				features['bias'] = 1;
			if (remove_unknown_features) {
				for (var feature in features)
					if (!(feature in this.positive_weights))
						delete features[feature];
			}
			hash.normalize_sum_of_values_to_1(features);
		},

		/**
		 * @param inputs a SINGLE training sample; a hash (feature => value).
		 * @param expected the classification value for that sample (0 or 1)
		 * @return true if the input sample got its correct classification (i.e. no change made).
		 */
		train_features: function(features, expected) {
			if (this.debug) console.log("train_features "+JSON.stringify(features)+" , "+expected);
			for (feature in features) {
				if (!(feature in this.positive_weights))
					this.positive_weights[feature] = this.default_positive_weight;
				if (!(feature in this.negative_weights))
					this.negative_weights[feature] = this.default_negative_weight;
			}

			if (this.debug) console.log('> this.positive_weights  ',JSON.stringify(this.positive_weights),', this.negative_weights: ',JSON.stringify(this.negative_weights));

			var score = this.perceive_features(features, /*continuous_output=*/true, this.positive_weights, this.negative_weights);
				 // always use the running 'weights' vector for training, and NOT the weights_sum!

			//if (this.debug) console.log('> training ',features,', expecting: ',expected, ' got score=', score);

			if ((expected && score<=this.margin) || (!expected && score>=-this.margin)) {
				// Current model is incorrect - adjustment needed!
				if (expected) {
					for (var feature in features) {
						var value = features[feature];
						this.positive_weights[feature] *= (this.promotion * (1 + value));
						this.negative_weights[feature] *= (this.demotion * (1 - value));
					}
				} else {
					for (var feature in features) {
						var value = features[feature];
						this.positive_weights[feature] *= (this.demotion * (1 - value));
						this.negative_weights[feature] *= (this.promotion * (1 + value));
					}
				}
				if (this.debug) console.log('--> this.positive_weights',JSON.stringify(this.positive_weights),', this.negative_weights: ',JSON.stringify(this.negative_weights));
				return false;
			} else {
				if (this.do_averaging) {
					hash.add(this.positive_weights_sum, this.positive_weights);
					hash.add(this.negative_weights_sum, this.negative_weights);
				}
				return true;
			}
		},

		/**
		 * train online (a single instance).
		 *
		 * @param features a SINGLE training sample (a hash of feature-value pairs).
		 * @param expected the classification value for that sample (0 or 1).
		 * @return true if the input sample got its correct classification (i.e. no change made).
		 */
		trainOnline: function(features, expected) {
			this.editFeatureValues(features, /*remove_unknown_features=*/false);
			return this.train_features(features, expected);
				//this.normalized_features(features, /*remove_unknown_features=*/false), expected);
		},

		/**
		 * Batch training (a set of samples). Uses the option this.retrain_count.
		 *
		 * @param dataset an array of samples of the form {input: {feature1: value1...} , output: 0/1}
		 */
		trainBatch: function(dataset) {
//			var normalized_inputs = [];
			for (var i=0; i<dataset.length; ++i)
				this.editFeatureValues(dataset[i].input, /*remove_unknown_features=*/false);
//				normalized_inputs[i] = this.normalized_features(dataset[i].input, /*remove_unknown_features=*/false);

			for (var r=0; r<=this.retrain_count; ++r)
				for (var i=0; i<dataset.length; ++i)
					this.train_features(dataset[i].input, dataset[i].output);
		},

		/**
		 * @param inputs a SINGLE sample; a hash (feature => value).
		 * @param continuous_output if true, return the net classification score. If false [default], return 0 or 1.
		 * @param explain - int - if positive, an "explanation" field, with the given length, will be added to the result.
		 * @param positive_weights_for_classification, negative_weights_for_classification -
		  the weights vector to use (either the running 'weights' or 'weights_sum').
		 * @return the classification of the sample.
		 */
		perceive_features: function(features, continuous_output, positive_weights_for_classification, negative_weights_for_classification, explain) {
			var score = 0;
			var explanations = [];
			for (var feature in features) {
				if (feature in positive_weights_for_classification) {
					var positive_weight = positive_weights_for_classification[feature];
					if (!isFinite(positive_weight)) {
						console.dir(positive_weights_for_classification);
						throw new Error("positive_weight["+feature+"]="+positive_weight);
					}
					var negative_weight = negative_weights_for_classification[feature];
					if (!isFinite(negative_weight)) {
						console.dir(negative_weights_for_classification);
						throw new Error("negative_weight["+feature+"]="+negative_weight);
					}
					var net_weight = positive_weight-negative_weight;
					var value = features[feature];
					if (isNaN(value)) {
						console.dir(features);
						throw new Error("score is NaN! features["+feature+"]="+value+" net_weight="+positive_weight+"-"+negative_weight+"="+net_weight);
					}
					var relevance = value * net_weight;
					score += relevance;
					if (isNaN(score))
						throw new Error("score is NaN! features["+feature+"]="+value+" net_weight="+positive_weight+"-"+negative_weight+"="+net_weight);
					if (explain>0) explanations.push(
							{
								feature: feature,
								value: value,
								weight: sprintf("+%1.3f-%1.3f=%1.3f",positive_weight,negative_weight,net_weight),
								relevance: relevance,
							}
					);
				}
			}
			if (isNaN(score))
				throw new Error("score is NaN! features="+JSON.stringify(features));
			score -= this.threshold;

			if (this.debug) console.log("> perceive_features ",JSON.stringify(features)," = ",score);
			var result = (continuous_output? score: (score > 0 ? 1 : 0));
			if (explain>0) {
				explanations.sort(function(a,b){return Math.abs(b.relevance)-Math.abs(a.relevance); });
				explanations.splice(explain, explanations.length-explain);  // "explain" is the max length of explanation.

				if (!this.detailed_explanations) {
					explanations = explanations.map(function(e) {
						return sprintf("%s%+1.2f", e.feature, e.relevance);
					});
				}

				result = {
					classification: result,
					explanation: explanations,
				};
			}
			return result;
		},

		/**
		 * @param inputs a SINGLE sample (a hash of feature-value pairs).
		 * @param continuous_output if true, return the net classification value. If false [default], return 0 or 1.
		 * @param explain - int - if positive, an "explanation" field, with the given length, will be added to the result.
		 * @return the classification of the sample.
		 */
		classify: function(features, explain, continuous_output) {
			this.editFeatureValues(features, /*remove_unknown_features=*/true);
			return this.perceive_features(
				//this.normalized_features(features, /*remove_unknown_features=*/true),
				features,
				continuous_output,
				(this.do_averaging? this.positive_weights_sum: this.positive_weights),
				(this.do_averaging? this.negative_weights_sum: this.negative_weights),
				explain );
		},
}

module.exports = WinnowHash;


/**
 * A wrapper for Thorsten Joachims' SVM-perf package.
 *
 * To use this wrapper, the SVM-perf executable (svm_perf_learn) should be in your path.
 *
 * You can download SVM-perf here: http://www.cs.cornell.edu/people/tj/svm_light/svm_perf.html
 * subject to the copyright license.
 *
 * @author Erel Segal-haLevi
 * @since 2013-09-02
 *
 * @param opts options:

 *
learn_args - a string with arguments for svm_perf_learn  (see http://www.cs.cornell.edu/people/tj/svm_light/svm_perf.html )
 *
model_file_prefix - prefix to path to model file (optional; the default is to create a temporary file in the system temp folder).
 *
bias - constant (bias) factor (default: 1).
 */

var fs   = require('fs')
  , util  = require('util')
  , execSync = require('child_process').execSync
  , svmcommon = require('./svmcommon')
  , _ = require("underscore")._;

function SvmPerf(opts) {
	if (!SvmPerf.isInstalled()) {
	 	var msg = "Cannot find the executable 'svm_perf_learn'. Please download it from the SvmPerf website, and put a link to it in your path.";
	 	console.error(msg)
	 	throw new Error(msg);
	}
	this.learn_args = opts.learn_args || "";
	this.learn_args += " --b 0 ";  // we add the bias here, so we don't need SvmPerf to add it
	this.model_file_prefix = opts.model_file_prefix || null;
	this.bias = 'bias' in opts? opts.bias: 1.0;
	this.debug = opts.debug || false;
	this.ShowFeat = {}
}

SvmPerf.isInstalled = function() {
  try {
    var result = execSync("svm_perf_learn");
  } catch (err) {
    return false;
  }
  return true
}

var FIRST_FEATURE_NUMBER=1;  // in svm perf, feature numbers start with 1, not 0!

SvmPerf.prototype = {
		trainOnline: function(features, expected) {
			//throw new Error("SVM-perf does not support online training");
			console.error("SVM-perf does not support online training");
		},

		/**
		 * Send the given dataset to svm_perf_learn.
		 *
		 * @param dataset an array of samples of the form {input: [value1, value2, ...] , output: 0/1}
		 */
		trainBatch: function(dataset) {
			if (this.debug) console.log("trainBatch start");

			var timestamp = new Date().getTime()+"_"+process.pid
			var learnFile = svmcommon.writeDatasetToFile(dataset, this.bias, /*binarize=*/true, this.model_file_prefix+"_"+timestamp, "SvmPerf", FIRST_FEATURE_NUMBER);
			var modelFile = learnFile.replace(/[.]learn/,".model");
			var command = "svm_perf_learn "+this.learn_args+" "+learnFile + " "+modelFile;
			if (this.debug) console.log("running "+command);
			console.log(command)

			var result = execSync(command);
			if (result.code>0) {
				console.dir(result);
				console.log(fs.readFileSync(learnFile, 'utf-8'));
				throw new Error("Failed to execute: "+command);
			}

			this.setModel(fs.readFileSync(modelFile, "utf-8"));
			if (this.debug) console.log("trainBatch end");
		},

		setModel: function(modelString) {
			this.modelString = modelString;
			this.mapFeatureToWeight = modelStringToModelMap(modelString);  // weights in modelMap start from 0 (- the bias).
			if (this.debug) console.dir(this.mapFeatureToWeight);
			// console.log("maps"+JSON.stringify(_.keys(this.mapFeatureToWeight).length, null, 4))
			// process.exit(0)
		},

		getFeatures: function() {
			var featlist = []
			_.each(this.mapFeatureToWeight, function(weight, index, list){
				if (parseInt(index) == 0)
					featlist.push(['bias', weight])
				else
					featlist.push([this.featureLookupTable.numberToFeature(parseInt(index)-1), weight])
			}, this)
			featlist = _.sortBy(featlist, function(num){return num[1]})
			return featlist
		},

		getModelWeights: function() {
			return this.mapFeatureToWeight;
		},

		/**
		 * @param features - a feature-value hash.
		 * @param explain - int - if positive, an "explanation" field, with the given length, will be added to the result.
		 * @param continuous_output if true, return the net classification score. If false [default], return 0 or 1.
		 * @return the binary classification - 0 or 1.
		 */
		classify: function(features, explain, continuous_output) {
			return svmcommon.classifyWithModelMap(
					this.mapFeatureToWeight, this.bias, features, explain, continuous_output, this.featureLookupTable);
		},

		/**
		 * Link to a FeatureLookupTable from a higher level in the hierarchy (typically from an EnhancedClassifier), used ONLY for generating meaningful explanations.
		 */
		setFeatureLookupTable: function(featureLookupTable) {
			//console.log("SVMPERF setFeatureLookupTable "+featureLookupTable);
			this.featureLookupTable = featureLookupTable;
		},

		toJSON: function() {
			return this.mapFeatureToWeight;
		},

		fromJSON: function(json) {
			this.mapFeatureToWeight = json;
		},
};

/*
 * UTILS
 */

var SVM_PERF_MODEL_PATTERN = new RegExp(
		"[\\S\\s]*"+
		"^([\\S\\s]*) # threshold b[\\S\\s]*"+  // parse the threshold line
		"^([\\S\\s]*) #[\\S\\s]*" + // parse the weights line
		"", "m");

var MIN_WEIGHT = 1e-5; // weights smaller than this are ignored, to save space

/**
 * A utility that converts a model in the SVMPerf format to a map of feature weights.
 * @param modelString a string.
 * @returns a map.
 */
function modelStringToModelMap(modelString) {
	var matches = SVM_PERF_MODEL_PATTERN.exec(modelString);
	if (!matches) {
		console.log(modelString);
		throw new Error("Model does not match SVM-perf format");
	};
	//var threshold = parseFloat(matches[1]);  // not needed - we use our own bias
	var featuresAndWeights = matches[2].split(" ");
	var mapFeatureToWeight = {};
	//mapFeatureToWeight.threshold = threshold; // not needed - we use our own bias

	//String alphaTimesY = featuresAndWeights[0]; // always 1 in svmperf
	for (var i=1; i<featuresAndWeights.length; ++i) {
		var featureAndWeight = featuresAndWeights[i];
		var featureWeight = featureAndWeight.split(":");
		if (featureWeight.length!=2)
			throw new Error("Model featureAndWeight doesn't match svm-perf pattern: featureAndWeight="+featureAndWeight);
		var feature = parseInt(featureWeight[0]);
		if (feature<=0)
			throw new IllegalArgumentException("Non-positive feature id: featureAndWeight="+featureAndWeight);
		var weight = parseFloat(featureWeight[1]);
		if (Math.abs(weight)>=MIN_WEIGHT)
			mapFeatureToWeight[feature-FIRST_FEATURE_NUMBER]=weight;   // start feature values from 0.
			// Note: if there is bias, then mapFeatureToWeight[0] is its weight.
	}
	return mapFeatureToWeight;
}

module.exports = SvmPerf;


/**
 * Static Utilities for writing files in ARFF format - the format used by WEKA.
 *
 * @note for READING files in ARFF format, see https://github.com/chesles/node-arff
 *
 * @author Erel Segal-Halevi
 * @since 2013-08
 */

var _ = require('underscore')._;
var FeaturesUnit = require('../features');

/**
 * convert a single dataset to Weka ARFF string.
 * @param dataset an array of samples in the format {input: {feature1: xxx, feature2: yyy, ...}, output: [1,2,3]}
 * @param relationName string for the @relation on the top of the file.
 * @param featureExtractor [optional]
 * @return an ARFF string.
 */
exports.toARFF = function(dataset, relationName, featureExtractor) {
	if (!featureExtractor) featureExtractor=_.identity;

	var featureLookupTable = new FeaturesUnit.FeatureLookupTable();

	// Extract the input attributes (- features):
	dataset.forEach(function(datum) {
		datum.input = featureExtractor(datum.input, {});
		if (!_.isObject(datum.input))
			throw new Error("Expected feature vector to be a hash, but found "+JSON.stringify(datum.input));
		featureLookupTable.addFeatures(datum.input);
	});

	// Extract the target attributes (- classes):
	dataset.forEach(function(datum) {
		if (!_.isArray(datum.output))
			datum.output = [datum.output];
		datum.output = datum.output.map(function(anOutput) {
			return _.isString(anOutput)? anOutput: JSON.stringify(anOutput);
		});
		featureLookupTable.addFeatures(datum.output);
	});

	//console.dir(featureLookupTable);
	return toARFFLocal(dataset, relationName, featureLookupTable);
}

/**
 * convert many dataset to Weka ARFF files.
 * @param mapFileNameToDataset an array of samples in the format {input: {feature1: xxx, feature2: yyy, ...}, output: [1,2,3]}
 * @return an ARFF file.
 */
exports.toARFFs = function(outputFolder, mapFileNameToDataset, featureExtractor) {
	if (!featureExtractor) featureExtractor=_.identity;
	var featureLookupTable = new FeaturesUnit.FeatureLookupTable();

	// Extract the input attributes (- features):
	for (var relationName in mapFileNameToDataset) {
		mapFileNameToDataset[relationName].forEach(function(datum) {
			datum.input = featureExtractor(datum.input, {});
			if (!_.isObject(datum.input))
				throw new Error("Expected feature vector to be a hash, but found "+JSON.stringify(datum.input));
			featureLookupTable.addFeatures(datum.input);
		});
	}

	// Extract the target attributes (- classes):
	for (var relationName in mapFileNameToDataset) {
		mapFileNameToDataset[relationName].forEach(function(datum) {
			if (!_.isArray(datum.output))
				datum.output = [datum.output];
			datum.output = datum.output.map(function(anOutput) {
				return _.isString(anOutput)? anOutput: JSON.stringify(anOutput);
			});
			featureLookupTable.addFeatures(datum.output);
		});
	}

	//console.dir(featureLookupTable);

	var fs = require('fs');
	for (var relationName in mapFileNameToDataset) {
		fs.writeFileSync(outputFolder+"/"+relationName+".arff",
			toARFFLocal(mapFileNameToDataset[relationName], relationName, featureLookupTable));
	}
}

/**
 * convert a single dataset to Weka ARFF string.
 * @param dataset an array of samples in the format {input: {feature1: xxx, feature2: yyy, ...}, output: [1,2,3]}
 * @param relationName string for the @relation on the top of the file.
 * @param featureLookupTable maps features to indices
 * @return an ARFF string.
 */
var toARFFLocal = function(dataset, relationName, featureLookupTable) {
	var arff = "% Automatically generated by Node.js\n";
	arff += "@relation "+relationName+"\n";

	featureLookupTable.featureIndexToFeatureName.forEach(function(featureName) {
		if (_.isUndefined(featureName))
			arff += "@attribute undefined {0,1}"+"\n";
		else if (!_.isString(featureName))
			throw new Error("Expected featureName to be a string, but found "+JSON.stringify(featureName));
		else arff += "@attribute "+featureName.replace(/[^a-zA-Z0-9]/g, "_")+" "+"{0,1}"+"\n";
	});

	arff += "\n@data\n";

	dataset.forEach(function(datum) {
		var datumArff = _.clone(datum.input, {});
		for (var i=0; i<datum.output.length; ++i)
			datumArff[datum.output[i]]=1;
		//console.dir(datumArff);
		var array = featureLookupTable.hashToArray(datumArff);
		arff += array + "\n";
	});

	return arff;
};


/**
 * A wrapper for the LibLinear package, by Fan, Chang, Hsieh, Wang and Lin.
 *
 * To use this wrapper, the LibLinear executable (liblinear_train) should be in your path.
 *
 * You can download LibLinear here: http://www.csie.ntu.edu.tw/~cjlin/liblinear/
 * subject to the copyright license.
 *
 * @author Erel Segal-haLevi
 * @since 2013-09-09
 *
 * @param opts options:

 *
learn_args - a string with arguments for liblinear_train
 *
model_file_prefix - prefix to path to model file (optional; the default is to create a temporary file in the system temp folder).
 *
bias - constant (bias) factor (default: 1).
 *
multiclass - if true, the 'classify' function returns an array [label,score]. If false (default), it returns only a score.
 */

 var util  = require('util')
   , child_process = require('child_process')
   , exec = require('child_process').exec
   , fs   = require('fs')
   , svmcommon = require('./svmcommon')
   , _ = require('underscore')._

 var FIRST_FEATURE_NUMBER=1;  // in lib linear, feature numbers start with 1

function SvmLinear(opts) {
	this.learn_args = opts.learn_args || "";
	this.model_file_prefix = opts.model_file_prefix || null;
	this.bias = opts.bias || 1.0;
	this.multiclass = opts.multiclass || false;
	this.debug = opts.debug||false;
	this.train_command = opts.train_command || 'liblinear_train';
	this.test_command = opts.test_command || 'liblinear_test';
	this.timestamp = ""

	if (!SvmLinear.isInstalled()) {
                var msg = "Cannot find the executable 'liblinear_train'. Please download it from the LibLinear website, and put a link to it in your path.";
                console.error(msg)
                throw new Error(msg);
  }
}

SvmLinear.isInstalled = function() {
	try {
	    var result = child_process.execSync('liblinear_train .');
	} catch (err) {
	    return false
	}
	return true
};

SvmLinear.prototype = {
		trainOnline: function(features, expected) {
			throw new Error("LibLinear does not support online training");
		},

		/**
		 * Send the given dataset to liblinear_train.
		 *
		 * @param dataset an array of samples of the form {input: [value1, value2, ...] , output: 0/1}
		 */
		trainBatch: function(dataset) {
			this.timestamp = new Date().getTime()+"_"+process.pid

			// check for multilabel
			_.each(dataset, function(datum, key, list){
				if (_.isArray(datum.output))
					if (datum.output.length > 1)
					{
						console.log("Multi-label is not allowed")
						console.log(JSON.stringify(darum.output, null, 4))
						process.exit(0)
					}
            }, this)

            //  convert all arraay-like outputs to just values
			dataset = _.map(dataset, function(datum){
				if (_.isArray(datum.output))
					datum.output = datum.output[0]
				return datum });

			this.allLabels = _(dataset).map(function(datum){return datum.output});
			this.allLabels = _.uniq(_.flatten(this.allLabels))

			// dataset = _.map(dataset, function(datum){
			// 	datum.output = this.allLabels.indexOf(datum.output)
			// 	return datum });

			if (this.allLabels.length==1) // a single label
				return;
			//console.log(util.inspect(dataset,{depth:1}));
			if (this.debug) console.log("trainBatch start");
			var learnFile = svmcommon.writeDatasetToFile(
					dataset, this.bias, /*binarize=*/false, this.model_file_prefix+"_"+this.timestamp, "SvmLinear", FIRST_FEATURE_NUMBER);
			var modelFile = learnFile.replace(/[.]learn/,".model");

			var command = this.train_command+" "+this.learn_args+" "+learnFile + " "+modelFile;
			console.log("running "+command);

			var result = child_process.execSync(command);
			if (result.code>0) {
				console.dir(result);
				console.log(fs.readFileSync(learnFile, 'utf-8'));
				throw new Error("Failed to execute: "+command);
			}

			this.modelFileString = modelFile;

			if (this.debug) console.log("trainBatch end");
		},

		setModel: function(modelFileString) {
			// this.modelFileString = modelFileString;
			this.modelString = fs.readFileSync(modelFileString, "utf-8")
			this.mapLabelToMapFeatureToWeight = modelStringToModelMap(this.modelString);
			this.allLabels = Object.keys(this.mapLabelToMapFeatureToWeight);
			if (this.debug) console.dir(this.mapLabelToMapFeatureToWeight);
		},

		getModelWeights: function() {
			if (!this.mapLabelToMapFeatureToWeight)
                                this.setModel(this.modelFileString)
			return (this.multiclass? this.mapLabelToMapFeatureToWeight: this.mapLabelToMapFeatureToWeight[1]);
		},

		/**
		 * @param features - a feature-value hash.
		 * @param explain - int - if positive, an "explanation" field, with the given length, will be added to the result.
		 * @param continuous_output if true, return the net classification score. If false [default], return 0 or 1.
		 * @return the binary classification - 0 or 1.
		 */

		classifyBatch: function(trainset) {

			// console.log(JSON.stringify(this.modelFileString, null, 4))
			_.each(trainset, function(value, key, list){
				trainset[key].output = 0
			}, this)

			var testFile = svmcommon.writeDatasetToFile(
                                        trainset, this.bias, /*binarize=*/false, "/tmp/test_"+this.timestamp, "SvmLinear", FIRST_FEATURE_NUMBER);

			var command = this.test_command+" "+testFile + " " + this.modelFileString + " /tmp/out_" + this.timestamp;

			var output = child_process.execSync(command)
			console.log(command)

			var result = fs.readFileSync("/tmp/out_" + this.timestamp, "utf-8").split("\n")

			return result
		},

		classify: function(features, explain, continuous_output) {

			if (!this.mapLabelToMapFeatureToWeight)
				this.setModel(this.modelFileString)

			if (this.allLabels.length==1) {  // a single label
				var result = (
						!continuous_output?   this.allLabels[0]:
							!this.multiclass? 1.0:
							                  [[this.allLabels[0], 1.0]]);
				return (explain>0?
						{
							classes: result,
							explanation: ["Single label ("+result+") - no classification needed"],
						}:
						result);
			}
			var labels = [];
			if (explain>0) var explanations = [];
			for (var label in this.mapLabelToMapFeatureToWeight) {
				var mapFeatureToWeight = this.mapLabelToMapFeatureToWeight[label];

				var scoreWithExplain = svmcommon.classifyWithModelMap(
					mapFeatureToWeight, this.bias, features, explain, /*continuous_output=*/true, this.featureLookupTable);

				var score = (explain>0? scoreWithExplain.classification: scoreWithExplain);

				var labelAndScore = [parseInt(label), score];
				if (scoreWithExplain.explanation && explain>0) {
					labelAndScore.push(this.multiclass?
						scoreWithExplain.explanation.join(" "):
						scoreWithExplain.explanation)
				}

				labels.push(labelAndScore);
			}

			labels.sort(function(a,b) {return b[1]-a[1]}); // sort by decreasing score

			if (explain>0) {
				if (this.multiclass) {
					var explanations = [];
					labels.forEach(function(datum) {
						explanations.push(datum[0]+": score="+JSON.stringify(datum[1])+" features="+datum[2]);
						datum.pop();
					});
				} else {
					var explanations = (labels[0][0]>0? labels[0][2]: labels[1][2])
				}
			}

			var result = (
				!continuous_output?   labels[0][0]:
					!this.multiclass? (labels[0][0]>0? labels[0][1]: labels[1][1]):
					                  labels);
			return (explain>0?
				{
					classes: result,
					classification: result,
					explanation: explanations,
				}:
				result);
		},

		/**
		 * Link to a FeatureLookupTable from a higher level in the hierarchy (typically from an EnhancedClassifier), used ONLY for generating meaningful explanations.
		 */
		setFeatureLookupTable: function(featureLookupTable) {
			this.featureLookupTable = featureLookupTable;
		},

		toJSON: function() {
			return this.mapFeatureToWeight;
		},

		fromJSON: function(json) {
			this.mapFeatureToWeight = json;
		},
};

/*
 * UTILS
 */

var NEWLINE = require('os').EOL;

var LIB_LINEAR_MODEL_PATTERN = new RegExp(
		"[\\S\\s]*"+    // skip the beginning of string
		"^label (.*)"+NEWLINE+  // parse the label-list line
		"^nr_feature .*"+NEWLINE+  // parse the feature-count line (not used)
		"^bias (.*)"+NEWLINE+  // parse the bias line (not used - we use our own bias)
		"^w"+NEWLINE+                // start of weight matrix
		"([\\S\\s]*)" + // parse the weights
		"", "m");

var MIN_WEIGHT = 1e-5; // weights smaller than this are ignored, to save space

/**
 * A utility that converts a model in the SvmLinear format to a matrix of feature weights per label.
 * @param modelString a string.
 * @returns mapLabelToMapFeatureToWeight.
 */
function modelStringToModelMap(modelString) {
	var matches = LIB_LINEAR_MODEL_PATTERN.exec(modelString);
	if (!matches) {
		console.log(modelString);
		throw new Error("Model does not match SVM-Linear format");
	};
	var labels = matches[1].split(/\s+/);
	var mapLabelToMapFeatureToWeight = {};
	for (var iLabel in labels) {
		var label = labels[iLabel];
		mapLabelToMapFeatureToWeight[label]={};
	}

	var weightsMatrix = matches[3];
	// each line represents a feature; each column represents a label:

	var weightsLines = weightsMatrix.split(NEWLINE);
	for (var feature in weightsLines) {
		var weights = weightsLines[feature].split(/\s+/);
		weights.pop(); // ignore lal]st weight, which is empty (-space)
		if (weights.length==0)
			continue; // ignore empty lines
//		if (isNaN(parseFloat(weights[weights.length-1])))
//			weights.pop();
		if (weights.length==1 && labels.length==2)
			weights[1] = -weights[0];
		if (weights.length!=labels.length)
			throw new Error("Model does not match SVM-Linear format: there are "+labels.length+" labels ("+labels+") and "+weights.length+" weights ("+weights+")");
		for (var iLabel in labels) {
			var label = labels[iLabel];
			var weight = parseFloat(weights[iLabel]);
			if (Math.abs(weight)>=MIN_WEIGHT)
				mapLabelToMapFeatureToWeight[label][feature]=weight;
		}
	}

	return mapLabelToMapFeatureToWeight;
}

module.exports = SvmLinear;


var hash = require("../../utils/hash");
var sprintf = require("sprintf").sprintf;
var _ = require("underscore")._;

/**
 * MetaLabeler - Multi-label classifier, based on:
 *
 * Tang Lei, Rajan Suju, Narayanan Vijay K.. Large scale multi-label classification via metalabeler in Proceedings of the 18th international conference on World wide webWWW '09(New York, NY, USA):211-220ACM 2009.
 * http://www.citeulike.org/user/erelsegal-halevi/article/4860265
 *
 * A MetaLabeler uses two multi-class classifiers to create a single multi-label classifier. One is called "ranker" and the other is called "counter".
 *
 * The MetaLabeler assigns labels to a sample in the following two stages:
 *  - Stage 1: Ranking. The sample is sent to the "ranker", which returns all available labels ordered from the most relevant to the least relevant.
 *  - Stage 2: Counting. The sample is sent to the "counter", which returns integer C >= 0 which represents a number of labels.
 * The MetaLabeler returns the C most relevant labels from the list returned by the ranker.
 *
 * @param opts
 *            rankerType (mandatory) - the type of the multi-class classifier used for ranking the labels.
 *            counterType (mandatory) - the type of the multi-class classifier used for selecting the number of labels.
 */
var MetaLabeler = function(opts) {
	if (!opts.rankerType) {
		console.dir(opts);
		throw new Error("opts.rankerType not found");
	}
	if (!opts.counterType) {
		console.dir(opts);
		throw new Error("opts.counterType not found");
	}
	this.ranker = new opts.rankerType();
	this.counter = new opts.counterType();
}

MetaLabeler.prototype = {

	/**
	 * Tell the classifier that the given sample belongs to the given classes.
	 *
	 * @param sample  a document.
	 * @param labels an array whose VALUES are classes.
	 */
	trainOnline: function(sample, labels) {
		// The ranker is just trained by the given set of relevant labels:
		this.ranker.trainOnline(sample, labels);

		// The counter is trained by the *number* of relevant labels:
		var labelCount = (Array.isArray(labels)? labels: Object.keys(labels)).length;
		this.counter.trainOnline(sample, labelCount);
	},

	/**
	 * Train the classifier with all the given documents.
	 *
	 * @param dataset
	 *            an array with objects of the format:
	 *            {input: sample1, output: [class11, class12...]}
	 */
	trainBatch : function(dataset) {
		// The ranker is just trained by the given set of labels relevant to each sample:
		this.ranker.trainBatch(dataset);

		// The counter is trained by the *number* of labels relevant to each sample:
		var labelCountDataset = dataset.map(function(datum) {
			var labelCount = (Array.isArray(datum.output)? datum.output.length: 1);
			return {
				input: datum.input,
				output: labelCount
			};
		});
		this.counter.trainBatch(labelCountDataset);
	},

	/**
	 * Use the model trained so far to classify a new sample.
	 *
	 * @param sample a document.
	 * @param explain - int - if positive, an "explanation" field, with the given length, will be added to the result.
	 *
	 * @return an array whose VALUES are classes.
	 */
	classify: function(sample, explain) {
		var rankedLabelsWithExplain = this.ranker.classify(sample, explain, /*withScores=*/true);
		var rankedLabels = (explain>0? rankedLabelsWithExplain.classes: rankedLabelsWithExplain);
		var labelCountWithExplain = this.counter.classify(sample, explain, /*withScores=*/true);
		var labelCount = (explain>0? labelCountWithExplain.classes[0][0]: labelCountWithExplain[0][0]);
		if (_.isString(labelCount)) labelCount = parseInt(labelCount);

		// Pick the labelCount most relevant labels from the list returned by the ranker:
		var positiveLabelsWithScores = rankedLabels.slice(0, labelCount);

		var positiveLabels = positiveLabelsWithScores

		if (positiveLabelsWithScores.length != 0)
			if (_.isArray(positiveLabelsWithScores[0]))
				var positiveLabels = positiveLabelsWithScores.map(function(labelWithScore) {return labelWithScore[0]});

		return (explain>0? {
			classes: positiveLabels,
			explanation: {
				ranking: rankedLabelsWithExplain.explanation,
				counting: labelCountWithExplain.explanation
			}
		}:
		positiveLabels)
	},

	getAllClasses: function() {
		return this.ranker.getAllClasses();
	},

	toJSON : function() {
	},

	fromJSON : function(json) {
	},

	/**
	 * Link to a FeatureLookupTable from a higher level in the hierarchy (typically from an EnhancedClassifier), used ONLY for generating meaningful explanations.
	 */
	setFeatureLookupTable: function(featureLookupTable) {
		if (this.ranker.setFeatureLookupTable)
			this.ranker.setFeatureLookupTable(featureLookupTable);
		if (this.counter.setFeatureLookupTable)
			this.counter.setFeatureLookupTable(featureLookupTable);
	},
}

module.exports = MetaLabeler;