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


#define VSTREAM_DOMAIN "HDF5"

#include <addons/hdf5/hdf5a_mia.hh>
#include <stack>
NS_MIA_BEGIN

using std::vector;
using std::string;
using std::stack;
using std::invalid_argument;
using std::runtime_error;



H5Handle::H5Handle(hid_t hid, const TSingleReferencedObject<hid_t>::Destructor& d):
       TSingleReferencedObject<hid_t>(hid, d)
{
}

void H5Handle::set_parent(const H5Handle& parent)
{
       m_parent = parent;
}

#define H5Destructor(TYPE, CALL)					\
	struct TYPE : public TSingleReferencedObject<hid_t>::Destructor {				\
		TYPE(){};						\
		virtual void operator ()(hid_t& handle)const {		\
			if (handle >= 0) {				\
				herr_t err = CALL(handle);		\
				if (err != 0) {				\
					throw std::runtime_error(#TYPE ": error closing handle."); \
				}					\
			}						\
		}							\
	};								\
	static const TYPE TYPE;						\

H5Destructor(H5GroupDestructor, H5Gclose);
H5Destructor(H5SpaceDestructor, H5Sclose);
H5Destructor(H5DatasetDestructor, H5Dclose);
H5Destructor(H5FileDestructor, H5Fclose);
H5Destructor(H5AttributeDestructor, H5Aclose);
H5Destructor(H5PropertyDestructor, H5Pclose);
H5Destructor(H5TypeDestructor, H5Tclose);

H5SpaceHandle::H5SpaceHandle(hid_t hid):
       H5Handle(hid, H5SpaceDestructor)
{
}

H5TypeHandle::H5TypeHandle(hid_t hid):
       H5Handle(hid, H5TypeDestructor)
{
}


H5GroupHandle::H5GroupHandle(hid_t hid):
       H5Handle(hid, H5GroupDestructor)
{
}

H5DatasetHandle::H5DatasetHandle(hid_t hid):
       H5Handle(hid, H5DatasetDestructor)
{
}

H5AttributeHandle::H5AttributeHandle(hid_t hid):
       H5Handle(hid, H5AttributeDestructor)
{
}

H5PropertyHandle::H5PropertyHandle(hid_t hid):
       H5Handle(hid, H5PropertyDestructor)
{
}

H5FileHandle::H5FileHandle(hid_t hid):
       H5Handle(hid, H5FileDestructor)
{
}

H5Base::H5Base(const H5Handle& handle):
       m_handle(handle)
{
}

void H5Base::set_parent(const H5Base& parent)
{
       m_handle.set_parent(parent.get_handle());
}

const H5Handle& H5Base::get_handle() const
{
       return m_handle;
}

struct SIterateData {
       CAttributedData& list;
       const H5Base& locator_id;
};


static herr_t convert_attribute_cb(hid_t MIA_PARAM_UNUSED(location_id), const char *attr_name,
                                   const H5A_info_t *MIA_PARAM_UNUSED(ainfo), void *op_data)
{
       SIterateData& iter_data = *reinterpret_cast<SIterateData *>(op_data);
       cvdebug() << "convert_attribute_cb: read '" << attr_name << "'\n";
       auto pattr = H5Attribute::read(iter_data.locator_id, attr_name);

       if (pattr)
              iter_data.list.set_attribute(attr_name, pattr);

       return 0;
}

CAttributedData H5Base::read_attributes() const
{
       CAttributedData result;
       read_and_append_attributes(result);
       return result;
}

void  H5Base::read_and_append_attributes(CAttributedData& target) const
{
       SIterateData id = {target, *this};
       H5Aiterate2(*this, H5_INDEX_CRT_ORDER, H5_ITER_NATIVE, NULL, convert_attribute_cb, &id);
}

H5Base::operator hid_t() const
{
       return m_handle;
}

template <typename I>
static void check_id(hid_t id, const char *domain, const char *action, I info)
{
       cvdebug() << "Checking id=" << id
                 << ", domain=" << domain
                 << ", action=" << action
                 << ", info=" << info << "\n";

       if (id < 0) {
              throw create_exception<invalid_argument>(domain, ": error in ", action, ":", info);
       }
}

H5Property::H5Property(hid_t id):
       H5Base(H5PropertyHandle(id))
{
}

H5Property H5Property::create(hid_t cls)
{
       auto id = H5Pcreate(cls);
       check_id(id, "H5Property", "Create", cls);
       return H5Property(id);
}


H5File::H5File(hid_t id):
       H5Base(H5FileHandle(id))
{
}

H5File H5File::create(const char *name, unsigned flags, hid_t  creation_prop, hid_t access_prop)
{
       return H5File(H5Fcreate(name, flags, creation_prop, access_prop));
}

H5File H5File::open(const char *name, unsigned flags, hid_t access_prop)
{
       return H5File(H5Fopen(name, flags, access_prop));
}


H5Space::H5Space (hid_t id):
       H5Base(H5SpaceHandle(id))
{
}

H5Space H5Space::create()
{
       return H5Space(H5Screate(H5S_SCALAR));
}

H5Space H5Space::create(hsize_t dim1)
{
       auto id = H5Screate_simple(1, &dim1, NULL);
       check_id(id, "H5Space", "create_simple 1d", dim1);
       return H5Space(id);
}

H5Space H5Space::create(unsigned rank, const hsize_t *dims)
{
       auto id = H5Screate_simple(rank, dims, NULL);
       check_id(id, "H5Space", "create_simple", rank);
       return H5Space(id);
}

H5Space H5Space::create(const std::vector<hsize_t>& dims)
{
       return create(dims.size(), &dims[0]);
}

std::vector<hsize_t> H5Space::get_size() const
{
       int  dims = H5Sget_simple_extent_ndims(*this);

       if (dims < 0)
              throw create_exception<runtime_error>("H5Dataset::get_size: error reading dimensions");

       vector <hsize_t> result(dims);

       if (dims > 0) {
              auto status = H5Sget_simple_extent_dims(*this,  &result[0], NULL);

              if (status < 0)
                     throw create_exception<runtime_error>("H5Dataset::get_size: error reading dimensions");
       }

       return result;
}

H5Group::H5Group (hid_t id):
       H5Base(H5GroupHandle(id))
{
}


struct SilenceH5Errors {
       SilenceH5Errors();
       ~SilenceH5Errors();
private:
       H5E_auto2_t  old_func;
       void *old_client_data;
};

SilenceH5Errors::SilenceH5Errors()
{
       H5Eget_auto(H5E_DEFAULT, &old_func, &old_client_data);
       H5Eset_auto(H5E_DEFAULT, NULL, NULL);
}

SilenceH5Errors::~SilenceH5Errors()
{
       H5Eset_auto(H5E_DEFAULT, old_func, old_client_data);
}


H5Base H5Group::open(const H5Base& parent, const std::string& name)
{
       assert(name.find_last_of('/') == string::npos);
       auto id = H5Gopen(parent, name.c_str(), H5P_DEFAULT);
       assert(id >= 0);
       return H5Group(id);
}


H5Base H5Group::create_or_open_hierarchy(const H5Base& parent, string& relative_name, bool create)
{
       H5Base pp = parent;
       // only deal with fully qualified names
       assert(relative_name[0] == '/');
       size_t last_slash = relative_name.find_last_of('/');

       // only root group required no need to do anything
       if (last_slash > 0)  {
              SilenceH5Errors err;
              string base = relative_name.substr(0, last_slash);
              cvdebug() << "Base: " << base << "\n";
              relative_name = relative_name.substr(last_slash + 1);
              stack<string> path;
              last_slash = base.find_last_of('/');

              while (last_slash > 0)  {
                     auto tail = base.substr(last_slash + 1);
                     path.push(base.substr(last_slash + 1));
                     base = base.substr(0, last_slash);
                     cvdebug() << "tail:" << tail << " "
                               << "base:" << base << "\n";
                     last_slash = base.find_last_of('/');
              }

              path.push(base.substr(1));

              while (!path.empty()) {
                     auto name = path.top();
                     path.pop();
                     cvdebug() << "do group '" << name << "'\n";

                     // it needs to be tested whether multiple slashes are to be merged
                     if (name.empty())
                            continue;

                     auto id = H5Gopen(parent, name.c_str(), H5P_DEFAULT);

                     if (id < 0 && create)
                            id = H5Gcreate(parent, name.c_str(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);

                     H5Group p(id);
                     p.set_parent(pp);
                     pp = p;
              }
       }

       return pp;
}

H5Type::H5Type (hid_t id):
       H5Base(H5TypeHandle(id))
{
}

H5Type H5Type::get_native_type() const
{
       auto id = H5Tget_native_type(*this, H5T_DIR_ASCEND);

       if (id < 0) {
              throw runtime_error("H5Type::get_native_type, Unable to deduct mem_type for reading data");
       }

       return H5Type(id);
}

int H5Type::get_mia_type_id() const
{
       H5T_class_t cls =  H5Tget_class(*this);
       size_t size = H5Tget_size(*this);

       switch (cls) {
       case H5T_INTEGER:
              switch (size) {
              case 1:
                     return 2 | H5Tget_sign(*this );

              case 2:
                     return 4 | H5Tget_sign(*this );

              case 4:
                     return 6 | H5Tget_sign(*this );

              case 8:
                     return 8 | H5Tget_sign(*this );

              default:
                     return EAttributeType::attr_unknown;
              }

       case H5T_FLOAT:
              if (size == 4)
                     return EAttributeType::attr_float;
              else if (size == 8)
                     return EAttributeType::attr_double;
              else
                     return EAttributeType::attr_unknown;

       case H5T_STRING:
              return EAttributeType::attr_string;

       case H5T_BITFIELD:
              if (size == 1)
                     return EAttributeType::attr_bool;
              else {
                     cvwarn() << "HDF5: bitfield type of size " << size << " not supported in MIA\n";
              }

       default:
              return do_get_mia_type_id();
       }
}

int H5Type::do_get_mia_type_id() const
{
//	cvwarn() << "HDF5: type class " << cls << " of size " << size << " not supported in MIA\n";
       return EAttributeType::attr_unknown;
}

H5Dataset::H5Dataset (hid_t id, const H5Space& space, const char *name):
       H5Base(H5DatasetHandle(id)),
       m_space(space),
       m_name(name)
{
}


static bool can_gzip()
{
       if (!H5Zfilter_avail(H5Z_FILTER_DEFLATE))
              return false;

       unsigned int filter_info;
       H5Zget_filter_info (H5Z_FILTER_DEFLATE, &filter_info);
       return ((filter_info & H5Z_FILTER_CONFIG_ENCODE_ENABLED) &&
               (filter_info & H5Z_FILTER_CONFIG_DECODE_ENABLED));
}

H5Dataset H5Dataset::create(const H5Base& parent, const char *name, hid_t type_id, const H5Space& space)
{
       string relative_name(name);
       H5Base p = H5Group::create_or_open_hierarchy(parent, relative_name, true);
       hid_t id;

       if (can_gzip()) {
              auto dcpl = H5Property::create (H5P_DATASET_CREATE);
              auto status =  H5Pset_deflate (dcpl, 9);

              if (status < 0) {
                     cvwarn() << "HDF5 gzip should be supported, but failed, store uncompressed\n";
                     id =  H5Dcreate(p, relative_name.c_str(), type_id, space, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
              } else {
                     // we compress the whole data in one chunk
                     auto chunk_size = space.get_size();
                     status = H5Pset_chunk (dcpl, chunk_size.size(), &chunk_size[0]);
                     id = H5Dcreate (p, relative_name.c_str(), type_id, space, H5P_DEFAULT, dcpl,
                                     H5P_DEFAULT);
                     cvdebug() << "HDF5: Dataset '" << name << "' created with gzip compression enabled\n";
              }
       } else {
              id =  H5Dcreate(p, relative_name.c_str(), type_id, space, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
       }

       check_id(id, "H5Dataset", "create", relative_name);
       H5Dataset set(id, space, name);
       set.set_parent(p);
       return set;
}

H5Dataset H5Dataset::open(const H5Base& parent, const char *name)
{
       string relative_name(name);
       H5Base p = parent;

       if (relative_name.find_last_of('/') != string::npos)
              p = H5Group::create_or_open_hierarchy(parent, relative_name, false);

       auto id =  H5Dopen(p, relative_name.c_str(), H5P_DEFAULT);
       check_id(id, "H5Dataset", "open", relative_name);
       auto space_id = H5Dget_space(id);
       check_id(space_id, "H5Dataset", "get space", name);
       H5Space space(space_id);
       H5Dataset set(id, space, name);
       set.set_parent(p);
       return set;
}

void  H5Dataset::write( hid_t type_id, const void *data)
{
       auto err =  H5Dwrite(*this, type_id, m_space,  H5S_ALL, H5P_DEFAULT, data);

       if (err < 0) {
              throw create_exception<runtime_error>("H5Dataset::write: error writing data set '", m_name, "'");
       }
}

void  H5Dataset::read( hid_t type_id, void *data) const
{
       auto err =  H5Dread(*this, type_id, m_space,  H5S_ALL, H5P_DEFAULT, data);

       if (err < 0) {
              throw create_exception<runtime_error>("H5Dataset::read: error reading data set  '", m_name, "'");
       }
}

vector <hsize_t> H5Dataset::get_size() const
{
       return m_space.get_size();
}

NS_MIA_END
