/*
 * Copyright (c) 2018-2019 ARM Limited
 * SPDX-License-Identifier: Apache-2.0
 *
 * 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.
 */

#include "CyDhcpServer.h"
#include "cy_utils.h"
#include "Callback.h"
#include "def.h"
#include "whd_types.h"

#ifdef DHCP_EXTENSIVE_DEBUG
extern "C" void dhcp_server_print_header_info(dhcp_packet_t *header, uint32_t datalen, const char *title);
#endif

/* UDP port numbers for DHCP server and client */
#define IP_PORT_DHCP_SERVER                         (67)
#define IP_PORT_DHCP_CLIENT                         (68)

/* BOOTP operations */
#define BOOTP_OP_REQUEST                            (1)
#define BOOTP_OP_REPLY                              (2)

/* DCHP message types */
#define DHCP_MSG_TYPE_DISCOVER                      (1)
#define DHCP_MSG_TYPE_OFFER                         (2)
#define DHCP_MSG_TYPE_REQUEST                       (3)
#define DHCP_MSG_TYPE_DECLINE                       (4)
#define DHCP_MSG_TYPE_ACK                           (5)
#define DHCP_MSG_TYPE_NACK                          (6)
#define DHCP_MSG_TYPE_RELEASE                       (7)
#define DHCP_MSG_TYPE_INFORM                        (8)
#define DHCP_MSG_TYPE_INVALID                       (255)

#define DHCP_MSG_MAGIC_COOKIE                       (0x63825363)

#define DHCP_STACK_SIZE                             (8*1024)

/********************* Options manipulation functions ***********************************/
static void addOption(dhcp_packet_t *dhcp, uint32_t &index, uint8_t optype)
{
    if (index + sizeof(dhcp_packet_t) - 1 + 1 >= DHCP_PACKET_SIZE) {
        printf("DHCP ERROR: Option index %d (Optype: %d) written to exceeds size of the packet", (int)index, (int)optype);
        return;
    }

    dhcp->Options[index++] = optype;

    return;
}

static void addOption(dhcp_packet_t *dhcp, uint32_t &index, uint8_t optype, uint8_t value)
{
    if (index + sizeof(dhcp_packet_t) - 1 + 3 >= DHCP_PACKET_SIZE) {
        printf("DHCP ERROR: Option index %d (Optype: %d) written to exceeds size of the packet", (int)index, (int)optype);
        return;
    }

    dhcp->Options[index++] = optype;
    dhcp->Options[index++] = 0x01;
    dhcp->Options[index++] = value;

    return;
}

static void addOption(dhcp_packet_t *dhcp, uint32_t &index, uint8_t optype, uint16_t value)
{
    if (index + sizeof(dhcp_packet_t) - 1 + 4 >= DHCP_PACKET_SIZE) {
        printf("DHCP ERROR: Option index %d (Optype: %d) written to exceeds size of the packet", (int)index, (int)optype);
        return;
    }

    dhcp->Options[index++] = optype;
    dhcp->Options[index++] = 0x02;
    dhcp->Options[index++] = static_cast<uint8_t>((value >> 0) & 0xFF);
    dhcp->Options[index++] = static_cast<uint8_t>((value >> 8) & 0xFF);

    return;
}

static void addOption(dhcp_packet_t *dhcp, uint32_t &index, uint8_t optype, uint32_t value)
{
    if (index + sizeof(dhcp_packet_t) - 1 + 6 >= DHCP_PACKET_SIZE) {
        printf("DHCP ERROR: Option index %d (Optype: %d) written to exceeds size of the packet", (int)index, (int)optype);
        return;
    }

    dhcp->Options[index++] = optype;
    dhcp->Options[index++] = 0x04;
    dhcp->Options[index++] = static_cast<uint8_t>((value >> 0) & 0xFF);
    dhcp->Options[index++] = static_cast<uint8_t>((value >> 8) & 0xFF);
    dhcp->Options[index++] = static_cast<uint8_t>((value >> 16) & 0xFF);
    dhcp->Options[index++] = static_cast<uint8_t>((value >> 24) & 0xFF);

    return;
}

