/* -*- mia-c++  -*-
 *
 * This file is part of MIA - a toolbox for medical image analysis 
 * Copyright (c) Leipzig, Madrid 1999-2017 Gert Wollny
 *
 * MIA is free software; you can redistribute it and/or modify
 * it under the terms of the GNU 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
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with MIA; if not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <mia/3d/filter/thinning.hh>
#include <mia/core/errormacro.hh>

using namespace thinning_3dimage_filter; 
using namespace mia; 

using std::vector; 
using std::invalid_argument; 
using std::ostream; 

C3DThinning::C3DThinning()
{
}

class CNeighborhood {
public: 
	CNeighborhood(const C3DBitImage& input, const C3DBounds& pos); 
	
	bool is_candidate(int dir)const; 

	bool is_simple_pixel()const; 

	void print(ostream& os) const; 
	
private: 
	bool is_boundary(int dir) const; 
	
	bool is_Euler_invariant() const; 

	static void ot_label1(char label, vector<char>& cube); 
	static void ot_label2(char label, vector<char>& cube); 
	static void ot_label3(char label, vector<char>& cube); 
	static void ot_label4(char label, vector<char>& cube); 
	static void ot_label5(char label, vector<char>& cube); 
	static void ot_label6(char label, vector<char>& cube); 
	static void ot_label7(char label, vector<char>& cube); 
	static void ot_label8(char label, vector<char>& cube); 

	typedef void (*FLabelFunction)(char label, vector<char>& cube); 
	static const FLabelFunction label_callback[26];
	
	vector<char> m_pixels;
	int m_nneigbors; 
	static const int m_direction_index[6]; 

	static const signed char eulerLUT_26n[128]; 
}; 

ostream& operator << (ostream& os, const CNeighborhood& n)
{
	n.print(os); 
	return os; 
}

const CNeighborhood::FLabelFunction CNeighborhood::label_callback[26]  = {
	ot_label1, ot_label1, ot_label2, // 2 
	ot_label1, ot_label1, ot_label2, // 5 
	ot_label3, ot_label3, ot_label4, // 8 
	
	ot_label1, ot_label1, ot_label2, // 11 
	ot_label1,/*center,*/ ot_label2, // 13
	ot_label3, ot_label3, ot_label4, // 16
	
	ot_label5, ot_label5, ot_label6, // 19
	ot_label5, ot_label5, ot_label6, // 22
	ot_label7, ot_label7, ot_label8  // 25
}; 

const int CNeighborhood::m_direction_index[6] = {10, 15, 13, 12, 21, 4 }; 

static void thinning_run(C3DBitImage& input) 
{
	int pixels_changed; 

	do {
		pixels_changed = 0; 
		for (int border = 0; border < 6; ++border) {

			vector<C3DBitImage::range_iterator> candidate_pixels; 
			for (auto i = input.begin_range(C3DBounds::_0,input.get_size()); 
			     i != input.end_range(C3DBounds::_0,input.get_size()); ++i) {

				// background? 
				if (!*i) 
					continue; 
				
				CNeighborhood neighbours(input, i.pos()); 
				if (!neighbours.is_candidate(border))
					continue; 
				
				candidate_pixels.push_back(i);
			}
			// this two-step procedure is only needed if the candidate collection is done 
			// in parallel 
			for (auto c = candidate_pixels.begin(); c != candidate_pixels.end(); ++c) {
				CNeighborhood n(input, c->pos()); 
				if (n.is_simple_pixel()) {
					**c = false;
					++pixels_changed; 
				}
			}
			cvinfo() << candidate_pixels.size() << "\n"; 
		}
		cvdebug() << "Pixels changed:" << pixels_changed << "\n"; 
		
	} while (pixels_changed > 0); 

}


mia::C3DFilter::result_type C3DThinning::do_filter(const mia::C3DImage& image) const
{
	if (image.get_pixel_type() != it_bit)  {
		throw create_exception<invalid_argument>("C3DThinning: only binary images are supported, "
							 "but pixel format is '", 
							 CPixelTypeDict.get_name(image.get_pixel_type()), "'"); 

	}
	auto result = image.clone(); 
	auto bit_image = dynamic_cast<C3DBitImage&>(*result); 
	thinning_run(bit_image); 
	return bit_image.clone(); 
}



C3DThinningFilterPlugin::C3DThinningFilterPlugin():
	C3DFilterPlugin("thinning")
{
}


mia::C3DFilter *C3DThinningFilterPlugin::do_create()const
{
	return new C3DThinning(); 
}

const std::string C3DThinningFilterPlugin::do_get_descr()const
{
	return "3D morphological thinning,  "
		"based on: Lee and Kashyap, 'Building Skeleton Models "
		"via 3-D Medial Surface/Axis Thinning Algorithms', "
		"Graphical Models and Image Processing, 56(6):462-478, 1994. "
		"This implementation only supports the 26 neighbourhood.";
}

