/*
 * Rbm.hpp
 *
 *  Created on: 21.09.2014
 *      Author: jens
 */

#ifndef RBM_HPP_
#define RBM_HPP_

#include "VisibleLayer.hpp"
#include "HiddenLayer.hpp"
#include "Weights.hpp"
#include <cmath>
#include <Eigen/Dense>

using namespace Eigen;

void mylog(const char* format, ...);
#define printf mylog

class Rbm;

class RbmListener
{
public:
	RbmListener() {}
	virtual ~RbmListener() {}

	virtual void onEpochTrained(const Rbm &obj) = 0;
};

class Rbm
{
public:
	Rbm(Weights &weights, RbmListener *pListener = nullptr)
	: m_w(weights)
	, m_pListener(pListener)
	, m_progress(0)
	, m_sigma(1.0)
	, m_sigmaDecay(1.0)
	, m_lambda(1.0)
	, m_sparsity(0)
	, m_useVisibleGaussian(false)
	, m_useExpectations(false)
	, m_doRaoBlackwell(false)
	, m_useProbsForHiddenReconstruction(false)
	, m_doRobbinsMonro(false)
	, m_doSparse(false)
	{
		Noise_Init(&m_noise, 0x32727155);
	}

	~Rbm()
	{
		Noise_Free(&m_noise);
	}

	void weightsUpdate(VisibleLayer &v, HiddenLayer &h, double mu)
	{
		MatrixXd &w = (MatrixXd&)m_w.weights();

		// Update weights
		w += mu*(v.states() * h.states().transpose());
	}

	void visibleBiasUpdate(VisibleLayer &v, double mu)
	{
		m_w.visibleBias().array() += mu*v.states().array();
	}

	void hiddenBiasUpdate(HiddenLayer &h, double mu)
	{
		m_w.hiddenBias().array() += mu*h.states().array();
	}

	void train(LayerArray<VisibleLayer> &vt, uint32_t numEpochs, double mu, uint32_t numGibbs = 1, double sigmaMin = 0.05)
	{
		uint32_t t, i;
		uint32_t epoch;
		uint32_t gibbs;
		double sigma;
		VisibleLayer v(m_w.getNumVisible());
		HiddenLayer h(m_w.getNumHidden());
		HiddenLayer *pH;
		LayerArray<HiddenLayer> ht(vt.getSize(), m_w.getNumHidden());

		sigma = m_sigma;
		Weights w = m_w;

		double dProgress = 1.0/numEpochs;
		m_progress = 0;

		if (m_useExpectations)
		{
			mu /= vt.getSize();
		}

		if (m_doRobbinsMonro)
		{
			for (i=0; i < vt.getSize(); i++)
			{
				// Create hidden layer base on training data
				ht[i].probsUpdateLogistic(vt[i], w, m_lambda, sigma);
			}
		}

		for (epoch=0; epoch < numEpochs; epoch++)
		{
			for (i=0; i < vt.getSize(); i++)
			{
				//t = (uint32_t)(0.5 + (vt.getSize()-1)*Noise_Uniform(&m_noise, 0.5));
				t = i;
				h.probsUpdateLogistic(vt[t], w, m_lambda, sigma);

				// Create hidden layer base on training data
				if (m_doRobbinsMonro)
				{
					pH = &ht[t];
				}
				else
				{
					pH = &h;
				}

				// Update weights (positive phase)
				if (m_doRaoBlackwell)
				{
					pH->states() = pH->probs();
				}
				else
				{
					pH->statesUpdateStochastic();
				}
				weightsUpdate(vt[t], h, +mu);
				visibleBiasUpdate(vt[t], +mu);
				if (!m_doSparse)
				{
					hiddenBiasUpdate(h, +mu);
				}

				for (gibbs=0; gibbs < numGibbs; gibbs++)
				{
					pH->statesUpdateStochastic();

					// Create visible reconstruction (a fantasy...)
					if (m_useProbsForHiddenReconstruction)
					{
						if (m_useVisibleGaussian)
						{
							v.probsUpdateGaussian(*pH, w, m_lambda, sigma);
						}
						else
						{
							v.probsUpdateLogistic(*pH, w, m_lambda, sigma);
						}
						v.states() = v.probs();
					}
					else
					{
						if (m_useVisibleGaussian)
						{
							v.sampleGaussian(*pH, w, m_lambda, sigma);
						}
						else
						{
							v.probsUpdateLogistic(*pH, w, m_lambda, sigma);
							v.statesUpdateStochastic();
						}
					}
					// Create hidden reconstruction
					pH->probsUpdateLogistic(v, w, m_lambda, sigma);
				}

				// Update weights (negative phase)
				if (m_doRaoBlackwell)
				{
					pH->states() = pH->probs();
				}
				else
				{
					pH->statesUpdateStochastic();
				}
				weightsUpdate(v, *pH, -mu);
				visibleBiasUpdate(v, -mu);
				if (!m_doSparse)
				{
					hiddenBiasUpdate(*pH, -mu);
				}
				if (!m_useExpectations)
				{
					w = m_w;
				}
			} // TrainingSize

			if (m_useExpectations)
			{
				w = m_w;
			}

			if (m_doSparse)
			{
				HiddenLayer th(m_w.getNumHidden());
				VectorXd m(m_w.getNumHidden());
				m.fill(0);

				for (i=0; i < vt.getSize(); i++)
				{
					th.probsUpdateLogistic(vt[i], w, m_lambda, sigma);
					m += th.probs();
				}
				m /= i;
				m.array() -= m_sparsity;
				th.states() = m;
				hiddenBiasUpdate(th, -mu);
			}
			if (sigma > sigmaMin)
			{
				sigma *= m_sigmaDecay;
			}

			m_progress += dProgress;
			if (m_pListener)
			{
				m_pListener->onEpochTrained(*this);
			}

		} // Number of epochs
	}

