/* -*- 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 <vtk/vtkimage.hh>

#include <vtkDataArray.h>
#include <vtkPointData.h>
#include <vtkBitArray.h>
#include <vtkSignedCharArray.h>
#include <vtkUnsignedCharArray.h>
#include <vtkShortArray.h>
#include <vtkUnsignedShortArray.h>
#include <vtkIntArray.h>
#include <vtkUnsignedIntArray.h>

#ifdef LONG_64BIT
#include <vtkLongArray.h>
#include <vtkUnsignedLongArray.h>
#endif
#include <vtkFloatArray.h>
#include <vtkDoubleArray.h>

#include <vtkXMLImageDataReader.h>
#include <vtkXMLImageDataWriter.h>

#include <vtkDataSetWriter.h>
#include <vtkDataSetReader.h>

#include <vtkMetaImageReader.h>
#include <vtkMetaImageWriter.h>

#include <vtkImageData.h>
#include <vtkSmartPointer.h>
#include <vtkPoints.h>

#include <type_traits>

using namespace mia;
using namespace std;
using namespace vtkimage;

// LONG_64BIT seems to be buggy in vtkDataReader
#ifdef LONG_64BIT
#undef LONG_64BIT
#endif

template <typename T>
struct __vtk_data_array {
       typedef void type;
       typedef false_type supported;
       \
};


#define VTK_ARRAY_TRANSLATE(TYPE, VTK_TYPE, ID)	\
	template <>			    \
	struct __vtk_data_array<TYPE> {	    \
		typedef VTK_TYPE type;	    \
		typedef true_type supported; \
		static const int value = ID; \
	};


#if  VTK_MAJOR_VERSION < 7
VTK_ARRAY_TRANSLATE(bool, vtkBitArray, VTK_BIT);
#endif

VTK_ARRAY_TRANSLATE(int8_t, vtkSignedCharArray, VTK_SIGNED_CHAR);
VTK_ARRAY_TRANSLATE(uint8_t, vtkUnsignedCharArray, VTK_UNSIGNED_CHAR);
VTK_ARRAY_TRANSLATE(int16_t, vtkShortArray, VTK_SHORT);
VTK_ARRAY_TRANSLATE(uint16_t, vtkUnsignedShortArray, VTK_UNSIGNED_SHORT);
VTK_ARRAY_TRANSLATE(int32_t, vtkIntArray, VTK_INT);
VTK_ARRAY_TRANSLATE(uint32_t, vtkUnsignedIntArray, VTK_UNSIGNED_INT);

#ifdef LONG_64BIT
VTK_ARRAY_TRANSLATE(int64_t, vtkLongArray, VTK_LONG);
VTK_ARRAY_TRANSLATE(uint64_t, vtkUnsignedLongArray, VTK_UNSIGNED_LONG);
#endif

VTK_ARRAY_TRANSLATE(float, vtkFloatArray, VTK_FLOAT);
VTK_ARRAY_TRANSLATE(double, vtkDoubleArray, VTK_DOUBLE);


template <typename T>
struct read_image {
       static C3DImage *apply(const C3DBounds& size, void *scalars)
       {
              cvdebug() << "VTK/MetaIO read image of type " <<  __type_descr<T>::value << "\n";
              const T *my_scalars = reinterpret_cast<const T *>(scalars);

              if (!my_scalars)
                     throw create_exception<logic_error>("CVtk3DImageIOPlugin::load: input image scalar type bogus");

              T3DImage<T> *result = new  T3DImage<T>(size);
              copy(my_scalars, my_scalars + result->size(), result->begin());
              return result;
       }
};

#if  VTK_MAJOR_VERSION < 7
template <>
struct read_image<bool> {
       static C3DImage *apply(const C3DBounds& size, void *scalars)
       {
              cvdebug() << "VTK/MetaIO read image of type " <<  __type_descr<bool>::value << "\n";
              const unsigned char *my_scalars = reinterpret_cast<const unsigned char *>(scalars);

              if (!my_scalars)
                     throw create_exception<logic_error>("CVtk3DImageIOPlugin::load: input image scalar type bogus");

              T3DImage<bool> *result = new  T3DImage<bool>(size);
              auto i = result->begin();
              auto e = result->end();

              while (i != e) {
                     unsigned char obyte = *my_scalars++;
                     unsigned char mask = 0x80;

                     for (int pos = 0; pos < 8 && i != e; ++i, ++pos, mask >>= 1) {
                            if (obyte & mask)
                                   *i = true;
                     }
              }

              return result;
       }
};
#endif

static C3DImage *image_vtk_to_mia(vtkImageData *vtk_image, const string& fname)
{
       int dim = vtk_image->GetDataDimension();

       if (dim != 3)
              throw create_exception<invalid_argument>("3D Vtk/MetaImageIO load (", fname,
                            "): Expect 3 dimensions but got ", dim);

       int components = vtk_image->GetNumberOfScalarComponents();

       if (components != 1)
              throw create_exception<invalid_argument>("3D Vtk/MetaImageIO load (", fname,
                            "): only scalar pixel values are allowed, "
                            "but got ", components, " components");

       auto dims = vtk_image->GetDimensions();
       C3DBounds size(dims[0], dims[1], dims[2]);
       auto array = vtk_image->GetScalarPointer();
       C3DImage *result_image = nullptr;

       switch 	 (vtk_image->GetScalarType()) {
#if  VTK_MAJOR_VERSION < 7

       case VTK_BIT:
              result_image = read_image<bool>::apply(size, array);
              break;
#endif

       case VTK_SIGNED_CHAR:
              result_image = read_image<int8_t>::apply(size, array);
              break;

       case VTK_UNSIGNED_CHAR:
              result_image = read_image<uint8_t>::apply(size, array);
              break;

       case VTK_SHORT:
              result_image = read_image<int16_t>::apply(size, array);
              break;

       case VTK_UNSIGNED_SHORT:
              result_image = read_image<uint16_t>::apply(size, array);
              break;

       case VTK_INT:
              result_image = read_image<int32_t>::apply(size, array);
              break;

       case VTK_UNSIGNED_INT:
              result_image = read_image<uint32_t>::apply(size, array);
              break;
#ifdef LONG_64BIT

       case VTK_LONG:
              result_image = read_image<int64_t>::apply(size, array);
              break;

       case VTK_UNSIGNED_LONG:
              result_image = read_image<uint64_t>::apply(size, array);
              break;
#endif

       case VTK_FLOAT:
              result_image = read_image<float>::apply(size, array);
              break;

       case VTK_DOUBLE:
              result_image = read_image<double>::apply(size, array);
              break;

       default:
              throw create_exception<invalid_argument>("3D Vtk/MetaImageIO load (", fname, "): "
                            "data type ", vtk_image->GetScalarTypeAsString(),
                            "(", vtk_image->GetScalarType(), ") not supported");
       }

       double sp[3];
       vtk_image->GetSpacing (sp);
       result_image->set_voxel_size(C3DFVector(sp[0], sp[1], sp[2]));
       vtk_image->GetOrigin (sp);
       result_image->set_origin(C3DFVector(sp[0], sp[1], sp[2]));
       return result_image;
}


template <typename T, typename supported>
struct __dispatch_convert {
       static void  apply(vtkImageData */*output*/, const T3DImage<T>& /*input*/)
       {
              throw create_exception<invalid_argument>("CVtk3DImageIOPlugin: no support saving '",
                            __type_descr<T>::value, "' pixel values");
       }
};