extern "C" EXPORT CPluginBase *get_plugin_interface()
{
	return new C3DThinningFilterPlugin(); 
}


CNeighborhood::CNeighborhood(const C3DBitImage& input, const C3DBounds& pos):
	m_pixels(26), 
	m_nneigbors(-1)
{
	vector<char> help(27); 
	auto i = help.begin(); 
	for(int z = -1; z < 2; ++z) {
		unsigned int iz = pos.z + static_cast<unsigned int>(z);
		for(int y = -1; y < 2; ++y) {
			unsigned int iy = pos.y + static_cast<unsigned int>(y);
			for(int x = -1; x < 2; ++x) {
				unsigned int ix = pos.x + static_cast<unsigned int>(x);
				if (input(ix,iy,iz)) {
					*i = 1; 
					++m_nneigbors; 
				}
				++i; 
			}
		}
	}

	copy(help.begin(), help.begin()+13, m_pixels.begin()); 
	copy(help.begin()+14, help.end(), m_pixels.begin()+13); 
}

bool CNeighborhood::is_candidate(int dir)const
{
	return (m_nneigbors > 1) && is_boundary(dir) && is_Euler_invariant() && is_simple_pixel(); 
}

bool CNeighborhood::is_boundary(int dir) const
{
	return !m_pixels[m_direction_index[dir]]; 
}

bool CNeighborhood::is_Euler_invariant() const
{
	signed char EulerChar = 0;
	unsigned int n; 
	
	// octant south-west-upper
	n = m_pixels[23] * 64 +  m_pixels[24] * 32 +  m_pixels[14] * 16 +
		m_pixels[15]  * 8 + m_pixels[20] * 4 + m_pixels[21] * 2 + m_pixels[12];
	EulerChar += eulerLUT_26n[n];

	// octant south-east-upper
	n = m_pixels[25] * 64 +  m_pixels[22] * 32 + m_pixels[16]  * 16 + 
		m_pixels[13] * 8 + m_pixels[24] * 4 + m_pixels[21] * 2 + m_pixels[15]; 
	EulerChar += eulerLUT_26n[n];
	
	// octant north-west-upper
	n = m_pixels[17] * 64 +  m_pixels[20] * 32 + m_pixels[ 9]  * 16 + 
		m_pixels[12] * 8 + m_pixels[18] * 4 + m_pixels[21] * 2 + m_pixels[10]; 
	EulerChar += eulerLUT_26n[n];

       	// octant north-east-upper
	n = m_pixels[19] * 64 +  m_pixels[22] * 32 + m_pixels[18]  * 16 + 
		m_pixels[21] * 8 + m_pixels[11] * 4 + m_pixels[13] * 2 + m_pixels[10]; 
	EulerChar += eulerLUT_26n[n];

        // octant south-west-below
	n = m_pixels[ 6] * 64 +  m_pixels[14] * 32 + m_pixels[7]  * 16 + 
		m_pixels[15] * 8 + m_pixels[ 3] * 4 + m_pixels[12] * 2 + m_pixels[4]; 
	EulerChar += eulerLUT_26n[n];

        // octant south-east-below
	n = m_pixels[ 8] * 64 +  m_pixels[ 7] * 32 + m_pixels[16]  * 16 + 
		m_pixels[15] * 8 + m_pixels[ 5] * 4 + m_pixels[ 4] * 2 + m_pixels[13]; 
	EulerChar += eulerLUT_26n[n];

	// octant north-west-below
	n = m_pixels[ 0] * 64 +  m_pixels[ 9] * 32 + m_pixels[ 3]  * 16 + 
		m_pixels[12] * 8 + m_pixels[ 1] * 4 + m_pixels[10] * 2 + m_pixels[4]; 
	EulerChar += eulerLUT_26n[n];

        // octant north-east-below
	n = m_pixels[ 2] * 64 +  m_pixels[ 1] * 32 + m_pixels[11]  * 16 + 
		m_pixels[10] * 8 + m_pixels[ 5] * 4 + m_pixels[4] * 2 + m_pixels[13]; 
	EulerChar += eulerLUT_26n[n];

	return EulerChar == 0; 
}

