/*********************************************
 * vim:sw=8:ts=8:si:et
 * To use the above modeline in vim you must have "set modeline" in your .vimrc
 * Author: Guido Socher
 * Copyright:LGPL V2
 * See http://www.gnu.org/licenses/old-licenses/lgpl-2.0.html
 *
 * A DHCP client.
 * This code uses the UDP_client framework. You need to enable UDP_client in ip_config.h to use this.
 *
 * DHCP requires the periodic renewal of addresses. For this
 * purpose you must call the function dhcp_6sec_tick() every 6 sec.
 * If you do not call it then you can still get an initial IP
 * (without retry in case of failure) but there will be no
 * lease renewal.
 *********************************************/
#include <avr/io.h>
#include <avr/pgmspace.h>
#include <string.h>
#include <stdlib.h>
#include "module/net.h"
#include "module/enc28j60.h"
#include "module/ip_arp_udp_tcp.h"
#include "module/ip_config.h"


static volatile uint8_t dhcp_6sec_cnt=0; // counts multiple of 6 sec
static uint8_t dhcp_yiaddr[4]= {0,0,0,0}; // your (client) IP
static uint8_t dhcp_opt_defaultgw[4]= {0,0,0,0}; // default gw
static uint8_t dhcp_opt_mask[4]= {0,0,0,0}; // netmask
static uint8_t dhcp_opt_server_id[4]= {0,0,0,0}; // server ip
static uint8_t dhcp_opt_message_type=0;
static uint8_t dhcp_tid=0;
static uint16_t dhcp_opt_leasetime_minutes=0;

//
// The relevant RFCs are
// DHCP protocol: http://tools.ietf.org/html/rfc1541
// newer version:
// DHCP protocol: http://tools.ietf.org/html/rfc2131
// message encoding: http://tools.ietf.org/html/rfc1533
//
// The normal message flow to get an IP address is:
// Client -> Server DHCPDISCOVER
// Server -> Client DHCPOFFER
// Client -> Server DHCPREQUEST
// Server -> Client DHCPACK
// There might be serveral seconds delay between the DHCPDISCOVER and
// the DHCPOFFER as the server does some test (e.g ping or arp) to see
// if the IP which is offered is really free.
//
// At lease renewal the message flow is:
// Client -> Server DHCPREQUEST
// Server -> Client DHCPACK
// The DHCPREQUEST for renewal is different from the one at inital assignment.
//
//
// DHCP_OPTION_OFFSET is a relative to UDP_DATA_P
#define DHCP_OPTION_OFFSET 240
#define MAGIC_COOKIE_P 236
#define DHCP_SRV_SRC_PORT 67
#define DHCP_SRV_DST_PORT 68
// message type values
#define DHCP_DISCOVER_V 0x01
#define DHCP_REQUEST_V 0x03

// Lease time renewal and time keeping.
// you must call this function every 6 seconds. It is save
// to do this from interrupt
void dhcp_6sec_tick(void)
{
    dhcp_6sec_cnt++;
}