static void addOption(dhcp_packet_t *dhcp, uint32_t &index, uint8_t optype, uint8_t *value, uint32_t size)
{
    if (index + sizeof(dhcp_packet_t) - 1 + 2 + size >= DHCP_PACKET_SIZE) {
        printf("DHCP ERROR: Option index %d (Optype: %d) written to exceeds size of the packet", (int)index, (int)optype);
        return;
    }

    dhcp->Options[index++] = optype;
    dhcp->Options[index++] = size;
    memcpy(&dhcp->Options[index], value, size);
    index += size;

    return;
}

static const uint8_t *findOption(const dhcp_packet_t *request, uint8_t option_num)
{
    const uint8_t *option_ptr = request->Options;
    while ((option_ptr[0] != DHCP_END_OPTION_CODE) &&
            (option_ptr[0] != option_num) &&
            (option_ptr < ((const uint8_t *)request) + DHCP_PACKET_SIZE)) {
        option_ptr += option_ptr[1] + 2;
    }

    /* Was the option found? */
    if (option_ptr[0] == option_num) {
        return &option_ptr[2];
    }
    return NULL;
}

static void addCommonOptions(dhcp_packet_t *dhcp, uint32_t &index, const uint32_t server_addr, const uint32_t netmask)
{
    /* Prepare the Web proxy auto discovery URL */
    char wpad_sample_url[] = "http://xxx.xxx.xxx.xxx/wpad.dat";
    char ip_str[16];
    ipv4_to_string(ip_str, htonl(server_addr));
    memcpy(&wpad_sample_url[7], &ip_str[0], 15);

    /* Server identifier */
    addOption(dhcp, index, DHCP_SERVER_IDENTIFIER_OPTION_CODE, server_addr);
    /* Lease Time */
    addOption(dhcp, index, DHCP_LEASETIME_OPTION_CODE, static_cast<uint32_t>(0x00015180));
    /* Subnet Mask */
    addOption(dhcp, index, DHCP_SUBNETMASK_OPTION_CODE, htonl(netmask));
    /* Web proxy auto discovery URL */
    addOption(dhcp, index, DHCP_WPAD_OPTION_CODE, (uint8_t *)&wpad_sample_url[0], strlen(wpad_sample_url));
    /* Router (gateway) */
    addOption(dhcp, index, DHCP_ROUTER_OPTION_CODE, htonl(server_addr));
    /* DNS server */
    addOption(dhcp, index, DHCP_DNS_SERVER_OPTION_CODE, htonl(server_addr));
    /* Interface MTU */
    addOption(dhcp, index, DHCP_MTU_OPTION_CODE, static_cast<uint16_t>(WHD_PAYLOAD_MTU));
}

static void sendPacket(UDPSocket *socket, dhcp_packet_t *dhcp, uint32_t size)
{
    nsapi_size_or_error_t err;
    uint32_t broadcast_ip = 0xFFFFFFFF;
    char string_addr[16];
    ipv4_to_string(string_addr, htonl(broadcast_ip));

    err = socket->sendto(string_addr, IP_PORT_DHCP_CLIENT, reinterpret_cast<uint8_t *>(dhcp), size);
    if (err < 0) {
        printf("DHCP ERROR: Packet send failure with error %d.", err);
    } else if (err != (int)size) {
        printf("DHCP ERROR: Could not send entire packet. Only %d bytes were sent.", err);
    }
}