const signed char CNeighborhood::eulerLUT_26n[128] = {
	  1,  -1,  -1,   1, 
	 -3,  -1,  -1,   1, 
	 -1,   1,   1,  -1, 
	  3,   1,   1,  -1, 
	 -3,  -1,   3,   1, 
	  1,  -1,   3,   1, 
	 -1,   1,   1,  -1, 
	  3,   1,   1,  -1, 

	 -3,   3,  -1,   1, 
	  1,   3,  -1,   1, 
	 -1,   1,   1,  -1, 
	  3,   1,   1,  -1, 
	  1,   3,   3,   1, 
	  5,   3,   3,   1, 
	 -1,   1,   1,  -1, 
	  3,   1,   1,  -1, 

	 -7,  -1,  -1,   1, 
	 -3,  -1,  -1,   1, 
	 -1,   1,   1,  -1, 
	  3,   1,   1,  -1, 
	 -3,  -1,   3,   1, 
	  1,  -1,   3,   1, 
	 -1,   1,   1,  -1, 
	  3,   1,   1,  -1, 

	 -3,   3,  -1,   1, 
	  1,   3,  -1,   1, 
	 -1,   1,   1,  -1, 
	  3,   1,   1,  -1, 
	  1,   3,   3,   1, 
	  5,   3,   3,   1, 
	 -1,   1,   1,  -1, 
	  3,   1,   1,  -1
}; 

bool CNeighborhood::is_simple_pixel()const
{
	vector<char> cube(m_pixels); 
	char label = 2;
	
	for(int i = 0; i < 26; i++ ) {
		if (cube[i] != 1)
			continue; 
		
		label_callback[i](label, cube); 
		++label; 
		if( label-2 >= 2 )
			return false;

	}
	return true; 
}

void CNeighborhood::ot_label1(char label, vector<char>& cube)
{
	if( cube[0] == 1 )
		cube[0] = label;

	if( cube[1] == 1 ) {
		cube[1] = label;        
		ot_label2(label, cube);
	}

	if( cube[3] == 1 )  {
		cube[3] = label;        
		ot_label3(label, cube);
	}

	if( cube[4] == 1 )  {
		cube[4] = label;        
		ot_label2(label, cube);
		ot_label3(label, cube);
		ot_label4(label, cube);
	}

	if( cube[9] == 1 )   {
		cube[9] = label;        
		ot_label5(label, cube);
	}

	if( cube[10] == 1 ) {
		cube[10] = label;        
		ot_label2(label, cube);
		ot_label5(label, cube);
		ot_label6(label, cube);
	}

	if( cube[12] == 1 )  {
		cube[12] = label;        
		ot_label3(label, cube);
		ot_label5(label, cube);
		ot_label7(label, cube);
	}
}
 
void CNeighborhood::ot_label2(char label, vector<char>& cube)
{
	if( cube[1] == 1 ) {
		cube[1] = label;
		ot_label1(label, cube);
	}

	if( cube[4] == 1 ) {
		cube[4] = label;        
		ot_label1(label, cube);
		ot_label3(label, cube);
		ot_label4(label, cube);
	}
	if( cube[10] == 1 ) {
		cube[10] = label;        
		ot_label1(label, cube);
		ot_label5(label, cube);
		ot_label6(label, cube);
	}

	if( cube[2] == 1 )
		cube[2] = label;        

	if( cube[5] == 1 )  {
		cube[5] = label;        
		ot_label4(label, cube);
	}

	if( cube[11] == 1 ) {
		cube[11] = label;        
		ot_label6(label, cube);
	}

	if( cube[13] == 1 )  {
		cube[13] = label;        
		ot_label4(label, cube);
		ot_label6(label, cube);
		ot_label8(label, cube);
	}

}

void CNeighborhood::ot_label3(char label, vector<char>& cube)
{
		
	if( cube[3] == 1 ) {
		cube[3] = label;        
		ot_label1(label, cube);
	}

	if( cube[4] == 1 ) {
		cube[4] = label;        
		ot_label1(label, cube);
		ot_label2(label, cube);
		ot_label4(label, cube);
	}

	if( cube[12] == 1 ) {
		cube[12] = label;        
		ot_label1(label, cube);
		ot_label5(label, cube);
		ot_label7(label, cube);
	}

	if( cube[6] == 1 )
		cube[6] = label;        

	if( cube[7] == 1 )  {
		cube[7] = label;        
		ot_label4(label, cube);
	}

	if( cube[14] == 1 ){
		cube[14] = label;        
		ot_label7(label, cube);
	}

	if( cube[15] == 1 )
	{
		cube[15] = label;        
		ot_label4(label, cube);
		ot_label7(label, cube);
		ot_label8(label, cube);
	}

}

void CNeighborhood::ot_label4(char label, vector<char>& cube)
{
	if( cube[4] == 1 ) {
		cube[4] = label;        
		ot_label1(label, cube);
		ot_label2(label, cube);
		ot_label3(label, cube);
	}

	if( cube[5] == 1 ) {
		cube[5] = label;        
		ot_label2(label, cube);
	}

	if( cube[13] == 1 ) {
		cube[13] = label;        
		ot_label2(label, cube);
		ot_label6(label, cube);
		ot_label8(label, cube);
	}

	if( cube[7] == 1 ) {
		cube[7] = label;        
		ot_label3(label, cube);
	}

	if( cube[15] == 1 ) {
		cube[15] = label;        
		ot_label3(label, cube);
		ot_label7(label, cube);
		ot_label8(label, cube);
	}

	if( cube[8] == 1 )
		cube[8] = label;        

	if( cube[16] == 1 ) {
		cube[16] = label;        
		ot_label8(label, cube);
	}

}