// This function writes a basic message template into buf
// It processes all fields excluding the options section.
// Most fields are initialized with zero.
void make_dhcp_message_template(uint8_t *buf,const uint8_t transactionID)
{
    uint8_t i=0;
    uint8_t allxff[6]= {0xff,0xff,0xff,0xff,0xff,0xff}; // all of it can be used as mac, the first 4 can be used as IP

    send_udp_prepare(buf,DHCP_SRV_DST_PORT,allxff,DHCP_SRV_SRC_PORT,allxff);
    // source IP is 0.0.0.0, a while loop produces smaller code than memset
    i=0;
    while(i<4)
    {
        buf[IP_SRC_P +i]=0;
        i++;
    }
    // now fill the bootstrap protocol layer starting at UDP_DATA_P
    buf[UDP_DATA_P]=1;// message type = boot request
    buf[UDP_DATA_P+1]=1;// hw type
    buf[UDP_DATA_P+2]=6;// mac len
    buf[UDP_DATA_P+i+3]=0;// hops
    // we use only one byte TIDs, we fill the first byte with 1 and
    // the rest with transactionID. The first byte is used to
    // distinguish inital requests from renew requests.
    buf[UDP_DATA_P+4]=1;
    i=0;
    while(i<3)
    {
        buf[UDP_DATA_P+i+5]=transactionID;
        i++;
    }
    // we set seconds always to zero
    //
    // set my own MAC the rest is empty:
    // a while loop produces smaller code than memset
    i=8; // start at 8
    while(i<28)
    {
        buf[UDP_DATA_P +i]=0;
        i++;
    }
    // own mac (send_udp_prepare did fill it at eth level):
    i=0;
    while(i<6)
    {
        buf[UDP_DATA_P+i+28]=buf[ETH_SRC_MAC +i];
        i++;
    }
    // now we need to write 202 bytes of zero
    // a while loop produces smaller code than memset, don't use memset
    i=34; // start at 8
    while(i<236)
    {
        buf[UDP_DATA_P +i]=0;
        i++;
    }
    // the magic cookie has to be:
    // 99, 130, 83, 99
    buf[UDP_DATA_P+MAGIC_COOKIE_P]=0x63;
    buf[UDP_DATA_P+MAGIC_COOKIE_P+1]=0x82;
    buf[UDP_DATA_P+MAGIC_COOKIE_P+2]=0x53;
    buf[UDP_DATA_P+MAGIC_COOKIE_P+3]=0x63;
}

// the answer to this message will come as a broadcast
uint8_t send_dhcp_discover(uint8_t *buf,const uint8_t transactionID)
{
    make_dhcp_message_template(buf,transactionID);
    // option dhcp message type:
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET]=0x35; // 53
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET+1]=1; //len
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET+2]=DHCP_DISCOVER_V;
    // option paramter request list:
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET+3]=0x37; // 55
    // we want: subnet mask, router
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET+4]=2; // len
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET+5]=1; // subnet mask
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET+6]=3; // router=default GW
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET+7]=0xff; // end of options
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET+8]=0;
    // no padding
    // the length of the udp message part is now DHCP_OPTION_OFFSET+8
    send_udp_transmit(buf,DHCP_OPTION_OFFSET+8);
    return(0);
}

// scan the options field for the message type field
// and return its value.
//
// Value   Message Type
// -----   ------------
//   1     DHCPDISCOVER
//   2     DHCPOFFER  (server to client)
//   3     DHCPREQUEST
//   4     DHCPDECLINE
//   5     DHCPACK  (server to client)
//   6     DHCPNAK  (server to client)
//   7     DHCPRELEASE
// return 0 on message type not found otherwise the numeric
// value for the message type as shown in the table above.
uint8_t dhcp_get_message_type(uint8_t *buf,uint16_t plen)
{
    uint16_t option_idx;
    uint8_t option_len;
    // the smallest option is 3 bytes
    if (plen<(UDP_DATA_P+DHCP_OPTION_OFFSET+3)) return(0);
    // options are coded in the form: option_type,option_len,option_val
    option_idx=UDP_DATA_P+DHCP_OPTION_OFFSET;
    while(option_idx+2 <plen )
    {
        option_len=buf[option_idx+1];
        if ((option_len<1) || ((option_idx + option_len + 1)> plen)) break;
        if (buf[option_idx]==53)
        {
            // found message type, return it:
            return(buf[option_idx+2]);
        }
        option_idx+=2+option_len;
    }
    return(0);
}

// use this on DHCPACK or DHCPOFFER messages to read "your ip address"
uint8_t dhcp_get_yiaddr(uint8_t *buf,uint16_t plen)
{
    // DHCP offer up to options section is at least 0x100 bytes
    if (plen<0x100) return(0);
    if (buf[UDP_DATA_P+16]!=0)
    {
        // we have a yiaddr
        memcpy(dhcp_yiaddr, buf+UDP_DATA_P+16, 4);
        return(1);
    }
    return(0);
}

