/* -*- 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/>.
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <errno.h>
#include <string>
#include <cstring>
#include <sstream>
#include <fstream>
#include <istream>

#include <mia/core/file.hh>
#include <mia/core/filter.hh>
#include <mia/core/msgstream.hh>
#include <mia/3d/imageio.hh>


NS_BEGIN(ns_3d_io_vff)
NS_MIA_USE

using namespace std;
using namespace boost;

struct SHeader {
	bool m_valid;
	C3DBounds m_size;
	EPixelType m_pixel_type;
	string m_subject;
	string m_title;
	int m_rank;
	float m_min;
	float m_max;
	float m_elementsize;
	C3DFVector m_spacing;
	SHeader():m_valid(false), 
		  m_pixel_type(it_unknown), 
		  m_rank(0), 
		  m_min(0), 
		  m_max(0), 
		  m_elementsize(0){
	}
};

typedef bool (*FDecode)(istream& is, SHeader& header);

static bool decode_valid(istream& is, SHeader& header);
static bool decode_size(istream& is, SHeader& header);
static bool decode_pixel_type(istream& is, SHeader& header);
static bool decode_title(istream& is, SHeader& header);
static bool decode_subject(istream& is, SHeader& header);
static bool decode_min(istream& is, SHeader& header);
static bool decode_rank(istream& is, SHeader& header);
static bool decode_max(istream& is, SHeader& header);
static bool decode_spacing(istream& is, SHeader& header);
static bool decode_elementsize(istream& is, SHeader& header);

struct SDecodeMap {
	const char *key;
	FDecode callback;
};

SDecodeMap decode_map[] = {
	{"ncaa", decode_valid},
	{"subject", decode_subject},
	{"title", decode_title},
	{"rank", decode_rank},
	{"type",  NULL },
	{"size", decode_size},
	{"origin", NULL},
	{"y_bin", NULL},
	{"z_bin", NULL},
	{"bands", NULL},
	{"bits", decode_pixel_type},
	{"format",NULL},
	{"date", NULL},
	{"center_of_rotation",NULL},
	{"central_slice",NULL},
	{"rfan_y", NULL},
	{"rfan_z",NULL},
	{"angle_increment", NULL},
	{"reverse_order", NULL},
	{"min", decode_min},
	{"max", decode_max},
	{"spacing",  decode_spacing},
	{"elementsize",decode_elementsize},
	{"water", NULL},
	{"air", NULL},
	{NULL, NULL}
};


static bool decode_valid(istream& /*is*/, SHeader& /*header*/)
{
	return true;
}

static bool decode_size(istream& is, SHeader& header)
{
	is >> header.m_size.x >> header.m_size.y >> header.m_size.z;
	cvdebug() << "Got size " << header.m_size << '\n';
	return !(is.rdstate() & (ifstream::failbit | ifstream::badbit));
}

static bool decode_pixel_type(istream& is, SHeader& header)
{
	int pixel_size = 0;
	is >> pixel_size;
	if (pixel_size == 16)
		header.m_pixel_type = it_sshort;
	else if (pixel_size == 8)
		header.m_pixel_type = it_ubyte;
	else
		return false;
	cvdebug() << "pixel size = " << pixel_size << " type= " << header.m_pixel_type << "\n";
	return true;
}

#define DECODE(NAME) static bool decode_##NAME(istream& is, SHeader& header) \
	{\
		is >> header.m_##NAME; \
		return !(is.rdstate() & (ifstream::failbit | ifstream::badbit));  \
	}

DECODE(title);
DECODE(subject);
DECODE(min);
DECODE(max);
DECODE(rank);
DECODE(elementsize);

#undef DECODE


static bool decode_spacing(istream& is, SHeader& header)
{
	is >> header.m_spacing.x >> header.m_spacing.y >> header.m_spacing.z;
	return !(is.rdstate() & (ifstream::failbit | ifstream::badbit));
}

class CVFF3DImageIO: public C3DImageIOPlugin {
public:
	CVFF3DImageIO();
private:
	virtual PData do_load(string const&  filename)const;
	virtual bool do_save(string const&  filename, const C3DImageVector& data) const;
	virtual const string do_get_descr() const;
	bool store_info(const char *key, const char *value, SHeader& h) const;
};

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


CVFF3DImageIO::CVFF3DImageIO():
	C3DImageIOPlugin("vff")
{
//	add_property(io_plugin_property_canpipe);
	add_supported_type(it_sshort);
	add_supported_type(it_ubyte);
	add_suffix(".vff");
	add_suffix(".VFF");
}


bool CVFF3DImageIO::store_info(const char *key, const char *value, SHeader& h) const
{
	const SDecodeMap *dm = decode_map;
	while (dm->key) {
		if (!strcmp(dm->key, key)) {
			if( dm->callback ) {
				istringstream s(value);
				return dm->callback(s, h);
			}
			else
				cvdebug() << "Ignoring key '" << key << "' with value '" << value << "\n";
		}
		++dm;
	}
	cvwarn() << "Unknown key '" << key << "' with value '" << value << "\n";
	return true;
}

template <typename T>
static bool load_image(T3DImage<T>& image, C3DFVector voxel, CInputFile& f)
{
	image.set_voxel_size(voxel);

	long load_size =  image.size();

	long read = fread(&image(0,0,0), sizeof(T), load_size, f);
	return read == load_size;
}

static void swap(char& a, char& b)
{
	char c = a;
	a = b;
	b = c;
}


static bool low_endian()
{
	typedef union {
		char c[2];
		short s;
	} helpme;

	helpme test;

	test.s = 0x0001;
	return test.c[0];
}

