/* SPDX-License-Identifier: Apache-2.0 */
/* Copyright (C) 2018 IBM Corp. */
#pragma once

#include <cstring>
#include <experimental/filesystem>
#include <memory>
#include <numeric>
#include <vector>

extern "C"
{
#include "backend.h"
#include "common.h"
#include "vpnor/backend.h"
#include "vpnor/ffs.h"
}

struct mbox_context;

namespace openpower
{
namespace virtual_pnor
{

namespace fs = std::experimental::filesystem;

using PartitionTable = std::vector<uint8_t>;
using checksum_t = uint32_t;

/** @brief Convert the input partition table to big endian.
 *
 *  @param[in] src - reference to the pnor partition table
 *
 *  @returns converted partition table
 */
PartitionTable endianFixup(const PartitionTable& src);

/** @brief Parse a ToC line (entry) into the corresponding FFS partition
 * object.
 *
 * @param[in] line - The ToC line to parse
 * @param[in] blockSize - The flash block size in bytes
 * @param[out] part - The partition object to populate with the information
 *                    parsed from the provided ToC line
 *
 * Throws: MalformedTocEntry, InvalidTocEntry
 */
void parseTocLine(const std::string& line, size_t blockSize,
                  pnor_partition& part);

namespace details
{

/** @brief Compute XOR-based checksum, by XORing consecutive words
 *         in the input data.
 *
 *  @param[in] data - input data on which checksum is computed
 *
 *  @returns computed checksum
 */
template <class T>
checksum_t checksum(const T& data)
{
    static_assert(sizeof(decltype(data)) % sizeof(checksum_t) == 0,
                  "sizeof(data) is not aligned to sizeof(checksum_t) boundary");

    /* Shut the compiler up about alignment, consider alternatives */
    const size_t n_elems = sizeof(decltype(data)) / sizeof(checksum_t);
    checksum_t csdata[n_elems];
    memcpy(csdata, &data, sizeof(csdata));
    auto end = csdata + n_elems;
    return std::accumulate(csdata, end, 0, std::bit_xor<checksum_t>());
}

} // namespace details

namespace partition
{

/** @class Table
 *  @brief Generates virtual PNOR partition table.
 *
 *  Generates virtual PNOR partition table upon construction. Reads
 *  the PNOR information generated by this tool :
 *  github.com/openbmc/openpower-pnor-code-mgmt/blob/master/generate-tar,
 *  which generates a minimalistic table-of-contents (toc) file and
 *  individual files to represent various partitions that are of interest -
 *  these help form the "virtual" PNOR, which is typically a subset of the full
 *  PNOR image.
 *  These files are stored in a well-known location on the PNOR.
 *  Based on this information, this class prepares the partition table whose
 *  structure is as outlined in partition.hpp.
 *
 *  The virtual PNOR supports 4KB erase blocks - partitions must be aligned to
 *  this size.
 */
class Table
{
  public:
    /** @brief Constructor accepting the path of the directory
     *         that houses the PNOR partition files.
     *
     *  @param[in] be - Acquire sizes and paths relevant to the table
     *
     * Throws MalformedTocEntry, InvalidTocEntry
     */
    Table(const struct backend* be);

    Table(const Table&) = delete;
    Table& operator=(const Table&) = delete;
    Table(Table&&) = delete;
    Table& operator=(Table&&) = delete;
    ~Table() = default;

    /** @brief Return the exact size of partition table in bytes
     *
     *  @returns size_t - size of partition table in bytes
     */
    size_t size() const
    {
        return szBytes;
    }

    /** @brief Return aligned size of partition table in bytes
     *
     *  The value returned will be greater-than or equal to size(), and
     *  aligned to blockSize.
     *
     *  @returns size_t - capacity of partition table in bytes
     */
    size_t capacity() const
    {
        return align_up(szBytes, blockSize);
    }

    /** @brief Return the size of partition table in blocks
     *
     *  @returns size_t - size of partition table in blocks
     */
    size_t blocks() const
    {
        return capacity() / blockSize;
    }

    /** @brief Return a partition table having byte-ordering
     *         that the host expects.
     *
     *  The host needs the partion table in big-endian.
     *
     *  @returns const reference to host partition table.
     */
    const pnor_partition_table& getHostTable() const
    {
        return *(reinterpret_cast<const pnor_partition_table*>(hostTbl.data()));
    }

    /** @brief Return a little-endian partition table
     *
     *  @returns const reference to native partition table
     */
    const pnor_partition_table& getNativeTable() const
    {
        return *(reinterpret_cast<const pnor_partition_table*>(tbl.data()));
    }