// this will as well update dhcp_yiaddr
uint8_t is_dhcp_msg_for_me(uint8_t *buf,uint16_t plen,const uint8_t transactionID)
{
    // DHCP offer up to options section is at least 0x100 bytes
    if (plen<0x100) return(0);
    if (buf[UDP_SRC_PORT_L_P] != DHCP_SRV_SRC_PORT) return(0);
    if (buf[UDP_DATA_P]!=2) return(0); // message type DHCP boot reply =2
    if (buf[UDP_DATA_P+5]!=transactionID) return(0);
    if (buf[UDP_DATA_P+6]!=transactionID) return(0);
    return(1);

}

// check if this message was part of a renew or
uint8_t dhcp_is_renew_tid(uint8_t *buf,uint16_t plen)
{
    if (plen<0x100) return(0);
    if (buf[UDP_DATA_P+4]==2) return(1); // we did set first byte in transaction ID to 2 to indicate renew request. This trick makes the processing of the DHCPACK message easier.
    return(0);
}

uint8_t dhcp_option_parser(uint8_t *buf,uint16_t plen)
{
    uint16_t option_idx;
    uint8_t option_len;
    uint8_t i;
    uint32_t ltime_int32=0;
    // the smallest option is 3 bytes
    if (plen<(UDP_DATA_P+DHCP_OPTION_OFFSET+3)) return(0);
    // options are coded in the form: option_type,option_len,option_val
    option_idx=UDP_DATA_P+DHCP_OPTION_OFFSET;
    while(option_idx+2 <plen )
    {
        option_len=buf[option_idx+1];
        if ((option_len<1) || ((option_idx + option_len + 1)> plen)) break;
        switch (buf[option_idx])
        {
        case 0:
            option_idx=plen; // stop loop, we are reading some padding bytes here (should not happen)
            break;

        case 1:
            if (option_len==4)
            {
                i=0;
                while(i<4)
                {
                    dhcp_opt_mask[i]=buf[option_idx+2+i];
                    i++;
                }
            }
            break;
        case 3:
            if (option_len==4)
            {
                i=0;
                while(i<4)
                {
                    dhcp_opt_defaultgw[i]=buf[option_idx+2+i];
                    i++;
                }
            }
            break;
        // Lease time: throughout the protocol, times are to
        // be represented in units of seconds.  The time value
        // of 0xffffffff is reserved to represent "infinity".
        // The max lease time size is therefore 32 bit.
        // The code for this option is 51, and its length is 4
        // as per RFC 1533.
        case 51:
            if (option_len!=4)
            {
                dhcp_opt_leasetime_minutes=1440; // just to have a reasonable value: 1 day
                break;
            }
            i=0;
            while(i<4)
            {
                ltime_int32=(ltime_int32<<8) | buf[option_idx+i+2];
                i++;
            }
            if (ltime_int32==0xffffffff)
            {
                // lease time is infinity
                dhcp_opt_leasetime_minutes=0xffff;
                break; // end of switch
            }
            ltime_int32=ltime_int32>>6; // an inexpesive way to divide by 64 (which is roughly equal to divide by 60)
            if (ltime_int32> 0xfffd)  //0xffff is not handeled here because of the above break
            {
                dhcp_opt_leasetime_minutes=0xfffd;
            }
            else
            {
                dhcp_opt_leasetime_minutes=ltime_int32 & 0xffff;
            }
            if (dhcp_opt_leasetime_minutes<5) dhcp_opt_leasetime_minutes=5;
            break;
        case 53:
            dhcp_opt_message_type=buf[option_idx+2];
            break;
        // rfc 2131: A DHCP server always returns its
        // own address in the 'server identifier' option
        case 54:
            if (option_len==4)
            {
                i=0;
                while(i<4)
                {
                    dhcp_opt_server_id[i]=buf[option_idx+2+i];
                    i++;
                }
            }
            break;
        }
        option_idx+=2+option_len;
    }
    return(1);
}