/********************* Cache utility functions ***********************************/
void CyDhcpServer::setAddress(const cy_mac_addr_t &mac_id, const cy_ip_addr_t &addr)
{
    uint32_t a;
    uint32_t first_empty_slot;
    uint32_t cached_slot;
    char empty_cache[NSAPI_IPv6_SIZE] = "";

    /* Search for empty slot in cache */
    for (a = 0, first_empty_slot = DHCP_IP_ADDRESS_CACHE_MAX, cached_slot = DHCP_IP_ADDRESS_CACHE_MAX; a < DHCP_IP_ADDRESS_CACHE_MAX; a++) {
        /* Check for matching MAC address */
        if (memcmp(&_mac_addr_cache[a], &mac_id, sizeof(mac_id)) == 0) {
            /* Cached device found */
            cached_slot = a;
            break;
        } else if (first_empty_slot == DHCP_IP_ADDRESS_CACHE_MAX && memcmp(&_mac_addr_cache[a], &empty_cache, sizeof(cy_mac_addr_t)) == 0) {
            /* Device not found in cache. Return the first empty slot */
            first_empty_slot = a;
        }
    }

    if (cached_slot != DHCP_IP_ADDRESS_CACHE_MAX) {
        /* Update IP address of cached device */
        _ip_addr_cache[cached_slot] = addr;
    } else if (first_empty_slot != DHCP_IP_ADDRESS_CACHE_MAX) {
        /* Add device to the first empty slot */
        _mac_addr_cache[first_empty_slot] = mac_id;
        _ip_addr_cache[first_empty_slot] = addr;
    } else {
        /* Cache is full. Add device to slot 0 */
        _mac_addr_cache[0] = mac_id;
        _ip_addr_cache [0] = addr;
    }
}

bool CyDhcpServer::lookupAddress(const cy_mac_addr_t &mac_id, cy_ip_addr_t &addr)
{
    /* Check whether device is already cached */
    for (uint32_t a = 0; a < DHCP_IP_ADDRESS_CACHE_MAX; a++) {
        if (memcmp(&_mac_addr_cache[a], &mac_id, sizeof(mac_id)) == 0) {
            addr = _ip_addr_cache[a];
            return true;
        }
    }
    return false;
}

void CyDhcpServer::freeAddress(const cy_mac_addr_t &mac_id)
{
    /* Check whether device is already cached */
    for (uint32_t a = 0; a < DHCP_IP_ADDRESS_CACHE_MAX; a++) {
        if (memcmp(&_mac_addr_cache[a], &mac_id, sizeof(mac_id)) == 0) {
            memset(&_mac_addr_cache[a], 0, sizeof(_mac_addr_cache[a]));
            memset(&_ip_addr_cache[a], 0, sizeof(_ip_addr_cache[a]));
        }
    }
}

void CyDhcpServer::handleDiscover(dhcp_packet_t *dhcp)
{
#ifdef DHCP_EXTENSIVE_DEBUG
    dhcp_server_print_header_info(dhcp, DHCP_PACKET_SIZE, "\n\nDHCP DISCOVER RECEIVED");
#endif

    uint32_t index;
    cy_mac_addr_t client_mac;
    cy_ip_addr_t client_ip;

    memcpy(&client_mac, dhcp->ClientHwAddr, sizeof(client_mac));
    if (!lookupAddress(client_mac, client_ip)) {
        client_ip = _available_addr;
    }

    memset(&dhcp->Legacy, 0, sizeof(dhcp->Legacy));
    memset(&dhcp->Options[0], 0, DHCP_PACKET_SIZE - sizeof(dhcp_packet_t) + 3);

    dhcp->Opcode = BOOTP_OP_REPLY;
    dhcp->YourIpAddr = htonl(client_ip.addrv4.addr);
    dhcp->MagicCookie = htonl(static_cast<uint32_t>(DHCP_MSG_MAGIC_COOKIE));

    /* Add options */
    index = 0;
    addOption(dhcp, index, DHCP_MESSAGETYPE_OPTION_CODE, static_cast<uint8_t>(DHCP_MSG_TYPE_OFFER));
    addCommonOptions(dhcp, index, _server_addr.addrv4.addr, _netmask.addrv4.addr);
    addOption(dhcp, index, static_cast<uint8_t>(DHCP_END_OPTION_CODE));

    uint32_t size = sizeof(dhcp_packet_t) + index - 1;
    CY_ASSERT(size <= DHCP_PACKET_SIZE);

#ifdef DHCP_EXTENSIVE_DEBUG
    dhcp_server_print_header_info(dhcp, size, "\n\nDHCP OFFER SENT");
#endif
    sendPacket(&_socket, dhcp, size);
}