    /** @brief Return partition corresponding to PNOR offset, the offset
     *         is within returned partition.
     *
     *  @param[in] offset - PNOR offset in bytes
     *
     *  @returns const reference to pnor_partition, if found, else an
     *           exception will be thrown.
     *
     *  Throws: UnmappedOffset
     */
    const pnor_partition& partition(size_t offset) const;

    /** @brief Return partition corresponding to input partition name.
     *
     *  @param[in] name - PNOR partition name
     *
     *  @returns const reference to pnor_partition, if found, else an
     *           exception will be thrown.
     *
     *  Throws: UnknownPartition
     */
    const pnor_partition& partition(const std::string& name) const;

  private:
    /** @brief Prepares a vector of PNOR partition structures.
     *
     *  @param[in] ctx - An mbox context providing partition locations
     *
     * Throws: MalformedTocEntry, InvalidTocEntry
     */
    void preparePartitions(const struct vpnor_data* ctx);

    /** @brief Prepares the PNOR header.
     */
    void prepareHeader();

    /** @brief Allocate memory to hold the partion table. Determine the
     *         amount needed based on the partition files in the toc file.
     *
     *  @param[in] tocFile - Table of contents file path.
     */
    void allocateMemory(const fs::path& tocFile);

    /** @brief Return a little-endian partition table
     *
     *  @returns reference to native partition table
     */
    pnor_partition_table& getNativeTable()
    {
        return *(reinterpret_cast<pnor_partition_table*>(tbl.data()));
    }

    /** @brief Size of the PNOR partition table -
     *         sizeof(pnor_partition_table) +
     *         (no. of partitions * sizeof(pnor_partition)),
     */
    size_t szBytes;

    /** @brief Partition table */
    PartitionTable tbl;

    /** @brief Partition table with host byte ordering */
    PartitionTable hostTbl;

    /** @brief Directory housing generated PNOR partition files */
    fs::path directory;

    /** @brief Number of partitions */
    size_t numParts;

    /** @brief PNOR block size, in bytes */
    size_t blockSize;

    /** @brief PNOR size, in bytes */
    size_t pnorSize;
};
} // namespace partition

/** @brief An exception type storing a reason string.
 *
 *  This looks a lot like how std::runtime_error might be implemented however
 *  we want to avoid extending it, as exceptions extending ReasonedError have
 *  an expectation of being handled (can be predicted and are inside the scope
 *  of the program).
 *
 *  From std::runtime_error documentation[1]:
 *
 *  > Defines a type of object to be thrown as exception. It reports errors
 *  > that are due to events beyond the scope of the program and can not be
 *  > easily predicted.
 *
 *  [1] http://en.cppreference.com/w/cpp/error/runtime_error
 *
 *  We need to keep the inheritance hierarchy separate: This avoids the
 *  introduction of code that overzealously catches std::runtime_error to
 *  handle exceptions that would otherwise derive ReasonedError, and in the
 *  process swallows genuine runtime failures.
 */
class ReasonedError : public std::exception
{
  public:
    ReasonedError(const std::string&& what) : _what(what)
    {}
    const char* what() const noexcept
    {
        return _what.c_str();
    };

  private:
    const std::string _what;
};

/** @brief Base exception type for errors related to ToC entry parsing.
 *
 *  Callers of parseTocEntry() may not be concerned with the specifics and
 *  rather just want to extract and log what().
 */
class TocEntryError : public ReasonedError
{
  public:
    TocEntryError(const std::string&& reason) : ReasonedError(std::move(reason))
    {}
};

/** @brief The exception thrown on finding a syntax error in the ToC entry
 *
 *  If the syntax is wrong, or expected values are missing, the ToC entry is
 *  malformed
 */
class MalformedTocEntry : public TocEntryError
{
  public:
    MalformedTocEntry(const std::string&& reason) :
        TocEntryError(std::move(reason))
    {}
};

/** @brief The exception thrown on finding a semantic error in the ToC entry
 *
 *  If the syntax of the ToC entry is correct but the semantics are broken,
 *  then we have an invalid ToC entry.
 */
class InvalidTocEntry : public TocEntryError
{
  public:
    InvalidTocEntry(const std::string&& reason) :
        TocEntryError(std::move(reason))
    {}
};

class UnmappedOffset : public std::exception
{
  public:
    UnmappedOffset(size_t base, size_t next) : base(base), next(next)
    {}

    const size_t base;
    const size_t next;
};

class OutOfBoundsOffset : public ReasonedError
{
  public:
    OutOfBoundsOffset(const std::string&& reason) :
        ReasonedError(std::move(reason))
    {}
};

class UnknownPartition : public ReasonedError
{
  public:
    UnknownPartition(const std::string&& reason) :
        ReasonedError(std::move(reason))
    {}
};

} // namespace virtual_pnor
} // namespace openpower

struct vpnor_partition_table
{
    openpower::virtual_pnor::partition::Table* table;
};