	double getProgress() const
	{
		return m_progress;
	}

	double getEnergy(VisibleLayer &v, HiddenLayer &h)
	{
		uint32_t i, j;
		double energy;

		energy = -v.getEnergy(m_w) - h.getEnergy(m_w);

		for (i=0; i < h.getNumUnits(); i++)
		{
			for (j=0; j < v.getNumUnits(); j++)
			{
//				energy -= v.getStates()[j] * h.getStates()[i] * m_w.getWeights()[i][j];
			}
		}

		// ToDo: make this correct
//		energy -= (v.states().transpose() * h.states()); // * m_w.weights();
		return energy;
	}

	void prob(LayerArray<VisibleLayer> &vts)
	{
		uint32_t i, j;
		double z;
		double p;

		HiddenLayer *h = new HiddenLayer[vts.getSize()];

		// Create hidden layer activations based on training data
		for (j=0; j < vts.getSize(); j++)
		{
			h[j].setNumUnits(m_w.getNumHidden());
			h[j].probsUpdateLogistic(vts.getAt(j), m_w, m_lambda, m_sigma);
//			h[j].statesAssignfromProbs();
			h[j].statesUpdateStochastic();
		}

		printf("pi(t) = (pi^, v>)\n");
		for (j=0; j < vts.getSize(); j++)
		{
			cout << h[j].probs() << endl;
		}
		cout << endl;

		printf("si(t) = (si^, v>)\n");
		for (j=0; j < vts.getSize(); j++)
		{
			cout << h[j].states() << endl;
		}
		cout << endl;

		printf("p(v) = (t^, v>)\n");
		for (i=0; i < vts.getSize(); i++)
		{
			z = 0;
			for (j=0; j < vts.getSize(); j++)
			{
				z += exp(-getEnergy(vts.getAt(j), h[i]));
			}
			for (j=0; j < vts.getSize(); j++)
			{
				p = exp(-getEnergy(vts.getAt(j), h[i]))/z;
				cout << p << endl;
			}
			cout << endl;
		}
		cout << endl;

		// Reconstruct
		for (i=0; i < vts.getSize(); i++)
		{
			vts.getAt(i).probsUpdateLogistic(h[i], m_w, m_lambda, m_sigma);
		}

		printf("A fantasy... (v^, t>)\n");
		for (j=0; j < vts.getSize(); j++)
		{
			cout << vts.getAt(j).probs() << endl;
		}

		delete [] h;
	}

	VectorXd toHidden(const VectorXd& visible)
	{
		HiddenLayer th(m_w.getNumHidden());
		VisibleLayer tv(m_w.getNumVisible(), (const VectorXd*)&visible);

		th.probsUpdateLogistic(tv, m_w, m_lambda, m_sigma);

		return th.probs();
	}