template <typename T>
struct __dispatch_convert<T, true_type> {
       static void  apply (vtkImageData *output, const T3DImage<T>& input)
       {
              cvdebug() << "Input is an image of pixel type " << __type_descr<T>::value << "\n";
#if  VTK_MAJOR_VERSION < 6
              output->SetScalarType(__vtk_data_array<T>::value);
              output->SetNumberOfScalarComponents(1);
              output->AllocateScalars();
#else
              output->AllocateScalars(__vtk_data_array<T>::value, 1);
#endif
              T *out_ptr =  reinterpret_cast<T *>(output->GetScalarPointer());
              copy(input.begin(), input.end(), out_ptr);
       }
};

#if  VTK_MAJOR_VERSION < 7
template <>
struct __dispatch_convert<bool, true_type> {
       static void  apply (vtkImageData *output, const T3DImage<bool>& input)
       {
              cvdebug() << "Input is an image of pixel type bool\n";
#if  VTK_MAJOR_VERSION < 6
              output->SetScalarType(__vtk_data_array<bool>::value);
              output->SetNumberOfScalarComponents(1);
              output->AllocateScalars();
#else
              output->AllocateScalars(__vtk_data_array<bool>::value, 1);
#endif
              unsigned char *out_ptr =  reinterpret_cast<unsigned char *>(output->GetScalarPointer());
              auto i = input.begin();
              auto e = input.end();
              int pos = 0;
              unsigned char mask = 0x80;
              unsigned char obyte = 0;

              while (i != e) {
                     if (*i)
                            obyte |= mask;

                     ++pos;
                     mask >>= 1;

                     if (pos == 8) {
                            *out_ptr++ = obyte;
                            obyte = 0;
                            mask = 0x80;
                            pos = 0;
                     }

                     ++i;
              }

              if ( pos > 0) {
                     *out_ptr = obyte;
              }
       }
};
#endif

