//www.networksorcery.com/enp/protocol/dhcp.htm

#define DHCP_OPCODE_BOOTREQUEST     0x01

#define DHCP_OPTION_SUBNET_MASK     1
#define DHCP_OPTION_ROUTER          3
#define DHCP_OPTION_DNS             6
#define DHCP_OPTION_DOMAIN_NAME     15

#define DHCP_OPTION_REQUESTED_IP    50
#define DHCP_OPTION_MESSAGETYPE     53
#define DHCP_OPTION_SERVER_ID       54
#define DHCP_OPTION_PARAMLIST       55

#define DHCP_MESSAGETYPE_DISCOVER   0x01
#define DHCP_MESSAGETYPE_OFFER      0x02
#define DHCP_MESSAGETYPE_REQUEST    0x03
#define DHCP_MESSAGETYPE_ACK        0x05

#define DHCP_COOKIE 0x63825363

#define DHCP_STATE_CLIENT_START         0
#define DHCP_STATE_CLIENT_DISCOVER      1
#define DHCP_STATE_CLIENT_REQUEST       2
#define DHCP_STATE_CLIENT_REQ_ACCEPTED  3

#define DHCP_TIMEOUT        3000
#define DHCP_MAX_RETRIES    5   // shrine has 3, why not 5 :^)

class CDHCPHeader
{
    U8  opcode;             // Opcode
    U8  hw_type;            // Hardware Type
    U8  hw_addr_len;        // Hardware Address Length
    U8  hops;               // Hop Count
    U32 xid;                // Transaction ID
    U16 seconds;            // Elapsed time in seconds since client began address acquisition or renewal process
    U16 flags;              // Flags
    U32 client_ip;          // Client IP Address
    U32 your_ip;            // Your IP Address
    U32 server_ip;          // Server IP Address
    U32 gateway_ip;         // Gateway IP Address
    U8  client_hw_addr[16]; // Client Hardware Address
    U8  server_name[64];    // Server Hostname
    U8  boot_file[128];     // Boot Filename
};

class CDHCPDiscoverOptions
{
    U32 cookie;
    U8  message_type;
    U8  message_length;
    U8  message; // dmt
    U8  param_req_list_type;
    U8  param_req_list_length;
    U8  param_req_list[4];
    U8  end;
};

class CDHCPRequestOptions
{
    U32 cookie;
    U8  message_type;
    U8  message_length;
    U8  message; // dmt
    U8  requested_ip_type;
    U8  requested_ip_length;
    U32 requested_ip;
    U8  server_id_type;
    U8  server_id_length;
    U32 server_id;
    U8  end;
};

U32 DHCPTransactionBegin()
{
    return RandU32;
}

I64 DHCPDiscoverSend(U32 xid)
{
    U8                      *dhcp_frame;
    I64                      de_index;
    CDHCPHeader             *dhcp;
    CDHCPDiscoverOptions    *opts;


    de_index = UDPPacketAllocate(&dhcp_frame,
                                 0x00000000,
                                 68,
                                 0xFFFFFFFF,
                                 67,
                                 sizeof(CDHCPHeader) + sizeof(CDHCPDiscoverOptions));
    if (de_index < 0)
    {
        NetErr("DHCP SEND DISCOVER: Failed, UDP Packet Allocate error.");
        return de_index;
    }

    dhcp = dhcp_frame;
    MemSet(dhcp, 0, sizeof(CDHCPHeader));

    dhcp->opcode        = DHCP_OPCODE_BOOTREQUEST;
    dhcp->hw_type       = HTYPE_ETHERNET;
    dhcp->hw_addr_len   = HLEN_ETHERNET;
    dhcp->hops          = 0;
    dhcp->xid           = EndianU32(xid);
    dhcp->seconds       = 0;
    dhcp->flags         = EndianU16(0x8000); // DHCP flag: accept Offer from Broadcast.
    dhcp->client_ip     = 0;
    dhcp->your_ip       = 0;
    dhcp->server_ip     = 0;
    dhcp->gateway_ip    = 0;
    MemCopy(dhcp->client_hw_addr, EthernetMACGet, MAC_ADDRESS_LENGTH);

    opts = dhcp_frame + sizeof(CDHCPHeader);

    opts->cookie                = EndianU32(DHCP_COOKIE);
    opts->message_type          = DHCP_OPTION_MESSAGETYPE;
    opts->message_length        = 1;
    opts->message               = DHCP_MESSAGETYPE_DISCOVER;
    opts->param_req_list_type   = DHCP_OPTION_PARAMLIST;
    opts->param_req_list_length = 4;
    opts->param_req_list[0]     = DHCP_OPTION_SUBNET_MASK;
    opts->param_req_list[1]     = DHCP_OPTION_ROUTER;
    opts->param_req_list[2]     = DHCP_OPTION_DNS;
    opts->param_req_list[3]     = DHCP_OPTION_DOMAIN_NAME;
    opts->end                   = 0xFF; // ??

    UDPPacketFinish(de_index);
    return de_index;
}

