/*
    Copyright (C) 2012 Bartosz Broda, Jacek Bochenek, Łukasz Biłeńkij, 
    Damian Jaworski, Wojciech Mazur, Dominik Piasecki, Maciej Piasecki, 
    Radosław Ramocki
    
    This program is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option)
any later version.

    This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. 

    See the LICENSE and COPYING files for more details.
*/

#include "SOMNet.h"
#include "HierarchicalSOM.h"
#include <boost/thread.hpp>


namespace clustering
{
	namespace som
	{
		size_t CSOMNet::m_stIdGen = 1;

		CSOMNet::CSOMNet(size_t cNeuronsInRow,  Matrices::SuperMatrixPtr documentMatrix,
			smartcomparator::simfunctions::LocalSimilarityFunctionPtr simfun, double mqe0,
			boost::shared_ptr<std::set<size_t> > indices , std::ostream &output)	:
				m_pMatrixIndices(indices), m_SimFunction(simfun), m_LogOut(output),	m_dMQE0(mqe0)
		{
			m_stId = m_stIdGen++;
			m_iMaxEpoch = 5;
			srand( (unsigned)time( NULL ) );
			m_Matrix = documentMatrix;

			CreateNet(cNeuronsInRow,m_Matrix->NumberOfColumns());

			m_dCurrentLearningRate = 0.8;
			m_iCurrentEpoch = 0;
			m_stParentId = 0;

			if( m_pMatrixIndices != NULL )
			{
				size_t sizeRows = m_pMatrixIndices->size();
				m_piIndices = new size_t[sizeRows];
				size_t index = 0;
				for(std::set<size_t>::iterator i = m_pMatrixIndices->begin(); i != m_pMatrixIndices->end(); i++, index++)
					m_piIndices[index] = *i;
			}
			else
			{
				size_t sizeRows = m_Matrix->NumberOfRows();
				m_piIndices = new size_t[sizeRows];
				for(size_t i = 0; i < sizeRows; i++) //insert all rows indices
					m_piIndices[i] = i;
			}
		}

		CSOMNet::~CSOMNet(void)
		{
			delete[] m_piIndices;
		}

		void CSOMNet::CreateNet(size_t cNeuronsInRow, size_t cWeights)
		{
			m_cWeights = cWeights;
			m_vNet.clear();
			m_vNeurons.clear();

			for(size_t y = 1 ; y <= cNeuronsInRow; y++)
			{
				m_vNet.push_back(std::vector< boost::shared_ptr<CSOMNeuron> >());
				for(size_t x = 1 ; x <= cNeuronsInRow; x++)
				{
					boost::shared_ptr<CSOMNeuron> neuron(new CSOMNeuron(m_cWeights,(double)x,(double)y, m_SimFunction));
					m_vNeurons.push_back(neuron  );
					m_vNet[y-1].push_back(neuron);
				}
			}
		}

		void CSOMNet::SetMaxEpochCount(int max)
		{
			m_iMaxEpoch = max;
		}
		void CSOMNet::TrainNet(int max)
		{
			m_iMaxEpoch = max;
			(*this)();
		}

		void CSOMNet::operator() ()
		{
			using namespace boost::posix_time;
			m_LogOut << "SOM is learning...\n";
			m_dInitialLearningRate = 0.8;
			m_dCurrentLearningRate = m_dInitialLearningRate;
			double mapRadius = sqrt((double)m_vNeurons.size());
			double time = m_iMaxEpoch/ log(mapRadius);
			ptime start = microsec_clock::local_time();
			time_duration total_time;

			size_t sizerows = m_Matrix->GetCRS()->NumberOfRows();
			if( m_pMatrixIndices != NULL )
				sizerows = m_pMatrixIndices->size();

			for(m_iCurrentEpoch = 1; m_iCurrentEpoch <= m_iMaxEpoch; m_iCurrentEpoch++)
			{//for each epoch
				double neighbourhoodRadius = mapRadius * exp( - m_iCurrentEpoch/time );
				double thetaFactor = 2. * neighbourhoodRadius*neighbourhoodRadius;

				size_t *randomIndices = GenerateRanodomIdices();
				for(unsigned int input = 0; input < sizerows; input++)
				{
					Matrices::CompressedVectorPtr row = m_Matrix->GetCRS()->GetCompressedRow(randomIndices[input]);
					int winner = FindBMU(row);

					//XXX should not need to interate through all neurons (performance)
					for(size_t i = 0; i < m_vNeurons.size(); i++)
					{//okreslmy sasiedztwno do zwyciezcy
						double distance2winner = m_vNeurons[winner]->DistanceTo(*m_vNeurons[i]);
						if(distance2winner < neighbourhoodRadius*neighbourhoodRadius)//jest w sasiedztwie!
						{
							double theta = exp( -distance2winner / thetaFactor);
							m_vNeurons[i]->UpdateWeights(row, m_dCurrentLearningRate, theta);
						}
					}

					ptime end = microsec_clock::local_time();
					time_duration len = end-start;
					if(len.total_milliseconds() > 5000)
					{
						total_time += len;
						double avg = total_time.total_milliseconds() / ((m_iCurrentEpoch-1)*sizerows + input+1 );
						double epochAvg = avg*sizerows;
						double eta =  avg*( m_iMaxEpoch*sizerows - (m_iCurrentEpoch-1)*sizerows - input-1);
						m_LogOut << "Epoch: " << m_iCurrentEpoch << " / " << m_iMaxEpoch << " Row: " << input
							<< " / " << sizerows << " ETA: " << eta << ". Epoch avg: " << epochAvg <<". Row avg: "
							<< avg << "           \r";
						m_LogOut.flush();
						start = microsec_clock::local_time();
					}
				}
				delete[] randomIndices;
				m_LogOut << "Epoch: " << m_iCurrentEpoch << " / " << m_iMaxEpoch << std::endl;
				m_dCurrentLearningRate = m_dInitialLearningRate * exp(-(double)m_iCurrentEpoch / (double)m_iMaxEpoch);
			}
			total_time += microsec_clock::local_time()-start;
			m_LogOut << "\nDone in " << total_time << "... Assigning data to neurons... " << std::endl;
			m_LogOut << "\nDone\n";

			AssingDataIndicesToNeurons();
			m_LogOut << "SOM is ready... \n";
		}