template <class Iterator>
void handle_endian(Iterator b, Iterator e)
{

	typedef typename iterator_traits<Iterator>::value_type Pixel;
	cvdebug() << "fix endianness\n";


	typedef union  {
		char s[sizeof(Pixel)];
		Pixel v;
	} shuffle;



	switch (sizeof(Pixel)) {
	case 2:
		cvdebug() << "Endian correct 16 bit data\n";
		while (b != e) {
			shuffle s;
			s.v = *b;
			swap(s.s[0], s.s[1]);
			*b = s.v;
			++b;
		}
		break;
	case 4:
		cvdebug() << "Endian correct 32 bit data\n";
		while (b != e) {
			shuffle s;
			s.v = *b;
			swap(s.s[0], s.s[3]);
			swap(s.s[1], s.s[2]);
			*b = s.v;
			++b;
		}
		break;
	case 8:
		cvdebug() << "Endian correct 64 bit data\n";
		while (b != e) {

			shuffle s;
			s.v = *b;

			swap(s.s[0],s.s[7]);
			swap(s.s[1],s.s[6]);
			swap(s.s[2],s.s[5]);
			swap(s.s[3],s.s[4]);

			*b = s.v;

			++b;
		}
		break;
	default:
		cvdebug() << "8 bit data, no correction needed\n";
	}
}

CVFF3DImageIO::PData CVFF3DImageIO::do_load(string const&  filename)const
{
	cvdebug() << "Try vff image type\n";

	CInputFile f(filename);
	if (!f){
		perror(filename.c_str());
		return PData();
	}

	char buffer[256];

	if (fgets(buffer, 256, f) == NULL) {
		perror(filename.c_str());
		return PData();
	}

	if (strncmp(buffer, "ncaa", 4))
		return PData();

	SHeader header;
	header.m_elementsize = 1.0;

	while (fgets(buffer, 256, f)) {
		cvdebug() << "Read: " << buffer << '\n';

		char *split = strchr(buffer, '=');
		if (split) {
			*split = 0;
			++split;
			char *end = strchr(split, ';');
			if (!end)
				throw invalid_argument(string("vff-load:Bogous header line in " + filename));

			*end = 0;

			if (!store_info(buffer, split, header)) {
				throw invalid_argument(string("vff-load:Read component '") 
						       + string(split)  + string("' failed"));
			}
		}else{
			if (buffer[0] != 0xC)
				throw invalid_argument(string("vff-load:Bogus header line:") + buffer);
			else
				break;
		}
	}

	if (header.m_rank != 3)
		throw invalid_argument(string("vff-load:Unsupported image type"));

	header.m_spacing *= header.m_elementsize;

	PData result(new C3DImageVector());

	switch (header.m_pixel_type) {
	case it_ubyte:{
		C3DUBImage *image = new C3DUBImage(header.m_size);
		std::shared_ptr<C3DImage > pimage(image);
		if (load_image(*image, header.m_spacing, f))
			result->push_back(pimage);
	}break;
	case it_sshort:{
		C3DSSImage *image = new C3DSSImage(header.m_size);
		std::shared_ptr<C3DImage > pimage(image);
		if (load_image(*image, header.m_spacing, f)) {
			if (low_endian())
				handle_endian(image->begin(), image->end());
			result->push_back(pimage);
		}
	}break;
	default:
		throw invalid_argument(string("vff-load: input format not implemented"));
	}

	if (result->size() > 0)
		return result;

	return PData();
}

template <typename T>
struct save_dispatch {
	static bool apply(const T3DImage<T>& image, COutputFile& f) {

		C3DFVector voxel = image.get_voxel_size();

		stringstream os;
		os << "ncaa;" << "\n";

		os << "size="
		   << image.get_size().x << " "
		   << image.get_size().y << " "
		   << image.get_size().z << ";\n";

		os << "rank=3;\n";
		os << "spacing=" << voxel.x << " "
		   << voxel.y << " "
		   << voxel.z << ";\n";

		os << "bits=" << 8 * sizeof(T) << ";\n";

		// end of header
		os << "\xC\n";

		if (fwrite(os.str().c_str(), os.str().size(), 1 , f) != 1) {
			perror("CVFFSaver");
			return false;
		}

		// endianness handling
		if (low_endian() && sizeof(T) > 1 ) {
			vector<T> v(image.size());
			copy(image.begin(), image.end(), v.begin());
			handle_endian(v.begin(), v.end());
			const T& vp = v[0];
			if (fwrite(&vp, sizeof(T), v.size(), f) != v.size()) {
				perror("CVFFSaver");
				return false;
			}
		}else if (fwrite(&image(0,0,0),sizeof(T), image.size(), f) != image.size()) {
			perror("CVFFSaver");
			return false;
		}
		return true;
	}
};

template <>
struct save_dispatch<bool> {
	static bool apply(const T3DImage<bool>& /*image*/, COutputFile& /*f*/) {
		throw invalid_argument("VFF doesn't support bit images");
	}
};

class CVFFSaver: public TFilter<bool> {
public:
	CVFFSaver(COutputFile& f):
		m_f(f)
	{
	}

	template <class T>
	bool operator ()(const T3DImage<T>& image) const
	{
		return save_dispatch<T>::apply(image, m_f);
	}

private:
	COutputFile& m_f;
};

bool CVFF3DImageIO::do_save(string const&  filename, const C3DImageVector& data)const
{
	if (data.size() != 1) {
		cverr() << "CVFF3DImageIO::save: support only 1 image\n";
		return false;
	}

	COutputFile f(filename);

	if (!f) {
		cverr() << filename << ":" << strerror(errno) << "\n";
		return false;
	}

	CVFFSaver s(f);

	return filter(s, **data.begin());
}

const string  CVFF3DImageIO::do_get_descr()const
{
	return string("VFF Sun raster format");
}

NS_END