// the answer to this message will come as a broadcast
uint8_t send_dhcp_request(uint8_t *buf,const uint8_t transactionID)
{
    uint8_t i=0;
    make_dhcp_message_template(buf,transactionID);
    // option dhcp message type:
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET]=0x35; // 53
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET+1]=1; //len
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET+2]=DHCP_REQUEST_V;
    i=3;
    if (dhcp_opt_server_id[0]!=0)
    {
        buf[UDP_DATA_P+DHCP_OPTION_OFFSET+i]=0x36; // 54=server identifier
        buf[UDP_DATA_P+DHCP_OPTION_OFFSET+i+1]=4; // len
        memcpy(buf+UDP_DATA_P+DHCP_OPTION_OFFSET+i+2,dhcp_opt_server_id, 4);
        i=i+6;
    }
    if (dhcp_yiaddr[0]!=0)
    {
        buf[UDP_DATA_P+DHCP_OPTION_OFFSET+i]=0x32; // 50=requested IP address
        buf[UDP_DATA_P+DHCP_OPTION_OFFSET+i+1]=4; // len
        memcpy(buf+UDP_DATA_P+DHCP_OPTION_OFFSET+i+2,dhcp_yiaddr, 4);
        i=i+6;
    }
    // option paramter request list:
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET+i]=0x37; // 55
    // we want: subnet mask, router
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET+i+1]=2; // len
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET+i+2]=1; // subnet mask
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET+i+3]=3; // router=default GW
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET+i+4]=0xff; // end of options
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET+i+5]=0;
    // the length of the udp message part is now DHCP_OPTION_OFFSET+i+5
    send_udp_transmit(buf,DHCP_OPTION_OFFSET+i+5);
    return(0);
}

// The renew procedure is described in rfc2131.
// We send DHCPREQUEST and 'server identifier' MUST NOT be filled
// in, 'requested IP address' option MUST NOT be filled in, 'ciaddr'
// MUST be filled.
// The rfc suggest that I can send the DHCPREQUEST in this case as
// a unicast message and not as a broadcast message but test with
// various DHCP servers show that not all of them listen to
// unicast. We send therefor a broadcast message but we expect
// a unicast answer directly to our mac and IP.
uint8_t send_dhcp_renew_request(uint8_t *buf,const uint8_t transactionID,uint8_t *yiaddr)
{
    make_dhcp_message_template(buf,transactionID);
    buf[UDP_DATA_P+4]=2; // set first byte in transaction ID to 2 to indicate renew_request. This trick makes the processing of the DHCPACK message easier.
    // source IP must be my IP since we renew
    memcpy(buf+IP_SRC_P, yiaddr, 4); // ip level source IP
    //
    memcpy(buf+UDP_DATA_P+12,yiaddr, 4); // ciaddr
    // option dhcp message type:
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET]=0x35; // 53
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET+1]=1; //len
    buf[UDP_DATA_P+DHCP_OPTION_OFFSET+2]=DHCP_REQUEST_V;
    // no option paramter request list is needed at renew
    send_udp_transmit(buf,DHCP_OPTION_OFFSET+3);
    // we will get a unicast answer, reception of broadcast packets is turned off
    return(0);
}


