#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h> //memset
#include <unistd.h> //STDIN_FILENO

#include <atvsms/handymacros.h>
#define MIN(a,b) ((a)<(b)?(a):(b))
#define MAX(a,b) ((a)>(b)?(a):(b))
#define IS_MULTICAST(ip4) (((ip4) & 0xf0000000) == 0xe0000000)

#define TTL_DEFAULT 7     // Max router hops
#define GRN_DEFAULT 188   // Suitable for MPEG transport streams
#define CAP_DEFAULT 7*188 // Fits in an IP packet

struct Chunksender{
	char * buf;        // Use as ringbuffer
	size_t rpos, wpos; // Current positions of fill() and send()
	size_t capacity;   // Must be a multiplum of granularity
	size_t granularity;
	int    fd;         // the socket
	struct sockaddr_in addr;
	
	Chunksender(char **args);
	~Chunksender();
	void send();
	bool fill();             // false on EOF or error
	
	size_t ravail(){
		size_t avail = wpos;
		if(rpos > wpos) avail += capacity;
		avail -= rpos;
		return avail;
	}
	size_t wavail(){
		size_t avail = rpos;
		if(wpos > rpos) avail += capacity;
		avail -= wpos;
		return avail;
	}
	size_t granularityfloor(size_t whatever){
		return whatever - whatever % granularity;
	}
};

Chunksender::Chunksender(char *args[])
: rpos(0), wpos(0), capacity(CAP_DEFAULT), granularity(GRN_DEFAULT)
{
	int ttl    = TTL_DEFAULT;
	char *ip   = (char*)"unspecified";
	short port = 0;

	while(*(++args)){/* Assume argv is NULL terminated (as in C99) */
		char *arg = (*args) + 1;
		switch(arg[-1]){
		case 'h': puts(
			"playout - Send fixed-size chunks by UDP without network fragmentation\n"
			"\nUsage:\tplayout [options]\n\n"
			"h        Show this help text\n"
			"v        Show version\n"
			"aSTRING  Set destination ip address    (mandatory)\n"
			"pNUMBER  Set destination UDP port      (mandatory)\n"
			"nNUMBER  Set maximum router hops       (default " TOSTRING(TTL_DEFAULT) ")\n"
			"bNUMBER  Set total buffer size         (default " TOSTRING(CAP_DEFAULT) ")\n"
			"gNUMBER  Set fragmentation granularity (default " TOSTRING(GRN_DEFAULT) ")\n"
			);
			exit(EXIT_SUCCESS);
		case 'v': puts(__FILE__ ": compiled " __DATE__);
			exit(EXIT_SUCCESS);
		case 'a': ip         =        arg ; break;
		case 'p': port       =   atoi(arg); break;
		case 'n': ttl        =   atoi(arg); break;
		case 'b': capacity   =strtoll(arg, NULL, 0); break;
		case 'g': granularity=strtoll(arg, NULL, 0); break;
		default:
			fprintf(stderr, "%s: No such option.\n", arg);
		}
	}
	
	if(port == 0){
		fputs("UDP port 0 is officially reserved.\n", stderr);
		exit(EXIT_FAILURE);
	}
	if(capacity % granularity){
		fputs("Rounding buffer size down to nearest multiple of granularity.\n", stderr);
		capacity = granularityfloor(capacity);
	}
	
	/* Make actual buffer size one granularity extra to avoid situation wpos==rpos
	 */
	capacity += granularity;
	
	buf = new char[capacity];

	fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if(fd == -1){
		perror("Failed to init socket");
		exit(EXIT_FAILURE);
	}
	memset(&addr, '\0', sizeof(addr));
	addr.sin_port = htons(port);
	addr.sin_family = AF_INET;
	if(1 != inet_pton(AF_INET, ip, &(addr.sin_addr))){
		fputs("Bad ip4 address.\n", stderr);
		exit(EXIT_FAILURE);
	}
	int ttlOption = IS_MULTICAST(addr.sin_addr.s_addr) ? IP_MULTICAST_TTL : IP_TTL;
	if(setsockopt(fd, IPPROTO_IP, ttlOption, &ttl, sizeof(ttl)) == -1){
		perror("Failed to set max router hops");
		exit(EXIT_FAILURE);
	}
}

Chunksender::~Chunksender(){
	delete[] buf;
}

/* Send the continuous buffer region starting at rpos, limited by granularity.
 * 
 * Limit the amount sent by the end of the buffer (because sendto() does not
 * understand that our ringbuffer is cyclic), then limit by granularity. This
 * necessitates that the buffer capacity is an exact multiple of granularity.
 * 
 * Other than not fragmenting below granularity, we want to aggressively empty
 * the buffer, in order to not keep old data very long.
 */
void Chunksender::send(){
	for(;;){
		size_t wpos_overflow = (wpos >= rpos) ? wpos : wpos + capacity;
		size_t amount = granularityfloor(MIN(wpos_overflow, capacity) - rpos);
		if(amount == 0) break;
		
		ssize_t bytes = sendto(fd, buf+rpos, amount, 0, (struct sockaddr *) &addr, sizeof(addr));
		if(bytes == -1){
			perror("sendto");
			exit(EXIT_FAILURE);
		}
		rpos += bytes;
		rpos %= capacity;
	}
}

/* Take what we get, don't care whether send has enough.
 */
bool Chunksender::fill(){
	size_t rpos_overflow = (rpos > wpos) ? rpos : rpos + capacity;
	size_t stop = rpos_overflow - 1; // ensure wpos != rpos
	size_t amount = MIN(stop, capacity) - wpos;
	
	ssize_t bytes = read(STDIN_FILENO, buf+wpos, amount);
	wpos += bytes;
	wpos %= capacity;
	return (bytes > 0);
}

int main(int argc, char* argv[])
{
	Chunksender cs(argv);
	while(cs.fill()) cs.send();
	return EXIT_SUCCESS;
}

