/*
 * Copyright (c) 2016-2019 Arm Limited
 * All rights reserved
 *
 * The license below extends only to copyright in the software and shall
 * not be construed as granting a license to any other intellectual
 * property including but not limited to intellectual property relating
 * to a hardware implementation of the functionality of the software
 * licensed hereunder.  You may use the software subject to the license
 * terms below provided that you ensure that this notice is replicated
 * unmodified and in its entirety in all distributions of the software,
 * modified or unmodified, in source code or in binary form.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met: redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer;
 * redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution;
 * neither the name of the copyright holders nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "base/stats/hdf5.hh"

#include "base/logging.hh"
#include "base/stats/info.hh"

/**
 * Check if all strings in a container are empty.
 */
template<typename T>
bool emptyStrings(const T &labels)
{
    for (const auto &s : labels) {
        if (!s.empty())
            return false;
    }
    return true;
}


namespace Stats {

Hdf5::Hdf5(const std::string &file, unsigned chunking,
           bool desc, bool formulas)
    : fname(file), timeChunk(chunking),
      enableDescriptions(desc), enableFormula(formulas),
      dumpCount(0)
{
    // Tell the library not to print exceptions by default. There are
    // cases where we rely on exceptions to determine if we need to
    // create a node or if we can just open it.
    H5::Exception::dontPrint();
}

Hdf5::~Hdf5()
{
}


void
Hdf5::begin()
{
    h5File = H5::H5File(fname,
                        // Truncate the file if this is the first dump
                        dumpCount > 0 ? H5F_ACC_RDWR : H5F_ACC_TRUNC);
    path.push(h5File.openGroup("/"));
}

void
Hdf5::end()
{
    assert(valid());

    dumpCount++;
}

bool
Hdf5::valid() const
{
    return true;
}


void
Hdf5::beginGroup(const char *name)
{
    auto base = path.top();

    // Try to open an existing stat group corresponding to the
    // name. Create it if it doesn't exist.
    H5::Group group;
    try {
        group = base.openGroup(name);
    } catch (const H5::FileIException& e) {
        group = base.createGroup(name);
    } catch (const H5::GroupIException& e) {
        group = base.createGroup(name);
    }

    path.push(group);
}

void
Hdf5::endGroup()
{
    assert(!path.empty());
    path.pop();
}

void
Hdf5::visit(const ScalarInfo &info)
{
    // Since this stat is a scalar, we need 1-dimensional value in the
    // stat file. The Hdf5::appendStat helper will populate the size
    // of the first dimension (time).
    hsize_t fdims[1] = { 0, };
    double data[1] = { info.result(), };

    appendStat(info, 1, fdims, data);
}

void
Hdf5::visit(const VectorInfo &info)
{
    appendVectorInfo(info);
}

void
Hdf5::visit(const DistInfo &info)
{
    warn_once("HDF5 stat files don't support distributions.\n");
}

void
Hdf5::visit(const VectorDistInfo &info)
{
    warn_once("HDF5 stat files don't support vector distributions.\n");
}

void
Hdf5::visit(const Vector2dInfo &info)
{
    // Request a 3-dimensional stat, the first dimension will be
    // populated by the Hdf5::appendStat() helper. The remaining two
    // dimensions correspond to the stat instance.
    hsize_t fdims[3] = { 0, info.x, info.y };
    H5::DataSet data_set = appendStat(info, 3, fdims, info.cvec.data());

    if (dumpCount == 0) {
        if (!info.subnames.empty() && !emptyStrings(info.subnames))
            addMetaData(data_set, "subnames", info.subnames);

        if (!info.y_subnames.empty() && !emptyStrings(info.y_subnames))
            addMetaData(data_set, "y_subnames", info.y_subnames);

        if (!info.subdescs.empty() && !emptyStrings(info.subdescs))
            addMetaData(data_set, "subdescs", info.subdescs);
    }
}

void
Hdf5::visit(const FormulaInfo &info)
{
    if (!enableFormula)
        return;

    H5::DataSet data_set = appendVectorInfo(info);

    if (dumpCount == 0)
        addMetaData(data_set, "equation", info.str());
}

void
Hdf5::visit(const SparseHistInfo &info)
{
    warn_once("HDF5 stat files don't support sparse histograms.\n");
}

H5::DataSet
Hdf5::appendVectorInfo(const VectorInfo &info)
{
    const VResult &vr(info.result());
    // Request a 2-dimensional stat, the first dimension will be
    // populated by the Hdf5::appendStat() helper. The remaining
    // dimension correspond to the stat instance.
    hsize_t fdims[2] = { 0, vr.size() };
    H5::DataSet data_set = appendStat(info, 2, fdims, vr.data());

    if (dumpCount == 0) {
        if (!info.subnames.empty() && !emptyStrings(info.subnames))
            addMetaData(data_set, "subnames", info.subnames);

        if (!info.subdescs.empty() && !emptyStrings(info.subdescs))
            addMetaData(data_set, "subdescs", info.subdescs);
    }

    return data_set;
}

H5::DataSet
Hdf5::appendStat(const Info &info, int rank, hsize_t *dims, const double *data)
{
    H5::Group group = path.top();
    H5::DataSet data_set;
    H5::DataSpace fspace;

    dims[0] = dumpCount + 1;

    if (dumpCount > 0) {
        // Get the existing stat if we have already dumped this stat
        // before.
        data_set = group.openDataSet(info.name);
        data_set.extend(dims);
        fspace = data_set.getSpace();
    } else {
        // We don't have the stat already, create it.

        H5::DSetCreatPropList props;

        // Setup max dimensions based on the requested file dimensions
        std::vector<hsize_t> max_dims(rank);
        std::copy(dims, dims + rank, max_dims.begin());
        max_dims[0] = H5S_UNLIMITED;

        // Setup chunking
        std::vector<hsize_t> chunk_dims(rank);
        std::copy(dims, dims + rank, chunk_dims.begin());
        chunk_dims[0] = timeChunk;
        props.setChunk(rank, chunk_dims.data());

        // Enable compression
        props.setDeflate(1);

        fspace = H5::DataSpace(rank, dims, max_dims.data());
        data_set = group.createDataSet(info.name, H5::PredType::NATIVE_DOUBLE,
                                       fspace, props);

        if (enableDescriptions && !info.desc.empty()) {
            addMetaData(data_set, "description", info.desc);
        }
    }

    // The first dimension is time which isn't included in data.
    dims[0] = 1;
    H5::DataSpace mspace(rank, dims);
    std::vector<hsize_t> foffset(rank, 0);
    foffset[0] = dumpCount;

    fspace.selectHyperslab(H5S_SELECT_SET, dims, foffset.data());
    data_set.write(data, H5::PredType::NATIVE_DOUBLE, mspace, fspace);

    return data_set;
}

void
Hdf5::addMetaData(H5::DataSet &loc, const char *name,
                  const std::vector<const char *> &values)
{
    H5::StrType type(H5::PredType::C_S1, H5T_VARIABLE);
    hsize_t dims[1] = { values.size(), };
    H5::DataSpace space(1, dims);
    H5::Attribute attribute = loc.createAttribute(name, type, space);
    attribute.write(type, values.data());
}

void
Hdf5::addMetaData(H5::DataSet &loc, const char *name,
                  const std::vector<std::string> &values)
{
    std::vector<const char *> cstrs(values.size());
    for (int i = 0; i < values.size(); ++i)
        cstrs[i] = values[i].c_str();

    addMetaData(loc, name, cstrs);
}

void
Hdf5::addMetaData(H5::DataSet &loc, const char *name,
                  const std::string &value)
{
    H5::StrType type(H5::PredType::C_S1, value.length() + 1);
    hsize_t dims[1] = { 1, };
    H5::DataSpace space(1, dims);
    H5::Attribute attribute = loc.createAttribute(name, type, space);
    attribute.write(type, value.c_str());
}

void
Hdf5::addMetaData(H5::DataSet &loc, const char *name, double value)
{
    hsize_t dims[1] = { 1, };
    H5::DataSpace space(1, dims);
    H5::Attribute attribute = loc.createAttribute(
        name, H5::PredType::NATIVE_DOUBLE, space);
    attribute.write(H5::PredType::NATIVE_DOUBLE, &value);
}


std::unique_ptr<Output>
initHDF5(const std::string &filename, unsigned chunking,
         bool desc, bool formulas)
{
    return  std::unique_ptr<Output>(
        new Hdf5(simout.resolve(filename), chunking, desc, formulas));
}

}; // namespace Stats
