#include <cerrno>
#include <stdexcept>
#include "includes/sock.hpp"
#include "includes/exception_consts.hpp"
#include "includes/easysock.hpp"

/* Function to create socket. This function doesn't actually create a socket
(and isn't meant to be called directly). Instead, the client and server classes
extend this function, and create the appropriate sockets. */

void Sock::create_socket()  {
	dest = (struct sockaddr *)malloc(sizeof(struct sockaddr));
	addrlen = sizeof(*dest);
}

/* Constructor - This function initializes the object attributes with the given
parameters. It throws an exception if an IPv4 address was given, but the type
given is IPv6 (or the other way around). */

Sock::Sock(int ip_ver, char protocol, const char* address, int port) {
	/* Error checking */
	if (ip_ver != 4 && ip_ver != 6) {
		throw std::invalid_argument("Invalid IP address type");
	}
	if (port < 1024 || port > 65535) {
		throw std::invalid_argument("Invalid port");
	}
	if (protocol != ES_TCP && protocol != ES_UDP) {
		throw std::invalid_argument("Invalid protocol");
	}

	this->ip_ver = ip_ver;
	this->protocol = protocol;
	this->port = port;
	this->address = std::string(address);

	/* Check to see if the given IP address matches the given ip_ver */
        if ((check_ip_ver(address) != 6 && ip_ver == 6) || (check_ip_ver(address) != 4 && ip_ver == 4)) {
		throw std::invalid_argument("Invalid IP address for given type.");
	}
}

/* This method sends the given data, through the  'other_sockt' variable.. Client
and server classes extend this method, by setting this variable to different values.
This function needs more testing for TCP, as it focuses on UDP right now. */

void Sock::sendAll(std::string to_send) {
	int str_length = to_send.length();
	int num_bytes_sent = 0; /* Number of bytes sent in one call to send */
	int total_bytes_sent = 0; /* Total number of bytes sent */

	/* For UDP sockets */
	if (this->protocol == ES_UDP) {
		sendto(this->sock_fd, to_send.data(), str_length, 0, dest, addrlen);
	}
	/* For TCP sockets */
	else {
		while (total_bytes_sent < str_length) {
			/* Send the data to the 'other_socket' variable, which should be set by the client and server methods */
			num_bytes_sent = send(this->other_socket, to_send.substr(total_bytes_sent).c_str(), str_length - total_bytes_sent, 0);
			if (num_bytes_sent < 0) {
				throw errno * -1;
			}
			total_bytes_sent += num_bytes_sent;
		}
	}
	
	return;
}

/* This method receives a (char *) and a size, and creates a std::string with it.
It then calls the method above, passing that string as a parameter. */
void Sock::sendAll(char* buffer, int size) {
	std::string to_send = std::string(buffer, size);
	sendAll(to_send);
}

/* Receives data from 'other_socket' into a char *, and returns that char *. For TCP, the
'recv' method is called until all the data has been read. For UDP, the 'recvfrom'
method is only called once. The 'select' function is used to poll data for UDP
This function also needs more testing for TCP. */
char* Sock::recvAll() {
	int num_bytes_received = 0;
	int total_bytes_received = 0;
	char* buffer = (char *)malloc(100);
	bool has_been_read = false;

	if (this->protocol == ES_UDP) {
		num_bytes_received = recvfrom(this->sock_fd, buffer, 99, 0, dest, &addrlen);
		if (num_bytes_received == 0) {
			return NULL;
		}
		/* Null-terminate the string */
		*(buffer + num_bytes_received) = '\0';
		return buffer;
	}
	/* For TCP sockets */
	else {
		while ((num_bytes_received = recv(this->other_socket, buffer + total_bytes_received, 100 - total_bytes_received, 0)) != 0) {

			if ((errno == EAGAIN || errno == EWOULDBLOCK)) {
				if (has_been_read) {
					break;
				} else {
					continue;
				}
			}

			if (num_bytes_received < 0) {
				throw errno * -1;
			}
			total_bytes_received += num_bytes_received;
			has_been_read = true;
		}

	}

	return buffer;
}

/* Non-blocking recv call - Uses 'select' to poll for data from the FD. */
char* Sock::recvAllNB() {
	struct timeval tv;
	fd_set readfs;
	tv.tv_sec = 0; // Set to 0 to poll instead of wait
	tv.tv_usec = 0; // Set to 0 to poll instead of wait
	FD_ZERO(&readfs);
	FD_SET(this->sock_fd, &readfs);
	
	select(this->sock_fd + 1, &readfs, NULL, NULL, &tv);
	
	if (FD_ISSET(this->sock_fd, &readfs)) {
		return Sock::recvAll();
	} else {
		return NULL;
	}
}

int Sock::getSockFD() {
	return sock_fd;
}
