/* 
 * BS API Layer
 * 
 * bs_api.h
 *
 * Interface of the BS API layer of the client side Book Share library.
 * 
 * Process each request, creating the PDU and making the request and
 * receiving the response
 *
 */

#define _POSIX_SOURCE

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <sys/un.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#include "bs_api.h"


// switch
static int V_flag;

/*
 * Internal private auxiliary functions
 */


static int bs_udp_socket_init(char *address, int port, struct sockaddr *serv_addr) {
	int sockfd;
	struct addrinfo hints, *servinfo, *p;
	int rv;
	char port_aux[MAX_PORT_SIZE];
	sprintf(port_aux, "%d", port);
	memset(&hints, 0, sizeof(hints));
	hints.ai_family = AF_INET; // use AF_INET6 to force IPv6
	hints.ai_socktype = SOCK_DGRAM;
	if ((rv = getaddrinfo(address, port_aux, &hints, &servinfo)) != 0) {
		vprintf("\33[1m[bs_api]\33[0m getaddrinfo error: %s.\n", gai_strerror(rv));
		return -1;
	}
	// loop through all the results and connect to the first we can
	for(p = servinfo; p != NULL; p = p->ai_next) {
		if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {
			vprintf("\33[1m[bs_api]\33[0m socket error: %s.\n", strerror(errno));
			continue;
		}
		break; // if we get here, we must have connected successfully
	}
	if (p == NULL) {
		// looped off the end of the list with no successful bind
		vprintf("\33[1m[bs_api]\33[0m failed to connect.\n");
		return -1;
	}
	
	*serv_addr = *(p->ai_addr);
	
	freeaddrinfo(servinfo); // all done with this structure
	
	// set socket as non blocking
	int flags = fcntl(sockfd, F_GETFL);
	fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
	
	return sockfd;
}


static void bs_udp_finish(int sockfd) {
	close(sockfd);
}


static int bs_udp_send_request(int sockfd, bs_pdu_req_t *req, int reqsz,
	struct sockaddr *serv_addr) {
	int status;
	
	do {
		// try to send the request
		status = sendto(sockfd, req, reqsz, 0, serv_addr, sizeof(*serv_addr));
	} while (status == -1 && errno == EAGAIN);
	
	if (status < 0) {
		vprintf("\33[1m[bs_api]\33[0m sendto error: %s.\n", strerror(errno));
		return STAT_ERROR;
	}
	
	if (status != reqsz) {
		vprintf("\33[1m[bs_api]\33[0m message size mismatch.\n");
	}
	
	vprintf("\33[1m[bs_api]\33[0m sent udp request.\n");
	return STAT_OK;
}


static int bs_udp_recv_response(int sockfd, bs_pdu_res_t *res, int *ressz) {
	do {
		// try to receive the response
		*ressz = recvfrom(sockfd, (void*)res, sizeof(*res), 0, NULL, NULL);
	} while (*ressz == -1 && errno == EAGAIN);
	
	if (*ressz < 0) {
		vprintf("\33[1m[bs_api]\33[0m recvfrom error: %s.\n", strerror(errno));
		return STAT_ERROR;
	}
	
	vprintf("\33[1m[bs_api]\33[0m received udp request.\n");
	return STAT_OK;
}


static int bs_tcp_socket_init(char *address, int port) {
	int sockfd;
	struct addrinfo hints, *servinfo, *p;
	int rv;
	char port_aux[MAX_PORT_SIZE];
	sprintf(port_aux, "%d", port);
	memset(&hints, 0, sizeof(hints));
	hints.ai_family = AF_INET; // use AF_INET6 to force IPv6
	hints.ai_socktype = SOCK_STREAM;
	if ((rv = getaddrinfo(address, port_aux, &hints, &servinfo)) != 0) {
		vprintf("\33[1m[bs_api]\33[0m getaddrinfo error: %s.\n", gai_strerror(rv));
		return -1;
	}
	// loop through all the results and connect to the first we can
	for(p = servinfo; p != NULL; p = p->ai_next) {
		if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {
			vprintf("\33[1m[bs_api]\33[0m socket error: %s.\n", strerror(errno));
			continue;
		}
		if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
			close(sockfd);
			vprintf("\33[1m[bs_api]\33[0m connect error: %s.\n", strerror(errno));
			continue;
		}
		break; // if we get here, we must have connected successfully
	}
	if (p == NULL) {
		// looped off the end of the list with no successful bind
		vprintf("\33[1m[bs_api]\33[0m failed to connect.\n");
		return -1;
	}
	freeaddrinfo(servinfo); // all done with this structure
	
	// set socket as non blocking
	int flags = fcntl(sockfd, F_GETFL);
	fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
	
	return sockfd;
}


