/*
 * Copyright 2017, 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 <linux/if_ether.h>
#include <netinet/in.h>
#include <stddef.h>
#include <string.h>

#include <initializer_list>

class Message {
  public:
    Message();
    Message(const uint8_t* data, size_t size);
    static Message discover(const uint8_t (&sourceMac)[ETH_ALEN]);
    static Message request(const uint8_t (&sourceMac)[ETH_ALEN], in_addr_t requestAddress,
                           in_addr_t serverAddress);
    static Message offer(const Message& sourceMessage, in_addr_t serverAddress,
                         in_addr_t offeredAddress, in_addr_t offeredNetmask,
                         in_addr_t offeredGateway, const in_addr_t* offeredDnsServers,
                         size_t numOfferedDnsServers);
    static Message ack(const Message& sourceMessage, in_addr_t serverAddress,
                       in_addr_t offeredAddress, in_addr_t offeredNetmask, in_addr_t offeredGateway,
                       const in_addr_t* offeredDnsServers, size_t numOfferedDnsServers);
    static Message nack(const Message& sourceMessage, in_addr_t serverAddress);

    // Ensure that the data in the message represent a valid DHCP message
    bool isValidDhcpMessage(uint8_t expectedOp) const;
    // Ensure that the data in the message represent a valid DHCP message and
    // has a xid (transaction ID) that matches |expectedXid|.
    bool isValidDhcpMessage(uint8_t expectedOp, uint32_t expectedXid) const;

    const uint8_t* data() const { return reinterpret_cast<const uint8_t*>(&dhcpData); }
    uint8_t* data() { return reinterpret_cast<uint8_t*>(&dhcpData); }
    const uint8_t* end() const { return data() + mSize; }

    size_t optionsSize() const;
    size_t size() const { return mSize; }
    void setSize(size_t size) { mSize = size; }
    size_t capacity() const { return sizeof(dhcpData); }

    // Get the DHCP message type
    uint8_t type() const;
    // Get the DHCP server ID
    in_addr_t serverId() const;
    // Get the requested IP
    in_addr_t requestedIp() const;

    struct Dhcp {
        uint8_t op;    /* BOOTREQUEST / BOOTREPLY    */
        uint8_t htype; /* hw addr type               */
        uint8_t hlen;  /* hw addr len                */
        uint8_t hops;  /* client set to 0            */

        uint32_t xid; /* transaction id             */

        uint16_t secs; /* seconds since start of acq */
        uint16_t flags;

        uint32_t ciaddr; /* client IP addr             */
        uint32_t yiaddr; /* your (client) IP addr      */
        uint32_t siaddr; /* ip addr of next server     */
                         /* (DHCPOFFER and DHCPACK)    */
        uint32_t giaddr; /* relay agent IP addr        */

        uint8_t chaddr[16]; /* client hw addr             */
        char sname[64];     /* asciiz server hostname     */
        char file[128];     /* asciiz boot file name      */

        uint8_t options[1024]; /* optional parameters        */
    } dhcpData;

  private:
    Message(uint8_t operation, const uint8_t (&macAddress)[ETH_ALEN], uint8_t type);

    void addOption(uint8_t type, const void* data, uint8_t size);
    template <typename T> void addOption(uint8_t type, T data) {
        static_assert(sizeof(T) <= 255, "The size of data is too large");
        addOption(type, &data, sizeof(data));
    }
    template <typename T, size_t N> void addOption(uint8_t type, T (&items)[N]) {
        static_assert(sizeof(T) * N <= 255, "The size of data is too large");
        uint8_t* opts = nextOption();
        *opts++ = type;
        *opts++ = sizeof(T) * N;
        for (const T& item : items) {
            memcpy(opts, &item, sizeof(item));
            opts += sizeof(item);
        }
        updateSize(opts);
    }
    void endOptions();

    const uint8_t* getOption(uint8_t optCode, uint8_t* length) const;
    uint8_t* nextOption();
    void updateSize(uint8_t* optionsEnd);
    size_t mSize;
};

static_assert(offsetof(Message::Dhcp, htype) == sizeof(Message::Dhcp::op),
              "Invalid packing for DHCP message struct");