void CyDhcpServer::handleRequest(dhcp_packet_t *dhcp)
{
#ifdef DHCP_EXTENSIVE_DEBUG
    dhcp_server_print_header_info(dhcp, DHCP_PACKET_SIZE, "\n\nDHCP REQUEST RECEIVED");
#endif

    cy_mac_addr_t client_mac;
    cy_ip_addr_t client_ip;
    cy_ip_addr_t req_ip;
    bool increment = false;
    uint32_t index;

    /* Check that the REQUEST is for this server */
    uint32_t *server_id_req = (uint32_t *)findOption(dhcp, DHCP_SERVER_IDENTIFIER_OPTION_CODE);
    if ((server_id_req == NULL) || ((server_id_req != NULL) && (_server_addr.addrv4.addr != *server_id_req))) {
        return; /* Server ID was not found or does not match local IP address */
    }

    /* Locate the requested address in the options and keep requested address */
    req_ip.addrv4.addr = ntohl(*(uint32_t *)findOption(dhcp, DHCP_REQUESTED_IP_ADDRESS_OPTION_CODE));

    memcpy(&client_mac, dhcp->ClientHwAddr, sizeof(client_mac));
    if (!lookupAddress(client_mac, client_ip)) {
        client_ip = _available_addr;
        increment = true;
    }

    memset(&dhcp->Legacy, 0, sizeof(dhcp->Legacy));
    memset(&dhcp->Options[0], 0, DHCP_PACKET_SIZE - sizeof(dhcp_packet_t) + 3);

    dhcp->Opcode = BOOTP_OP_REPLY;
    dhcp->MagicCookie = htonl(static_cast<uint32_t>(DHCP_MSG_MAGIC_COOKIE));

    index = 0;
    /* Check if the requested IP address matches one we have assigned */
    if (req_ip.addrv4.addr != client_ip.addrv4.addr) {
        /* Request is not for the assigned IP - force client to take next available IP by sending NAK */
        addOption(dhcp, index, DHCP_MESSAGETYPE_OPTION_CODE, static_cast<uint8_t>(DHCP_MSG_TYPE_NACK));
        addOption(dhcp, index, DHCP_SERVER_IDENTIFIER_OPTION_CODE, _server_addr.addrv4.addr);
        printf("\n\nDHCP_THREAD: %d REQUEST NAK\n", __LINE__);
    } else {
        dhcp->YourIpAddr = htonl(client_ip.addrv4.addr);

        addOption(dhcp, index, DHCP_MESSAGETYPE_OPTION_CODE, static_cast<uint8_t>(DHCP_MSG_TYPE_ACK));
        addCommonOptions(dhcp, index, _server_addr.addrv4.addr, _netmask.addrv4.addr);

        if (increment) {
            uint32_t ip_mask = ~(_netmask.addrv4.addr);
            uint32_t subnet = _server_addr.addrv4.addr & _netmask.addrv4.addr;
            do {
                _available_addr.addrv4.addr = subnet | ((_available_addr.addrv4.addr + 1) & ip_mask);
            } while (_available_addr.addrv4.addr == _server_addr.addrv4.addr);
        }
        setAddress(client_mac, client_ip);
    }
    addOption(dhcp, index, static_cast<uint8_t>(DHCP_END_OPTION_CODE));

    uint32_t size = sizeof(dhcp_packet_t) + index - 1;
    CY_ASSERT(size <= DHCP_PACKET_SIZE);

#ifdef DHCP_EXTENSIVE_DEBUG
    dhcp_server_print_header_info(dhcp, DHCP_PACKET_SIZE, "\n\nDHCP REQUEST REPLY SENT");
#endif
    sendPacket(&_socket, dhcp, size);
}