static void bs_tcp_finish(int sockfd) {
	close(sockfd);
}


static int bs_tcp_send_request(int sockfd, bs_pdu_req_t *req, int reqsz) {
	int lensz = 0;
	int status = 0;
	
	while (1) {
		int sz;
		
		if (lensz < sizeof(reqsz)) {
			sz = send(sockfd, (void*)(((char*)&reqsz) + lensz), sizeof(reqsz) - lensz, 0);
		} else {
			sz = send(sockfd, (void*)((char*)req + status), reqsz - status, 0);
		}
			
		if (sz == -1 && errno == EAGAIN) {
			continue;
		} else if (sz < 0) {
			vprintf("\33[1m[bs_api]\33[0m write error: %s.\n", strerror(errno));
			return STAT_ERROR;
		} else if (sz == 0) {
			if (status != reqsz) {
				vprintf("\33[1m[bs_api]\33[0m message size mismatch.\n");
			}
			return STAT_ERROR;
		} else {
			vprintf("\33[1m[bs_api]\33[0m sent tcp datagram with %d bytes.\n", sz);
			
			if (lensz < sizeof(reqsz)) {
				lensz += sz;
				continue;
			}
			
			status += sz;
			
			if (status == reqsz) {
				break;
			}
		}
	}
	
	vprintf("\33[1m[bs_api]\33[0m sent tcp request.\n");
	return STAT_OK;
}


static int bs_tcp_recv_response(int sockfd, bs_pdu_res_t **res, int *ressz) {
	int len = 0;
	int lensz = 0;
	*res = NULL;
	*ressz = 0;
	
	while (1) {
		int sz;
		
		if (lensz < sizeof(len)) {
			sz = recv(sockfd, (void*)(((char*)&len) + lensz), sizeof(len) - lensz, 0);
		} else {
			if (*res == NULL) {
				*res = (bs_pdu_res_t *) malloc(sizeof(char) * len);
			}
			sz = recv(sockfd, (void*)((char*)*res + *ressz), len - *ressz, 0);
		}
		
		if (sz == -1 && errno == EAGAIN) {
			continue;
		} else if (sz < 0) {
			vprintf("\33[1m[bs_api]\33[0m read error: %s.\n", strerror(errno));
			return STAT_ERROR;
		} else if (sz == 0) {
			return STAT_ERROR;
		} else {
			vprintf("\33[1m[bs_api]\33[0m received tcp datagram with %d bytes.\n", sz);
			
			if (lensz < sizeof(len)) {
				lensz += sz;
				continue;
			}
			
			*ressz += sz;
			
			if (*ressz == len) {
				break;
			}
		}
	}
	
	vprintf("\33[1m[bs_api]\33[0m received tcp request.\n");
	return STAT_OK;
}


/*
 * BS API implementation
 */


void bs_init(int v) {
	V_flag = v;
}


bs_call_status_t bs_give(char *address, int port, char *titles) {
	if (address == NULL || port < 0 || titles == NULL) {
		vprintf("[bs_give] malformed arguments.\n");
		return STAT_ERROR;
	}
	
	vprintf("[bs_give] Giving '%s' to the predefined counter: '%s:%d'.\n", titles, address, port);
	
	bs_pdu_req_t req;
	
	memset(&req, 0, sizeof(req));
	
	req.type = REQ_GIVE;
	strncpy(req.body.give.titles, titles, MAX_TITLES_SIZE - 1);
	
	struct sockaddr serv_addr;
	
	int sockfd = bs_udp_socket_init(address, port, &serv_addr);
	
	if (sockfd == -1) {
		return STAT_ERROR;
	}
	
	bs_udp_send_request(sockfd, &req, sizeof(req) - sizeof(req.body) + sizeof(req.body.give),
		&serv_addr);
	
	bs_udp_finish(sockfd);
	
	return STAT_OK;
}