void CNeighborhood::ot_label5(char label, vector<char>& cube)
{
	if( cube[9] == 1 ) {
		cube[9] = label;        
		ot_label1(label, cube);
	}

	if( cube[10] == 1 ) {
		cube[10] = label;        
		ot_label1(label, cube);
		ot_label2(label, cube);
		ot_label6(label, cube);
	}

	if( cube[12] == 1 ) {
		cube[12] = label;        
		ot_label1(label, cube);
		ot_label3(label, cube);
		ot_label7(label, cube);
	}

	if( cube[17] == 1 )
		cube[17] = label;        

	if( cube[18] == 1 ) {
		cube[18] = label;        
		ot_label6(label, cube);
	}

	if( cube[20] == 1 ) {
		cube[20] = label;        
		ot_label7(label, cube);
	}

	if( cube[21] == 1 ){
		cube[21] = label;        
		ot_label6(label, cube);
		ot_label7(label, cube);
		ot_label8(label, cube);
	}

}

void CNeighborhood::ot_label6(char label, vector<char>& cube)
{
	if( cube[10] == 1 ) {
		cube[10] = label;        
		ot_label1(label, cube);
		ot_label2(label, cube);
		ot_label5(label, cube);
	}

	if( cube[11] == 1 ) {
		cube[11] = label;        
		ot_label2(label, cube);
	}

	if( cube[13] == 1 ) {
		cube[13] = label;        
		ot_label2(label, cube);
		ot_label4(label, cube);
		ot_label8(label, cube);
	}

	if( cube[18] == 1 ) {
		cube[18] = label;        
		ot_label5(label, cube);
	}

	if( cube[21] == 1 ) {
		cube[21] = label;        
		ot_label5(label, cube);
		ot_label7(label, cube);
		ot_label8(label, cube);
	}

	if( cube[19] == 1 )
		cube[19] = label;        

	if( cube[22] == 1 ) {
		cube[22] = label;        
		ot_label8(label, cube);
	}

}

void CNeighborhood::ot_label7(char label, vector<char>& cube)
{
	if( cube[12] == 1 ) {
		cube[12] = label;        
		ot_label1(label, cube);
		ot_label3(label, cube);
		ot_label5(label, cube);
	}

	if( cube[14] == 1 ) {
		cube[14] = label;        
		ot_label3(label, cube);
	}

	if( cube[15] == 1 ) {
		cube[15] = label;        
		ot_label3(label, cube);
		ot_label4(label, cube);
		ot_label8(label, cube);
	}

	if( cube[20] == 1 ) {
		cube[20] = label;        
		ot_label5(label, cube);
	}

	if( cube[21] == 1 ) {
		cube[21] = label;        
		ot_label5(label, cube);
		ot_label6(label, cube);
		ot_label8(label, cube);
	}

	if( cube[23] == 1 )
		cube[23] = label;        

	if( cube[24] == 1 ) {
		cube[24] = label;        
		ot_label8(label, cube);
	}

}

void CNeighborhood::ot_label8(char label, vector<char>& cube)
{
	if( cube[13] == 1 ) {
		cube[13] = label;        
		ot_label2(label, cube);
		ot_label4(label, cube);
		ot_label6(label, cube);
	}

	if( cube[15] == 1 ) {
		cube[15] = label;        
		ot_label3(label, cube);
		ot_label4(label, cube);
		ot_label7(label, cube);
	}
 
	if( cube[16] == 1 ) {
		cube[16] = label;        
		ot_label4(label, cube);
	}

	if( cube[21] == 1 ) {
		cube[21] = label;        
		ot_label5(label, cube);
		ot_label6(label, cube);
		ot_label7(label, cube);
	}

	if( cube[22] == 1 ) {
		cube[22] = label;        
		ot_label6(label, cube);
	}

	if( cube[24] == 1 ){
		cube[24] = label;        
		ot_label7(label, cube);
	}

	if( cube[25] == 1 )
		cube[25] = label;        

}


void CNeighborhood::print(ostream& os) const
{
	static const char pixel_identifier[27] = "****B*****N*WO*S*****U****";

	for(int i = 0; i < 13; i++ ) 
		os << (m_pixels[i] ? pixel_identifier[i]  : '.'); 
	os << "X"; 
	for(int i = 13; i < 26; i++ ) 
		os << (m_pixels[i] ? pixel_identifier[i]  : '.'); 
}