		int CSOMNet::FindBMU(Matrices::CompressedVectorPtr  document)
		{
			double min = -9999;
			int winner = -1;
			double sim;
			for(size_t i = 0; i < m_vNeurons.size(); i++)
			{
				sim = m_vNeurons[i]->SimilarityTo(document);
				if( sim > min)
				{
					min = sim;
					winner =(int) i;
				}
			}
			return winner;
		}

		void CSOMNet::SaveState(std::string filename)
		{
			std::ofstream out(filename.c_str());
			if(!out)
				throw std::runtime_error("CSOMNet::SaveState : cannot open file: " + filename);

			// funkcja informacyjna
			out << typeid(*m_SimFunction).name() << std::endl;

			out << m_vNet.size() << " " << m_vNet[0].size()  << " "	<< m_cWeights << std::endl;

			for(size_t row = 0; row < m_vNet.size(); row++)
				for(size_t columnId = 0; columnId < m_vNet[row].size() ; columnId++)
					m_vNet[row][columnId]->SaveState(out);
		}
		void CSOMNet::LoadState(std::string filename)
		{
			std::ifstream in(filename.c_str());
			if(!in)
				throw std::runtime_error("CSOMNet::LoadState : cannot open file: " + filename);

			// wyczszczenie sieci
			for(size_t i = 0; i < m_vNet.size(); i++)
				m_vNet[i].clear();
			m_vNet.clear();
			m_vNeurons.clear();

			std::string line;

			const size_t BUF_LEN = 256;
			char buf[BUF_LEN];

			in.getline(buf, BUF_LEN);
			line=buf;
			boost::algorithm::trim(line);

			if( line != typeid(*m_SimFunction).name() )
				std::cout << "Sim function used earilier: " << line << ". Now using: "
					<<  typeid(*m_SimFunction).name() << std::endl;

			size_t rows, columns;

			in >> rows >> columns >> m_cWeights;

			if(m_cWeights != m_Matrix->NumberOfColumns() )
				throw std::runtime_error(std::string("Number of weights/neuron isn't equal to number of features in given matrix") );

			for(size_t row = 0; row < rows; row++)
			{
				m_vNet.push_back(std::vector< boost::shared_ptr<CSOMNeuron> >());
				for(size_t column = 0; column < columns; column++)
				{
					//wczytanie neuronu
					boost::shared_ptr<CSOMNeuron> neuron(
							new CSOMNeuron(m_cWeights,(double)row,(double)column, m_SimFunction));
					neuron->LoadState(in);
					m_vNeurons.push_back(neuron  );
					m_vNet[row].push_back(neuron);
				}
			}
		}

