/* -*- 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 <limits>
#include <errno.h>

#include <boost/algorithm/minmax_element.hpp>
#include <cstring>
#include <string>
#include <sstream>

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


NS_BEGIN(IMAGEIO_3D_ANALYZE)

NS_MIA_USE
using namespace std;
using namespace boost;

#pragma pack(1)
struct analyze_header_key {      /* header key     */
       /* off + size     */
       int sizeof_hdr;            /*  0 + 4         */
       char data_type[10];      /*  4 + 10        */
       char db_name[18];        /* 14 + 18        */
       int extents;             /* 32 + 4         */
       short int session_error; /* 36 + 2         */
       char regular;            /* 38 + 1         */
       char hkey_un0;           /* 39 + 1         */
};                               /* total=40 bytes */

struct analyze_image_dimension {
       /* off + size     */
       short int dim[8];        /* 0 + 16         */
       unsigned char vox_units[4];       /* 16 + 2         */
       unsigned char cal_units[8];       /* 18 + 2         */
       short int unused1;      /* 28 + 2         */
       short int datatype;      /* 30 + 2         */
       short int bitpix;        /* 32 + 2         */
       short int dim_un0;       /* 34 + 2         */
       float pixdim[8];         /* 36 + 32        */
       /*
         pixdim[] specifies the voxel dimensitons:
         pixdim[1] - voxel width
         pixdim[2] - voxel height
         pixdim[3] - interslice distance
         ...etc
       */
       float vox_offset;        /* 68 + 4          */
       float funused1;          /* 72 + 4          */
       float funused2;          /* 76 + 4          */
       float funused3;          /* 80 + 4          */
       float cal_max;           /* 84 + 4          */
       float cal_min;           /* 88 + 4          */
       float compressed;        /* 92 + 4          */
       float verified;          /* 96 + 4          */
       int glmax, glmin;        /* 100 + 8         */
};                               /* total=108 bytes */

struct analyze_data_history {
       /* off + size     */
       char descrip[80];        /* 0 + 80         */
       char aux_file[24];       /* 80 + 24        */
       char orient;             /* 104 + 1        */
       char originator[10];     /* 105 + 10       */
       char generated[10];      /* 115 + 10       */
       char scannum[10];        /* 125 + 10       */
       char patient_id[10];     /* 135 + 10       */
       char exp_date[10];       /* 145 + 10       */
       char exp_time[10];       /* 155 + 10       */
       char hist_un0[3];        /* 165 + 3        */
       int views;                /* 168 + 4        */
       int vols_added;          /* 172 + 4        */
       int start_field;         /* 176 + 4        */
       int field_skip;          /* 180 + 4        */
       int omax, omin;          /* 184 + 8        */
       int smax, smin;          /* 192 + 8        */
};
struct analyze_dsr {
       struct analyze_header_key hk;          /* 0 + 40           */
       struct analyze_image_dimension dime;   /* 40 + 108         */
       struct analyze_data_history hist;      /* 148 + 200        */
};                                     /* total= 348 bytes */
#pragma pack()

/* Acceptable values for datatype */
#define DTA_NONE                      0
#define DTA_UNKNOWN                   0
#define DTA_BINARY                    1
#define DTA_UNSIGNED_CHAR             2
#define DTA_SIGNED_SHORT              4
#define DTA_SIGNED_INT                8
#define DTA_FLOAT                     16
#define DTA_COMPLEX                   32
#define DTA_DOUBLE                    64
#define DTA_RGB                       128
#define DTA_ALL                       255

enum EAnaOrientation {
       ao_transverse_unflipped = 0,
       ao_coronal_unflipped,
       ao_saggital_unflipped,
       ao_transverse_flipped,
       ao_coronal_flipped,
       ao_saggital_flipped,
       ao_unknown
};

typedef struct {
       float real;
       float imag;
} COMPLEX;