I64 DHCPRequestSend(U32 xid, U32 requested_ip, U32 server_ip)
{
    U8                  *dhcp_frame;
    I64                  de_index;
    CDHCPHeader         *dhcp;
    CDHCPRequestOptions *opts;

    de_index = UDPPacketAllocate(&dhcp_frame,
                                 0x00000000,
                                 68,
                                 0xFFFFFFFF,
                                 67,
                                 sizeof(CDHCPHeader) + sizeof(CDHCPRequestOptions));
    if (de_index < 0)
    {
        NetErr("DHCP SEND REQUEST: Failed, UDP Packet Allocate error.");
    }

    dhcp = dhcp_frame;
    MemSet(dhcp, 0, sizeof(CDHCPHeader));

    dhcp->opcode        = DHCP_OPCODE_BOOTREQUEST;
    dhcp->hw_type       = HTYPE_ETHERNET;
    dhcp->hw_addr_len   = HLEN_ETHERNET;
    dhcp->hops          = 0;
    dhcp->xid           = EndianU32(xid);
    dhcp->seconds       = 0;
    dhcp->flags         = EndianU16(0x0000); // DHCP flag: accept ACK from Unicast.
    dhcp->client_ip     = 0;
    dhcp->your_ip       = 0;
    dhcp->server_ip     = EndianU32(server_ip);
    dhcp->gateway_ip    = 0;
    MemCopy(dhcp->client_hw_addr, EthernetMACGet, MAC_ADDRESS_LENGTH);

    opts = dhcp_frame + sizeof(CDHCPHeader);

    opts->cookie                = EndianU32(DHCP_COOKIE);
    opts->message_type          = DHCP_OPTION_MESSAGETYPE;
    opts->message_length        = 1;
    opts->message               = DHCP_MESSAGETYPE_REQUEST;
    opts->requested_ip_type     = DHCP_OPTION_REQUESTED_IP;
    opts->requested_ip_length   = 4;
    opts->requested_ip          = EndianU32(requested_ip);
    opts->server_id_type        = DHCP_OPTION_SERVER_ID;
    opts->server_id_length      = 4;
    opts->server_id             = EndianU32(server_ip);
    opts->end                   = 0xFF;

    UDPPacketFinish(de_index);
    return 0;
}

I64 DHCPBeginParse(U8 **data_inout, I64 *length_inout, CDHCPHeader **header_out)
{
    U8  *data   = *data_inout;
    I64  length = *length_inout;
    U32 *cookie;

    if (length < sizeof(CDHCPHeader) + 4) // + 4?
    {
        NetErr("DHCP PARSE BEGIN: Failed, length too short.");
        return -1;
    }

    cookie = data + sizeof(CDHCPHeader);

    if (EndianU32(*cookie) != DHCP_COOKIE)
    {
        NetErr("DHCP PARSE BEGIN: Failed, cookie doesn't match DHCP-cookie.");
        return -1;
    }

    *header_out     = data;
    *data_inout     = data   + sizeof(CDHCPHeader) + 4; // ?
    *length_inout   = length - sizeof(CDHCPHeader) + 4; // ?..

    return 0;
}

I64 DHCPOptionParse(U8 **data_inout, I64 *length_inout, U8 *type_out, U8 *value_length_out, U8 **value_out)
{
    U8 *data    = *data_inout;
    I64 length  = *length_inout;

    if (length < 2 || length < 2 + data[1]) // ??? what is the 1
    {
        NetErr("DHCP PARSE OPTION: Failed, length too short.");
        return -1;
    }

    if (data[0] == 0xFF) // ahead, data[0] is type_out, so data[0] is perhaps usually type?
    {
        NetLog("DHCP PARSE OPTION: Saw 0xFF, returning 0.");
        return 0;
    }

    *type_out           = data[0];
    *value_length_out   = data[1];
    *value_out          = data + 2;

    *data_inout     = data   + 2 + *value_length_out;
    *length_inout   = length - 2 + *value_length_out;

    return data[0]; // returns ... type?
}

