/* -*- 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 <boost/mpl/vector.hpp>
#include <boost/mpl/for_each.hpp>


NS_MIA_BEGIN

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



typedef boost::mpl::vector<int8_t,
        uint8_t,
        int16_t,
        uint16_t,
        int32_t,
        uint32_t,
        int64_t,
        uint64_t,
        float,
        double
        > HDF5BasicPixelTypes;

template <typename I>
static void check_id(hid_t id, const char *domain, const char *action, I info)
{
       if (id < 0) {
              throw create_exception<invalid_argument>(domain, ": error in ", action, ":", info);
       }
}


H5Attribute::H5Attribute(hid_t id, const H5Space& space):
       H5Base(H5AttributeHandle(id)),
       m_space(space)
{
}

H5Attribute H5Attribute::write(const H5Base& parent, const char *name, const CAttribute& attr)
{
       return H5AttributeTranslatorMap::instance().translate(parent, name, attr);
}

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

H5Space H5Attribute::get_space()const
{
       return m_space;
}

H5Type H5Attribute::get_type() const
{
       H5Type file_type(H5Aget_type(*this));
       return file_type.get_native_type();
}

PAttribute H5Attribute::read(const H5Base& parent, const char *name)
{
       auto id = H5Aopen(parent, name, H5P_DEFAULT);
       check_id(id, "H5Attribute", "open", name);
       auto space_id = H5Aget_space(id);
       check_id(space_id, "H5Attribute", "get_space", name);
       H5Space space(space_id);
       H5Attribute attr(id, space);
       return 	H5AttributeTranslatorMap::instance().translate(name, attr);
}


template <typename T>
class H5TAttributeTranslator: public H5AttributeTranslator
{
       H5Attribute apply(const H5Base& parent, const char *name, const CAttribute& attr) const;
       virtual PAttribute apply(const char *name, const H5Attribute& attr ) const;
};

template <typename T>
struct  __dispatch_H5TAttributeTranslator {
       static H5Attribute apply(const H5Base& parent, const char *name, const TAttribute<T>& attr);
};

template <typename T>
struct  __dispatch_H5TAttributeTranslator<vector<T>> {
       static H5Attribute apply(const H5Base& parent, const char *name, const TAttribute<vector<T>>& attr);
};


template <typename T>
H5Attribute  __dispatch_H5TAttributeTranslator<T>::apply(const H5Base& parent, const char *name, const TAttribute<T>& attr)
{
       auto file_datatype = Mia_to_h5_types<T>::file_datatype();
       auto mem_datatype = Mia_to_h5_types<T>::mem_datatype();
       T value = attr;
       auto space = H5Space::create();
       auto id = H5Acreate(parent, name, file_datatype, space, H5P_DEFAULT, H5P_DEFAULT);
       check_id(id, "H5Attribute", "translate", name);
       H5Attribute result(id, space);
       H5Awrite(result, mem_datatype, &value);
       result.set_parent(parent);
       return result;
}

template <typename T>
H5Attribute  __dispatch_H5TAttributeTranslator<vector<T>>::apply(const H5Base& parent, const char *name,
              const TAttribute<vector<T>>& attr)
{
       auto file_datatype = Mia_to_h5_types<T>::file_datatype();
       auto mem_datatype = Mia_to_h5_types<T>::mem_datatype();
       const std::vector<T>& value = attr;
       auto space = H5Space::create(value.size());
       auto id = H5Acreate(parent, name, file_datatype, space, H5P_DEFAULT, H5P_DEFAULT);
       check_id(id, "H5Attribute", "translate", name);
       H5Attribute result(id, space);
       H5Awrite(result, mem_datatype, &value[0]);
       result.set_parent(parent);
       return result;
}

template <typename T>
H5Attribute H5TAttributeTranslator<T>::apply(const H5Base& parent, const char *name, const CAttribute& __attr) const
{
       auto type_id = __attr.type_id();

       if (EAttributeType::is_vector(type_id)) {
              auto& attr = dynamic_cast<const TAttribute<vector<T>>&>(__attr);
              return __dispatch_H5TAttributeTranslator<vector<T>>::apply(parent, name, attr);
       } else {
              auto& attr = dynamic_cast<const TAttribute<T>&>(__attr);
              return __dispatch_H5TAttributeTranslator<T>::apply(parent, name, attr);
       }
}

template <typename T>
PAttribute H5TAttributeTranslator<T>::apply(const char *name, const H5Attribute& attr) const
{
       auto dim = attr.get_size();

       if (dim.empty()) {
              T result_value;
              H5Aread(attr, Mia_to_h5_types<T>::mem_datatype(), &result_value);
              return PAttribute(new TAttribute<T>(result_value));
       } else if (dim.size() == 1) {
              vector<T> data(dim[0]);
              H5Aread(attr, Mia_to_h5_types<T>::mem_datatype(), &data[0]);
              return PAttribute(new TAttribute<vector<T>>(data));
       } else {
              throw create_exception<runtime_error>("H5TAttributeTranslator: attribute(", name, "): with ", dim.size(),
                                                    " not supported not supported by translator");
       }
}

class H5TAttributeStringTranslator: public H5AttributeTranslator
{
       H5Attribute apply(const H5Base& parent, const char *name, const CAttribute& attr) const;
       virtual PAttribute apply(const char *name, const H5Attribute& attr ) const;

       H5Attribute generic_apply(const H5Base& parent, const char *name, const CAttribute& attr) const;
       H5Attribute apply(const H5Base& parent, const char *name, const CVStringAttribute& attr) const;

};

H5Attribute H5TAttributeStringTranslator::generic_apply(const H5Base& parent, const char *name, const CAttribute& attr) const
{
       const string value = attr.as_string();
       cvdebug() << "Write string attribute '" << attr << "'\n";
       H5Type stype(H5Tcopy (H5T_C_S1));
       H5Tset_size(stype, H5T_VARIABLE);
       vector<hsize_t> size(1, 1);
       auto space = H5Space::create(size);
       auto id = H5Acreate(parent, name, stype, space, H5P_DEFAULT, H5P_DEFAULT);
       check_id(id, "H5Attribute", "translate string", name);
       H5Attribute result(id, space);
       const char *s = value.c_str();

       if (H5Awrite(result, stype, &s) < 0)
              cvwarn() << "error writing attribute '" << name << "'\n";

       result.set_parent(parent);
       cvdebug() << "done with '" << attr << "'\n";
       return result;
}

H5Attribute H5TAttributeStringTranslator::apply(const H5Base& parent, const char *name, const CVStringAttribute& attr) const
{
       const vector<string> value = attr;
       vector<hsize_t> dims(1, value.size());
       H5Type stype(H5Tcopy (H5T_C_S1));
       H5Tset_size(stype, H5T_VARIABLE);
       H5Space space = H5Space::create(dims);
       auto id = H5Acreate(parent, name, stype, space, H5P_DEFAULT, H5P_DEFAULT);
       check_id(id, "H5Attribute", "translate string vector", name);
       H5Attribute result(id, space);
       vector<const char *> wstrings(value.size());
       transform(value.begin(), value.end(), wstrings.begin(),
       [](const string & s) {
              return s.c_str();
       });

       if (H5Awrite(result, stype, &wstrings[0]) < 0)
              cvwarn() << "error writing vector<string> attribute '" << name << "'\n";

       result.set_parent(parent);
       return result;
}

H5Attribute H5TAttributeStringTranslator::apply(const H5Base& parent, const char *name, const CAttribute& __attr) const
{
       if (EAttributeType::is_vector(__attr.type_id())) {
              return apply(parent, name, dynamic_cast<const CVStringAttribute&>(__attr));
       } else {
              return generic_apply(parent, name, __attr);
       }
}

PAttribute H5TAttributeStringTranslator::apply(const char *name, const H5Attribute& attr ) const
{
       PAttribute result;
       auto size = attr.get_size();
       auto space = attr.get_space();

       if (size.size() > 1) {
              cvwarn() << "Support for " << size.size() << "-dimensional string attributes not implemented."
                       << " Reading only first dimension.\n";
       }

       H5Type memtype(H5Tcopy (H5T_C_S1));
       H5Tset_size (memtype, H5T_VARIABLE);

       if (size.empty() || size[0] == 1) {
              vector<char *> rdata(1);
              auto status = H5Aread (attr, memtype, &rdata[0]);

              if (status < 0) {
                     cvwarn() << "Error reading string attribute\n";
                     return PAttribute();
              }

              cvdebug() << "post-convert '" << name << "' from '" << rdata[0] << "'\n";
              result = CStringAttrTranslatorMap::instance().to_attr(name, string(rdata[0]));
              H5Dvlen_reclaim (memtype, space, H5P_DEFAULT, &rdata[0]);
       } else {
              vector<char *> rdata(size[0]);
              auto status = H5Aread (attr, memtype, &rdata[0]);

              if (status < 0) {
                     cvwarn() << "Error reading vector<string> attribute\n";
                     return PAttribute();
              }

              vector<string> result_value;

              for (size_t i = 0; i < size[0]; ++i) {
                     result_value.push_back(string(rdata[i]));
              }

              result.reset(new CVStringAttribute(result_value));
              H5Dvlen_reclaim (memtype, space, H5P_DEFAULT, &rdata[0]);
       }

       return result;
}

struct translator_append {
       translator_append(H5AttributeTranslatorMap& map): m_map(map) {};
       template <typename T>
       void operator () (T& MIA_PARAM_UNUSED(dummy))
       {
              m_map.register_translator(attribute_type<T>::value,
                                        PH5AttributeTranslator(new H5TAttributeTranslator<T>));
       }
       H5AttributeTranslatorMap& m_map;
};

H5AttributeTranslatorMap::H5AttributeTranslatorMap()
{
       boost::mpl::for_each<HDF5BasicPixelTypes>(translator_append(*this));
       register_translator(attribute_type<string>::value,
                           PH5AttributeTranslator(new H5TAttributeStringTranslator()));
}

H5AttributeTranslatorMap& H5AttributeTranslatorMap::instance()
{
       static H5AttributeTranslatorMap map;
       return map;
}

void H5AttributeTranslatorMap::register_translator(int type_id, PH5AttributeTranslator translator)
{
       assert(m_map.find(type_id) == m_map.end());
       m_map[type_id] = translator;
}

const H5AttributeTranslator& H5AttributeTranslatorMap::get_translator(int type_id) const
{
       auto itranslator = m_map.find(type_id);

       if (itranslator != m_map.end()) {
              return *itranslator->second;
       } else {
              auto scalar_type_id = EAttributeType::scalar_type(type_id);
              auto itranslator = m_map.find(scalar_type_id);

              if (itranslator == m_map.end()) {
                     auto tid = attribute_type<string>::value;
                     itranslator = m_map.find(tid);
                     assert(itranslator != m_map.end());
              }

              return *itranslator->second;
       }
}

PAttribute  H5AttributeTranslatorMap::translate(const char *name, const H5Attribute& attr)
{
       return get_translator(attr.get_type().get_mia_type_id()).apply(name, attr);
}

H5Attribute H5AttributeTranslatorMap::translate(const H5Base& parent, const char *name, const CAttribute& attr)
{
       return get_translator(attr.type_id()).apply(parent, name, attr);
}

void translate_to_hdf5_attributes(const H5Base& target, const CAttributedData& data)
{
       for ( auto a = data.begin_attributes(); a != data.end_attributes(); ++a)
              H5AttributeTranslatorMap::instance().translate(target, a->first.c_str(), *a->second);
}

NS_MIA_END