const TDictMap<EPixelType>::Table analyze_type_table[] = {
       {"UNKNOWN",  it_unknown, "unknown pixel type"},
       {"BINARY",   it_bit,     "binary pixel type"},
       {"CHAR",     it_ubyte,   "unsigned 8 bit"},
       {"SHORT",    it_sshort,  "signed 16 bit"},
       {"INT",      it_sint,    "signed 32 bit"},
       {"FLOAT",    it_float,   "single floating point"},
       {"COMPLEX",  it_unknown, "complex, not supported"},
       {"DOUBLE",   it_double,  "double floating point"},
       {"RGB",      it_unknown, "RGB, not supported"},
       {0, it_unknown, ""}
};



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

CAnalyze3DImageIOPlugin::CAnalyze3DImageIOPlugin():
       C3DImageIOPlugin("analyze"),
       m_type_table(analyze_type_table),
       m_swap_endian(false)
{
//	add_supported_type(it_bit);
       add_supported_type(it_ubyte);
       add_supported_type(it_sshort);
       add_supported_type(it_sint);
       add_supported_type(it_float);
       add_supported_type(it_double);
       add_suffix(".hdr");
       add_suffix(".HDR");
}

template <typename T>
inline void swap_32(T& v)
{
       char outp[4];
       const char *inp = reinterpret_cast<const char *>(&v);
       outp[3] = inp[0];
       outp[2] = inp[1];
       outp[1] = inp[2];
       outp[0] = inp[3];
       memcpy(&v, outp, 4);
}

template <typename T>
inline void swap_64(T& v)
{
       char outp[8];
       const char *inp = reinterpret_cast<const char *>(&v);
       outp[7] = inp[0];
       outp[6] = inp[1];
       outp[5] = inp[2];
       outp[4] = inp[3];
       outp[3] = inp[4];
       outp[2] = inp[5];
       outp[1] = inp[6];
       outp[0] = inp[7];
       memcpy(&v, outp, 8);
}

inline void swap_int16(short& v)
{
       v = (v << 8 ) | ( ( v >> 8 ) & 0xFF);
}

void  CAnalyze3DImageIOPlugin::swap_hdr(analyze_dsr& hdr) const
{
       swap_32(hdr.hk.sizeof_hdr) ;
       swap_32(hdr.hk.extents) ;
       swap_int16(hdr.hk.session_error) ;
       swap_int16(hdr.dime.dim[0]) ;
       swap_int16(hdr.dime.dim[1]) ;
       swap_int16(hdr.dime.dim[2]) ;
       swap_int16(hdr.dime.dim[3]) ;
       swap_int16(hdr.dime.dim[4]) ;
       swap_int16(hdr.dime.dim[5]) ;
       swap_int16(hdr.dime.dim[6]) ;
       swap_int16(hdr.dime.dim[7]) ;
       swap_int16(hdr.dime.unused1) ;
       swap_int16(hdr.dime.datatype) ;
       swap_int16(hdr.dime.bitpix) ;
       swap_32(hdr.dime.pixdim[0]) ;
       swap_32(hdr.dime.pixdim[1]) ;
       swap_32(hdr.dime.pixdim[2]) ;
       swap_32(hdr.dime.pixdim[3]) ;
       swap_32(hdr.dime.pixdim[4]) ;
       swap_32(hdr.dime.pixdim[5]) ;
       swap_32(hdr.dime.pixdim[6]) ;
       swap_32(hdr.dime.pixdim[7]) ;
       swap_32(hdr.dime.vox_offset) ;
       swap_32(hdr.dime.funused1) ;
       swap_32(hdr.dime.funused2) ;
       swap_32(hdr.dime.cal_max) ;
       swap_32(hdr.dime.cal_min) ;
       swap_32(hdr.dime.compressed) ;
       swap_32(hdr.dime.verified) ;
       swap_int16(hdr.dime.dim_un0) ;
       swap_32(hdr.dime.glmax) ;
       swap_32(hdr.dime.glmin) ;
}


template <typename Iterator, int size>
struct __swap_endian {
       static void apply(Iterator /*begin*/, Iterator /*end*/)
       {
              // standard is do nothing
       }
};