I64 DHCPOfferParse(U32 xid, U8 *data, I64 length,
                   U32 *your_ip_out,
                   U32 *dns_ip_out,
                   U32 *router_ip_out,
                   U32 *subnet_mask_out)
{
    CDHCPHeader *header;
    I64          error       = DHCPBeginParse(&data, &length, &header);
    Bool         have_type   = FALSE;
    Bool         have_dns    = FALSE;
    Bool         have_router = FALSE;
    Bool         have_subnet = FALSE;
    U8           type;
    U8           value_length;
    U8          *value;
    U32          address;
    

    if (EndianU32(header->xid) != xid)
    {
        NetErr("DHCP PARSE OFFER: Failed, parsed and parameter Transaction IDs do not match.");
        return -1;
    }

    while (length)
    {
        error = DHCPOptionParse(&data, &length, &type, &value_length, &value);

        if (error < 0)
        {
            NetErr("DHCP PARSE OFFER: Failed at DHCP Parse Option.");
            return error;
        }
        if (error == 0)
        {
            break;
        }

        address = EndianU32(*value(U32 *));

        switch (type)
        {
            case DHCP_OPTION_MESSAGETYPE:
                NetLog("DHCP PARSE OFFER: Parsed Option, Type MESSAGETYPE.");
                if (value_length == 1 && value[0] == DHCP_MESSAGETYPE_OFFER)
                    have_type = TRUE;
                break;

            case DHCP_OPTION_DNS:
                NetLog("DHCP PARSE OFFER: Parsed Option, Type DNS.");
                if (value_length == 4)
                {
                    *dns_ip_out = address;
                    have_dns    = TRUE;
                }
                break;

            case DHCP_OPTION_ROUTER:
                NetLog("DHCP PARSE OFFER: Parsed Option, Type ROUTER.");
                if (value_length == 4)
                {
                    *router_ip_out  = address;
                    have_router     = TRUE;
                }
                break;

            case DHCP_OPTION_SUBNET_MASK:
                NetLog("DHCP PARSE OFFER: Parsed Option, Type SUBNET MASK.");
                if (value_length == 4)
                {
                    *subnet_mask_out = address;
                    have_subnet      = TRUE;
                }
                break;
        }
    }

    if (have_type && have_dns && have_subnet && have_router)
    {
        *your_ip_out = EndianU32(header->your_ip);
        NetLog("DHCP PARSE OFFER: Success, got your-ip from DHCP Header.");
        return 0;
    }
    else
    {
        NetErr("DHCP PARSE OFFER: Failed, did not have needed Options.");
        NetErr("                  have_type: %Z", have_type, "ST_FALSE_TRUE");
        NetErr("                  have_dns: %Z", have_dns, "ST_FALSE_TRUE");
        NetErr("                  have_router: %Z", have_router, "ST_FALSE_TRUE");
        NetErr("                  have_subnet: %Z", have_subnet, "ST_FALSE_TRUE");
        return -1;
    }
}

I64 DHCPAckParse(U32 xid, U8 *data, I64 length)
{
    CDHCPHeader *header;
    I64          error = DHCPBeginParse(&data, &length, &header);
    U8           type;
    U8           value_length;
    U8          *value;

    if (EndianU32(header->xid) != xid)
    {
        NetErr("DHCP PARSE ACK: Failed, parsed and parameter Transaction IDs do not match.");
        return -1;
    }

    while (length)
    {
        error = DHCPOptionParse(&data, &length, &type, &value_length, &value);

        if (error < 0)
        {
            NetErr("DHCP PARSE ACK: Failed at DHCP Parse Option.");
            return error;
        }
        if (error == 0)
        {
            break;
        }

        switch (type)
        {
            case DHCP_OPTION_MESSAGETYPE:
                if (value_length == 1 && value[0] == DHCP_MESSAGETYPE_ACK)
                    return 0;
                break;
        }
    }

    NetErr("DHCP PARSE ACK: Failed.");
    return -1;
}

