/*
 * Copyright (C) 2020-2021 Aloysius Indrayanto
 *
 * This file is part of the eCxx library, see LICENSE file for the license details.
 */

/*
 * ----------------------------------------------------------------------------------
 * The code in this file is copied (with a slight modifications) from:
 *     lwIPDHCPServer.cpp - DHCP server
 * ----------------------------------------------------------------------------------
 * Copyright (c) 2016 Espressif. All rights reserved.
 * Copyright (c) 2020 esp8266 arduino. All rights reserved.
 *
 * This file is part of the esp8266 core for Arduino environment.
 *
 * This library is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any later
 * version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along
 * with this library; if not, write to the Free Software Foundation, Inc., 51
 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 *
 * ----------------------------------------------------------------------------------
 * Original sources (no license provided)
 *     ESP8266_NONOS_SDK/third_party/lwip/app/dhcpserver.c
 *     ESP8266_NONOS_SDK/third_party/include/lwip/app/dhcpserver.h
 * ----------------------------------------------------------------------------------
 */


#define USE_DNS
#include <lwip/udp.h>

#include "LwipDHCPServer.h"


extern "C" {

    int fw_has_started_softap_dhcps = 0;

    // wifi_softap_set_station_info() is missing in user_interface.h
    extern void wifi_softap_set_station_info(uint8_t* mac, struct ipv4_addr*);

}


