#define IPV4_ERR_ADDR_INVALID           -200001
#define IPV4_ERR_HOST_UNREACHABLE       -200002

#define IPV4_TTL        64

//Look up IP Protocol Numbers online to see many more.
#define IP_PROTOCOL_ICMP        0x01
#define IP_PROTOCOL_TCP         0x06
#define IP_PROTOCOL_UDP         0x11

class CIPV4Packet
{
        CEthernetFrame  *ethernet_frame;

        U32                              source_ip_address;
        U32                              destination_ip_address;

        U8                               protocol;
        U8                               padding[7];

        U8                              *data;
        I64                              length;
};

class CIPV4Header
{ // note: U4's in some U8s.
        U8      version_ihl;                    // Version for IPV4 is 4. IHL=Internet Header Length
        U8      dscp_ecn;                               // DSCP=Differentiated Services Code Point. ECN=Explicit Congestion Notification

        U16 total_length;                       // min 20B max 65535
        U16 identification;
        U16 flags_fragment_offset;      // flags first(?) 3 bits. fragment offset min 0 max 65528
                                                                // flag: bit 0: reserved must be 0. bit 1: don't fragment. bit 2: more fragments

        U8      time_to_live;                   // specified in seconds, wikipedia says nowadays serves as a hop count
        U8      protocol;

        U16 header_checksum;

        U32 source_ip_address;
        U32 destination_ip_address;
};

class CIPV4Globals
{ // _be indicates Big Endian
        U32 local_ip;
        U32 local_ip_be;

        U32 ipv4_router_address;
        U32 ipv4_subnet_mask;

} ipv4_globals;

U0 IPV4GlobalsInit()
{
        ipv4_globals.local_ip                    = 0;
        ipv4_globals.local_ip_be                 = 0;
        ipv4_globals.ipv4_router_address = 0;
        ipv4_globals.ipv4_subnet_mask    = 0;
};

// For now, trusting Shrine's implement
// of checksum. Shrine links back to
// http://stackoverflow.com/q/26774761/2524350

U16 IPV4Checksum(U8 *header, I64 length)
{       //todo. make names clearer, and better comments.
        I64  nleft = length;
        U16 *w = header;
        I64  sum = 0;

        while (nleft > 1)
        {
                sum += *w++;
                nleft -= 2;
        }

        // "mop up an odd byte, if necessary"
        if (nleft == 1)
        {
                sum += *w & 0x00FF;
        }

        // "add back carry outs from top 16 bits to low 16 bits"
        sum = sum >> 16 + sum & 0xFFFF; // "add hi 16 to low 16"
        sum += sum >> 16; // add carry
        return ~sum & 0xFFFF;

}

I64 IPV4AddressMACGet(U32 ip_address, U8 **mac_out)
{
        CARPHash        *entry;
        I64                      retries;
        I64                      attempt;

        if (ip_address == 0)
        {
                NetErr("GET MAC FOR IP: Failed. Address = 0");
                return IPV4_ERR_ADDR_INVALID;
        }
        if (ip_address == 0xFFFFFFFF)
        {
                NetLog("GET MAC FOR IP: Returning ethernet broadcast");
                *mac_out = ethernet_globals.ethernet_broadcast;

                return 0;
        }

        // "outside this subnet; needs routing"
        if (ip_address & ipv4_globals.ipv4_subnet_mask != ipv4_globals.local_ip & ipv4_globals.ipv4_subnet_mask)
        {
                NetWarn("GET MAC FOR IP: TODO: Doing IPV4AddressMACGet recursion, could infinite loop and overflow stack.");
                return IPV4AddressMACGet(ipv4_globals.ipv4_router_address, mac_out);
        }
        else // "local network"
        {
                NetLog("GET MAC FOR IP: Attempting ARP Find by IP for address: %0X.", ip_address);
                entry = ARPCacheFind(ip_address);

                if (entry)
                {
                        *mac_out = entry->mac_address;
                        return 0;
                }
                //else, not in cache, need to request it

                // "Up to 4 retries, 500 ms each"
                retries = 4;
                while (retries)
                {
                        ARPSend(ARP_REQUEST,
                                        ethernet_globals.ethernet_broadcast,
                                        EthernetMACGet,
                                        ipv4_globals.local_ip_be,
                                        ethernet_globals.ethernet_null,
                                        EndianU32(ip_address));

                        attempt = 0;
                        for (attempt = 0; attempt < 50; attempt++)
                        {
                                Sleep(10);
                                entry = ARPCacheFind(ip_address);
                                if (entry)
                                        break;
                        }

                        if (entry)
                        {
                                *mac_out = entry->mac_address;
                                return 0;
                        }

                        retries--;
                }

                //Shrine does some in_addr mess to log error
                NetErr("GET MAC FOR IP: Failed to resolve address %d", ip_address);
                return IPV4_ERR_HOST_UNREACHABLE;
        }
}

