#pragma once

// Copyright 2015 SuperNode Development Foundation and contributors. Licensed
// under the Apache License, Version 2.0. See the COPYING file at the root
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0

#include "crypto/ByteSlice.h"
#include "crypto/SHA.h"
#include "util/Logging.h"
#include "xdrpp/marshal.h"
#include <fstream>
#include <string>
#include <vector>

namespace supernode
{

/**
 * Helper for loading a sequence of XDR objects from a file one at a time,
 * rather than all at once.
 */
class XDRInputFileStream
{
    std::ifstream mIn;
    std::vector<char> mBuf;
    unsigned int mSizeLimit;

  public:
    XDRInputFileStream(unsigned int sizeLimit = 0) : mSizeLimit{sizeLimit}
    {
    }

    void
    close()
    {
        mIn.close();
    }

    void
    open(std::string const& filename)
    {
        mIn.open(filename, std::ifstream::binary);
        if (!mIn)
        {
            std::string msg("failed to open XDR file: ");
            msg += filename;
            msg += ", reason: ";
            msg += std::to_string(errno);
            CLOG(ERROR, "Fs") << msg;
            throw std::runtime_error(msg);
        }
    }

    operator bool() const
    {
        return mIn.good();
    }

    template <typename T>
    bool
    readOne(T& out)
    {
        char szBuf[4];
        if (!mIn.read(szBuf, 4))
        {
            return false;
        }

        // Read 4 bytes of size, big-endian, with XDR 'continuation' bit cleared
        // (high bit of high byte).
        uint32_t sz = 0;
        sz |= static_cast<uint8_t>(szBuf[0] & '\x7f');
        sz <<= 8;
        sz |= static_cast<uint8_t>(szBuf[1]);
        sz <<= 8;
        sz |= static_cast<uint8_t>(szBuf[2]);
        sz <<= 8;
        sz |= static_cast<uint8_t>(szBuf[3]);

        if (mSizeLimit != 0 && sz > mSizeLimit)
        {
            return false;
        }
        if (sz > mBuf.size())
        {
            mBuf.resize(sz);
        }
        if (!mIn.read(mBuf.data(), sz))
        {
            throw xdr::xdr_runtime_error("malformed XDR file");
        }
        xdr::xdr_get g(mBuf.data(), mBuf.data() + sz);
        xdr::xdr_argpack_archive(g, out);
        return true;
    }
};

class XDROutputFileStream
{
    std::ofstream mOut;
    std::vector<char> mBuf;

  public:
    void
    close()
    {
        mOut.close();
    }

    void
    open(std::string const& filename)
    {
        mOut.open(filename, std::ofstream::binary | std::ofstream::trunc);
        if (!mOut)
        {
            std::string msg("failed to open XDR file: ");
            msg += filename;
            msg += ", reason: ";
            msg += std::to_string(errno);
            CLOG(FATAL, "Fs") << msg;
            throw std::runtime_error(msg);
        }
    }

    operator bool() const
    {
        return mOut.good();
    }

    template <typename T>
    bool
    writeOne(T const& t, SHA256* hasher = nullptr, size_t* bytesPut = nullptr)
    {
        uint32_t sz = (uint32_t)xdr::xdr_size(t);
        assert(sz < 0x80000000);

        if (mBuf.size() < sz + 4)
        {
            mBuf.resize(sz + 4);
        }

        // Write 4 bytes of size, big-endian, with XDR 'continuation' bit set on
        // high bit of high byte.
        mBuf[0] = static_cast<char>((sz >> 24) & 0xFF) | '\x80';
        mBuf[1] = static_cast<char>((sz >> 16) & 0xFF);
        mBuf[2] = static_cast<char>((sz >> 8) & 0xFF);
        mBuf[3] = static_cast<char>(sz & 0xFF);

        xdr::xdr_put p(mBuf.data() + 4, mBuf.data() + 4 + sz);
        xdr_argpack_archive(p, t);

        if (!mOut.write(mBuf.data(), sz + 4))
        {
            return false;
        }
        if (hasher)
        {
            hasher->add(ByteSlice(mBuf.data(), sz + 4));
        }
        if (bytesPut)
        {
            *bytesPut += (sz + 4);
        }
        return true;
    }
};
}
