#include "logistic_regression.h"


//from liblinear readme:
//
//"struct problem describes the problem :
//
//struct problem
//{
//	int l, n;
//	int *y;
\
//	struct feature_node **x;
//	double bias;
//};
//
//where `l' is the number of training data.If bias >= 0, we assume
//that one additional feature is added to the end of each data
//instance. `n' is the number of feature(including the bias feature
//if bias >= 0). `y' is an array containing the target values. (integers
//	in classification, real numbers in regression) And `x' is an array
//	of pointers, each of which points to a sparse representation(array
//	of feature_node) of one training vector.
//For example, if we have the following training data :
//
//LABEL       ATTR1   ATTR2   ATTR3   ATTR4   ATTR5
//---- - ---- - ---- - ---- - ---- - ---- -
//1           0       0.1     0.2     0       0
//2           0       0.1     0.3 - 1.2     0
//1           0.4     0       0       0       0
//2           0       0.1     0       1.4     0.5
//3 - 0.1 - 0.2     0.1     1.1     0.1
//
//and bias = 1, then the components of problem are :
//
//l = 5
//n = 6
//
//y -> 1 2 1 2 3
//
//x ->[] -> (2, 0.1) (3, 0.2) (6, 1) (-1, ? )
//[] -> (2, 0.1) (3, 0.3) (4, -1.2) (6, 1) (-1, ? )
//[] -> (1, 0.4) (6, 1) (-1, ? )
//[] -> (2, 0.1) (4, 1.4) (5, 0.5) (6, 1) (-1, ? )
//[] -> (1, -0.1) (2, -0.2) (3, 0.1) (4, 1.1) (5, 0.1) (6, 1) (-1, ? )"

//converts X,y to liblinear format
liblinear::problem*
LogisticRegression::_Xy_to_problem(const Matrix<data_t>& data,
const vector<label_t>& targets) const {
    liblinear::problem* res = new liblinear::problem;
    res->n = data.n_cols() + 1;	  //number of features + bias
    res->l = data.n_rows();		  //number of samples
    res->bias = 1;
    res->x = new liblinear::feature_node*[res->l];
    for(size_t i = 0; i < static_cast<size_t>(res->l); i++)
        res->x[i] = _sample_to_feature_node(data(i));

    res->y = new double[targets.size()];
    for(size_t i = 0; i < targets.size(); i++)
        res->y[i] = static_cast<double>(targets[i]);
	res->sample_weight = new double[targets.size()];
	for (size_t i = 0; i < targets.size(); i++)
		res->sample_weight[i] = 1.0;
    return res;
}

//converts X,y to liblinear format
liblinear::problem*
LogisticRegression::_Xy_to_problem(const Matrix<data_t>& data,
const vector<label_t>& targets, const vector<data_t>& sample_weight) const {
	liblinear::problem* res = _Xy_to_problem(data, targets);
    for(size_t i = 0; i < targets.size(); i++)
        res->sample_weight[i] = sample_weight[i];
    return res;
}

//feature vector to liblinear format
liblinear::feature_node*
LogisticRegression::_sample_to_feature_node(const vector<data_t>& sample) const {
    //n features + bias + one node that indicate end of array  
    liblinear::feature_node* res = new liblinear::feature_node[sample.size() + 2];
    for(size_t j = 0; j < sample.size(); j++) {
        res[j].index = (j + 1);
        res[j].value = sample[j];
    }
    //bias
    res[sample.size()].index = sample.size() + 1;
    res[sample.size()].value = 1.0;
    //end
    res[sample.size() + 1].index = -1;
    return res;
}

LogisticRegression::LogisticRegression(double eps, double C, int max_iter) {
    _lin_parameter = new liblinear::parameter();
    _lin_parameter->C = C;
    _lin_parameter->eps = eps;
    _lin_parameter->nr_weight = 0;
    _lin_parameter->p = 0;
    _lin_parameter->solver_type = liblinear::L2R_LR;
    ///_lin_parameter->init_sol = nullptr;
    _lin_parameter->weight = nullptr;
    _lin_parameter->weight_label = nullptr;
	_lin_parameter->max_iter = max_iter;
    _lin_problem = nullptr;
}


void
LogisticRegression::_fit(const Matrix<data_t>& data, const vector<label_t>& targets) {
    if(_lin_problem != nullptr) {
        for(size_t i = 0; i < static_cast<size_t>(_lin_problem->l); i++)
            delete[] _lin_problem->x[i];
        delete[] _lin_problem->x;
        delete[] _lin_problem->y;
        delete _lin_problem;
    }

    liblinear::free_and_destroy_model(&_lin_model);
    _lin_problem = _Xy_to_problem(data, targets);
    const char* res = check_parameter(_lin_problem, _lin_parameter);
    if(res != nullptr)
        throw std::invalid_argument(res);
    _lin_model = train(_lin_problem, _lin_parameter);
}