class FSetArray: public TFilter<void>
{
public:
       FSetArray(vtkImageData *output): m_output(output) {}

       template <typename T>
       void  operator () (const T3DImage<T>& input) const
       {
              typedef typename __vtk_data_array<T>::supported supported;
              __dispatch_convert<T, supported>::apply(m_output, input);
       }
private:
       vtkImageData *m_output;
};


static vtkSmartPointer<vtkImageData> image_mia_to_vtk(const C3DImage& mia_image)
{
       auto outimage = vtkSmartPointer<vtkImageData>::New();
       auto origin = mia_image.get_origin();
       outimage->SetOrigin(origin.x, origin.y, origin.z);
       auto dx = mia_image.get_voxel_size();
       outimage->SetSpacing(dx.x, dx.y, dx.z);
       outimage->SetDimensions(mia_image.get_size().x, mia_image.get_size().y, mia_image.get_size().z);
       FSetArray set_array(outimage);
       mia::filter(set_array, mia_image);
       return outimage;
}

CVtk3DImageIOPlugin::CVtk3DImageIOPlugin():
       C3DImageIOPlugin("vtk")
{
       // indicate support for all pixel types available (only scalar types are possible)
#if  VTK_MAJOR_VERSION < 7
       add_supported_type(it_bit);
#endif
       add_supported_type(it_sbyte);
       add_supported_type(it_ubyte);
       add_supported_type(it_sshort);
       add_supported_type(it_ushort);
       add_supported_type(it_sint);
       add_supported_type(it_uint);
#ifdef LONG_64BIT
       add_supported_type(it_slong);
       add_supported_type(it_ulong);
#endif
       add_supported_type(it_float);
       add_supported_type(it_double);
       add_suffix(".vtk");
       add_suffix(".VTK");
       add_suffix(".vtkimage");
       add_suffix(".VTKIMAGE");
}

CVtk3DImageIOPlugin::PData CVtk3DImageIOPlugin::do_load(const string&  fname) const
{
       auto reader = vtkSmartPointer<vtkDataSetReader>::New();
       reader->SetFileName(fname.c_str());

       if (!reader->OpenVTKFile())
              return CVtk3DImageIOPlugin::PData();

       reader->Update();
       auto vtk_image = dynamic_cast<vtkImageData *>(reader->GetOutput());

       if (!vtk_image)
              return PData();

       CVtk3DImageIOPlugin::PData result(new Data);
       result->push_back(P3DImage(image_vtk_to_mia(vtk_image, fname)));
       return result;
}





bool CVtk3DImageIOPlugin::do_save(const string& fname, const Data& data) const
{
       if (data.size() != 1)
              throw create_exception<invalid_argument>("CVtk3DImageIOPlugin::save only supports writing one image, "
                            "but you passed in ", data.size(), " images");

       const auto& image = *data[0];
       auto outimage = image_mia_to_vtk(image);
       // add writing of the image
       auto writer = vtkSmartPointer<vtkDataSetWriter>::New();
#if  VTK_MAJOR_VERSION < 6
       writer->SetInput(outimage);
#else
       writer->SetInputData(outimage);
#endif
       writer->SetFileTypeToBinary();
       writer->SetFileName(fname.c_str());
       writer->Write();
       return true;
}

const string CVtk3DImageIOPlugin::do_get_descr() const
{
       return "3D VTK image legacy in- and output (experimental).";
}


