/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#pragma once

#include <android-base/macros.h>
#include <libnl++/Buffer.h>
#include <libnl++/types.h>

#include <linux/netlink.h>

#include <string>

namespace android::nl {

class MessageFactoryBase {
  protected:
    static nlattr* add(nlmsghdr* msg, size_t maxLen, nlattrtype_t type, const void* data,
                       size_t dataLen);
    static void closeNested(nlmsghdr* msg, nlattr* nested);
};

/**
 * Wrapper around NETLINK_ROUTE messages, to build them in C++ style.
 *
 * \param T Message payload type (such as ifinfomsg).
 * \param BUFSIZE how much space to reserve for attributes.
 */
template <class T, unsigned int BUFSIZE = 128>
class MessageFactory : private MessageFactoryBase {
    struct alignas(NLMSG_ALIGNTO) Message {
        nlmsghdr header;
        T data;
        uint8_t attributesBuffer[BUFSIZE];
    };

  public:
    /**
     * Create empty message.
     *
     * \param type Message type (such as RTM_NEWLINK).
     * \param flags Message flags (such as NLM_F_REQUEST).
     */
    MessageFactory(nlmsgtype_t type, uint16_t flags)
        : header(mMessage.header), data(mMessage.data) {
        mMessage.header.nlmsg_len = offsetof(Message, attributesBuffer);
        mMessage.header.nlmsg_type = type;
        mMessage.header.nlmsg_flags = flags;
    }

    /**
     * Netlink message header.
     *
     * This is a generic Netlink header containing information such as message flags.
     */
    nlmsghdr& header;

    /**
     * Netlink message data.
     *
     * This is a payload specific to a given message type.
     */
    T& data;

    T* operator->() { return &mMessage.data; }

    /**
     * Build netlink message.
     *
     * In fact, this operation is almost a no-op, since the factory builds the message in a single
     * buffer, using native data structures.
     *
     * A likely failure case is when the BUFSIZE template parameter is too small to acommodate
     * added attributes. In such a case, please increase this parameter.
     *
     * \return Netlink message or std::nullopt in case of failure.
     */
    std::optional<Buffer<nlmsghdr>> build() const {
        if (!mIsGood) return std::nullopt;
        return {{&mMessage.header, mMessage.header.nlmsg_len}};
    }

    /**
     * Adds an attribute of a trivially copyable type.
     *
     * Template specializations may extend this function for other types, such as std::string.
     *
     * If this method fails (i.e. due to insufficient space), a warning will be printed to the log
     * and the message will be marked as bad, causing later \see build call to fail.
     *
     * \param type attribute type (such as IFLA_IFNAME)
     * \param attr attribute data
     */
    template <class A>
    void add(nlattrtype_t type, const A& attr) {
        addInternal(type, &attr, sizeof(attr));
    }

    // It will always send the last null character, otherwise use addBuffer
    // variant instead
    template <>
    void add(nlattrtype_t type, const std::string& s) {
        addInternal(type, s.c_str(), s.size() + 1);
    }

    void addBuffer(nlattrtype_t type, const std::string_view& s) {
        addInternal(type, s.data(), s.size());
    }

    /** Guard class to frame nested attributes. \see addNested(nlattrtype_t). */
    class [[nodiscard]] NestedGuard {
      public:
        NestedGuard(MessageFactory& req, nlattrtype_t type)
            : mReq(req), mAttr(req.addInternal(type)) {}
        ~NestedGuard() { closeNested(&mReq.mMessage.header, mAttr); }

      private:
        MessageFactory& mReq;
        nlattr* mAttr;

        DISALLOW_COPY_AND_ASSIGN(NestedGuard);
    };

    /**
     * Add nested attribute.
     *
     * The returned object is a guard for auto-nesting children inside the argument attribute.
     * When the guard object goes out of scope, the nesting attribute is closed.
     *
     * Example usage nesting IFLA_CAN_BITTIMING inside IFLA_INFO_DATA, which is nested
     * inside IFLA_LINKINFO:
     *    MessageFactory<ifinfomsg> req(RTM_NEWLINK, NLM_F_REQUEST);
     *    {
     *        auto linkinfo = req.addNested(IFLA_LINKINFO);
     *        req.addBuffer(IFLA_INFO_KIND, "can");
     *        {
     *            auto infodata = req.addNested(IFLA_INFO_DATA);
     *            req.add(IFLA_CAN_BITTIMING, bitTimingStruct);
     *        }
     *    }
     *    // use req
     *
     * \param type attribute type (such as IFLA_LINKINFO)
     */
    NestedGuard addNested(nlattrtype_t type) { return {*this, type}; }

  private:
    Message mMessage = {};
    bool mIsGood = true;

    nlattr* addInternal(nlattrtype_t type, const void* data = nullptr, size_t len = 0) {
        if (!mIsGood) return nullptr;
        auto attr = MessageFactoryBase::add(&mMessage.header, sizeof(mMessage), type, data, len);
        if (attr == nullptr) mIsGood = false;
        return attr;
    }
};

}  // namespace android::nl