I64 IPV4PacketAllocate(U8 **frame_out, 
                                           U8 protocol,
                                           U32 source_ip_address,
                                           U32 destination_ip_address,
                                           I64 length)
{
        U8                      *ipv4_frame;
        U8                      *destination_mac_address;
        I64                      error;
        I64                      de_index;
        I64                      internet_header_length;
        CIPV4Header *header;

        error = IPV4AddressMACGet(destination_ip_address, &destination_mac_address);

        if (error < 0)
        {
                NetLog("IPV4 PACKET ALLOCATE: Failed to get MAC for destination.");
                return error;
        }

        de_index = EthernetFrameAllocate(&ipv4_frame,
                                                                         EthernetMACGet,
                                                                         destination_mac_address,
                                                                         ETHERTYPE_IPV4,
                                                                         sizeof(CIPV4Header) + length);
        if (de_index < 0)
        {
                NetLog("IPV4 PACKET ALLOCATE: Ethernet Frame Allocate failed.");
                return de_index;
        }

        internet_header_length = 5;// ... why. need a #define

        header = ipv4_frame;

        header->version_ihl                             = internet_header_length | 4 << 4;// ? TODO: needs #define
        header->dscp_ecn                                = 0; // a clear define of what this actually means would be good
        header->total_length                    = EndianU16(internet_header_length * 4 + length); //...why?
        header->identification                  = 0; // define would be clearer
        header->flags_fragment_offset   = 0; // define would be clearer
        header->time_to_live                    = IPV4_TTL;
        header->protocol                                = protocol;
        header->header_checksum                 = 0; // why is 0 ok?
        header->source_ip_address               = EndianU32(source_ip_address);
        header->destination_ip_address  = EndianU32(destination_ip_address);
        header->header_checksum                 = IPV4Checksum(header, internet_header_length * 4);//why the 4's...

        *frame_out = ipv4_frame + sizeof(CIPV4Header);
        return de_index;
}

U0 IPV4PacketFinish(I64 de_index) //alias for EthernetFrameFinish
{
        EthernetFrameFinish(de_index);
}

U32 IPV4AddressGet()
{
        return ipv4_globals.local_ip;
}

U0 IPV4AddressSet(U32 ip_address)
{
        ipv4_globals.local_ip    = ip_address;
        ipv4_globals.local_ip_be = EndianU32(ip_address);

        ARPLocalIPV4Set(ip_address);
        
}

U0 IPV4SubnetSet(U32 router_address, U32 subnet_mask)
{
        ipv4_globals.ipv4_router_address = router_address;
        ipv4_globals.ipv4_subnet_mask    = subnet_mask;
}

//I64
U0 IPV4PacketParse(CIPV4Packet *packet_out, CEthernetFrame *ethernet_frame)
{
        //...if ethertype not ipv4 error?

        // TODO: Check ethernet_frame length ! ... we need to know what's appropriate

        CIPV4Header *header = ethernet_frame->data;
        I64                      header_length = (header->version_ihl & 0x0F) * 4;//this Has to go. at least abstract or something..
        U16                      total_length = EndianU16(header->total_length);

        packet_out->ethernet_frame                      = ethernet_frame;
        packet_out->source_ip_address           = EndianU32(header->source_ip_address);
        packet_out->destination_ip_address      = EndianU32(header->destination_ip_address);
        packet_out->protocol                            = header->protocol;
        packet_out->data                                        = ethernet_frame->data + header_length;
        packet_out->length                                      = total_length - header_length;

//      return 0;
}

U0 IPV4Rep()
{
        "$LTBLUE$IPV4 Report:$FG$\n\n";

        "Local IPV4:                    %d.%d.%d.%d\n",
                ipv4_globals.local_ip.u8[3],
                ipv4_globals.local_ip.u8[2],
                ipv4_globals.local_ip.u8[1],
                ipv4_globals.local_ip.u8[0];
        "Router IPV4:           %d.%d.%d.%d\n",
                ipv4_globals.ipv4_router_address.u8[3],
                ipv4_globals.ipv4_router_address.u8[2],
                ipv4_globals.ipv4_router_address.u8[1],
                ipv4_globals.ipv4_router_address.u8[0];
        "Subnet IPV4:           %d.%d.%d.%d\n",
                ipv4_globals.ipv4_subnet_mask.u8[3],
                ipv4_globals.ipv4_subnet_mask.u8[2],
                ipv4_globals.ipv4_subnet_mask.u8[1],
                ipv4_globals.ipv4_subnet_mask.u8[0];
        "\n";
}

// IPV4 handler moved to NetHandlerTask file.
IPV4GlobalsInit;