template <typename Iterator>
struct __swap_endian<Iterator, 2> {
       static void apply(Iterator begin, Iterator end)
       {
              while (begin != end)  {
                     *begin =  (*begin << 8 ) | ( ( *begin >> 8 ) & 0xFF);
                     ++begin;
              }
       }
};

template <typename Iterator>
struct __swap_endian<Iterator, 4> {
       static void apply(Iterator begin, Iterator end)
       {
              while (begin != end)  {
                     swap_32(*begin);
                     ++begin;
              }
       }
};

template <typename Iterator>
struct __swap_endian<Iterator, 8> {
       static void apply(Iterator begin, Iterator end)
       {
              while (begin != end)  {
                     swap_64(*begin);
                     ++begin;
              }
       }
};

template <typename T>
void swap_endian(T3DImage<T>& image)
{
       __swap_endian<typename T3DImage<T>::iterator, sizeof(T)>::apply(image.begin(), image.end());
}


template <typename T>
struct do_read_image {
       static C3DImage *apply(const C3DBounds& size, CInputFile& data_file, bool do_swap_endian)
       {
              T3DImage<T> *result = new T3DImage<T>(size);

              if (fread(&(*result)(0, 0, 0), sizeof(T), result->size(), data_file) != result->size())
                     throw runtime_error("Analyze: unable to read data");

              if (do_swap_endian)
                     swap_endian(*result);

              return result;
       }
};


C3DImage *CAnalyze3DImageIOPlugin::read_image(const C3DBounds& size, short datatype, CInputFile& data_file) const
{
       if (datatype & 128)
              cvwarn() << "Got an RGB indicator but I will ignore it\n";

       switch (datatype & 0xFF) {
//	case DTA_BINARY       :return do_read_image<bool>::apply(size, data_file, m_swap_endian);
       case DTA_UNSIGNED_CHAR:
              return do_read_image<unsigned char>::apply(size, data_file, m_swap_endian);

       case DTA_SIGNED_SHORT :
              return do_read_image<signed short>::apply(size, data_file, m_swap_endian);

       case DTA_SIGNED_INT   :
              return do_read_image<signed int>::apply(size, data_file, m_swap_endian);

       case DTA_FLOAT        :
              return do_read_image<float>::apply(size, data_file, m_swap_endian);

       case DTA_DOUBLE       :
              return do_read_image<double>::apply(size, data_file, m_swap_endian);

       default:
              stringstream msg;
              msg << "Analyze: unsupported image type:" << datatype;
              throw invalid_argument(msg.str());
       }
}


void set_typeinfo(analyze_image_dimension& dime, EPixelType pixel_type)
{
       switch (pixel_type) {
       case it_ubyte:
              dime.datatype = DTA_UNSIGNED_CHAR;
              dime.bitpix   = 8;
              break;

       case it_sshort:
              dime.datatype = DTA_SIGNED_SHORT;
              dime.bitpix   = 16;
              break;

       case it_sint:
              dime.datatype = DTA_SIGNED_INT;
              dime.bitpix   = 32;
              break;

       case it_float:
              dime.datatype = DTA_FLOAT;
              dime.bitpix   = 32;
              break;

       case it_double:
              dime.datatype = DTA_DOUBLE;
              dime.bitpix   = 64;
              break;

       default:
              throw invalid_argument("Analyze: input pixel type not suporrted");
       }
}

