/******************************************************************************
  This source file is part of the Avogadro project.
  This source code is released under the 3-Clause BSD License, (see "LICENSE").
******************************************************************************/

#ifndef AVOGADRO_HDF5DATAFORMAT_H
#define AVOGADRO_HDF5DATAFORMAT_H

#include "avogadroioexport.h"

#include <avogadro/core/matrix.h> // can't forward declare eigen types

#include <cstddef>
#include <string>
#include <vector>

namespace Avogadro {
namespace Core {
template <typename T>
class Array;
}
namespace Io {

/**
 * @class Hdf5DataFormat hdf5dataformat.h <avogadro/io/hdf5dataformat.h>
 * @brief The Hdf5DataFormat class provides access to data stored in HDF5 files.
 * @author Allison Vacanti
 *
 * This class is intended to supplement an existing format reader/writer by
 * providing the option to write large data to an HDF5 file store. The purpose
 * is to keep text format files at a manageable size.
 *
 * To use this class, open or create an HDF5 file with the openFile method,
 * using the appropriate OpenMode for the intended operation. Data can be
 * written to the file using the writeDataset methods and retrieved using the
 * readDataset methods. When finished, call closeFile to release the file
 * resources from the HDF5 library.
 *
 * A complete set of datasets available in an open file can be retrieved with
 * the datasets() method, and the existence of a particular dataset can be
 * tested with datasetExists(). removeDataset() can be used to unlink an
 * existing dataset from the file, though this will not free any space on disk.
 * The space occupied by an unlinked dataset may be reclaimed by new write
 * operations, but only if they occur before the file is closed.
 *
 * A convenient thresholding system is implemented to help the accompanying text
 * format writer determine which data is "large" enough to be stored in HDF5. A
 * size threshold (in bytes) may be set with the setThreshold() function (the
 * default is 1KB). A data object may be passed to the exceedsThreshold method
 * to see if the size of the data in the container exceeds the currently set
 * threshold. If so, it should be written into the HDF5 file by writeDataset.
 * If not, it should be serialized into the text file in a suitable format. The
 * thresholding operations are optional; the threshold size does not affect the
 * behavior of the read/write methods and are only for user convenience.
 */
class AVOGADROIO_EXPORT Hdf5DataFormat
{
public:
  Hdf5DataFormat();

  /** Destructor. Closes any open file before freeing memory. */
  ~Hdf5DataFormat();

  /** Open modes for use with openFile(). */
  enum OpenMode
  {
    /** Open an existing file in read-only mode. The file must exist. */
    ReadOnly = 0,
    /**
     * Create a file in read/write mode, removing any existing file with the
     * same name.
     */
    ReadWriteTruncate,
    /**
     * Open an file in read/write mode. If the file exist, its contents will be
     * preserved. If it does not, a new file will be created.
     */
    ReadWriteAppend
  };

  /** @return true if a file is open. */
  bool isOpen() const;

  /**
   * @brief openFile Open a file for use by this reader/writer.
   * @param filename_ Name of the file to open.
   * @param mode OpenMode for the file. Default is ReadWriteAppend.
   * @note Only a single file may be opened at a time. Attempting to open
   * multiple files without calling closeFile() will fail.
   * @return True if the file is successfully opened/create by the HDF5
   * subsystem, false otherwise.
   */
  bool openFile(const std::string& filename_, OpenMode mode = ReadWriteAppend);

  /**
   * @return The name of the open file, or an empty string if no file is open.
   */
  std::string filename() const;

  /**
   * @brief closeFile Close the file and reset the reader/writer. Another file
   * may be opened after calling this function.
   * @return true if the file is successfully released by the HDF5 subsystem.
   */
  bool closeFile();

  /**
   * @brief setThreshold Set the threshold size in bytes that will be used in
   * the exceedsThreshold functions. The threshold can be used to determine
   * which
   * data is considered "large enough" to be stored in HDF5, rather than an
   * accompanying format.
   * @param bytes The size in bytes for the threshold. Default: 1KB.
   */
  void setThreshold(size_t bytes);

