#include "stdhead.h"
#include "solicit.h"
#include "clilib.h"
#include "parse.h"
#include "request.h"
#include "decline.h"
#include "renew.h"
#include "rebind.h"
#include "release.h"

// Indicates the state of the client
int g_state;
// Stores the transaction id
u_int32_t g_trans_id = 0;
// Socket structure variable
struct sockaddr_in6 sa, ca;
// File descriptor for the event log file
int event_log_fd;
socklen_t sl;

int sfd, cfd;

struct DHCP_MESSAGE * reply_for_request = 0;

struct DUID * server_duid_ptr;

// Signal handler function for alarm signal
void sig_alarm_init_delay (int signo)
{
    printf ("\nAlarm occured. Time to transmit for the first time\n");
}
    
// Signal handler function for Interrupt signal
void quit (int signo)
{
    exit (0);
}

void intialize_event_logging ()
{
    if ((event_log_fd = open (DEFAULT_EVENT_LOG_FILE, O_CREAT | O_APPEND | O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1)
   {
	printf ("\nFailed to open event log file %s. Check permissions.\n", DEFAULT_EVENT_LOG_FILE);
	exit (0);
    }
}

int intialize_client (char *interface_name)
{
    int on = if_nametoindex (interface_name);
    signal (SIGINT, quit);  
   
    g_state = INIT;
    write_to_log ("Moving into INIT state", 1);    
   // Create sending socket
   sfd = socket (PF_INET6, SOCK_DGRAM, 0);
   if (sfd == -1)
	return 0;
    // Initialize sending socket using macro
   INITIALIZE_SOCKADDR (sa);  
   // Set the destination port to 547 
   sa.sin6_port = htons (AGENT_PORT);
   // Set destination address to ff02::1:2
   inet_pton (AF_INET6, ALL_DHCP_AGENTS, &sa.sin6_addr);
   // Allow multicast on the sending socket
    if (setsockopt (sfd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &on, sizeof (on)) == -1)
	return 0;
   
   
   cfd = socket (PF_INET6, SOCK_DGRAM, 0);
   if (cfd == -1)
	return 0;
    // Initialize the client socket
    INITIALIZE_SOCKADDR (ca);   
    // Set the client socket to 546 port
    ca.sin6_port = htons (CLIENT_PORT);
    // Bind the client socket
    bind (cfd, (struct sockaddr *) &ca, sizeof (ca));
    // Allow multicast on eth0
     if (setsockopt (cfd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &on, sizeof (on)) == -1)
        return 0;
    
    return 1;
}

struct DHCP_MESSAGE ** solicitize (char *interface_name, int * adv_msg_count)
{
    struct DHCP_MESSAGE ** advertise;
    struct DHCP_MESSAGE * dhcp_message_ptr;
    char buff[MIN_MESSAGE_SIZE];
    int n, init_delay, i;
    
      // Create the solicit message
   dhcp_message_ptr = create_solicit_message (interface_name);
   write_to_log ("Solicit message constructed.", 1);
   // Print the contents of the solicit message
   print_linked_list_contents (dhcp_message_ptr);
   // Convert the solicit message into a array
   n = store_in_buffer (dhcp_message_ptr, buff);
   // CALCULATE THE INITIAL DELAY
   init_delay = calculate_initial_delay ();
   printf ("\nThe initial delay is %d\n", init_delay);
    // SET THE SIGNAL FOR INITIAL DELAY
    signal (SIGALRM, sig_alarm_init_delay);
    alarm (init_delay);
    pause ();
    
    // Send the solicit message to the server
    sendto (sfd, buff, n, 0, (struct sockaddr *) &sa, sl);
    
    // Set the client state to selecting
    g_state = SELECTING;
    write_to_log ("Solicit message has been sent for the first time.", 1);
    write_to_log ("Moving into selecting state.", 1);
    
    
    // Receive the advertise message
    write_to_log ("Waiting for ADVERTISE message.", 1);
    advertise = wait_until_response (sfd, cfd, SOL_IRT, SOL_MAX_RT, SOL_MRC, SOLICIT, ADVERTISE, buff, n, adv_msg_count);
    // If no advertise message are received then exit
    if (!advertise)
    {
	printf ("No server found.\n");
	write_to_log ("No ADVERTISE message found. Bye Bye.", 1);
	exit (1);
    }
    else
    {
      write_to_log ("I got ADVERTISEMENTS", 1);
	// Print all advertise messages
	printf ("\n\n Printing Advertise messages.\n");
	for (i = 0; i < * adv_msg_count; i++)
	{
	    printf ("\n\n Advertise message number %d.\n", i+1);
	    print_linked_list_contents (advertise[i]);
	}
    }
    write_to_log ("\n\n\n--------------------------------------------------\n\n\n", 0);
    return advertise;
}

struct DHCP_MESSAGE * requesting (char * interface_name, struct DHCP_MESSAGE ** server_advertise, int * adv_msg_count)
{
    struct DHCP_MESSAGE * best_server_advertise, * client_request;
    struct DHCP_MESSAGE ** server_reply;
    char buff[MIN_MESSAGE_SIZE];
    int n, msg_count;

    // Select the best server advertise message
    while (best_server_advertise = select_server (server_advertise, * adv_msg_count, interface_name))
    {
    BLOG("BLOG: select server1\n");
	write_to_log ("Best ADVERTISE message has been found.", 1);
    
	// Set the client state to Requesting
	g_state = REQUESTING;
	write_to_log ("Moving into the REQUESTING state", 1);
	// Create and print the Request message
	printf ("\n\n Printing Request message.\n");
	client_request = create_request_message (best_server_advertise, interface_name);
    printf ("\n\n Printing Request message done.\n");
    write_to_log ("REQUEST message has been constructed.", 1);
	print_linked_list_contents (client_request);
    
	// Convert the Request message into an array
	n = store_in_buffer (client_request, buff);
    BLOG("BLOG: store in request buffer done, len: %d\n",n);
	// Multicast the Request message
	sendto (sfd, buff, n, 0, (struct sockaddr *) &sa, sl);
	write_to_log ("REQUEST message has been sent.", 1);
	// Wait for server reply
	write_to_log ("Waiting for REPLY (for REQUEST) message from server.", 1);
	server_reply = wait_until_response (sfd, cfd, REQ_IRT, REQ_MRT, REQ_MRC, REQUEST, REPLY, buff, n, &msg_count);
	
	if (!server_reply)
	{
	    write_to_log ("No REPLY (for REQUEST) message from server. Searching for the next best ADVERTISE message ...", 1);
	    server_reply = purge_message (server_advertise, best_server_advertise, adv_msg_count);
	    continue;
	}
	else
	{
	    // Assuming that i have got back a valid REPLY message from the server
	    print_linked_list_contents (* server_reply);
	    write_to_log ("REPLY (for REQUEST) message received from server.", 1);
	    break;
	}
    }
    write_to_log ("\n\n\n---------------------------------------------------\n\n\n", 0);
    if (server_reply)
        return * server_reply;
    else
	return 0;
}

struct DHCP_MESSAGE * decline_server (char * interface_name, struct DHCP_MESSAGE * server_reply)
{
    struct DHCP_MESSAGE ** server_decline_reply;
    struct DHCP_MESSAGE * dhcp_message_ptr;
    char buff[MIN_MESSAGE_SIZE];
    int n, msg_count;

    // send decline message
    dhcp_message_ptr = create_decline_message (server_reply, interface_name);
    n = store_in_buffer (dhcp_message_ptr, buff);
    sendto (sfd, buff, n, 0, (struct sockaddr *) &sa, sl);
    write_to_log ("DECLINE message has been sent.", 1);
    write_to_log ("Waiting for REPLY (for DECLINE) message from server.", 1);
    server_decline_reply = wait_until_response (sfd, cfd, DEC_IRT, DEC_MRT, DEC_MRC, DECLINE, REPLY, buff, n, &msg_count);
    
    if (!server_decline_reply)
        write_to_log ("No REPLY (for DECLINE) message from server. Abandoning attempts ...", 1);
    else
    {
        if (check_message (*server_decline_reply, REPLY, DECLINE))
        {
	    print_linked_list_contents (*server_decline_reply);
    	    write_to_log ("REPLY (for DECLINE) message received from server.", 1);
        }
    }
    write_to_log ("\n\n\n------------------------------------------------------\n\n\n", 0);
    
    if (server_decline_reply)
        return * server_decline_reply;
    else
	return 0;
}

void bind_and_count (char * interface_name , struct DHCP_MESSAGE * server_reply, int * allow_for_renewal)
{
    struct timeval timeout;
    u_int32_t renewal_timer, rebind_timer, valid_lifetime_timer;
    
    
    get_timers (server_reply, &renewal_timer, &rebind_timer, &valid_lifetime_timer);
    
    printf ("The valid lifetime is %d\n", valid_lifetime_timer);
    printf ("The renewal time is %d\n", renewal_timer);
    printf ("The rebind time is %d\n", rebind_timer);
    if (valid_lifetime_timer > renewal_timer)
    {
	   timeout.tv_sec = renewal_timer;
   	   write_to_log ("Setting renewal timer ....", 1);
	   printf ("Setting renewal timer ....\n");
	   * allow_for_renewal = 1;
    }
    else
    {
        timeout.tv_sec = valid_lifetime_timer;
        write_to_log ("Setting Valid lifetime timer ....", 1);
	  printf ("Setting Valid lifetime timer ....\n");
	  * allow_for_renewal = 0;
    }
    timeout.tv_usec = 0;
    select (0, 0, 0, 0, &timeout);
}


struct DHCP_MESSAGE * renew_lease (char * interface_name, struct DHCP_MESSAGE * server_reply)
{	
    struct DHCP_MESSAGE ** server_renew_reply;
    struct DHCP_MESSAGE * dhcp_message_ptr;
    char buff[MIN_MESSAGE_SIZE];
    int n, msg_count;
    
    dhcp_message_ptr = create_renew_message (server_reply, interface_name);
    write_to_log ("Renew message constructed.", 1);
    print_linked_list_contents (dhcp_message_ptr);
    n = store_in_buffer (dhcp_message_ptr, buff);
    write_to_log ("Sending Renew message ...", 1);
    sendto (sfd, buff, n, 0, (struct sockaddr *) &sa, sl);
    write_to_log ("Waiting for REPLY (for RENEW) message ...", 1);
    server_renew_reply = wait_until_response (sfd, cfd, REN_IRT, REN_MRT, REN_MRC, RENEW, REPLY, buff, n, &msg_count);
    write_to_log ("\n\n\n------------------------------------------------------\n\n\n", 0);
    if (server_renew_reply)
    {
	write_to_log ("Reply (for Renew) message received from server.", 1);
	return * server_renew_reply;
    }
    else
    {
	write_to_log ("No Reply (for Renew) message received from server.", 1);
	return 0;
    }
}

struct DHCP_MESSAGE * rebind (char * interface_name, struct DHCP_MESSAGE * server_reply)
{
    struct DHCP_MESSAGE ** server_rebind_reply;
    struct DHCP_MESSAGE * dhcp_message_ptr;
    char buff[MIN_MESSAGE_SIZE];
    int n, msg_count;

    dhcp_message_ptr = create_rebind_message (server_reply, interface_name);
    write_to_log ("Rebind message constructed.", 1);
    print_linked_list_contents (dhcp_message_ptr);
    n = store_in_buffer (dhcp_message_ptr, buff);
    write_to_log ("Sending Rebind message ...", 1);
    sendto (sfd, buff, n, 0, (struct sockaddr *) &sa, sl);
    write_to_log ("Waiting for REPLY (for REBIND) message ...", 1);
    server_rebind_reply = wait_until_response (sfd, cfd, REB_IRT, REB_MRT, REB_MRC, REBIND, REPLY, buff, n, &msg_count);
    write_to_log ("\n\n\n------------------------------------------------------\n\n\n", 0);
    if (server_rebind_reply)
    {
	write_to_log ("Reply (for Rebind) message received from server.", 1);
	return * server_rebind_reply;   
    } 
    else
    {
	write_to_log ("No Reply (for Rebind) message received from server.", 1);
	return 0;
    }
}
	    
struct DHCP_MESSAGE * release_lease (char * interface_name, struct DHCP_MESSAGE * server_reply)
{
    struct DHCP_MESSAGE ** server_release_reply;
    struct DHCP_MESSAGE * dhcp_message_ptr;
    char buff[MIN_MESSAGE_SIZE];
    int n, msg_count;

    // send Release message
    dhcp_message_ptr = create_release_message (server_reply, interface_name);
    write_to_log ("RELEASE message has been constructed", 1);
    n = store_in_buffer (dhcp_message_ptr, buff);
    sendto (sfd, buff, n, 0, (struct sockaddr *) &sa, sl);
    write_to_log ("RELEASE message has been sent.", 1);
    write_to_log ("Waiting for REPLY (for RELEASE) message from server.", 1);
    server_release_reply = wait_until_response (sfd, cfd, REL_IRT, REL_MRT, REL_MRC, RELEASE, REPLY, buff, n, &msg_count);
    
    if (!server_release_reply)
        write_to_log ("No REPLY (for RELEASE) message from server. Abandoning attempts ...", 1);
    else
    {
        if (check_message (* server_release_reply, REPLY, RELEASE))
        {
	    print_linked_list_contents (* server_release_reply);
    	    write_to_log ("REPLY (for RELEASE) message received from server.", 1);
        }
    }
    write_to_log ("\n\n\n------------------------------------------------------\n\n\n", 0);
    if (server_release_reply)
	return * server_release_reply;
    else
	return 0;
}

void store_current_server_duid (struct OPTIONS * options_ptr)
{
   struct DUID * duid_ptr;
   struct DUID1 * duid1_ptr;
   struct DUID2 * duid2_ptr;
   struct DUID3 * duid3_ptr;
   
   if (!options_ptr)
	return;
	
   if (server_duid_ptr)
   {
	duid_ptr = server_duid_ptr;
	switch (duid_ptr -> u_duid_type.duid_type)
	{
	   case 1 :
		duid1_ptr = (struct DUID1 *) duid_ptr -> duid_type;
		free (duid1_ptr);
		break;
	   
	   case 2 :
		duid2_ptr = (struct DUID2 *) duid_ptr -> duid_type;
		free (duid2_ptr);
		break;
	   
	   case 3 :
		duid3_ptr = (struct DUID3 *) duid_ptr -> duid_type;
		free (duid3_ptr);
		break;
	}
	free (duid_ptr);
   }	
   
   duid_ptr = (struct DUID *) options_ptr -> opt_data;
   
   server_duid_ptr = (struct DUID *) malloc (sizeof (struct DUID));
   server_duid_ptr -> u_duid_type.duid_type = duid_ptr -> u_duid_type.duid_type;
   server_duid_ptr -> opt = 0;
   
   switch (duid_ptr -> u_duid_type.duid_type)
   {
   
	case 1 :
	   duid1_ptr = (struct DUID1 *) malloc (sizeof (struct DUID1));
	   server_duid_ptr -> duid_type = duid1_ptr;
	   copy_duid1 ((struct DUID1 *) duid_ptr -> duid_type, duid1_ptr);
	   break;
	   
	case 2 :
	   duid2_ptr = (struct DUID2 *) malloc (sizeof (struct DUID2));
	   server_duid_ptr -> duid_type = duid2_ptr;
	   copy_duid2 ((struct DUID2 *) duid_ptr -> duid_type, duid2_ptr);
	   break;
	
	case 3 :
	   duid3_ptr = (struct DUID3 *) malloc (sizeof (struct DUID3));
	   server_duid_ptr -> duid_type = duid3_ptr;
	   copy_duid3 ((struct DUID3 *) duid_ptr -> duid_type, duid3_ptr);
	   break;
   }
}

// Main Function
int main(int argc, char ** argv)
{
   struct DHCP_MESSAGE * server_decline_reply, * server_renew_reply, * server_rebind_reply, *server_release_reply;
   struct DHCP_MESSAGE ** server_advertise;
   int adv_msg_count = 0, allow_for_renewal;
   struct timeval timeout;
   u_int32_t renewal_timer, rebind_timer, valid_lifetime_timer;
   char * interface_name;
   //int wait_for_key;
    
   sl = sizeof (sa);
   
   if (argc == 1)
	return 1;
    else if (argc == 2)
	interface_name = argv[1];
	
   
   intialize_event_logging ();
   
   if (!intialize_client (interface_name))
   {
	printf ("Failed to initialize client.Exiting ...\n");
	exit (0);
    }

    fflush (stdin);
        
    while (1)
    {
	printf ("\nEntering solicitize function ...\n");
	//wait_for_key = getchar();
	server_advertise = solicitize (interface_name, &adv_msg_count);
	if (!server_advertise)
	{
	    printf ("No server found. Bye Bye \n");
	    write_to_log ("No server found. Bye Bye", 1);
	    exit (0);
	 }
   
	printf ("\nEntering requesting function ... %d\n",adv_msg_count);
	//wait_for_key = getchar();
        reply_for_request = requesting (interface_name, server_advertise, &adv_msg_count);
    
    	g_state = CHECKING;
	if (check_address_in_use (get_options_ptr (reply_for_request, OPTION_IAADDR)))
	{
	    printf ("\nEntering decline_server function ...");
	    //wait_for_key = getchar();
	    server_decline_reply = decline_server (interface_name, reply_for_request);
	}
	    
	else
	{
	    g_state = BOUND;
	    write_to_log ("Setting the IPv6 address ...", 1);
	    printf ("\nSetting IPv6 address ...\n");
	    //wait_for_key = getchar();
	    set_ipv6_address (get_options_ptr (reply_for_request, OPTION_IAADDR), interface_name);
	    store_current_server_duid (get_options_ptr (reply_for_request, OPTION_SERVERID));
	      
	    while (1)
	    {
		write_to_log ("Getting the timers and starting the timers...", 1);
	        get_timers (reply_for_request, &renewal_timer, &rebind_timer, &valid_lifetime_timer);
	        bind_and_count (interface_name, reply_for_request, &allow_for_renewal);
	
		if (allow_for_renewal)
		{
		    g_state = RENEWING;	
		    write_to_log ("Moving to Renewal state ...", 1);
		    printf ("\nEntering renew_lease function ...\n");
		    //wait_for_key = getchar();
	    	    server_renew_reply = renew_lease (interface_name, reply_for_request);
		    if (!server_renew_reply)
	    	    {
			write_to_log ("No server reply (REPLY) messages ...", 1);
			if (valid_lifetime_timer >= rebind_timer)
			{
			    g_state = REBINDING;
			    write_to_log ("Moving to Rebinding state ...", 1);
			    printf ("\nEntering rebind lease function ...\n");
			    //wait_for_key = getchar();
			    server_rebind_reply = rebind (interface_name, reply_for_request);
			    if (!server_rebind_reply)
			    {
				// Create and send a release    
				g_state = END;
				write_to_log ("Moving into End state ...", 1);
				server_release_reply = release_lease (interface_name, reply_for_request);
				// DELETE THE PREVIOUS IPV6 ADDRESS
				write_to_log ("Unsetting the IPv6 address ...", 1);
				unset_ipv6_address (get_options_ptr (reply_for_request, OPTION_IAADDR), interface_name);
				break;
			    }
			    else if (check_ia_status (server_rebind_reply))
			    {
				g_state = REQUESTING;
				write_to_log ("Moving to Requesting state ...", 1);
				reply_for_request = requesting (interface_name, server_advertise, &adv_msg_count);
			    }   
    			    else
			    {
				g_state = BOUND;
				write_to_log ("Moving back to Bound state ...", 1);
			    	reply_for_request = server_rebind_reply;
			    }
			}
			else
			{
			    // Create and send a release    
			    g_state = END;
			    write_to_log ("Moving into End state ...", 1);
			    server_release_reply = release_lease (interface_name, reply_for_request);
			    // DELETE THE PREVIOUS IPV6 ADDRESS
			    write_to_log ("Unsetting the IPv6 address ...", 1);
			    unset_ipv6_address (get_options_ptr (reply_for_request, OPTION_IAADDR), interface_name);
			    break;
			}
		    }
		    else
		    {
			if (check_ia_status (server_renew_reply))
			{
			    g_state = REQUESTING;
			    write_to_log ("Moving to Requesting state ...", 1);
			    reply_for_request = requesting (interface_name, server_advertise, &adv_msg_count);
			}    
			else
			{
			    g_state = BOUND;
			    write_to_log ("Moving back to Bound state ...", 1);
			    reply_for_request = server_renew_reply;
			}
		    }
		}
		else
		{
		    // Create and send a release    
		    write_to_log ("Moving into End state ...", 1);
		    g_state = END;
		    printf ("\nEntering release_lease function ...\n");
		    //wait_for_key = getchar();
		    server_release_reply = release_lease (interface_name, reply_for_request);
		    // DELETE THE PREVIOUS IPV6 ADDRESS
		    unset_ipv6_address (get_options_ptr (reply_for_request, OPTION_IAADDR), interface_name);
		    break;
		}
	    }
	}
    }
    // Exit the program
    exit (0);
}