bs_call_status_t bs_search(char *address, int port, char *username, char *password,
	char *titles, int weight, sys_list_t **response) {
	if (address == NULL || port < 0 || username == NULL || password == NULL
		|| titles == NULL) {
		vprintf("[bs_search] malformed arguments.\n");
		return STAT_ERROR;
	}
	
	*response = NULL;
	
	vprintf("[bs_search] Searching '%s' on the predefined counter: '%s:%d'.\n", titles, address,
		port);
	
	bs_pdu_req_t req;
	
	memset(&req, 0, sizeof(req));
	
	req.type = REQ_SEARCH;
	strncpy(req.body.search.username, username, MAX_USERNAME_SIZE - 1);
	strncpy(req.body.search.password, password, MAX_PASSWORD_SIZE - 1);
	strncpy(req.body.search.titles, titles, MAX_TITLES_SIZE - 1);
	req.body.search.weight = weight;
	
	struct sockaddr serv_addr;
	
	int sockfd = bs_udp_socket_init(address, port, &serv_addr);
	
	if (sockfd == -1) {
		return STAT_ERROR;
	}
	
	bs_udp_send_request(sockfd, &req, sizeof(req) + sizeof(req.body.search), &serv_addr);
	
	*response = sys_lst_new();
	
	int nr_titles = 1;
	for (int i = 0; i < strlen(titles); i++) {
		if (titles[i] == ' ') {
			nr_titles++;
		}
	}
	
	for (int i = 0; i < nr_titles; i++) {
		bs_pdu_res_t res;
		int ressz;
		
		bs_udp_recv_response(sockfd, &res, &ressz);
		
		sys_lst_iitem_t *item = (sys_lst_iitem_t *) malloc(sizeof(sys_lst_iitem_t));
		
		item->status = (res.status == RES_OK) ? OK : ERROR;
		item->type = SEARCH;
		strncpy(item->system_response.search.title, res.body.search.title, MAX_BOOKNAME_SIZE);
		strncpy(item->system_response.search.counter, res.body.search.counter, MAX_COUNTER_SIZE);
		item->system_response.search.weight = res.body.search.weight;
		strncpy(item->system_response.search.neighbours, res.body.search.neighbours,
			MAX_NEIGHBOURS_SIZE);
		strncpy(item->system_response.search.reason, res.body.search.reason, MAX_REASON_SIZE);
		
		sys_lst_insert(*response, item);
	}
	
	bs_udp_finish(sockfd);
	
	return STAT_OK;
}


bs_call_status_t bs_cancel(char *address, int port, char *username, char *titles) {
	if (address == NULL || port < 0 || username == NULL || titles == NULL) {
		vprintf("[bs_cancel] malformed arguments.\n");
		return STAT_ERROR;
	}
	
	vprintf("[bs_cancel] Canceling '%s' on: '%s:%d'.\n", titles, address, port);
	
	bs_pdu_req_t req;
	
	memset(&req, 0, sizeof(req));
	
	//format request
	req.type = REQ_CANCEL;
	strncpy(req.body.cancel.username, username, MAX_USERNAME_SIZE - 1);
	strncpy(req.body.cancel.titles, titles, MAX_TITLES_SIZE - 1);
	
	struct sockaddr serv_addr;
	
	int sockfd = bs_udp_socket_init(address, port, &serv_addr);
	
	if (sockfd == -1) {
		return STAT_ERROR;
	}
	
	bs_udp_send_request(sockfd, &req, sizeof(req) + sizeof(req.body.cancel), &serv_addr);
	
	bs_udp_finish(sockfd);
	
	return STAT_OK;
}


bs_call_status_t bs_take(char *address, int port, char *username, char *titlecs,
	sys_list_t **response) {
	if (address == NULL || port < 0 || username == NULL || titlecs == NULL) {
		vprintf("[bs_take] malformed arguments.\n");
		return STAT_ERROR;
	}
	
	*response = NULL;
	
	vprintf("[bs_take] Taking '%s' from the predefined counter: '%s:%d'.\n", titlecs, address,
		port);
	
	bs_pdu_req_t req;
	
	memset(&req, 0, sizeof(req));
	
	//format request
	req.type = REQ_TAKE;
	strncpy(req.body.take.username, username, MAX_USERNAME_SIZE - 1);
	strncpy(req.body.take.titlecs, titlecs, MAX_TITLES_SIZE - 1);
	
	int sockfd = bs_tcp_socket_init(address, port);
	
	if (sockfd == -1) {
		return STAT_ERROR;
	}
	
	bs_tcp_send_request(sockfd, &req, sizeof(req) - sizeof(req.body) + sizeof(req.body.take));
	
	*response = sys_lst_new();
		
	int nr_titlecs = 1;
	for (int i = 0; i < strlen(titlecs); i++) {
		if (titlecs[i] == ' ') {
			nr_titlecs++;
		}
	}
	
	for (int i = 0; i < nr_titlecs; i++) {
		bs_pdu_res_t *res;
		int ressz;
		
		if (bs_tcp_recv_response(sockfd, &res, &ressz) != STAT_OK) {
			bs_tcp_finish(sockfd);
			return STAT_ERROR;
		}
		
		sys_lst_iitem_t *item = (sys_lst_iitem_t *) malloc(sizeof(sys_lst_iitem_t));
		
		item->status = (res->status == RES_OK) ? OK : ERROR;
		item->type = TAKE;
		strncpy(item->system_response.take.title, res->body.take.title, MAX_BOOKNAME_SIZE);
		strncpy(item->system_response.take.counter, res->body.take.counter, MAX_COUNTER_SIZE);
		strncpy(item->system_response.take.reason, res->body.take.reason, MAX_REASON_SIZE);
		item->system_response.take.size = res->body.take.size;
		if (res->body.take.size > 0) {
			item->system_response.take.cover = (char*) malloc(sizeof(char) * res->body.take.size);
			memcpy(item->system_response.take.cover, &(res->body.take.cover), res->body.take.size);
		}
		
		sys_lst_insert(*response, item);
		
		free(res);
	}
	
	bs_tcp_finish(sockfd);
		
	return STAT_OK;
}


