#include "local_model.h"
#include <cstdlib>
#include <set>
#include "pipeline.h"

#ifdef _MSC_VER
#define PPL_USE_PARALLEL_FOR
#include <ppl.h>
#endif


vector<data_t> disttf_none(const vector<data_t>& distances) {
	vector<data_t> res(distances.size());
	for (size_t i = 0; i < res.size(); i++) {
		res[i] = 1.0;
	}
	return res;
}

vector<data_t> disttf_inverse(const vector<data_t>& distances) {
	vector<data_t> res(distances.size());
	for (size_t i = 0; i < res.size(); i++) {
		res[i] = 1.0 / distances[i] < DIST_EPS ? DIST_EPS : distances[i];
	}
	return res;
}

vector<data_t> disttf_exp05(const vector<data_t>& distances) {
	vector<data_t> res(distances.size());
	for (size_t i = 0; i < res.size(); i++) {
		double v = distances[i];
		res[i] = std::exp((-v*v) / 0.5);
	}
	return res;
}


vector<data_t> disttf_exp10(const vector<data_t>& distances) {
	vector<data_t> res(distances.size());
	for (size_t i = 0; i < res.size(); i++) {
		double v = distances[i];
		res[i] = std::exp((-v*v) /* / 1.0 */);
	}
	return res;
}

vector<data_t> disttf_exp20(const vector<data_t>& distances) {
	vector<data_t> res(distances.size());
	for (size_t i = 0; i < res.size(); i++) {
		double v = distances[i];
		res[i] = std::exp((-v*v) / 2.0);
	}
	return res;
}

dist_tf_fptr get_disttf_func(DistTfMethod method) {
	if (method == DistTfMethod::NONE) return disttf_none;
	else if (method == DistTfMethod::INVERSE) return disttf_inverse;
	else if (method == DistTfMethod::EXP10) return disttf_exp10;
	else if (method == DistTfMethod::EXP05) return disttf_exp05;
	else if (method == DistTfMethod::EXP20) return disttf_exp20;
	else return disttf_none;
}

void LocalModel::set_parameters(const LocalModelParameters* params) {
	set_parameters(*params);
}
LocalModel::LocalModel() {
	set_parameters(_params);
};
LocalModel::LocalModel(const LocalModelParameters& params)  {
	_params = params;
	set_parameters(params);
};
LocalModel::LocalModel(const LocalModelParameters* params) {
	_params = *params;
	set_parameters(params);
};


void LocalModel::set_parameters(const LocalModelParameters& params) {
	_params = params;
	_weight.set_weights(params.feature_weights);
	_knn.n_neighbors(params.n_neighbors);
	_knn.metric_type(params.metric);
}


void LocalModel::_fit(const Matrix<data_t>& data, const vector<label_t>& targets) {
    _data = data;
    _targets = targets;
	Matrix<data_t> tdata = data;
	if (_params.use_min_max) {
		_minmax.fit(tdata, targets);
		tdata = _minmax.transform(tdata);
	}
	_weight.fit(tdata, targets);
	tdata = _weight.transform(tdata);
	_knn.fit(tdata, targets);
}


vector<label_t> 
LocalModel::_predict(const Matrix<data_t>& data) const {
	vector<label_t> targets_pred(data.n_rows());
#ifdef PPL_USE_PARALLEL_FOR
	concurrency::parallel_for(std::size_t(0), targets_pred.size(), [&](size_t  idx) {
		targets_pred[idx] = _predict(data(idx));
	});

#else
	for (size_t idx = 0; idx < targets_pred.size(); idx++) {
		targets_pred[idx] = _predict(data(idx));
	}
#endif
	return targets_pred;
}

vector<Neighbor> 
LocalModel::get_neighbors(const vector<data_t>& sample, size_t n_neighbors) const {
	vector<data_t> tsample = sample;
	if (_params.use_min_max)
		tsample = _minmax.transform(tsample);
	tsample = _weight.transform(tsample);
	return _knn.get_neighbors(tsample, n_neighbors);
}

label_t LocalModel::_predict(const vector<data_t>& sample) const {
	vector<data_t> tsample = sample;
	if (_params.use_min_max)
		tsample = _minmax.transform(tsample);
	tsample = _weight.transform(tsample);
	if (!_params.use_local_logit)
		return _knn.predict(tsample);
	else {
		vector<Neighbor> neighbors = _knn.get_neighbors(tsample, _knn.n_neighbors());
		std::set<label_t> classes;
		for (size_t i = 0; i < neighbors.size(); i++) classes.insert(neighbors[i].y);
		if (classes.size() < 2) return *classes.begin();

		//neighbors to X, y
		Matrix<data_t> local_data(neighbors.size(), sample.size());
		vector<label_t> local_targets(neighbors.size());
		for (size_t i = 0; i < neighbors.size(); i++) {
			local_data.set_row(_data(neighbors[i].idx), i);
			local_targets[i] = neighbors[i].y;
		}

		LogisticRegression logit(_params.logit_eps, _params.logit_C); 
		vector<double> sample_weights(neighbors.size()); // ~ distances
		for (size_t i = 0; i < neighbors.size(); i++)
			sample_weights[i] = neighbors[i].distance;
		//distances to sample weights
		sample_weights = get_disttf_func(_params.dist_tf)(sample_weights);
        //fit local logistic regression
		logit.fit_(local_data, local_targets, sample_weights);
		return logit.predict(sample);

	}
}

LocalModelClassificationDetails 
LocalModel::get_classif_details(const vector<data_t>& sample) {
	LocalModelClassificationDetails details;
	vector<data_t> tsample = sample;
	if (_params.use_min_max)
		tsample = _minmax.transform(tsample);
	tsample = _weight.transform(tsample);

	vector<Neighbor> neighbors = _knn.get_neighbors(tsample, _knn.n_neighbors());
	std::set<label_t> classes;
	for (size_t i = 0; i < neighbors.size(); i++) classes.insert(neighbors[i].y);

	//neighbors to X, y
	Matrix<data_t> local_data(neighbors.size(), sample.size());
	vector<label_t> local_targets(neighbors.size());
	for (size_t i = 0; i < neighbors.size(); i++) {
		local_data.set_row(_data(neighbors[i].idx), i);
		local_targets[i] = neighbors[i].y;
	}

	LogisticRegression logit(_params.logit_eps, _params.logit_C);
	vector<double> sample_weights(neighbors.size()); // ~ distances
	for (size_t i = 0; i < neighbors.size(); i++)
		sample_weights[i] = neighbors[i].distance;
	//distances to sample weights
	sample_weights = get_disttf_func(_params.dist_tf)(sample_weights);

	logit.fit_(local_data, local_targets, sample_weights);

	details.local_classes_probs = logit.predict_probabilty(sample);
	details.neighbors = neighbors;
	details.local_classes = classes;
	details.predicted = logit.predict(sample);
	
	return details;

}