CVtkXML3DImageIOPlugin::CVtkXML3DImageIOPlugin():
       C3DImageIOPlugin("vti")
{
       // indicate support for all pixel types available (only scalar types are possible)
       add_supported_type(it_sbyte);
       add_supported_type(it_ubyte);
       add_supported_type(it_sshort);
       add_supported_type(it_ushort);
       add_supported_type(it_sint);
       add_supported_type(it_uint);
#ifdef LONG_64BIT
       add_supported_type(it_slong);
       add_supported_type(it_ulong);
#endif
       add_supported_type(it_float);
       add_supported_type(it_double);
       add_suffix(".vti");
       add_suffix(".VTI");
}

CVtkXML3DImageIOPlugin::PData CVtkXML3DImageIOPlugin::do_load(const string&  fname) const
{
       auto reader = vtkSmartPointer<vtkXMLImageDataReader>::New();
       reader->SetFileName(fname.c_str());
       reader->Update();
       auto vtk_image = reader->GetOutput();

       if (!vtk_image)
              return PData();

       CVtkXML3DImageIOPlugin::PData result(new Data);
       result->push_back(P3DImage(image_vtk_to_mia(vtk_image, fname)));
       return result;
}





bool CVtkXML3DImageIOPlugin::do_save(const string& fname, const Data& data) const
{
       if (data.size() != 1)
              throw create_exception<invalid_argument>("CVtkXML3DImageIOPlugin::save only supports writing one image, "
                            "but you passed in ", data.size(), " images");

       const auto& image = *data[0];
       auto outimage = image_mia_to_vtk(image);
       // add writing of the image
       auto writer = vtkSmartPointer<vtkXMLImageDataWriter>::New();
#if  VTK_MAJOR_VERSION == 5
       writer->SetInput(outimage);
#else
       writer->SetInputData(outimage);
#endif
       writer->SetFileName(fname.c_str());
       writer->Write();
       return true;
}

const string CVtkXML3DImageIOPlugin::do_get_descr() const
{
       return "3D image VTK-XML in- and output (experimental).";
}



CMhd3DImageIOPlugin::CMhd3DImageIOPlugin():
       C3DImageIOPlugin("mhd")
{
       // indicate support for all pixel types available (only scalar types are possible)
       add_supported_type(it_sbyte);
       add_supported_type(it_ubyte);
       add_supported_type(it_sshort);
       add_supported_type(it_ushort);
       add_supported_type(it_sint);
       add_supported_type(it_uint);
#ifdef LONG_64BIT
       add_supported_type(it_slong);
       add_supported_type(it_ulong);
#endif
       add_supported_type(it_float);
       add_supported_type(it_double);
       add_suffix(".mhd");
       add_suffix(".MHD");
       add_suffix(".mha");
       add_suffix(".MHA");
}

const std::string CMhd3DImageIOPlugin::do_get_descr() const
{
       return "MetaIO 3D image IO using the VTK implementation (experimental).";
}

CMhd3DImageIOPlugin::PData CMhd3DImageIOPlugin::do_load(const std::string&  filename) const
{
       auto reader = vtkSmartPointer<vtkMetaImageReader>::New();
       reader->SetFileName(filename.c_str());
       reader->Update();
       auto vtk_image = reader->GetOutput();

       if (!vtk_image)
              return PData();

       CVtkXML3DImageIOPlugin::PData result(new Data);
       result->push_back(P3DImage(image_vtk_to_mia(vtk_image, filename)));
       cvdebug() << "Pixel type set to" << (*result)[0]->get_pixel_type() << "\n";
       return result;
}


bool CMhd3DImageIOPlugin::do_save(const std::string& fname, const Data& data) const
{
       if (data.size() != 1)
              throw create_exception<invalid_argument>("CMhd3DImageIOPlugin::save only supports writing one image, "
                            "but you passed in ", data.size(), " images");

       const auto& image = *data[0];
       auto outimage = image_mia_to_vtk(image);
       // add writing of the image
       auto writer = vtkSmartPointer<vtkMetaImageWriter>::New();
#if  VTK_MAJOR_VERSION < 6
       writer->SetInput(outimage);
#else
       writer->SetInputData(outimage);
#endif
       writer->SetFileName(fname.c_str());
       // seems that compression is broken for (un)signed char.
       writer->SetCompression(false);
       writer->Write();
       return true;
}



extern "C" EXPORT CPluginBase *get_plugin_interface()
{
       auto plugin =  new CVtk3DImageIOPlugin();
       plugin->append_interface(new CVtkXML3DImageIOPlugin);
       plugin->append_interface(new CMhd3DImageIOPlugin);
       return plugin;
}