bs_call_status_t bs_release(char *address, int port, char *username, char *titles,
	int makeavailable, sys_list_t **response) {
	if (address == NULL || port < 0 || username == NULL || titles == NULL) {
		vprintf("[bs_release] malformed arguments.\n");
		return STAT_ERROR;
	}
	
	*response = NULL;
	
	vprintf("[bs_release] Releasing '%s' to the counter: '%s:%d'.\n", titles, address,
		port);
	
	bs_pdu_req_t req;
	
	memset(&req, 0, sizeof(req));
	
	//format request
	req.type = REQ_RELEASE;
	strncpy(req.body.release.username, username, MAX_USERNAME_SIZE - 1);
	strncpy(req.body.release.titles, titles, MAX_TITLES_SIZE - 1);
	req.body.release.makeavailable = makeavailable;
	
	int sockfd = bs_tcp_socket_init(address, port);
	
	if (sockfd == -1) {
		return STAT_ERROR;
	}
	
	if (bs_tcp_send_request(sockfd, &req, sizeof(req) - sizeof(req.body)
			+ sizeof(req.body.release)) != STAT_OK) {
		return STAT_ERROR;
	}
	
	*response = sys_lst_new();
	
	int nr_titles = 1;
	for (int i = 0; i < strlen(titles); i++) {
		if (titles[i] == ' ') {
			nr_titles++;
		}
	}
	
	for (int i = 0; i < nr_titles; i++) {
		bs_pdu_res_t *res;
		int ressz;
		
		if (bs_tcp_recv_response(sockfd, &res, &ressz) != STAT_OK) {
			bs_tcp_finish(sockfd);
			return STAT_ERROR;
		}
		
		sys_lst_iitem_t *item = (sys_lst_iitem_t *) malloc(sizeof(sys_lst_iitem_t));
		
		item->status = (res->status == RES_OK) ? OK : ERROR;
		item->type = RELEASE;
		strncpy(item->system_response.release.title, res->body.release.title, MAX_BOOKNAME_SIZE);
		strncpy(item->system_response.release.counter, res->body.release.counter, MAX_COUNTER_SIZE);
		strncpy(item->system_response.release.reason, res->body.release.reason, MAX_REASON_SIZE);
		
		sys_lst_insert(*response, item);
		
		free(res);
	}
	
	bs_tcp_finish(sockfd);
		
	return STAT_OK;
}


bs_call_status_t bs_return(char *address, int port, char *titles, sys_list_t **response) {
	if (address == NULL || port < 0 || titles == NULL) {
		vprintf("[bs_return] malformed arguments.\n");
		return STAT_ERROR;
	}

	*response = NULL;
	
	vprintf("[bs_return] Releasing '%s' to the predefined counter: '%s:%d'.\n", titles, address,
		port);
	
	bs_pdu_req_t req;
	
	memset(&req, 0, sizeof(req));
	
	//format request
	req.type = REQ_RETURN;
	strncpy(req.body.returns.titles, titles, MAX_TITLES_SIZE - 1);
	
	int sockfd = bs_tcp_socket_init(address, port);
	
	if (sockfd == -1) {
		return STAT_ERROR;
	}
	
	if (bs_tcp_send_request(sockfd, &req, sizeof(req) - sizeof(req.body)
			+ sizeof(req.body.returns)) != STAT_OK) {
		return STAT_ERROR;
	}
	
	*response = sys_lst_new();
	
	bs_pdu_res_t *res;
	int ressz;
		
	if (bs_tcp_recv_response(sockfd, &res, &ressz) != STAT_OK) {
		bs_tcp_finish(sockfd);
		return STAT_ERROR;
	}
		
	sys_lst_iitem_t *item = (sys_lst_iitem_t *) malloc(sizeof(sys_lst_iitem_t));
		
	item->status = (res->status == RES_OK) ? OK : ERROR;
	item->type = RETURN;
		
	sys_lst_insert(*response, item);
		
	free(res);
	
	bs_tcp_finish(sockfd);
		
	return STAT_OK;
}