void
LogisticRegression::fit_(const Matrix<data_t>& data, const vector<label_t>& targets, const vector<data_t>& sample_weight) {
    ClassifierABC::_check_fit_data(data, targets);
    ClassifierABC::_n_features = data.n_cols();
    ClassifierABC::_n_samples = data.n_rows();

    if(_lin_problem != nullptr) {
        for(size_t i = 0; i < n_samples(); i++)
            delete[] _lin_problem->x[i];
        delete[] _lin_problem->x;
        delete[] _lin_problem->y;
        delete _lin_problem;
    }

    liblinear::free_and_destroy_model(&_lin_model);
    _lin_problem = _Xy_to_problem(data, targets, sample_weight);
    const char* res = check_parameter(_lin_problem, _lin_parameter);
    if(res != nullptr)
        throw std::invalid_argument(res);
    _lin_model = train(_lin_problem, _lin_parameter);
}

vector<label_t>
LogisticRegression::_predict(const Matrix<data_t>& data) const {
	//logit prediction is very fast, there is no reason to parallelization
    vector<label_t> targets_pred(data.n_rows());
    for(size_t idx = 0; idx < targets_pred.size(); idx++)
        targets_pred[idx] = _predict(data(idx));

    return targets_pred;
}

label_t
LogisticRegression::_predict(const vector<data_t>& sample) const {
    liblinear::feature_node* f_node = _sample_to_feature_node(sample);
    double pred = liblinear::predict(_lin_model, f_node);
    delete[] f_node;
    return static_cast<label_t>(pred);
}

Matrix<double> 
LogisticRegression::predict_probabilty(const Matrix<data_t>& data) const {
    Matrix<double> res(data.n_rows(), static_cast<size_t>(_lin_model->nr_class));
    for(size_t idx = 0; idx < res.n_rows(); idx++)
        res.set_row(predict_probabilty(data(idx)), idx);
    return res;
}
vector<double>
LogisticRegression::predict_probabilty(const vector<data_t> sample) const {
    liblinear::feature_node* f_node = _sample_to_feature_node(sample);
    vector<double> res(_lin_model->nr_class);
    liblinear::predict_probability(_lin_model, f_node, res.data());
    delete[] f_node;
	//scikit-learn made some strange things with binary probs, they swapped them:
	//https://github.com/scikit-learn/scikit-learn/pull/546
	//we swap them back
	if (res.size() == 2) {
		res[0] = 1 - res[0];
		res[1] = 1 - res[1];
	}
    return res;
}

Matrix<double> 
LogisticRegression::coef() const {
    if(_lin_model == nullptr)
        return Matrix<double>();
    size_t n_classes = static_cast<size_t>(_lin_model->nr_class);
    Matrix<double> coef(n_classes, n_features() + 1);
    //liblinear w = [class_1_w_1, class_2_w_1,... ,class_n_w_1 ,...,class_1_w_2, ...]
    for(size_t i = 0; i < n_classes; i++)
        for(size_t j = 0; j < n_features() + 1; j++)
            coef(i, j) = _lin_model->w[j*n_classes + i];
    return coef;
}

LogisticRegression::~LogisticRegression() {

    if(_lin_problem != nullptr) {
        for(size_t i = 0; i < n_samples(); i++)
            delete[] _lin_problem->x[i];
        delete[] _lin_problem->x;
        delete[] _lin_problem->y;
        if(_lin_problem->sample_weight != nullptr)
            delete[] _lin_problem->sample_weight;
        delete _lin_problem;
    }
    liblinear::destroy_param(_lin_parameter);
    delete _lin_parameter;
    liblinear::free_and_destroy_model(&_lin_model);
}

const double LogisticRegression::eps() const {
	return _lin_parameter->eps;
}
const double LogisticRegression::C() const {
	return _lin_parameter->C;
}
const int LogisticRegression::max_iter() const {
	return _lin_parameter->max_iter;
}
const vector<double> LogisticRegression::sample_weights() const {
	return vector<double>(_lin_parameter->weight, _lin_parameter->weight + n_samples());
}

LogisticRegression& LogisticRegression::eps(double val) {
	if (val < 0.0) {
		throw std::invalid_argument("eps should be > 0");
	}
	else {
		_lin_parameter->eps = val;
	}
	return *this;
}
LogisticRegression& LogisticRegression::C(double val) {
	if (val < 0.0) {
		throw std::invalid_argument("C should be > 0");
	}
	else {
		_lin_parameter->C = val;
	}
	return *this;
}
LogisticRegression& LogisticRegression::max_iter(int val) {
	if (val < 0) {
		throw std::invalid_argument("max_iter should be > 0");
	}
	else {
		_lin_parameter->max_iter = val;
	}
	return *this;
}
