#include "metrics.h"

#include <stdexcept>
#include <iostream>
#include <sstream>

#include <stdio.h>
#include <math.h>
#include <iomanip>

#include <set>

using std::vector;

namespace metrics
{

namespace pairwise
{ 
//Computes manhattan distance between two vectors
data_t manhattan_distance(const data_t* x, const data_t* y, size_t  size)
{
    data_t result = 0.0;
    for (size_t  i = 0; i < size; i++)
		result += std::abs((x[i] - y[i])); 
	return result;
}


//Computes euclidean distance between two vectors
data_t
squared_euclidean_distance(const data_t* x, const data_t* y, size_t  size)
{
    data_t result = 0.0;
    for (size_t  i = 0; i < size; i++)
		result += (x[i] - y[i]) * (x[i] - y[i]);
	return result;
}

data_t euclidean_distance(const data_t* x, const data_t* y, size_t  size)
{
	return std::sqrt(squared_euclidean_distance(x, y, size));
}


dist_fptr distance(Metric metric_type)
{
	if (metric_type == SQUARED_EUCLIDEAN)
		return &squared_euclidean_distance;
	else if (metric_type == EUCLIDEAN)
		return &euclidean_distance;
    else if (metric_type == MANHATTAN)
        return &manhattan_distance;
    else
        throw std::invalid_argument("invalid matric_type");
}

} //namespace pairwise

namespace classification
{

void _check_targets(const vector<label_t>& y_true, const vector<label_t>& y_pred)
{
    if (y_true.size() != y_pred.size())
        throw std::invalid_argument("true and predicted targets have different lengths");
}

score_t accuracy(const vector<label_t>& y_true, const vector<label_t>& y_pred)
{
	_check_targets(y_true, y_pred);
	size_t  true_cl = 0;
	for (size_t  i = 0; i < y_true.size(); i++)
		if(y_true[i] == y_pred[i])  true_cl++;
	return true_cl / static_cast<score_t>(y_true.size());
}

vector<score_t> precision(const vector<label_t>& y_true, const vector<label_t>& y_pred)
{
	_check_targets(y_true, y_pred);
	CONF_MAT conf_mat = confusion_matrix(y_true, y_pred);
	size_t  n_labels = conf_mat.n_cols();
	vector<data_t> result(n_labels);
	for (size_t  i = 0; i < n_labels; i++)
	{
		//true positives
		unsigned tp = conf_mat(i, i);
		//false positives
		unsigned fp = 0;
		for (size_t  j = 0; j < n_labels; j++)
			fp += conf_mat(j, i);
		fp = fp - tp;

		result[i] = tp / static_cast<score_t>(tp + fp); // precision
	}
	return result;
}

vector<score_t>  recall(const vector<label_t>& y_true, const vector<label_t>& y_pred)
{
	_check_targets(y_true, y_pred);
	CONF_MAT conf_mat = confusion_matrix(y_true, y_pred);
	size_t  n_labels = conf_mat.n_cols();
	vector<score_t> result(n_labels);
	for (size_t  i = 0; i < n_labels; i++)
	{
		//true positives
		unsigned tp = conf_mat(i, i);
		//false negatives
		unsigned fn = 0;
		for (size_t  j = 0; j < n_labels; j++)
			fn += conf_mat(i, j);
		fn = fn - tp;
		score_t r = tp / static_cast<score_t>(tp + fn); // recall
		result[i] = r;
	}
	return result;
}

vector<score_t>  f1_score(const vector<label_t>& y_true, const vector<label_t>& y_pred)
{
	_check_targets(y_true, y_pred);
	CONF_MAT conf_mat = confusion_matrix(y_true, y_pred);
	size_t  n_labels = conf_mat.n_cols();
	vector<score_t> result(n_labels);
	for (size_t i = 0; i < n_labels; i++)
	{
		//true positives
		unsigned tp = conf_mat(i, i);
		//false positives
		unsigned fp = 0;
		for (size_t j = 0; j < n_labels; j++)
			fp += conf_mat(j, i);
		fp = fp - tp;
		//false negatives
		unsigned fn = 0;
		for (size_t j = 0; j < n_labels; j++)
			fn += conf_mat(i, j);
		fn = fn - tp;
		data_t p = tp / static_cast<data_t>(tp + fp); // precision
		data_t r = tp / static_cast<data_t>(tp + fn); // recall
		if (tp == 0 || (tp + fp) == 0 || (tp + fn) == 0)
			result[i] = 0.0;
		else
			result[i] = 2 * p * r / (p + r);

	}
	return result;
}

score_t f1_macro(const vector<label_t>& y_true, const vector<label_t>& y_pred)
{
	data_t result = 0.0;
	vector<data_t> f1_per_label = f1_score(y_true, y_pred);
	for (size_t i = 0; i < f1_per_label.size(); i++)
		result += f1_per_label[i];
	return result / f1_per_label.size();

}

score_t f1_micro(const vector<label_t>& y_true, const vector<label_t>& y_pred)
{
	_check_targets(y_true, y_pred);
	CONF_MAT conf_mat = confusion_matrix(y_true, y_pred);
	size_t  n_labels = conf_mat.n_cols();
	unsigned total_tp = 0;
	unsigned total_fp = 0;
	unsigned total_fn = 0;
	for (size_t  i = 0; i < n_labels; i++)
	{
		//true positives
		unsigned tp = conf_mat(i, i);
		//false positives
		unsigned fp = 0;
		for (size_t  j = 0; j < n_labels; j++)
			fp += conf_mat(j, i);
		fp = fp - tp;
		//false negatives
		unsigned fn = 0;
		for (size_t  j = 0; j < n_labels; j++)
			fn += conf_mat(i, j);
		fn = fn - tp;
		total_tp += tp;
		total_fp += fp;
		total_fn += fn;
	}

	data_t p = total_tp / static_cast<score_t>(total_tp + total_fp); // precision
	data_t r = total_tp / static_cast<score_t>(total_tp + total_fn); // recall

	return  2 * p * r / (p + r);
}


multiclass_metric_fptr get_score_func(Score score_type)
{
	if (score_type == Score::ACCURACY) return accuracy;
	else if (score_type == Score::F1_MACRO) return f1_macro;
	else if (score_type == Score::F1_MICRO) return f1_micro;
	else throw std::invalid_argument("invalid score_type");
}


//first dimenstion - true, second - predicted
CONF_MAT confusion_matrix(const vector<label_t>& y_true, const vector<label_t>& y_pred)
{
    _check_targets(y_true, y_pred);
    std::set<label_t> labels(y_true.begin(), y_true.end());
	labels.insert(y_pred.begin(), y_pred.end());
	//key - original labels, value - new label in range <0 ; labels.size() - 1>
	std::map<label_t, std::size_t> label_codes; 
    size_t  idx = 0;
    for (std::set<label_t>::iterator it = labels.begin(); it !=labels.end(); it++)
        label_codes.insert(std::make_pair(*it, idx++));

	CONF_MAT result(labels.size(), labels.size(), 0);
    for (size_t  i = 0; i < y_true.size(); i++)
        result(label_codes.find(y_true[i])->second, label_codes.find(y_pred[i])->second) += 1;

    return result;

}

std::string make_report(const vector<label_t>& y_true, const vector<label_t>& y_pred) {
	_check_targets(y_true, y_pred);
	std::stringstream ss;
	std::set<label_t> labels(y_true.begin(), y_true.end());
	std::vector<label_t> labels_s(labels.begin(), labels.end());

	metrics::classification::CONF_MAT conf_mat = metrics::classification::confusion_matrix(y_true, y_pred);
	vector<size_t> class_count(labels.size()); //sum across columns
    vector<size_t> row_sum(labels.size()); //sum 
	for (size_t i = 0; i < class_count.size(); i++) {
		class_count[i] = 0;
        for(size_t j = 0; j < class_count.size(); j++) {
			class_count[i] += conf_mat(i, j);
            row_sum[i] += conf_mat(j, i);
        }
	}

	double f1_macro_score = metrics::classification::f1_macro(y_true, y_pred);
	double f1_micro_score = metrics::classification::f1_micro(y_true, y_pred);
	double acc_score = metrics::classification::accuracy(y_true, y_pred);

	ss << "f1 macro score: " << f1_macro_score << std::endl;
	ss << "f1 micro score: " << f1_micro_score << std::endl;
	ss << "accuracy score: " << acc_score << std::endl;

	ss << "Confusion matrix (0 dim - true, 1 dim - predicted)" << std::endl;

	ss << "class:";
	ss << "\t";
	for (size_t i = 0; i < conf_mat.n_cols(); i++) {
		ss << "\t" << labels_s[i];
	}
	ss << std::endl;
	for (size_t i = 0; i < conf_mat.n_cols(); i++){
		ss << "\t" << labels_s[i];
		for (size_t j = 0; j < conf_mat.n_rows(); j++){
			ss << "\t" << conf_mat(i, j);
		}
		ss << std::endl;
	}

	ss << "Confusion matrix normalized across rows (0 dim - true, 1 dim - predicted)" << std::endl;

	ss << "class:";
	ss << "\t";
	for (size_t i = 0; i < conf_mat.n_cols(); i++) {
		ss << "\t" << labels_s[i];
	}
	ss << std::endl;
	for (size_t i = 0; i < conf_mat.n_cols(); i++){
		ss << "\t" << labels_s[i];
		for (size_t j = 0; j < conf_mat.n_rows(); j++){
			ss << "\t" << std::fixed << std::setprecision(3) << conf_mat(i, j) / static_cast<double>(row_sum[j]);
		}
		ss << std::endl;
	}

	ss << "Confusion matrix normalized across columns (0 dim - true, 1 dim - predicted)" << std::endl;

	ss << "class:";
	ss << "\t";
	for (size_t i = 0; i < conf_mat.n_cols(); i++) {
		ss << "\t" << labels_s[i];
	}
	ss << std::endl;
	for (size_t i = 0; i < conf_mat.n_cols(); i++){
		ss << "\t" << labels_s[i];
		for (size_t j = 0; j < conf_mat.n_rows(); j++){
			ss << "\t" << conf_mat(i, j) / static_cast<double>(class_count[j]);
		}
		ss << std::endl;
	}

	vector<score_t> precision_per_class =
		metrics::classification::precision(y_true, y_pred);

	vector<score_t> recall_per_class =
		metrics::classification::recall(y_true, y_pred);

	vector<score_t> f1_per_class =
		metrics::classification::f1_score(y_true, y_pred);
	ss << "Classification report: " << std::endl;

	ss << "\t" << "label" << "\t"
		<< "pr" << "\t"
		<< "recall" << "\t"
		<< "f1 score" << "\t"
		<< "support" << std::endl;

	for (size_t i = 0; i < precision_per_class.size(); i++)
		ss << std::setprecision(3)
		<< "\t" << labels_s[i] << "\t"
		<< precision_per_class[i] << "\t"
		<< recall_per_class[i] << "\t"
		<< f1_per_class[i] << "\t\t" 
		<< class_count[i] << std::endl;

	return ss.str();
}


} //namespace classification
} //namespace metrics