void CyDhcpServer::runServer(void)
{
    nsapi_size_or_error_t err_or_size;

    _running = true;

    /* Create receive DHCP socket */
    _socket.open(_nstack);
    char iface_name[8] = {0};
    _niface->get_interface_name(iface_name);
    _socket.setsockopt(NSAPI_SOCKET, NSAPI_BIND_TO_DEVICE, iface_name, strlen(iface_name));
    _socket.bind((uint16_t)IP_PORT_DHCP_SERVER);

    /* Save the current netmask to be sent in DHCP packets as the 'subnet mask option' */
    _server_addr.addrv4.addr = string_to_ipv4(_niface->get_ip_address());
    _netmask.addrv4.addr = string_to_ipv4(_niface->get_netmask());

#ifdef DHCP_EXTENSIVE_DEBUG
    printf("DHCP Server started.\n");
    printf("DHCP Server: IP     : %s\n", _niface->get_ip_address());
    printf("DHCP Server: Netmask: %s\n", _niface->get_netmask());
    printf("DHCP Server: Gateway: %s\n", _niface->get_gateway());
    printf("DHCP Server: MAC    : %s\n\n", _niface->get_mac_address());
#endif

    /* Calculate the first available IP address which will be served - based on the netmask and the local IP */
    uint32_t ip_mask = ~(_netmask.addrv4.addr);
    uint32_t subnet = _server_addr.addrv4.addr & _netmask.addrv4.addr;
    _available_addr.addrv4.addr = subnet | ((_server_addr.addrv4.addr + 1) & ip_mask);

    while (_running) {
        /* Sleep until data is received from socket. */
        err_or_size = _socket.recv(_buff, DHCP_PACKET_SIZE);
        /* Options field in DHCP header is variable length. We are looking for option "DHCP Message Type" that is 3 octets in size (code, length and type) */
        /* If the return value is <0, it is an error; if it is >=0, it is the received length */
        if (err_or_size < 0 || err_or_size < (int32_t)sizeof(dhcp_packet_t)) {
            continue;
        }

        dhcp_packet_t *dhcp = reinterpret_cast<dhcp_packet_t *>(_buff);
        /* Check if the option in the dhcp header is "DHCP Message Type", code value for option "DHCP Message Type" is 53 as per rfc2132 */
        if (dhcp->Options[0] != DHCP_MESSAGETYPE_OPTION_CODE) {
            printf("%d: %s received option code wrong: %d != %d\n", __LINE__, __func__, dhcp->Options[0], DHCP_MESSAGETYPE_OPTION_CODE);
            continue;
        }

        uint8_t msg_type = dhcp->Options[2];
        switch (msg_type) {
            case DHCP_MSG_TYPE_DISCOVER:
                handleDiscover(dhcp);
                break;
            case DHCP_MSG_TYPE_REQUEST:
                handleRequest(dhcp);
                break;
            default:
                printf("DHCP ERROR: Unhandled dhcp packet type, %d", msg_type);
                break;
        }
    }
}

void CyDhcpServer::threadWrapper(CyDhcpServer *obj)
{
    obj->runServer();
}

CyDhcpServer::CyDhcpServer(NetworkStack *nstack, NetworkInterface *niface)
    : _nstack(nstack),
      _niface(niface),
      _thread(osPriorityNormal, DHCP_STACK_SIZE, NULL, "DHCPserver") {}

CyDhcpServer::~CyDhcpServer()
{
    stop();
}

cy_rslt_t CyDhcpServer::start(void)
{
    cy_rslt_t result = CY_RSLT_SUCCESS;
    if (!_running) {
        CY_ASSERT(_nstack != NULL);

        /* Clear cache */
        memset(_mac_addr_cache, 0, sizeof(_mac_addr_cache));
        memset(_ip_addr_cache, 0, sizeof(_ip_addr_cache));

        /* Start DHCP server */
        if (osOK != _thread.start(mbed::callback(threadWrapper, this))) {
            result = CY_DHCP_THREAD_CREATION_FAILED;
        }
    }
    return result;
}

cy_rslt_t CyDhcpServer::stop(void)
{
    cy_rslt_t result = CY_RSLT_SUCCESS;
    if (_running) {
        _running = false;
        if (NSAPI_ERROR_OK != _socket.close()) {
            printf("DHCP ERROR: DHCP socket closure failed.\n");
            result = CY_DHCP_STOP_FAILED;
        }
        if (osOK != _thread.join()) {
            printf("DHCP ERROR: DHCP thread join failed.\n");
            result = CY_DHCP_STOP_FAILED;
        }
    }
    return result;
}