CAnalyze3DImageIOPlugin::PData CAnalyze3DImageIOPlugin::do_load(const string&  filename) const
{
       CInputFile f(filename);

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

       analyze_dsr hdr;

       if (fread(&hdr, 1, sizeof(analyze_dsr), f) != sizeof(analyze_dsr)) {
              cvdebug() << filename.c_str() << ":" << "unable to read analyze header\n";
              return PData();
       }

       if (hdr.dime.dim[0] < 0 || hdr.dime.dim[0] > 15) {
              swap_hdr(hdr);
              m_swap_endian = true;
       } else
              m_swap_endian = false;

       if (hdr.dime.dim[0] < 0 || hdr.dime.dim[0] > 15) {
              cvdebug() << filename.c_str() << ":" << "not an analyze  header\n";
              return PData();
       }

       if ((unsigned int)hdr.hk.sizeof_hdr < sizeof(hdr)) {
              cvdebug() << filename.c_str() << ":" << "not an analyze  header\n";
              return PData();
       }

       if (hdr.dime.dim[0] < 3) {
              cvdebug() << filename.c_str() << ":" << "not a supported analyze  header\n";
              return PData();
       }

       // get the size
       C3DBounds size(hdr.dime.dim[1], hdr.dime.dim[2], hdr.dime.dim[3]);
       C3DFVector voxel(hdr.dime.pixdim[1], hdr.dime.pixdim[2], hdr.dime.pixdim[3]);
       cvdebug() << "Analyze: got voxel size " << voxel << "\n";
       size_t num_img = 1;

       for (short int i = 4; i < hdr.dime.dim[0] ; ++i ) {
              num_img *= hdr.dime.dim[i];
       }

       // open data fiele
       const string data_file_name = filename.substr(0, filename.length() - 3) + string("img");
       CInputFile data_file(data_file_name);

       if (!data_file)
              throw runtime_error(string("Analyze: unable to find data file:") + data_file_name );

       // create output list
       PData result(new C3DImageVector());
       int voffset = static_cast<int>(hdr.dime.vox_offset);

       // Coverty will complain about an untrusted value.
       // This is no problem, because if voffset is off the scale, the
       // data reading will fail and the plug-in will throw.
       if (voffset > 0)
              if (!fseek(data_file, voffset, SEEK_CUR))
                     throw create_exception<runtime_error>("Analyze: unable seek in data file '",
                                                           data_file_name, "':", strerror(errno) );

       // read data
       while (num_img > 0) {
              --num_img;
              E3DImageOrientation orientation = ior_unknown;

              switch ( hdr.hist.orient ) {
              case ao_transverse_unflipped:
                     orientation = ior_axial;
                     break;

              case ao_transverse_flipped:
                     orientation = ior_axial_flipped;
                     break;

              case ao_coronal_unflipped:
                     orientation = ior_coronal;
                     break;

              case ao_coronal_flipped:
                     orientation = ior_coronal_flipped ;
                     break;

              case ao_saggital_unflipped:
                     orientation = ior_saggital;
                     break;

              case ao_saggital_flipped:
                     orientation = ior_saggital_flipped;
                     break;

              default:
                     orientation = ior_unknown;
              }

              if (voffset < 0) {
                     if (!fseek(data_file, -voffset, SEEK_CUR))
                            throw create_exception<runtime_error>("Analyze: unable seek in data file '",
                                                                  data_file_name, "':", strerror(errno) );
              }

              P3DImage image(read_image(size, hdr.dime.datatype, data_file));
              image->set_voxel_size(voxel);
              image->set_orientation(orientation);
              result->push_back(image);
       }

       // clean up
       return result;
}

template <typename T>
struct __do_fwrite {
       static size_t apply(const T3DImage<T>& image, size_t a, size_t b, FILE *f)
       {
              return fwrite(&image(0, 0, 0), a,  b, f);
       }
};

template <>
struct __do_fwrite<bool> {
       static size_t apply(const T3DImage<bool>& /*image*/, size_t /*a*/, size_t /*b*/, FILE */*f*/)
       {
              throw invalid_argument("Analyze:Saving boolen not yet supported");
       }
};

class CSavefilter: public TFilter<bool>
{
public:
       CSavefilter(COutputFile& f, analyze_image_dimension& dime):
              m_f(f),
              m_dime(dime)
       {
       }