  /** @return The current threshold size in bytes. Default: 1KB. */
  size_t threshold() const;

  /**
   * @brief exceedsThreshold Test if a data set is "large enough" to be stored
   * in HDF5 format. If this function returns true, the number of bytes tested
   * is larger than the threshold and the data should be written into the HDF5
   * file. If false, the data should be written into the accompanying format.
   * @param bytes The size of the dataset in bytes
   * @return true if the size exceeds the threshold set by setThreshold.
   */
  bool exceedsThreshold(size_t bytes) const;

  /**
   * @brief exceedsThreshold Test if a data set is "large enough" to be stored
   * in HDF5 format. If this function returns true, the size of the data in the
   * object is larger than the threshold and should be written into the HDF5
   * file. If false, the data should be written into the accompanying format.
   * @param data Data object to test.
   * @return true if the size of the serializable data in @a data exceeds the
   * threshold set by setThreshold.
   */
  bool exceedsThreshold(const MatrixX& data) const;

  /**
   * @brief exceedsThreshold Test if a data set is "large enough" to be stored
   * in HDF5 format. If this function returns true, the size of the data in the
   * object is larger than the threshold and should be written into the HDF5
   * file. If false, the data should be written into the accompanying format.
   * @param data Data object to test.
   * @return true if the size of the serializable data in @a data exceeds the
   * threshold set by setThreshold.
   */
  bool exceedsThreshold(const std::vector<double>& data) const;

  /**
   * @brief exceedsThreshold Test if a data set is "large enough" to be stored
   * in HDF5 format. If this function returns true, the size of the data in the
   * object is larger than the threshold and should be written into the HDF5
   * file. If false, the data should be written into the accompanying format.
   * @param data Data object to test.
   * @return true if the size of the serializable data in @a data exceeds the
   * threshold set by setThreshold.
   */
  bool exceedsThreshold(const Core::Array<double>& data) const;

  /**
   * @brief datasetExists Test if the currently open file contains a dataset at
   * the HDF5 absolute path @a path.
   * @param path An absolute path into the HDF5 data.
   * @return true if the object at @a path both exists and is a dataset, false
   * otherwise.
   */
  bool datasetExists(const std::string& path) const;

  /**
   * @brief removeDataset Remove a dataset from the currently opened file.
   * @param path An absolute path into the HDF5 data.
   * @return true if the dataset exists and has been successfully removed.
   * \warning Removing datasets can be expensive in terms of filesize, as
   * deleted space cannot be reclaimed by HDF5 once the file is closed, and the
   * file will not decrease in size as datasets are removed. For details, see
   * http://www.hdfgroup.org/HDF5/doc/H5.user/Performance.html#Freespace.
   */
  bool removeDataset(const std::string& path) const;

  /**
   * @brief datasetDimensions Find the dimensions of a dataset.
   * @param path An absolute path into the HDF5 data.
   * @return A vector containing the dimensionality of the data, major dimension
   * first. If an error is encountered, an empty vector is returned.
   */
  std::vector<int> datasetDimensions(const std::string& path) const;

  /**
   * @brief writeDataset Write the data to the currently opened file at the
   * specified absolute HDF5 path.
   * @param path An absolute path into the HDF5 data.
   * @param data The data container to serialize to HDF5.
   * @return true if the data is successfully written, false otherwise.
   */
  bool writeDataset(const std::string& path, const MatrixX& data) const;

  /**
   * @brief writeDataset Write the data to the currently opened file at the
   * specified absolute HDF5 path.
   * @param path An absolute path into the HDF5 data.
   * @param data The data container to serialize to HDF5.
   * @param ndims The number of dimensions in the data. Default: 1.
   * @param dims The dimensionality of the data, major dimension first. Default:
   * data.size().
   * @note Since std::vector is a flat container, the dimensionality data is
   * only used to set up the dataset metadata in the HDF5 container. Omitting
   * the dimensionality parameters will write a flat array.
   * @return true if the data is successfully written, false otherwise.
   */
  bool writeDataset(const std::string& path, const std::vector<double>& data,
                    int ndims = 1, size_t* dims = nullptr) const;