// Initial_tid can be a random number for every board. E.g the last digit
// of the mac address. It is not so important that the number is random.
// It is more important that it is unique and no other board on the same
// Lan has the same number. This is because there might be a power outage
// and all boards reboot afterwards at the same time. At that moment they
// must all have different TIDs otherwise there will be an IP address mess-up.
//
// The function returns 1 once we have a valid IP.
// At this point you must not call the function again.
uint8_t packetloop_dhcp_initial_ip_assignment(uint8_t *buf,uint16_t plen,uint8_t initial_tid)
{
    static uint16_t init=0x5fff; // about 2 sec delay
    uint8_t cmd;
    // do nothing if the link is down
    if (!enc28j60linkup()) return(0);
    // first time that this function is called:
    if (plen==0)
    {
        if (init==2)
        {
            init=1;
            dhcp_6sec_cnt=0;
            dhcp_tid=initial_tid;
            // Reception of broadcast packets is turned off by default, but
            // the DHCP offer message that the DHCP server sends will be
            // a broadcast packet. Enable here and disable later.
            enc28j60EnableBroadcast();
            send_dhcp_discover(buf,dhcp_tid);
            return(0);
        }
        if (dhcp_yiaddr[0]==0 && dhcp_6sec_cnt > 5)
        {
            // still no IP after 30 sec
            dhcp_tid++;
            dhcp_6sec_cnt=0;
            // Reception of broadcast packets is turned off by default, but
            // the DHCP offer message that the DHCP server sends will be
            // a broadcast packet. Enable here and disable later.
            enc28j60EnableBroadcast();
            send_dhcp_discover(buf,dhcp_tid);
            return(0);
        }
        // We have a little delay (about 2sec) at startup. Sometimes
        // the power fluctuates or the programmer causes
        // the board to reset and then immediately reset again.
        // We wait with the sending of a send_dhcp_discover just in case
        // we did already so at the last reset which was possibly less
        // than a second ago.
        if (init>2)
        {
            init--;
        }
        return(0);
    }
    // plen > 0
    if (is_dhcp_msg_for_me(buf,plen,dhcp_tid))
    {
        // It's really a borderline case that we the the dhcp_is_renew_tid
        // function call for. It could only happen if the board is power cyceled
        // during operation.
        if (dhcp_is_renew_tid(buf,plen)==1) return(0); // should have been initial tid, just return
        cmd=dhcp_get_message_type(buf,plen);
        if (cmd==2)  // DHCPOFFER =2
        {
            init=1; // no more init needed
            dhcp_get_yiaddr(buf,plen);
            dhcp_option_parser(buf,plen);
            // answer offer with a request:
            send_dhcp_request(buf,dhcp_tid);
        }
        if (cmd==5)  // DHCPACK =5
        {
            // success, DHCPACK, we have the IP
            init=1; // no more init needed
            enc28j60DisableBroadcast();
            return(1);
        }
    }
    return(0);
}

// call this to get the current IP
// returns {0,0,0,0} in assigend_yiaddr if called before we have a valid IP been offered
// otherwise returns back the IP address (4bytes) in assigend_yiaddr.
// assigend_netmask will hold the netmask and assigend_gw the default gateway
// You can fill fields that you don't want (not interessted in) to NULL
void dhcp_get_my_ip(uint8_t *assigend_yiaddr,uint8_t *assigend_netmask, uint8_t *assigend_gw)
{
    if (assigend_yiaddr) memcpy(assigend_yiaddr,dhcp_yiaddr,4);
    if (assigend_netmask) memcpy(assigend_netmask,dhcp_opt_mask,4);
    if (assigend_gw) memcpy(assigend_gw,dhcp_opt_defaultgw,4);
}

// Put the following function into your main packet loop.
// returns plen of original packet if buf is not touched.
// returns 0 if plen was originally zero. returns 0 if DHCP messages
// was processed.
// We don't need to expect changing IP addresses. We can stick
// to the IP that we got once. The server has really no power to
// do anything about that.
uint16_t packetloop_dhcp_renewhandler(uint8_t *buf,uint16_t plen)
{
    if (dhcp_6sec_cnt> 8)   // we let it run a bit faster than once every minute because it is better this expires too early than too late
    {
        dhcp_6sec_cnt=0;
        // count down unless the lease was infinite
        if (dhcp_opt_leasetime_minutes < 0xffff && dhcp_opt_leasetime_minutes>1)
        {
            dhcp_opt_leasetime_minutes--;
        }
    }
    if (plen ==0 && dhcp_opt_leasetime_minutes < 3)
    {
        if (!enc28j60linkup()) return(plen); // do nothing if link is down
        dhcp_tid++;
        send_dhcp_renew_request(buf,dhcp_tid,dhcp_yiaddr);
        dhcp_opt_leasetime_minutes=5; // repeat in two minutes if no answer
        return(0);
    }
    if (plen && is_dhcp_msg_for_me(buf,plen,dhcp_tid))
    {
        // we check the dhcp_renew_tid because if
        if (dhcp_get_message_type(buf,plen)==5)  // DHCPACK =5
        {
            // success, DHCPACK, we have the IP
            if (dhcp_is_renew_tid(buf,plen))
            {
                dhcp_option_parser(buf,plen); // get new lease time, it will as well GW and netmask but those should not change
            }
        }
        return(0);
    }
    return(plen);
}

// === end of DHCP client