       template <class T>
       bool operator ()(const T3DImage<T>& image) const
       {
              if (__do_fwrite<T>::apply(image, sizeof(T), image.size(), m_f) != image.size()) {
                     throw runtime_error(string("Analyze: Error writing file") + strerror(errno));
              }

              pair<typename T3DImage<T>::const_iterator, typename T3DImage<T>::const_iterator>
              image_minmax = ::boost::minmax_element(image.begin(), image.end());

              if ( (int)*image_minmax.first < m_dime.glmin)
                     m_dime.glmin = (int) * image_minmax.first;

              if ( (int)*image_minmax.second > m_dime.glmax)
                     m_dime.glmax = (int) * image_minmax.second;

              return true;
       }

private:
       COutputFile& m_f;
       analyze_image_dimension& m_dime;
};


bool CAnalyze3DImageIOPlugin::save_data(const string& fname, const Data& data, analyze_image_dimension& dime) const
{
       const string data_file_name = fname.substr(0, fname.length() - 3) + string("img");
       COutputFile data_file(data_file_name);

       if (!data_file)
              throw runtime_error(string("Analyze: unable to open '") + data_file_name + "' for writing");

       CSavefilter saver(data_file, dime);

       for (Data::const_iterator k = data.begin(); k != data.end(); ++k) {
              mia::filter(saver, **k);
       }

       return true;
}

bool CAnalyze3DImageIOPlugin::do_save(const string& fname, const Data& data) const
{
       analyze_dsr hdr;
       memset(&hdr, 0, sizeof(hdr));
       hdr.hk.sizeof_hdr =  sizeof(hdr);
       hdr.dime.dim[0] = 4;
       hdr.dime.dim[4] = data.size();

       if (data.empty())
              throw invalid_argument("Trying to save empty image list");

       Data::const_iterator k = data.begin();
       C3DBounds size = (*k)->get_size();
       C3DFVector voxel = (*k)->get_voxel_size();
       EPixelType pixel_type = (*k)->get_pixel_type();
       auto orient = (*k)->get_orientation();

       switch (orient) {
       case ior_axial:
              hdr.hist.orient = ao_transverse_unflipped;
              break;

       case ior_axial_flipped:
              hdr.hist.orient = ao_transverse_flipped;
              break;

       case ior_coronal:
              hdr.hist.orient = ao_coronal_unflipped;
              break;

       case ior_coronal_flipped:
              hdr.hist.orient = ao_coronal_flipped;
              break;

       case ior_saggital:
              hdr.hist.orient = ao_saggital_unflipped;
              break;

       case ior_saggital_flipped:
              hdr.hist.orient = ao_saggital_flipped;
              break;

       default:
              hdr.hist.orient = ao_unknown;
       }

       while (k != data.end()) {
              if (size != (*k)->get_size() ||
                  pixel_type != (*k)->get_pixel_type() ||
                  voxel != (*k)->get_voxel_size() ) {
                     throw invalid_argument("analyze only support images series of same size and type");
              }

              ++k;
       }

       k = data.begin();
       hdr.dime.dim[1] = size.x;
       hdr.dime.dim[2] = size.y;
       hdr.dime.dim[3] = size.z;
       hdr.dime.pixdim[1] = voxel.x;
       hdr.dime.pixdim[2] = voxel.y;
       hdr.dime.pixdim[3] = voxel.z;
       cvdebug() << voxel << "\n";
       hdr.hk.extents = 16384;
       hdr.hk.regular = 'r';
       set_typeinfo(hdr.dime, pixel_type);
       hdr.dime.glmin = numeric_limits<int>::max();
       save_data(fname, data, hdr.dime);
       COutputFile hdr_file(fname);

       if (!hdr_file)
              throw runtime_error(string("Analyze: unable to open '") + fname + "' for writing");

       if (fwrite(&hdr, 1, sizeof(hdr), hdr_file) != sizeof(hdr))
              throw runtime_error(string("Analyze: error writing header '") + fname);

       return true;
}

const std::string CAnalyze3DImageIOPlugin::do_get_preferred_suffix() const
{
       return "hdr";
}


const string CAnalyze3DImageIOPlugin::do_get_descr() const
{
       return "Analyze 7.5 image";
}

NS_END