namespace eCxx {

namespace NodeMCU {


static constexpr uint32_t DHCPS_LEASE_TIME_DEF = 120;


struct dhcps_state {
    int16_t state;
};


struct dhcps_msg {
    uint8_t  op;
    uint8_t  htype;
    uint8_t  hlen;
    uint8_t  hops;
    uint8_t  xid[4];
    uint16_t secs;
    uint16_t flags;
    uint8_t  ciaddr[4];
    uint8_t  yiaddr[4];
    uint8_t  siaddr[4];
    uint8_t  giaddr[4];
    uint8_t  chaddr[16];
    uint8_t  sname[64];
    uint8_t  file[128];
    uint8_t  options[312];
};


#ifndef LWIP_OPEN_SRC


struct dhcps_lease {
    bool      enable;
    ipv4_addr start_ip;
    ipv4_addr end_ip;
};


enum dhcps_offer_option {
    OFFER_START  = 0x00,
    OFFER_ROUTER = 0x01,
    OFFER_END
};


#endif


enum dhcps_type_t {
    DHCPS_TYPE_DYNAMIC,
    DHCPS_TYPE_STATIC
};


enum dhcps_state_t {
    DHCPS_STATE_ONLINE,
    DHCPS_STATE_OFFLINE
} ;


struct dhcps_pool {
    ipv4_addr     ip;
    uint8_t       mac[6];
    uint32_t      lease_timer;
    dhcps_type_t  type;
    dhcps_state_t state;

};


#define DHCPS_LEASE_TIMER dhcps_lease_time
#define DHCPS_MAX_LEASE   0x64
#define BOOTP_BROADCAST   0x8000

#define DHCP_REQUEST          1
#define DHCP_REPLY            2
#define DHCP_HTYPE_ETHERNET   1
#define DHCP_HLEN_ETHERNET    6
#define DHCP_MSG_LEN        236

#define DHCPS_SERVER_PORT 67
#define DHCPS_CLIENT_PORT 68

#define DHCPDISCOVER 1
#define DHCPOFFER    2
#define DHCPREQUEST  3
#define DHCPDECLINE  4
#define DHCPACK      5
#define DHCPNAK      6
#define DHCPRELEASE  7

#define DHCP_OPTION_SUBNET_MASK                1
#define DHCP_OPTION_ROUTER                     3
#define DHCP_OPTION_DNS_SERVER                 6
#define DHCP_OPTION_REQ_IPADDR                50
#define DHCP_OPTION_LEASE_TIME                51
#define DHCP_OPTION_MSG_TYPE                  53
#define DHCP_OPTION_SERVER_ID                 54
#define DHCP_OPTION_INTERFACE_MTU             26
#define DHCP_OPTION_PERFORM_ROUTER_DISCOVERY  31
#define DHCP_OPTION_BROADCAST_ADDRESS         28
#define DHCP_OPTION_REQ_LIST                  55
#define DHCP_OPTION_END                      255

#define DHCPS_DEBUG     0
#define MAX_STATION_NUM 8

#define DHCPS_STATE_OFFER   1
#define DHCPS_STATE_DECLINE 2
#define DHCPS_STATE_ACK     3
#define DHCPS_STATE_NAK     4
#define DHCPS_STATE_IDLE    5
#define DHCPS_STATE_RELEASE 6

#define dhcps_router_enabled(OFFER) ( (OFFER & OFFER_ROUTER) != 0 )


#ifdef MEMLEAK_DEBUG
const char mem_debug_file[] ICACHE_RODATA_ATTR = __FILE__;
#endif


#if DHCPS_DEBUG
#define LWIP_IS_OK(WHAT, ERR) ( { int ret = 1, errval = (ERR); if (errval != ERR_OK) { os_printf("DHCPS ERROR: %s (lwip:%d)\n", WHAT, errval); ret = 0; } ret; } )
#else
#define LWIP_IS_OK(WHAT, ERR) ( (ERR) == ERR_OK )
#endif


// https://tools.ietf.org/html/rfc1497
const uint32_t DHCPServer::magic_cookie = 0x63538263;


DHCPServer::DHCPServer(netif* netif)
: _netif(netif)
{
    pcb_dhcps        = nullptr;
    dns_address.addr = 0;
    plist            = nullptr;
    offer            = 0xFF;
    renew            = false;
    dhcps_lease_time = DHCPS_LEASE_TIME_DEF;

    if(netif->num == SOFTAP_IF && fw_has_started_softap_dhcps == 1) {
        // When SDK starts DHCP server at boot:
        //     1. 'fw_has_started_softap_dhcps' is already initialized to 1
        //     2. Global ctor DHCPServer 'dhcpSoftAP( &netif_git[SOFTAP_IF] )' is called
        //     3. (that is here) => 'begin(legacy-values)' is called
        ip_info ip = {
            { 0x0104A8C0 }, // IP      192.168.4.1
            { 0x00FFFFFF }, // Netmask 255.255.255.0
            { 0          }  // Gateway 0.0.0.0
        };
        begin(&ip);
        fw_has_started_softap_dhcps = 2; // End the intial boot sequence by setting it to not 1
    }
};


DHCPServer::~DHCPServer()
{ end(); }


void DHCPServer::dhcps_set_dns(int /*num*/, const ipv4_addr_t* dns)
{ if( !ip4_addr_isany(dns) ) ip4_addr_copy(dns_address, *dns); }


void DHCPServer::node_insert_to_list(list_node** phead, list_node* pinsert)
{
           list_node*  plist       = nullptr;
    struct dhcps_pool* pdhcps_pool = nullptr;
    struct dhcps_pool* pdhcps_node = nullptr;

    if(!*phead) {
        *phead = pinsert;
    }
    else {
        plist       = *phead;
        pdhcps_node = reinterpret_cast<struct dhcps_pool*>(pinsert->pnode);
        pdhcps_pool = reinterpret_cast<struct dhcps_pool*>(plist  ->pnode);

        if(pdhcps_node->ip.addr < pdhcps_pool->ip.addr) {
            pinsert->pnext = plist;
            *phead         = pinsert;
        }
        else {
            while(plist->pnext) {
                pdhcps_pool = reinterpret_cast<struct dhcps_pool*>(plist->pnext->pnode);
                if(pdhcps_node->ip.addr < pdhcps_pool->ip.addr) {
                    pinsert->pnext = plist->pnext;
                    plist->pnext   = pinsert;
                    break;
                }
                plist = plist->pnext;
            }

            if(!plist->pnext) plist->pnext = pinsert;
        }
    }
}


void DHCPServer::node_remove_from_list(list_node** phead, list_node* pdelete)
{
    list_node* plist = nullptr;

    plist = *phead;

    if(!plist) {
        *phead = nullptr;
    }
    else {
        if(plist == pdelete) {
            *phead         = plist->pnext;
            pdelete->pnext = nullptr;
        }
        else {
            while(plist) {
                if(plist->pnext == pdelete) {
                    plist->pnext   = pdelete->pnext;
                    pdelete->pnext = nullptr;
                }
                plist = plist->pnext;
            }
        }
    }
}


bool DHCPServer::add_dhcps_lease(uint8_t* macaddr)
{
    struct dhcps_pool* pdhcps_pool = nullptr;
           list_node*  pback_node  = nullptr;

    uint32_t start_ip = dhcps_lease.start_ip.addr;
    uint32_t end_ip   = dhcps_lease.end_ip.addr;

    for(pback_node = plist; !!pback_node; pback_node = pback_node->pnext) {
        pdhcps_pool = reinterpret_cast<dhcps_pool*>(pback_node->pnode);
        if( memcmp( pdhcps_pool->mac, macaddr, sizeof(pdhcps_pool->mac) ) == 0 ) {
#if DHCPS_DEBUG
            os_printf("this mac already exist");
#endif
            return false;
        }
        else {
            start_ip = htonl( ( ntohl(start_ip) + 1 ) );
        }
    }

    if(start_ip > end_ip) {
#if DHCPS_DEBUG
        os_printf("no more ip available");
#endif
        return false;
    }

    pdhcps_pool              = reinterpret_cast<struct dhcps_pool*>( zalloc( sizeof(struct dhcps_pool) ) );
    pdhcps_pool->ip.addr     = start_ip;
    pdhcps_pool->lease_timer = DHCPS_LEASE_TIMER;
    pdhcps_pool->type        = DHCPS_TYPE_STATIC;
    pdhcps_pool->state       = DHCPS_STATE_ONLINE;
    memcpy( pdhcps_pool->mac, macaddr, sizeof(pdhcps_pool->mac) );

    pback_node               = reinterpret_cast<list_node*>( zalloc( sizeof(list_node) ) );
    pback_node->pnode        = pdhcps_pool;
    pback_node->pnext        = nullptr;

    node_insert_to_list(&plist, pback_node);

    return true;
}


uint8_t* DHCPServer::add_msg_type(uint8_t* optptr, uint8_t type)
{

    *optptr++ = DHCP_OPTION_MSG_TYPE;
    *optptr++ = 1;
    *optptr++ = type;
    return optptr;
}


uint8_t* DHCPServer::add_offer_options(uint8_t* optptr)
{
    #define ipadd (_netif->ip_addr)
    #define if_ip (*_netif)

    *optptr++ = DHCP_OPTION_SUBNET_MASK;
    *optptr++ = 4;
    *optptr++ = ip4_addr1( ip_2_ip4(&if_ip.netmask) );
    *optptr++ = ip4_addr2( ip_2_ip4(&if_ip.netmask) );
    *optptr++ = ip4_addr3( ip_2_ip4(&if_ip.netmask) );
    *optptr++ = ip4_addr4( ip_2_ip4(&if_ip.netmask) );

    *optptr++ = DHCP_OPTION_LEASE_TIME;
    *optptr++ = 4;
    *optptr++ = ( (DHCPS_LEASE_TIMER * 60) >> 24 ) & 0xFF;
    *optptr++ = ( (DHCPS_LEASE_TIMER * 60) >> 16 ) & 0xFF;
    *optptr++ = ( (DHCPS_LEASE_TIMER * 60) >>  8 ) & 0xFF;
    *optptr++ = ( (DHCPS_LEASE_TIMER * 60) >>  0 ) & 0xFF;

    *optptr++ = DHCP_OPTION_SERVER_ID;
    *optptr++ = 4;
    *optptr++ = ip4_addr1( ip_2_ip4(&ipadd) );
    *optptr++ = ip4_addr2( ip_2_ip4(&ipadd) );
    *optptr++ = ip4_addr3( ip_2_ip4(&ipadd) );
    *optptr++ = ip4_addr4( ip_2_ip4(&ipadd) );

    if( dhcps_router_enabled(offer) && ip_2_ip4(&if_ip.gw)->addr ) {
        *optptr++ = DHCP_OPTION_ROUTER;
        *optptr++ = 4;
        *optptr++ = ip4_addr1( ip_2_ip4(&if_ip.gw) );
        *optptr++ = ip4_addr2( ip_2_ip4(&if_ip.gw) );
        *optptr++ = ip4_addr3( ip_2_ip4(&if_ip.gw) );
        *optptr++ = ip4_addr4( ip_2_ip4(&if_ip.gw) );
    }

#ifdef USE_DNS
    *optptr++ = DHCP_OPTION_DNS_SERVER;
    *optptr++ = 4;
    if(!dns_address.addr) {
        *optptr++ = ip4_addr1( ip_2_ip4(&ipadd) );
        *optptr++ = ip4_addr2( ip_2_ip4(&ipadd) );
        *optptr++ = ip4_addr3( ip_2_ip4(&ipadd) );
        *optptr++ = ip4_addr4( ip_2_ip4(&ipadd) );
    }
    else {
        *optptr++ = ip4_addr1(&dns_address);
        *optptr++ = ip4_addr2(&dns_address);
        *optptr++ = ip4_addr3(&dns_address);
        *optptr++ = ip4_addr4(&dns_address);
    }
#endif

    *optptr++ = DHCP_OPTION_BROADCAST_ADDRESS;
    *optptr++ = 4;
    *optptr++ = ip4_addr1( ip_2_ip4(&ipadd) );
    *optptr++ = ip4_addr2( ip_2_ip4(&ipadd) );
    *optptr++ = ip4_addr3( ip_2_ip4(&ipadd) );
    *optptr++ = 255;

    *optptr++ = DHCP_OPTION_INTERFACE_MTU;
    *optptr++ = 2;
    *optptr++ = 0x05;
    *optptr++ = 0xdc; // 1500

    *optptr++ = DHCP_OPTION_PERFORM_ROUTER_DISCOVERY;
    *optptr++ = 1;
    *optptr++ = 0x00;

    return optptr;

    #undef ipadd
    #undef if_ip
}


uint8_t* DHCPServer::add_end(uint8_t* optptr)
{
    *optptr++ = DHCP_OPTION_END;
    return optptr;
}


void DHCPServer::create_msg(struct dhcps_msg* m)
{
    ipv4_addr client;

    client.addr = client_address.addr;

    m->op    = DHCP_REPLY;
    m->htype = DHCP_HTYPE_ETHERNET;
    m->hlen  = 6;
    m->hops  = 0;
    m->secs  = 0;
    m->flags = htons(BOOTP_BROADCAST);

    memcpy( m->yiaddr,  &client.addr,  sizeof(m->yiaddr   ) );
    memset( m->ciaddr,  0,             sizeof(m->ciaddr   ) );
    memset( m->siaddr,  0,             sizeof(m->siaddr   ) );
    memset( m->giaddr,  0,             sizeof(m->giaddr   ) );
    memset( m->sname,   0,             sizeof(m->sname    ) );
    memset( m->file,    0,             sizeof(m->file     ) );
    memset( m->options, 0,             sizeof(m->options  ) );
    memcpy( m->options, &magic_cookie, sizeof(magic_cookie) );
}


void DHCPServer::send_offer(struct dhcps_msg* m)
{
    uint8_t* end;
    pbuf*    p;
    pbuf*    q;
    uint8_t* data;
    uint16_t cnt = 0;
    uint16_t i;
    create_msg(m);

    end = add_msg_type(&m->options[4], DHCPOFFER);
    end = add_offer_options(end);
    end = add_end(end);

    p = pbuf_alloc( PBUF_TRANSPORT, sizeof(struct dhcps_msg), PBUF_RAM );

#if DHCPS_DEBUG
    os_printf("udhcp: send_offer>>p->ref = %d\n", p->ref);
#endif

    if(p) {
#if DHCPS_DEBUG
        os_printf("dhcps: send_offer>>pbuf_alloc succeed\n");
        os_printf("dhcps: send_offer>>p->tot_len = %d\n", p->tot_len);
        os_printf("dhcps: send_offer>>p->len = %d\n", p->len);
#endif
        q = p;
        while(q) {
            data = reinterpret_cast<uint8_t*>(q->payload);
            for(i = 0; i < q->len; ++i) {
                data[i] = ( reinterpret_cast<uint8_t*>(m) )[cnt++];
            }
            q = q->next;
        }
    }
    else {
#if DHCPS_DEBUG
        os_printf("dhcps: send_offer>>pbuf_alloc failed\n");
#endif
        return;
    }

    if( !LWIP_IS_OK( "send_offer", udp_sendto(pcb_dhcps, p, &broadcast_dhcps, DHCPS_CLIENT_PORT) ) ) {
#if DHCPS_DEBUG
        os_printf("dhcps: send_offer>>udp_sendto\n");
#endif
    }

    if(p->ref != 0) {
#if DHCPS_DEBUG
        os_printf("udhcp: send_offer>>free pbuf\n");
#endif
        pbuf_free(p);
    }
}


void DHCPServer::send_nak(struct dhcps_msg* m)
{
    uint8_t* end;
    pbuf*    p;
    pbuf*    q;
    uint8_t* data;
    uint16_t cnt = 0;
    uint16_t i;
    create_msg(m);

    end = add_msg_type(&m->options[4], DHCPNAK);
    end = add_end(end);

    p = pbuf_alloc( PBUF_TRANSPORT, sizeof(struct dhcps_msg), PBUF_RAM );

#if DHCPS_DEBUG
    os_printf("udhcp: send_nak>>p->ref = %d\n", p->ref);
#endif

    if(p) {
#if DHCPS_DEBUG
        os_printf("dhcps: send_nak>>pbuf_alloc succeed\n");
        os_printf("dhcps: send_nak>>p->tot_len = %d\n", p->tot_len);
        os_printf("dhcps: send_nak>>p->len = %d\n", p->len);
#endif
        q = p;
        while(q) {
            data = reinterpret_cast<uint8_t*>(q->payload);
            for(i = 0; i < q->len; ++i) {
                data[i] = ( reinterpret_cast<uint8_t*>(m) )[cnt++];
            }
            q = q->next;
        }
    }
    else {
#if DHCPS_DEBUG
        os_printf("dhcps: send_nak>>pbuf_alloc failed\n");
#endif
        return;
    }

    LWIP_IS_OK( "dhcps send nak", udp_sendto(pcb_dhcps, p, &broadcast_dhcps, DHCPS_CLIENT_PORT) );

    if(p->ref != 0) {
#if DHCPS_DEBUG
        os_printf("udhcp: send_nak>>free pbuf\n");
#endif
        pbuf_free(p);
    }
}


void DHCPServer::send_ack(struct dhcps_msg* m)
{

    uint8_t* end;
    pbuf*    p;
    pbuf*    q;
    uint8_t* data;
    uint16_t cnt = 0;
    uint16_t i;
    create_msg(m);

    end = add_msg_type(&m->options[4], DHCPACK);
    end = add_offer_options(end);
    end = add_end(end);

    p = pbuf_alloc( PBUF_TRANSPORT, sizeof(struct dhcps_msg), PBUF_RAM );

#if DHCPS_DEBUG
    os_printf("udhcp: send_ack>>p->ref = %d\n", p->ref);
#endif

    if(p) {
#if DHCPS_DEBUG
        os_printf("dhcps: send_ack>>pbuf_alloc succeed\n");
        os_printf("dhcps: send_ack>>p->tot_len = %d\n", p->tot_len);
        os_printf("dhcps: send_ack>>p->len = %d\n", p->len);
#endif
        q = p;
        while(q) {
            data = reinterpret_cast<uint8_t*>(q->payload);
            for(i = 0; i < q->len; ++i) {
                data[i] = ( reinterpret_cast<uint8_t*>(m) )[cnt++];
            }
            q = q->next;
        }
    }
    else {

#if DHCPS_DEBUG
        os_printf("dhcps: send_ack>>pbuf_alloc failed\n");
#endif
        return;
    }

    if( !LWIP_IS_OK( "dhcps send ack", udp_sendto(pcb_dhcps, p, &broadcast_dhcps, DHCPS_CLIENT_PORT) ) ) {
#if DHCPS_DEBUG
        os_printf("dhcps: send_ack>>udp_sendto\n");
#endif
    }

    if(p->ref != 0) {
#if DHCPS_DEBUG
        os_printf("udhcp: send_ack>>free pbuf\n");
#endif
        pbuf_free(p);
    }
}


uint8_t DHCPServer::parse_options(uint8_t* optptr, int16_t len)
{
    ipv4_addr   client;
    bool        is_dhcp_parse_end = false;
    dhcps_state s;

    client.addr = client_address.addr;

    uint8_t* end  = optptr + len;
    uint16_t type = 0;

    s.state = DHCPS_STATE_IDLE;

    while(optptr < end) {
#if DHCPS_DEBUG
        os_printf( "dhcps: (int16_t) *optptr = %d\n", reinterpret_cast<int16_t>(*optptr) );
#endif

        switch ( static_cast<int16_t>(*optptr) ) {
            case DHCP_OPTION_MSG_TYPE: // 53
                type = *(optptr + 2);
                break;

            case DHCP_OPTION_REQ_IPADDR: // 50
                if( memcmp( reinterpret_cast<char*>(&client.addr), reinterpret_cast<char*>(optptr) + 2, 4) == 0 ) {
#if DHCPS_DEBUG
                    os_printf("dhcps: DHCP_OPTION_REQ_IPADDR = 0 ok\n");
#endif
                    s.state = DHCPS_STATE_ACK;
                }
                else {
#if DHCPS_DEBUG
                    os_printf("dhcps: DHCP_OPTION_REQ_IPADDR != 0 err\n");
#endif
                    s.state = DHCPS_STATE_NAK;
                }
                break;

            case DHCP_OPTION_END:
                is_dhcp_parse_end = true;
                break;
        }

        if(is_dhcp_parse_end) break;

        optptr += optptr[1] + 2;
    }

    switch(type) {
        case DHCPDISCOVER: // 1
            s.state = DHCPS_STATE_OFFER;
#if DHCPS_DEBUG
            os_printf("dhcps: DHCPD_STATE_OFFER\n");
#endif
            break;

        case DHCPREQUEST: // 3
            if( !(s.state == DHCPS_STATE_ACK || s.state == DHCPS_STATE_NAK) ) {
                if(renew) {
                    s.state = DHCPS_STATE_ACK;
                }
                else {
                    s.state = DHCPS_STATE_NAK;
                }
#if DHCPS_DEBUG
                os_printf("dhcps: DHCPD_STATE_NAK\n");
#endif
            }
            break;

        case DHCPDECLINE: // 4
            s.state = DHCPS_STATE_IDLE;
#if DHCPS_DEBUG
            os_printf("dhcps: DHCPD_STATE_IDLE\n");
#endif
            break;

        case DHCPRELEASE: // 7
            s.state = DHCPS_STATE_RELEASE;
    #if DHCPS_DEBUG
            os_printf("dhcps: DHCPD_STATE_IDLE\n");
#endif
            break;
    }

#if DHCPS_DEBUG
    os_printf("dhcps: return s.state = %d\n", s.state);
#endif

    return s.state;
}


int16_t DHCPServer::parse_msg(struct dhcps_msg*m, uint16_t len)
{
    if( memcmp( m->options, &magic_cookie, sizeof(magic_cookie) ) ==  0) {
        ipv4_addr ip;
        memcpy( &ip.addr, m->ciaddr, sizeof(ip.addr) );
        client_address.addr = dhcps_client_update(m->chaddr, &ip);

        const int16_t ret = parse_options(&m->options[4], len);

        if(ret == DHCPS_STATE_RELEASE) {
            dhcps_client_leave(m->chaddr, &ip, true); // Force to delete
            client_address.addr = ip.addr;
        }
        return ret;
    }
    return 0;
}


void DHCPServer::S_handle_dhcp(void* arg, struct udp_pcb* pcb, struct pbuf* p, const ip_addr_t* addr, uint16_t port)
{
    DHCPServer* instance = reinterpret_cast<DHCPServer*>(arg);
    instance->handle_dhcp(pcb, p, addr, port);
}


void DHCPServer::handle_dhcp(struct udp_pcb* /*pcb*/, struct pbuf* p, const ip_addr_t* /*addr*/, uint16_t /*port*/)
{
    struct dhcps_msg* pmsg_dhcps    = nullptr;
           int16_t    tlen          = 0;
           uint16_t   i             = 0;
           uint16_t   dhcps_msg_cnt = 0;
           uint8_t*   p_dhcps_msg   = nullptr;
           uint8_t*   data          = nullptr;

#if DHCPS_DEBUG
    os_printf("dhcps: handle_dhcp-> receive a packet\n");
#endif

    if(!p) return;

    pmsg_dhcps = reinterpret_cast<struct dhcps_msg*>( zalloc( sizeof(struct dhcps_msg) ) );

    if(!pmsg_dhcps) {
        pbuf_free(p);
        return;
    }

    p_dhcps_msg = reinterpret_cast<uint8_t*>(pmsg_dhcps);
    tlen        = p->tot_len;
    data        = reinterpret_cast<uint8_t*>(p->payload);

#if DHCPS_DEBUG
    os_printf("dhcps: handle_dhcp-> p->tot_len = %d\n", tlen);
    os_printf("dhcps: handle_dhcp-> p->len = %d\n", p->len);
#endif

    for(i = 0; i < p->len; ++i) {
        p_dhcps_msg[dhcps_msg_cnt++] = data[i];
    }

    if(p->next)  {
#if DHCPS_DEBUG
        os_printf("dhcps: handle_dhcp-> p->next != nullptr\n");
        os_printf("dhcps: handle_dhcp-> p->next->tot_len = %d\n", p->next->tot_len);
        os_printf("dhcps: handle_dhcp-> p->next->len = %d\n", p->next->len);
#endif
        data = reinterpret_cast<uint8_t*>(p->next->payload);
        for(i = 0; i < p->next->len; ++i) {
            p_dhcps_msg[dhcps_msg_cnt++] = data[i];
        }
    }

#if DHCPS_DEBUG
    os_printf("dhcps: handle_dhcp-> parse_msg(p)\n");
#endif

    switch( parse_msg(pmsg_dhcps, tlen - 240) ) {
        case DHCPS_STATE_OFFER: // 1
#if DHCPS_DEBUG
            os_printf("dhcps: handle_dhcp-> DHCPD_STATE_OFFER\n");
#endif
            send_offer(pmsg_dhcps);
            break;

        case DHCPS_STATE_ACK: // 3
#if DHCPS_DEBUG
            os_printf("dhcps: handle_dhcp-> DHCPD_STATE_ACK\n");
#endif
            send_ack(pmsg_dhcps);
            if(_netif->num == SOFTAP_IF) {
                wifi_softap_set_station_info(pmsg_dhcps->chaddr, &client_address);
            }
            break;

        case DHCPS_STATE_NAK: // 4
#if DHCPS_DEBUG
            os_printf("dhcps: handle_dhcp-> DHCPD_STATE_NAK\n");
#endif
            send_nak(pmsg_dhcps);
            break;

        default :
            break;
    }

#if DHCPS_DEBUG
    os_printf("dhcps: handle_dhcp-> pbuf_free(p)\n");
#endif

    pbuf_free(p);
    free(pmsg_dhcps);
    pmsg_dhcps = nullptr;
}


void DHCPServer::init_dhcps_lease(uint32_t ip)
{
    uint32_t softap_ip = 0;
    uint32_t local_ip  = 0;
    uint32_t start_ip  = 0;
    uint32_t end_ip    = 0;

    if(dhcps_lease.enable) {
        softap_ip = htonl(ip);
        start_ip  = htonl(dhcps_lease.start_ip.addr);
        end_ip    = htonl(dhcps_lease.end_ip.addr);

        // config ip information cannot contain local IP
        if( (start_ip <= softap_ip) && (softap_ip <= end_ip) ) {
            dhcps_lease.enable = false;
        }
        else {
            // config ip information must be in the same segment as the local IP
            softap_ip >>= 8;

            if( ( (start_ip >> 8 != softap_ip) || (end_ip >> 8 != softap_ip) ) ||
                ( end_ip - start_ip > DHCPS_MAX_LEASE )
              ) {
                dhcps_lease.enable = false;
            }
        }
    }

    if(!dhcps_lease.enable) {
        local_ip   = htonl(ip);
        softap_ip  = local_ip;
        softap_ip &= 0xFFFFFF00;
        local_ip  &= 0xFF;
        if(local_ip >= 0x80) {
            local_ip -= DHCPS_MAX_LEASE;
        }
        else {
            ++local_ip;
        }

        bzero( &dhcps_lease, sizeof(dhcps_lease) );

        dhcps_lease.start_ip.addr = softap_ip | local_ip;
        dhcps_lease.end_ip.addr   = softap_ip | (local_ip + DHCPS_MAX_LEASE - 1);
        dhcps_lease.start_ip.addr = htonl(dhcps_lease.start_ip.addr);
        dhcps_lease.end_ip.addr   = htonl(dhcps_lease.end_ip.addr);
    }
}


bool DHCPServer::begin(struct ip_info* info)
{
    if(pcb_dhcps) udp_remove(pcb_dhcps);

    pcb_dhcps = udp_new();
    if(!pcb_dhcps || !info) {
        os_printf("dhcps_start(): could not obtain pcb\n");
        return false;
    }

    // Wrong           : answer will go to STA IP4_ADDR(&broadcast_dhcps, 255, 255, 255, 255);
    // Good            : going to AP IP4_ADDR(&broadcast_dhcps, 192, 168, 4, 255);
    // Semi proper way :
    broadcast_dhcps = _netif->ip_addr;
    ip_2_ip4(&broadcast_dhcps)->addr &=  ip_2_ip4(&_netif->netmask)->addr;
    ip_2_ip4(&broadcast_dhcps)->addr |= ~ip_2_ip4(&_netif->netmask)->addr;

    server_address = info->ip;
    init_dhcps_lease(server_address.addr);

    udp_bind(pcb_dhcps, IP_ADDR_ANY, DHCPS_SERVER_PORT);
    udp_recv(pcb_dhcps, S_handle_dhcp, this);

#if DHCPS_DEBUG
    os_printf("dhcps:dhcps_start->udp_recv function Set a receive callback handle_dhcp for UDP_PCB pcb_dhcps\n");
#endif

    if(_netif->num == SOFTAP_IF) {
        wifi_set_ip_info(SOFTAP_IF, info);
    }
    _netif->flags |= NETIF_FLAG_UP | NETIF_FLAG_LINK_UP;

    return true;
}


void DHCPServer::end()
{
    if(!pcb_dhcps) return;

    udp_disconnect(pcb_dhcps);
    udp_remove(pcb_dhcps);
    pcb_dhcps = nullptr;

           list_node*  pnode      = nullptr;
           list_node*  pback_node = nullptr;
    struct dhcps_pool* dhcp_node  = nullptr;
    struct ipv4_addr   ip_zero;

    memset( &ip_zero, 0x0, sizeof(ip_zero) );
    pnode = plist;

    while(pnode) {
        pback_node = pnode;
        pnode      = pback_node->pnext;
        node_remove_from_list(&plist, pback_node);
        dhcp_node  = reinterpret_cast<struct dhcps_pool*>(pback_node->pnode);

        if(_netif->num == SOFTAP_IF) {
            wifi_softap_set_station_info(dhcp_node->mac, &ip_zero);
        }

        free(pback_node->pnode);
        pback_node->pnode = nullptr;
        free(pback_node);
        pback_node        = nullptr;
    }
}


bool DHCPServer::isRunning()
{ return !!_netif->state; }


bool DHCPServer::set_dhcps_lease(struct dhcps_lease* please)
{
    uint32_t softap_ip = 0;
    uint32_t start_ip  = 0;
    uint32_t end_ip    = 0;

    if(_netif->num == SOFTAP_IF || _netif->num == STATION_IF) {
        const uint8_t opmode = wifi_get_opmode();
        if(opmode == STATION_MODE || opmode == NULL_MODE) return false;
    }

    if( !please || isRunning() ) return false;

    if(please->enable) {
        softap_ip = ip_2_ip4(&_netif->ip_addr)->addr;

        // config ip information cannot contain local IP
        if( ( start_ip <= softap_ip) && (softap_ip <= end_ip) ) return false;

        // config ip information must be in the same segment as the local IP
        softap_ip >>= 8;
        if( (start_ip >> 8 != softap_ip) || (end_ip >> 8 != softap_ip) ) return false;

        if(end_ip - start_ip > DHCPS_MAX_LEASE) return false;

        bzero( &dhcps_lease, sizeof(dhcps_lease) );
        dhcps_lease.start_ip.addr = please->start_ip.addr;
        dhcps_lease.end_ip.addr   = please->end_ip.addr;
    }

    dhcps_lease.enable = please->enable;

    return true;
}


bool DHCPServer::get_dhcps_lease(struct dhcps_lease* please)
{
    if(_netif->num == SOFTAP_IF) {
        const uint8_t opmode = wifi_get_opmode();
        if(opmode == STATION_MODE || opmode == NULL_MODE) return false;
    }

    if(!please) return false;

    if(!dhcps_lease.enable) {
        if( isRunning() ) return false;
    }

    please->start_ip.addr = dhcps_lease.start_ip.addr;
    please->end_ip.addr   = dhcps_lease.end_ip.addr;

    return true;
}


void DHCPServer::kill_oldest_dhcps_pool()
{
           list_node*  pre         = nullptr;
           list_node*  p           = nullptr;
           list_node*  minpre      = nullptr;
           list_node*  minp        = nullptr;
    struct dhcps_pool* pdhcps_pool = nullptr;
    struct dhcps_pool* pmin_pool   = nullptr;


    pre    = plist;
    p      = pre->pnext;
    minpre = pre;
    minp   = p;

    while(p) {
        pdhcps_pool = reinterpret_cast<struct dhcps_pool*>(p->pnode);
        pmin_pool   = reinterpret_cast<struct dhcps_pool*>(minp->pnode);
        if(pdhcps_pool->lease_timer < pmin_pool->lease_timer) {
            minp   = p;
            minpre = pre;
        }
        pre = p;
        p   = p->pnext;
    }

    minpre->pnext = minp->pnext; pdhcps_pool->state = DHCPS_STATE_OFFLINE;
    free(minp->pnode);

    minp->pnode = nullptr;
    free(minp);

    minp = nullptr;
}


void DHCPServer::dhcps_coarse_tmr()
{
           uint8_t     num_dhcps_pool = 0;
           list_node*  pback_node     = nullptr;
           list_node*  pnode          = nullptr;
    struct dhcps_pool* pdhcps_pool    = nullptr;

    pnode = plist;

    while(pnode) {
        pdhcps_pool = reinterpret_cast<struct dhcps_pool*>(pnode->pnode);
        if(pdhcps_pool->type == DHCPS_TYPE_DYNAMIC) {
            --pdhcps_pool->lease_timer;
        }
        if(!pdhcps_pool->lease_timer) {
            pback_node = pnode;
            pnode      = pback_node->pnext;
            node_remove_from_list(&plist, pback_node);
            free(pback_node->pnode);

            pback_node->pnode = nullptr;
            free(pback_node);

            pback_node = nullptr;
        }
        else {
            pnode = pnode ->pnext;
            ++num_dhcps_pool;
        }
    }

    if(num_dhcps_pool >= MAX_STATION_NUM) kill_oldest_dhcps_pool();
}


bool DHCPServer::set_dhcps_offer_option(uint8_t level, void* optarg)
{
    bool offer_flag = true;


    if( !optarg && !isRunning() ) return false;
    if( level <= OFFER_START || level >= OFFER_END ) return false;

    switch(level) {
        case OFFER_ROUTER:
            offer = ( *reinterpret_cast<uint8_t*>(optarg) ) & 0x01;
            offer_flag = true;
            break;

        default :
            offer_flag = false;
            break;
    }

    return offer_flag;
}


bool DHCPServer::set_dhcps_lease_time(uint32_t minute)
{
    if(_netif->num == SOFTAP_IF) {
        const uint8_t opmode = wifi_get_opmode();
        if(opmode == STATION_MODE || opmode == NULL_MODE) return false;
    }

    if( isRunning() ) return false;

    if(!minute) return false;

    dhcps_lease_time = minute;

    return true;
}


bool DHCPServer::reset_dhcps_lease_time()
{
    if(_netif->num == SOFTAP_IF) {
        const uint8_t opmode = wifi_get_opmode();
        if(opmode == STATION_MODE || opmode == NULL_MODE) return false;
    }

    if( isRunning() ) return false;

    dhcps_lease_time = DHCPS_LEASE_TIME_DEF;

    return true;
}


uint32_t DHCPServer::get_dhcps_lease_time()
{ return dhcps_lease_time; }


void DHCPServer::dhcps_client_leave(uint8_t* bssid, ipv4_addr* ip, bool force)
{
    struct dhcps_pool* pdhcps_pool = nullptr;
           list_node*  pback_node  = nullptr;

    if(!bssid || !ip) return;

    for(pback_node = plist; !!pback_node; pback_node = pback_node->pnext) {
        pdhcps_pool = reinterpret_cast<struct dhcps_pool*>(pback_node->pnode);

        if( memcmp( pdhcps_pool->mac, bssid, sizeof(pdhcps_pool->mac) ) == 0 ) {
            if( memcmp( &pdhcps_pool->ip.addr, &ip->addr, sizeof(pdhcps_pool->ip.addr) ) == 0 ) {
                if( (pdhcps_pool->type == DHCPS_TYPE_STATIC) || force ) {
                    if(pback_node) {
                        node_remove_from_list(&plist, pback_node);
                        free(pback_node);
                        pback_node = nullptr;
                    }
                    if(pdhcps_pool) {
                        free(pdhcps_pool);
                        pdhcps_pool = nullptr;
                    }
                }
                else {
                    pdhcps_pool->state = DHCPS_STATE_OFFLINE;
                }

                ipv4_addr ip_zero;
                memset( &ip_zero, 0x0, sizeof(ip_zero) );

                if(_netif->num == SOFTAP_IF) wifi_softap_set_station_info(bssid, &ip_zero);
                break;
            }
        }
    }
}


uint32_t DHCPServer::dhcps_client_update(uint8_t* bssid, ipv4_addr* ip)
{
    struct dhcps_pool*  pdhcps_pool = nullptr;
           list_node*   pback_node  = nullptr;
           list_node*   pmac_node   = nullptr;
           list_node*   pip_node    = nullptr;
           bool         flag        = false;
           uint32_t     start_ip    = dhcps_lease.start_ip.addr;
           uint32_t     end_ip      = dhcps_lease.end_ip.addr;
           dhcps_type_t type        = DHCPS_TYPE_DYNAMIC;

    if(!bssid) return IPADDR_ANY;

    if(ip) {
        if(IPADDR_BROADCAST == ip->addr) {
            return IPADDR_ANY;
        }
        else if(IPADDR_ANY == ip->addr) {
            ip = nullptr;
        }
        else {
            type = DHCPS_TYPE_STATIC;
        }
    }

    renew = false;
    for(pback_node = plist; !!pback_node; pback_node = pback_node->pnext) {
        pdhcps_pool = reinterpret_cast<struct dhcps_pool*>(pback_node->pnode);

        if( memcmp( pdhcps_pool->mac, bssid, sizeof(pdhcps_pool->mac) ) == 0 ) {
            pmac_node = pback_node;
            if(!ip) {
                flag = true;
                break;
            }
        }
        if(ip) {
            if( memcmp( &pdhcps_pool->ip.addr, &ip->addr, sizeof(pdhcps_pool->ip.addr) ) == 0 ) {
                pip_node = pback_node;
            }
        }
        else if(!flag) {
            if( memcmp( &pdhcps_pool->ip.addr, &start_ip, sizeof(pdhcps_pool->ip.addr) ) != 0 ) {
                flag = true;
            }
            else {
                start_ip = htonl( ( ntohl(start_ip) + 1 ) );
            }
        }
    }

    if( !ip && (flag == false) ) {
        if(!plist) {
            if(start_ip <= end_ip) {
                flag = true;
            }
            else {
                return IPADDR_ANY;
            }
        }
        else {
            if(start_ip > end_ip) return IPADDR_ANY;
            flag = true;
        }
    }

    if(pmac_node) { // Update new IP
        if(pip_node) {
            pdhcps_pool = reinterpret_cast<struct dhcps_pool*>(pip_node->pnode);

            if(pip_node != pmac_node) {
                if(pdhcps_pool->state != DHCPS_STATE_OFFLINE) {
                    // IP is used
                    return IPADDR_ANY;
                }

                // MAC exists and IPp exists in other node, delete MAC
                node_remove_from_list(&plist, pmac_node);
                free(pmac_node->pnode);
                pmac_node->pnode = nullptr;
                free(pmac_node);
                pmac_node        = pip_node;
                memcpy( pdhcps_pool->mac, bssid, sizeof(pdhcps_pool->mac) );
            }
            else {
                renew = true;
                type  = DHCPS_TYPE_DYNAMIC;
            }

            pdhcps_pool->lease_timer = DHCPS_LEASE_TIMER;
            pdhcps_pool->type        = type;
            pdhcps_pool->state       = DHCPS_STATE_ONLINE;

        }
        else {
            pdhcps_pool = reinterpret_cast<struct dhcps_pool*>(pmac_node->pnode);
            if(ip) {
                pdhcps_pool->ip.addr = ip->addr;
            }
            else if(flag) {
                pdhcps_pool->ip.addr = start_ip;
            }
            else {
                // No IP to distribute
                return IPADDR_ANY;
            }

            node_remove_from_list(&plist, pmac_node);
            pdhcps_pool->lease_timer = DHCPS_LEASE_TIMER;
            pdhcps_pool->type        = type;
            pdhcps_pool->state       = DHCPS_STATE_ONLINE;
            node_insert_to_list(&plist, pmac_node);
        }
    }
    else { // New station
        if(pip_node) { // Maybe ip has been used
            pdhcps_pool = reinterpret_cast<struct dhcps_pool*>(pip_node->pnode);
            if(pdhcps_pool->state != DHCPS_STATE_OFFLINE) return IPADDR_ANY;

            memcpy( pdhcps_pool->mac, bssid, sizeof(pdhcps_pool->mac) );
            pdhcps_pool->lease_timer = DHCPS_LEASE_TIMER;
            pdhcps_pool->type        = type;
            pdhcps_pool->state       = DHCPS_STATE_ONLINE;
        }
        else {
            pdhcps_pool = reinterpret_cast<struct dhcps_pool*>( zalloc( sizeof(struct dhcps_pool) ) );
            if(ip) {
                pdhcps_pool->ip.addr = ip->addr;
            }
            else if(flag) {
                pdhcps_pool->ip.addr = start_ip;
            }
            else { // No IP to distribute
                free(pdhcps_pool);
                return IPADDR_ANY;
            }
            if(pdhcps_pool->ip.addr > end_ip) {
                free(pdhcps_pool);
                return IPADDR_ANY;
            }
            memcpy(pdhcps_pool->mac, bssid, sizeof(pdhcps_pool->mac));
            pdhcps_pool->lease_timer = DHCPS_LEASE_TIMER;
            pdhcps_pool->type        = type;
            pdhcps_pool->state       = DHCPS_STATE_ONLINE;
            pback_node               = reinterpret_cast<list_node*>( zalloc( sizeof(list_node) ) );
            pback_node->pnode        = pdhcps_pool;
            pback_node->pnext        = nullptr;
            node_insert_to_list(&plist, pback_node);
        }
    }

    return pdhcps_pool->ip.addr;
}


} // namespace NodeMCU

} // namespace eCxx
