/*
 * 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");