		void CSOMNet::AssingDataIndicesToNeurons()
		{
			using namespace boost::posix_time;

			for(size_t i = 0; i < m_vNeurons.size() ; i++)
				m_vNeurons[i]->ClearData();

			ptime start = microsec_clock::local_time();
			time_duration total_time;

			//WM: this->sizeRows instead of if?
			size_t sizerows = m_Matrix->GetCRS()->NumberOfRows();
			if( m_pMatrixIndices != NULL )
				sizerows = m_pMatrixIndices->size();

			// przypisz dokumenty
			for(size_t i = 0; i < sizerows ; i++)
			{
				Matrices::CompressedVectorPtr v = m_Matrix->GetCRS()->GetCompressedRow(m_piIndices[i]);

				int winner = FindBMU(v);

				m_vNeurons[winner]->AddDataIndex((int)m_piIndices[i]);
				//WM: vector<size_t> clusters[ m_piIndices[i] ] = m_vNeurons[winner].GetID() (sth like this)


				ptime end = microsec_clock::local_time();
				time_duration len = end-start;
				if(len.total_milliseconds() > 5000)
				{
					total_time += len;
					double avg = total_time.total_milliseconds() / (i+1);
					double eta =  avg*(sizerows - i);

					m_LogOut << i << " / " << sizerows << " ETA: " << eta <<" Avg: " << avg << "       \r";
					m_LogOut.flush();
					start = microsec_clock::local_time();

				}
			}
			m_LogOut << std::endl;
		}


		size_t* CSOMNet::GenerateRanodomIdices()
		{

			size_t sizeRows = m_Matrix->NumberOfRows();
			if( m_pMatrixIndices != NULL )
				sizeRows = m_pMatrixIndices->size();

			size_t * rndIndices = new size_t[sizeRows];

			memcpy(rndIndices, m_piIndices, sizeof(size_t)*sizeRows);

			size_t size = sizeRows/4;

			size_t RANGE_MIN = 0;
			size_t RANGE_MAX = sizeRows-1;

			for (size_t i = 0;    i < size ; i++ )
			{
				size_t rand1 = (size_t)(((double) rand() /
					(double) RAND_MAX) * RANGE_MAX + RANGE_MIN);
				size_t rand2 = (size_t)(((double) rand() /
					(double) RAND_MAX) * RANGE_MAX + RANGE_MIN);

				size_t temp = rndIndices[rand1];
				rndIndices[rand1] = rndIndices[rand2];
				rndIndices[rand2] = temp;

			}
			return rndIndices;
		}


		ClusteringBase::ClusterSetPtr CSOMNet::GetClusters()
		{
			ClusteringBase::ClusterSetPtr set(new ClusteringBase::ClusterSet());
			size_t size = m_vNeurons.size();
			for(size_t i = 0; i < size ; i++)
			{
				ClusteringBase::ClusterPtr cluster(new ClusteringBase::Cluster());

				boost::shared_ptr<std::set<size_t> > data =  m_vNeurons[i]->GetDataIndices();
				for(std::set<size_t>::iterator dataindex = data->begin();
					dataindex != data->end(); dataindex++)
					cluster->AddPoint(*dataindex);
				set->insert(cluster);

			}
			return set;
		}

		std::vector< std::vector<double> >& CSOMNet::CountMQE()
		{
			m_vMQE.clear();
			for(size_t row = 0; row < m_vNet.size(); row++)
			{
				m_vMQE.push_back( std::vector<double>());
				std::vector<boost::shared_ptr<CSOMNeuron> >& neurons = m_vNet[row];
				for(size_t column = 0; column < neurons.size(); column++)
				{
					boost::shared_ptr<CSOMNeuron>& neuron = neurons[column];
					double mqe = 0;
					boost::shared_ptr<std::set<size_t> > data =  neuron->GetDataIndices();

					for(std::set<size_t>::iterator doc = data->begin(); doc != data->end(); doc++)
					{
						Matrices::CompressedVectorPtr document = m_Matrix->GetCRS()->GetCompressedRow(*doc);
						mqe += neuron->SimilarityTo(document);
					}

					if(data->size() == 0)
						mqe = 0;
					else
						mqe = mqe / (double)data->size();

					neuron->SetMQE(mqe);
					m_vMQE[row].push_back(mqe);
				}
			}
			return m_vMQE;
		}

		double CSOMNet::GetMQE0() const
		{
			return m_dMQE0;
		}

		boost::shared_ptr<std::set<size_t> > CSOMNet::GetData(size_t row, size_t column)
		{
			return m_vNet[row][column]->GetDataIndices();

		}

		void CSOMNet::SetParentId(size_t id)
		{
			m_stParentId = id;
		}
		size_t CSOMNet::GetParentId() const
		{
			return m_stParentId;
		}

		size_t CSOMNet::GetNumberOfNeurons() const
		{
			return m_vNet.size();
		}

		void CSOMNet::SetNumberOfNeurons(size_t cneurons)
		{
			CreateNet(cneurons, m_cWeights);
		}

		std::vector< std::vector<boost::shared_ptr<CSOMNeuron> > >& CSOMNet::GetNeurons()
		{
			return m_vNet;
		}

		size_t CSOMNet::GetId() const
		{
			return m_stId;
		}

	}//namespace SOM

}//namespace Clustering