I64 DHCPInnerConfigure(CUDPSocket *udp_socket,
                       U32 *your_ip_out,
                       U32 *dns_ip_out,
                       U32 *router_ip_out,
                       U32 *subnet_mask_out)
{
    I64 state   = DHCP_STATE_CLIENT_START;
    I64 retries = 0;
    I64 timeout = DHCP_TIMEOUT;
    I64 error   = 0;
    U32 xid;
    U32 dhcp_addr;
    U8  buffer[2048];
    I64 count;

    CSocketAddressIPV4  ipv4_addr;
    CSocketAddressIPV4  ipv4_addr_in;

    //Shrine: setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO_MS, &timeout, sizeof(timeout)))
    udp_socket->receive_timeout_ms = timeout;

    ipv4_addr.family            = AF_INET;
    ipv4_addr.port              = EndianU16(68);
    ipv4_addr.address.address   = INADDR_ANY;

    if (UDPSocketBind(udp_socket, &ipv4_addr) < 0)
    {
        NetErr("DHCP CONFIGURE INNER: Failed to Bind UDP Socket.");
        return -1;
    }

    xid = DHCPTransactionBegin;

    while (state != DHCP_STATE_CLIENT_REQ_ACCEPTED)
    {
        switch (state)
        {
            case DHCP_STATE_CLIENT_START:
                state   = DHCP_STATE_CLIENT_DISCOVER;
                retries = 0;

                break;

            case DHCP_STATE_CLIENT_DISCOVER:
                NetLog("DHCP CONFIGURE INNER: Trying Discover.");
                error = DHCPDiscoverSend(xid);
                if (error < 0)
                {
                    NetErr("DHCP CONFIGURE INNER: Failed, DHCP Send Discover error.");
                    return error;
                }

                count = UDPSocketReceiveFrom(udp_socket, buffer, sizeof(buffer), &ipv4_addr_in);

                if (count > 0)
                { // 'Try a parse offer'
                    NetLog("DHCP CONFIGURE INNER: Trying Parse Offer.");
                    error = DHCPOfferParse(xid, buffer, count, your_ip_out, dns_ip_out, router_ip_out, subnet_mask_out);

                    if (error < 0)
                        NetWarn("DHCP CONFIGURE INNER: Unsuccessful DHCP Parse Offer.");
                }

                if (count > 0 && error >= 0)
                {
                    dhcp_addr   = EndianU32(ipv4_addr_in.address.address);
                    state       = DHCP_STATE_CLIENT_REQUEST;
                    retries     = 0;
                }
                else if (++retries == DHCP_MAX_RETRIES)
                {
                    NetErr("DHCP CONFIGURE INNER: Failed, hit max retries in DHCP DISCOVER state.");
                    return -1;
                }

                break;

            case DHCP_STATE_CLIENT_REQUEST:
                NetLog("DHCP CONFIGURE INNER: Trying Send Request.");
                error = DHCPRequestSend(xid, *your_ip_out, dhcp_addr);

                if (error < 0)
                {
                    NetErr("DHCP CONFIGURE INNER: Failed, unsuccessful DHCP Send Request.");
                    return error;
                }

                count = UDPSocketReceiveFrom(udp_socket, buffer, sizeof(buffer), &ipv4_addr_in);

                if (count > 0)
                { // 'Try parse Ack'
                    error = DHCPAckParse(xid, buffer, count);

                    if (error < 0)
                        NetWarn("DHCP CONFIGURE INNER: Unsuccessful DHCP Parse Ack.");
                }

                if (count > 0 && error >= 0)
                {
                    dhcp_addr   = EndianU32(ipv4_addr_in.address.address);
                    state       = DHCP_STATE_CLIENT_REQ_ACCEPTED;
                }
                else if (++retries == DHCP_MAX_RETRIES)
                {
                    NetErr("DHCP CONFIGURE INNER: Failed, hit max retries in DHCP REQUEST state.");
                    return -1;
                }

                break;
        }
    }

    return state;
}

I64 DHCPConfigure()
{
    CUDPSocket      *udp_socket = UDPSocket(AF_INET);
    CIPV4Address     address;
    U32              your_ip;
    U32              dns_ip;
    U32              router_ip;
    U32              subnet_mask;
    I64              state = DHCPInnerConfigure(udp_socket, &your_ip, &dns_ip, &router_ip, &subnet_mask);

    UDPSocketClose(udp_socket);

    if (state == DHCP_STATE_CLIENT_REQ_ACCEPTED)
    {
        address.address = EndianU32(your_ip);
        NetLog("$BG,2$$FG,15$DHCP CONFIGURE: Obtained IPV4 Address! : %s $BG$$FG$", NetworkToPresentation(AF_INET, &address));

        IPV4AddressSet(your_ip);
        IPV4SubnetSet(router_ip, subnet_mask);
        DNSResolverIPV4Set(dns_ip);
        return 0;
    }
    else
    {
        NetErr("$BG,4$DHCP CONFIGURE: Failed, incorrect state.$BG$");
        return -1;
    }
}


U0 NetConfigure()
{
    I64 error;

    NetLog("\n==== Configuring Network. ====\n");
    error = DHCPConfigure;

    if (error < 0)
        NetErr("==== Network Configure Failed ====");
    else
        NetLog("$BG,2$$FG,15$==== Network Configure Success ====$FG$$BG$");
}

U0 NetRep()
{
    "\n$LTGREEN$Network Report:$FG$\n\n";
    UDPRep;
    TCPRep;
    DNSRep;
    ARPRep;
    IPV4Rep;
}