	VectorXd toVisible(const VectorXd& hidden)
	{
		HiddenLayer th(m_w.getNumHidden(), (const VectorXd*)&hidden);
		VisibleLayer tv(m_w.getNumVisible());
		if (m_useVisibleGaussian)
		{
			tv.probsUpdateGaussian(th, m_w, m_lambda, m_sigma);
		}
		else
		{
			tv.probsUpdateLogistic(th, m_w, m_lambda, m_sigma);
		}
		return tv.probs();
	}

	VectorXd expectHidden(VectorXd visible, uint32_t numIter)
	{
      	uint32_t i;
		VisibleLayer v(m_w.getNumVisible(), (const VectorXd*)&visible);
		HiddenLayer h(m_w.getNumHidden());

    	for (i=0; i < numIter; i++)
    	{
    		h.probsUpdateLogistic(v, (Weights&)m_w, m_lambda, m_sigma);
    		if (m_useVisibleGaussian)
    		{
    			v.probsUpdateGaussian(h, (Weights&)m_w, m_lambda, m_sigma);
    		}
    		else
    		{
    			v.probsUpdateLogistic(h, (Weights&)m_w, m_lambda, m_sigma);
    		}
    	}

		return h.probs();
	}

   VectorXd expectVisible(VectorXd visible, uint32_t numIter)
	{
      	uint32_t i;
		VisibleLayer v(m_w.getNumVisible(), (const VectorXd*)&visible);
		HiddenLayer h(m_w.getNumHidden());

    	for (i=0; i < numIter; i++)
    	{
    		h.probsUpdateLogistic(v, (Weights&)m_w, m_lambda, m_sigma);
    		if (m_useVisibleGaussian)
    		{
    			v.probsUpdateGaussian(h, (Weights&)m_w, m_lambda, m_sigma);
    		}
    		else
    		{
    			v.probsUpdateLogistic(h, (Weights&)m_w, m_lambda, m_sigma);
    		}
    	}

		return v.probs();
	}

   void setSigma(double value)
   {
	   m_sigma = value;
   }

   void setSigmaDecay(double value)
   {
	   m_sigmaDecay = value;
   }

   void setLambda(double value)
   {
	   m_lambda = value;
   }

   void setSparsity(double value)
   {
	   m_sparsity = value;
   }

   void setUseVisibleGaussian(bool flag)
   {
	   m_useVisibleGaussian = flag;
   }

   void setUseExpectations(bool flag)
   {
	   m_useExpectations = flag;
   }

   void setDoRaoBlackwell(bool flag)
   {
	   m_doRaoBlackwell = flag;
   }

   void setUseProbsForHiddenReconstruction(bool flag)
   {
	   m_useProbsForHiddenReconstruction = flag;
   }

   void setDoRobbinsMonro(bool flag)
   {
	   m_doRobbinsMonro = flag;
   }

   void setDoSparse(bool flag)
   {
	   m_doSparse = flag;
   }

   double getSparsity()
   {
	   return m_sparsity;
   }

   double getSigma()
   {
	   return m_sigma;
   }

   double getSigmaDecay()
   {
	   return m_sigmaDecay;
   }

   double getLambda()
   {
	   return m_lambda;
   }

   bool getUseVisibleGaussian()
   {
	   return m_useVisibleGaussian;
   }

   bool getUseExpectations()
   {
	   return m_useExpectations;
   }

   bool getDoRaoBlackwell()
   {
	   return m_doRaoBlackwell;
   }

   bool getUseProbsForHiddenReconstruction()
   {
	   return m_useProbsForHiddenReconstruction;
   }

   bool getRobbinsMonro()
   {
	   return m_doRobbinsMonro;
   }

   bool getDoSparse()
   {
	   return m_doSparse;
   }

private:
	Weights &m_w;
	RbmListener *m_pListener;
	noise_gen_t m_noise;
	double m_progress;
	double m_sigma;
	double m_sigmaDecay;
	double m_lambda;
	double m_sparsity;
	bool m_useVisibleGaussian;
	bool m_useExpectations;
	bool m_doRaoBlackwell;
	bool m_useProbsForHiddenReconstruction;
	bool m_doRobbinsMonro;
	bool m_doSparse;

};




#endif /* RBM_HPP_ */
