#include "neural_net.h"

#include <algorithm>
#include <cassert>
#include <fstream>
#include <iostream>
#include <limits>
#include <math.h>

#include "../../timer.h"

#define MIN_TRAINING_EPOCHS 20

using namespace std;

NeuralNet::NeuralNet(const NetTopology &topology)
	:error(0.0){
	/* Create all the neurons in the network, as specified in topology.
	 */
	unsigned number_of_layers = topology.size();
	for(unsigned layer_number=0;layer_number<number_of_layers;layer_number++){
		/* Fill the layer with neurons
		   add a bias neuron to each layer (<=topology[layer_number])
		*/
		layers.push_back(Layer());
		unsigned number_of_outputs = 0;
		if(layer_number < topology.size()-1){
			number_of_outputs = topology[layer_number+1];
		}
		for(int neuron_number=0;neuron_number<=topology[layer_number];neuron_number++){
			if(layer_number == topology.size()-1){
					layers.back().push_back(new LinearNeuron(number_of_outputs, neuron_number));
//					layers.back().push_back(new Neuron(number_of_outputs, neuron_number));
			}else{
					layers.back().push_back(new Neuron(number_of_outputs, neuron_number));
			}
		}
		//force bias to have output 1.0
		layers.back().back()->set_output_value(1.0);
	}
}

void NeuralNet::feed_forward(const vector<double> &input_values){
	/* Check if the size of the inputs match the size of the first layer
	   without the bias neuron.
	 */
	assert(input_values.size() == layers[0].size() -1);
	/* Assign the input values to the first layer.
	 */
	Layer &first_layer = layers[0];
	for(unsigned i=0;i<input_values.size();i++){
		first_layer[i]->set_output_value(input_values[i]);
	}

	//propagate the inputs further through the network
	for(unsigned layer_number=1;layer_number<layers.size();layer_number++){
		Layer &previous_layer = layers[layer_number-1];
		Layer &actual_layer = layers[layer_number];
		for(unsigned i=0;i<actual_layer.size()-1;i++){		//-1 for excluding the bias
			actual_layer[i]->feed_forward(previous_layer);
		}
	}

}


void NeuralNet::get_results(vector<double> &out_results) const{
	out_results.clear();
//	Layer &last_layer = layers[layers.size()-1];
	for(unsigned i=0;i<layers.back().size()-1;i++){	//omit the bias in the last layer
		out_results.push_back(layers.back()[i]->get_output_value());
	}
}

double NeuralNet::get_error(/*const vector<double> expected*/) const{
	/*vector<double> delta;
	for(unsigned i=0;i<layers.back().size()-1;i++){	//omit the bias in the last layer
		delta.push_back(layers.back()[i]->get_output_value()-expected[i]);
	}
	calculate_error(delta);*/
	return error;
}

void NeuralNet::back_propagation(const vector<double> &expected_results){
	Layer &last_layer = layers.back();
	assert(expected_results.size() == last_layer.size() -1);
	vector<double> delta;
	for(unsigned i=0;i<expected_results.size();i++){
		delta.push_back(expected_results[i] - last_layer[i]->get_output_value());
	}
	calculate_error(delta);

	av_error = (av_error * training_set.size() + get_error()) / (training_set.size() + 1.0);

	/* calculate gradients for last layer
	 */
	for(unsigned i=0;i<last_layer.size()-1;i++){	//omit bias in last layer
		last_layer[i]->calculate_output_gradients(expected_results[i]);
	}

	/* calculate gradients for hidden layers
	 */
	for (unsigned layer_number = layers.size() - 2; layer_number > 0; layer_number--) {
		Layer &hidden_layer = layers[layer_number];
		Layer &next_layer = layers[layer_number + 1];
		for (unsigned n = 0; n < hidden_layer.size(); ++n) {
			hidden_layer[n]->calculate_hidden_gradients(next_layer);
		}
	}

	/* update the weights for all connections between neurons
	 */
	 for (unsigned layer_number = layers.size() - 1; layer_number > 0; layer_number--) {
		 Layer &actual_layer = layers[layer_number];
		 Layer &previous_layer = layers[layer_number - 1];

		 for (unsigned i=0;i<actual_layer.size()-1;i++) {
			 actual_layer[i]->update_input_weights(previous_layer);
		 }
	 }
}

void NeuralNet::calculate_error(const vector<double> &delta){
	error = 0.0;
	for(unsigned i=0;i<delta.size();i++){
		error += delta[i] * delta[i];
	}
	error = error / delta.size();
	error = sqrt(error);
}

void NeuralNet::add_training_set_entry(const vector<double> &features, double result){
	TrainingSetEntry tse;
	tse.features = features;
	tse.result = result;
	training_set.push_back(tse);
}

void NeuralNet::train_network(int num_epochs){
	epochs = num_epochs;
	train_network();
}

void NeuralNet::train_network(){
	cout << "Start training..." << endl;
	cout << "Size of training set: " << training_set.size() << endl;
	Timer timer;
	vector<double> outputs;
	vector<double> error;
	int monotonic_increase = 0;
	double percentage = 0.7;
	double err = 0;
	double sum_err = 0;
	double old_error = 0;
	ofstream f_error("/tmp/error.txt");
	//ofstream hist_error("/tmp/error_hist.txt");
	random_shuffle(training_set.begin(), training_set.end());
	NNTrainingSet actual_training_set(training_set.begin(), training_set.begin() + percentage * training_set.size());
	NNTrainingSet validation_set(training_set.begin() + percentage * training_set.size(), training_set.end());
	for(int i=0;i<epochs;i++){
		error.clear();
		for(TrainingSetEntry tse : actual_training_set){
			feed_forward(tse.features);
			get_results(outputs);
			back_propagation(vector<double> {tse.result});
		}
		for(TrainingSetEntry tse : validation_set){
			feed_forward(tse.features);
			get_results(outputs);
			err = outputs.back() - tse.result;
			error.push_back(err * err);
			//hist_error << err * err << endl;
		}

		sum_err = 0;
		for(double d : error){
			sum_err += d;
		}
		f_error << sum_err << endl;
		if(sum_err - old_error > 0 && i > MIN_TRAINING_EPOCHS){
			monotonic_increase++;
		}else{
			monotonic_increase = 0;
		}
		if(monotonic_increase > 20){
			break;
		}
		old_error = sum_err;
	}
	cout << "Neural net training time: " << timer << endl;
}

void NeuralNet::dump_training_set(){
	ofstream outfile("/tmp/training_set.txt");
	bool first = true;
	if(outfile.is_open()){
		for(TrainingSetEntry tse : training_set){
			outfile << tse.result << ",";
			for(double d : tse.features){
				if(!first)outfile << ",";
				outfile << d;
				first = false;
			}
			outfile << endl;
			first = true;
		}
	}
}

void NeuralNet::set_epochs(int epochs){
	this->epochs = epochs;
}