  /**
   * @brief writeDataset Write the data to the currently opened file at the
   * specified absolute HDF5 path.
   * @param path An absolute path into the HDF5 data.
   * @param data The data container to serialize to HDF5.
   * @param ndims The number of dimensions in the data. Default: 1.
   * @param dims The dimensionality of the data, major dimension first. Default:
   * data.size().
   * @note Since this is a flat container, the dimensionality data is
   * only used to set up the dataset metadata in the HDF5 container. Omitting
   * the dimensionality parameters will write a flat array.
   * @return true if the data is successfully written, false otherwise.
   */
  bool writeDataset(const std::string& path, const Core::Array<double>& data,
                    int ndims = 1, size_t* dims = nullptr) const;

  /**
   * @brief readDataset Populate the data container @data with data at from the
   * specified path in the currently opened HDF5 file.
   * @param path An absolute path into the HDF5 data.
   * @param data The data container to into which the HDF5 data shall be
   * deserialized. @a data will be resized to fit the data.
   * @return true if the data is successfully read, false otherwise. If the
   * read fails, the @a data object may be left in an unpredictable state.
   */
  bool readDataset(const std::string& path, MatrixX& data) const;

  /**
   * @brief readDataset Populate the data container @data with data at from the
   * specified path in the currently opened HDF5 file.
   * @param path An absolute path into the HDF5 data.
   * @param data The data container to into which the HDF5 data shall be
   * deserialized. @a data will be resized to fit the data.
   * @return A vector containing the dimensionality of the dataset, major
   * dimension first. If an error occurs, an empty vector is returned and *data
   * will be set to nullptr.
   */
  std::vector<int> readDataset(const std::string& path,
                               std::vector<double>& data) const;

  /**
   * @brief readDataset Populate the data container @data with data at from the
   * specified path in the currently opened HDF5 file.
   * @param path An absolute path into the HDF5 data.
   * @param data The data container to into which the HDF5 data shall be
   * deserialized. @a data will be resized to fit the data.
   * @return A vector containing the dimensionality of the dataset, major
   * dimension first. If an error occurs, an empty vector is returned and *data
   * will be set to nullptr.
   */
  std::vector<int> readDataset(const std::string& path,
                               Core::Array<double>& data) const;

  /**
   * @brief datasets Traverse the currently opened file and return a list of all
   * dataset objects in the file.
   * @return A list of datasets in the current file.
   * \warning The list is not cached internal and is recalculated on each call.
   * This may be expensive on large HDF5 files, so external caching is
   * recommended if this data is frequently needed.
   */
  std::vector<std::string> datasets() const;

  /** Used to abstract details of container resizing. */
  class ResizeContainer;

private:
  /**
   * @brief writeRawDataset Write the data to the currently opened file at the
   * specified absolute HDF5 path.
   * @param path An absolute path into the HDF5 data.
   * @param data The data container to serialize to HDF5.
   * @param ndims The number of dimensions in the data.
   * @param dims The data dimensions, major dimension first.
   * @note Since a double[] is a flat container, the dimensionality data is
   * only used to set up the dataset metadata in the HDF5 container. The result
   * of multiplying all values in @a dims must equal the length of the @a data.
   * @return true if the data is successfully written, false otherwise.
   */
  bool writeRawDataset(const std::string& path, const double data[], int ndims,
                       size_t dims[]) const;

  /**
   * @brief readRawDataset Populate the data container @data with data from the
   * specified path in the currently opened HDF5 file.
   * @param path An absolute path into the HDF5 data.
   * @param container A subclass of ResizeContainer with the container to read
   * data into.
   * @return A vector containing the dimensionality of the dataset, major
   * dimension first. If an error occurs, an empty vector is returned.
   */
  std::vector<int> readRawDataset(const std::string& path,
                                  ResizeContainer& container) const;

  class Private;
  /** Internal storage, used to encapsulate HDF5 data. */
  Private* const d;
};

} // namespace Io
} // namespace Avogadro

#endif // AVOGADRO_HDF5DATAFORMAT_H
