#include "compat.h"

#ifdef HAVE_SYS_SOCKET_H
#include <sys/socket.h>
#endif
#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif
#ifdef HAVE_SYS_STAT_H
#include <sys/stat.h>
#endif
#ifdef HAVE_STRING_H
#include <string.h>
#endif
#ifdef HAVE_STDLIB_H
#include <stdlib.h>
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif
#ifdef HAVE_STDIO_H
#include <stdio.h>
#endif
#ifdef HAVE_GETPEERNAME
#ifdef HAVE_SYS_SOCKET_H
#include <sys/socket.h>
#endif
#ifdef HAVE_NETINET_IN_H
#include <netinet/in.h>
#endif
#endif

#include "net.h"
#include "opennet.h"

#ifndef MSG_WAITALL
#define MSG_WAITALL 0
#endif

/*
 * Legacy stub, please ignore.
 */
int opennet_init(void) {
	return(0);
}

/*
	Replace 
		@@OSNM@@	OS Name (linux, freebsd, sunos, etc)
		@@OSVR@@	OS version (2.2.x, 4.2, 5.8, etc)
		@@OSVS@@	OS version (short) (2.2, 4.2, 5.8, etc)
		@@ARCH@@	Arch (i386, sparc64, sun4u, sun4m, etc)
*/
char *parse_url_subst(const char *src) {
	char *retval = NULL;

	if (!strstr(src, "@@")) {
		return(strdup(src));
	}

	return(retval);
}

/*
 * Parse a URL into its components
 */
static int parse_url(char *url, char **scheme, char **user, char **pass, char **host, int *port, char **path) {
	char *tmptr, *schemeptr, *pathptr, *userptr, *passptr, *hostptr, *portptr;

	if (!url) {
		return(-1);
	}

	tmptr = strstr(url, "://");
	if (!tmptr) {
		return(-1);
	}

	if (strlen(tmptr) <= 3) {
		return(-1);
	}

	*tmptr = '\0';
	schemeptr = url;

	tmptr += 3;

	pathptr = strchr(tmptr, '/');

	if (pathptr) {
		*pathptr = '\0';
		pathptr++;
	} else {
		pathptr = "";
	}

	hostptr = strchr(tmptr, '@');
	if (hostptr) {
		*hostptr = '\0';
		hostptr++;
		userptr = tmptr;
		passptr = strchr(userptr, ':');
		if (passptr) {
			*passptr = '\0';
			passptr++;
		}
	} else {
		userptr = NULL;
		passptr = NULL;
		hostptr = tmptr;
	}

	portptr = strchr(hostptr, ':');
	if (portptr) {
		*portptr = '\0';
		portptr++;
		*port = strtoul(portptr, NULL, 10);
	} else {
		if (strcasecmp(schemeptr, "http") == 0) {
			*port = 80;
		} else if (strcasecmp(schemeptr, "ftp") == 0) {
			*port = 21;
		} else {
			*port = 0;
		}
	}

	*scheme = schemeptr;
	*user = userptr;
	*pass = passptr;
	*host = hostptr;
	*path = pathptr;

	return(0);
}

static ssize_t read_net_internal(int fd, void *buf, size_t count, int socketStatus) {
	ssize_t retval = 0;
	ssize_t read_ret;
	int socket = 0;
#ifdef HAVE_GETPEERNAME
	int gpn_ret;
	struct sockaddr_in tmp;
	int tmplen;

	if (socketStatus < 0) {
		tmplen = sizeof(tmp);

		gpn_ret = getpeername(fd, (struct sockaddr *) &tmp, &tmplen);

		if (gpn_ret >= 0) {
			socket = 1;
		} else {
			socket = 0;
		}
	} else {
		socket = socketStatus;
	}
#else
	socket = socketStatus;
#endif

	while (count) {
		if (socket) {
			read_ret = recv(fd, buf, count, 0);
		} else {
			read_ret = read(fd, buf, count);
		}

		if (read_ret == 0) {
			break;
		}

		if (read_ret < 0) {
			retval = read_ret;
			break;
		}

		count -= read_ret;
		buf += read_ret;
		retval += read_ret;
	}

	return(retval);
}

static int open_net_http(char *user, char *pass, char *host, int port, char *path, int flags, off_t offset, off_t *length) {
	unsigned long long content_length = 0;
	ssize_t read_ret, sink_bytes = 0;
	ssize_t send_ret, recv_ret;
	char httpcmd[4096], charbuf, httpreply[1024], *http_response;
	char garbage[4096];
	int bytes_to_read;
	int fd;
	int snprintf_ret;
	int http_response_code = 0;
	int newlinecount = 0, linenum = 0, i = 0;

	fd = net_connect_tcp(host, port);

	if (fd < 0) {
		return(-1);
	}

	if (offset == 0) {
		snprintf_ret = snprintf(httpcmd, sizeof(httpcmd), "GET /%s HTTP/1.0\015\012Host: %s\015\012\015\012", path, host);
	} else {
		snprintf_ret = snprintf(httpcmd, sizeof(httpcmd), "GET /%s HTTP/1.0\015\012Host: %s\015\012Range: bytes=%llu-\015\012\015\012", path, host, (unsigned long long) offset);
	}

	if (snprintf_ret >= sizeof(httpcmd)) {
		return(-1);
	}

	send_ret = send(fd, httpcmd, snprintf_ret, 0);
	if (send_ret != snprintf_ret) {
		net_close(fd);
		return(-1);
	}

	while (1) {
		recv_ret = recv(fd, &charbuf, 1, MSG_WAITALL);

		if (charbuf == '\015' || charbuf == '\012') {
			if (i > 0) {
				httpreply[i] = '\0';
				if (linenum == 0) {

					if (strlen(httpreply) < 9) {
						net_close(fd);
						return(-1);
					}

					http_response = httpreply + 9;
					http_response_code = strtoul(http_response, NULL, 10);
					switch (http_response_code) {
						case 206:
							if (offset == 0) {
								CHECKPOINT;
								net_close(fd);
								return(-1);
							}
							break;
						case 200:
							if (offset != 0) {
								sink_bytes = offset;
							}
							break;
						case 301:
						case 302:
							break;
						case 404:
							CHECKPOINT;
						default:
							net_close(fd);
							return(-1);
					}
				} else {
					if (strncasecmp(httpreply, "Content-Length:", 15) == 0) {
						content_length = strtoull(httpreply + 16, NULL, 10);
						if (offset != 0 && http_response_code == 206) {
							content_length += (unsigned long long) offset;
						}
						if (length) {
							*length = content_length;
						}
					}
					if (http_response_code == 301 || http_response_code == 302) {
						if (strncasecmp(httpreply, "Location:", 9) == 0) {
							net_close(fd);
							return(open_net(httpreply + 10, flags, 0));
						}
					}
				}
			}
			newlinecount++;
			linenum++;
			i = 0;
		} else {
			newlinecount = 0;
			httpreply[i++] = charbuf;
		}

		if (newlinecount == 4) {
			break;
		}

		if (recv_ret <= 0) {
			net_close(fd);
			return(-1);
		}
	}

	if (sink_bytes) {
		while (sink_bytes) {
			bytes_to_read = sink_bytes;
			if (bytes_to_read > sizeof(garbage)) {
				bytes_to_read = sizeof(garbage);
			}

			read_ret = read_net_internal(fd, garbage, bytes_to_read, 1);
			if (read_ret <= 0) {
				SPOTVAR_I(read_ret);
				net_close(fd);
				return(-1);
			}

			sink_bytes -= read_ret;
		}
	}

	return(fd);
}

static int open_net_ftp(char *user, char *pass, char *host, int port, char *path, int flags, off_t offset, off_t *length) {
	return(-1);
}

static int open_net_internal(const char *pathname, int flags, mode_t mode, int *socket, off_t offset, off_t *length) {
	char *pathname_dup;
	char *scheme, *user, *pass, *host, *path;
	off_t lseek_ret;
	int port = 0;
	int parse_ret;
	int retval = -1;

	if (socket) {
		*socket = 0;
	}

	if (!pathname) {
		retval = open(pathname, flags, mode);
		if (offset && retval >= 0) {
			lseek_ret = lseek(retval, offset, SEEK_SET);
			if (lseek_ret != offset) {
				close(retval);
				return(-1);
			}
		}
		return(retval);
	}

	pathname_dup = strdup(pathname);

	if (!pathname_dup) {
		retval = open(pathname, flags, mode);
		if (offset && retval >= 0) {
			lseek_ret = lseek(retval, offset, SEEK_SET);
			if (lseek_ret != offset) {
				close(retval);
				return(-1);
			}
		}
		return(retval);
	}

	parse_ret = parse_url(pathname_dup, &scheme, &user, &pass, &host, &port, &path);

	if (parse_ret < 0 || port == 0 || !host || !scheme) {
		free(pathname_dup);
		retval = open(pathname, flags, mode);
		if (offset && retval >= 0) {
			lseek_ret = lseek(retval, offset, SEEK_SET);
			if (lseek_ret != offset) {
				close(retval);
				return(-1);
			}
		}
		return(retval);

	}

	if (length) {
		*length = -1;
	}

	if (strcasecmp(scheme, "http") == 0) {
		retval = open_net_http(user, pass, host, port, path, flags, offset, length);
	}

	if (strcasecmp(scheme, "ftp") == 0) {
		retval = open_net_ftp(user, pass, host, port, path, flags, offset, length);
	}

	free(pathname_dup);

	if (retval < 0) {
		retval = open(pathname, flags, mode);
		if (offset && retval >= 0) {
			lseek_ret = lseek(retval, offset, SEEK_SET);
			if (lseek_ret != offset) {
				close(retval);
				return(-1);
			}
		}
	} else {
		if (socket) {
			*socket = 1;
		}
	}

	return(retval);
}

NETFILE *fopen_net(const char *pathname, const char *mode) {
	NETFILE *ret;
	int is_sock;
	off_t length;

	ret = malloc(sizeof(*ret));

	if (!ret) {
		return(NULL);
	}

	ret->fd = open_net_internal(pathname, O_RDONLY, 0666, &is_sock, 0, &length);

	if (ret->fd < 0) {
		free(ret);
		return(NULL);
	}

	ret->free_buf = 1;
	ret->bufsize_s = ret->bufsize = 32768;
	ret->buf_s = ret->buf = malloc(ret->bufsize);
	ret->bufused = 0;
	ret->eof = 0;
	ret->pos = 0;
	ret->socket = is_sock;
	ret->length = length;
	ret->url = strdup(pathname);

	return(ret);
}

int feof_net(NETFILE *stream) {
	if (!stream) {
		return(-1);
	}

	return(stream->eof);
}

size_t fread_net(void *ptr, size_t size, size_t nmemb, NETFILE *stream) {
	ssize_t recv_ret;
	size_t retval;
	int bytes_to_copy;

	if (!stream) {
		return(0);
	}

	if (stream->fd >= 0) {
		while (stream->bufused < (size * nmemb)) {
			if (stream->socket) {
				recv_ret = recv(stream->fd, stream->buf + stream->bufused, stream->bufsize - stream->bufused, MSG_WAITALL);
			} else {
				recv_ret = read(stream->fd, stream->buf + stream->bufused, stream->bufsize - stream->bufused);
			}

			if (recv_ret <= 0) {
				net_close(stream->fd);
				stream->fd = -1;
				break;
			}

			stream->bufused += recv_ret;

			if (stream->bufused >= (size * nmemb)) {
				break;
			}

			/*
			 * If there is more data to be read, but the buffer is full
			 * try to make room, or quit trying to read data
			 */
			if (stream->bufused == stream->bufsize) {
				if (stream->bufsize_s != stream->bufsize) {
					CHECKPOINT;
					memmove(stream->buf_s, stream->buf, stream->bufused);
					stream->buf = stream->buf_s;
					stream->bufsize = stream->bufsize_s;
				} else {
					break;
				}
			}
		}
	}

	if (stream->bufused == 0) {
		if (stream->fd < 0) {
			stream->eof = 1;
		}
		return(0);
	}

	bytes_to_copy = (size * nmemb);

	if (stream->bufused < bytes_to_copy) {
		bytes_to_copy = stream->bufused;

		/* Adjust bytes_to_copy to be a multiple of size */
		bytes_to_copy /= size;
		bytes_to_copy *= size;
	}

	memcpy(ptr, stream->buf, bytes_to_copy);
	stream->buf += bytes_to_copy;
	stream->bufused -= bytes_to_copy;
	stream->bufsize -= bytes_to_copy;

	if (stream->bufused == 0) {
		stream->buf = stream->buf_s;
		stream->bufsize = stream->bufsize_s;
	}

	retval = bytes_to_copy / size;
	stream->pos += retval;

	return(retval);
}

char *fgets_net(char *s, int size, NETFILE *stream) {
	ssize_t recv_ret;
	int bytes_to_copy;
	char *stopptr;

	if (!stream) {
		return(NULL);
	}

	if (stream->fd >= 0) {
		while (stream->bufused < size) {
			if (stream->socket) {
				recv_ret = recv(stream->fd, stream->buf + stream->bufused, stream->bufsize - stream->bufused, MSG_WAITALL);
			} else {
				recv_ret = read(stream->fd, stream->buf + stream->bufused, stream->bufsize - stream->bufused);
			}

			if (recv_ret <= 0) {
				net_close(stream->fd);
				stream->fd = -1;
				break;
			}

			stream->bufused += recv_ret;

			if (memchr(stream->buf, '\n', stream->bufused)) {
				break;
			}
		}
	}

	if (stream->bufused == 0) {
		if (stream->fd < 0) {
			stream->eof = 1;
		}
		return(NULL);
	}

	stopptr = memchr(stream->buf, '\n', stream->bufused);

	if (!stopptr) {
		stopptr = stream->buf + stream->bufused;
	} else {
		stopptr++;
	}

	bytes_to_copy = stopptr - stream->buf;

	if (size < bytes_to_copy) {
		bytes_to_copy = size;
	}

	memcpy(s, stream->buf, bytes_to_copy);

	if (bytes_to_copy < size) {
		s[bytes_to_copy] = '\0';
	} else {
		s[size - 1] = '\0';
	}

	stream->buf += bytes_to_copy;
	stream->bufused -= bytes_to_copy;
	stream->bufsize -= bytes_to_copy;
	stream->pos += bytes_to_copy;

	if (stream->bufused == 0) {
		stream->buf = stream->buf_s;
		stream->bufsize = stream->bufsize_s;
	}

	return(s);
}

int fclose_net(NETFILE *stream) {
	int fd;
	int retval;
	int is_sock;

	if (!stream) {
		return(-1);
	}

	fd = stream->fd;
	is_sock = stream->socket;

	if (stream->buf_s && stream->free_buf) {
		free(stream->buf_s);
	}
	if (stream->url) {
		free(stream->url);
	}

	free(stream);

	if (is_sock) {
		retval = net_close(fd);
	} else {
		retval = close(fd);
	}

	return(retval);
}

int setvbuf_net(NETFILE *stream, char *buf, int mode, size_t size) {
	if (!stream) {
		return(-1);
	}

	if (size < stream->bufused) {
		return(-1);
	}

	if (stream->buf_s && stream->free_buf) {
		free(stream->buf_s);
	}

	memcpy(buf, stream->buf, stream->bufused);

	stream->buf = buf;
	stream->buf_s = buf;
	stream->bufsize = size;
	stream->bufsize_s = size;
	stream->free_buf = 0;

	return(0);
}

int open_net(const char *pathname, int flags, mode_t mode) {
	int tmp;

	return(open_net_internal(pathname, flags, mode, &tmp, 0, NULL));
}

ssize_t read_net(int fd, void *buf, size_t count) {
	return(read_net_internal(fd, buf, count, -1));
}

off_t lseek_net(int filedes, off_t offset, int whence) {
	return(-1);
}

int fseeko_net(NETFILE *stream, off_t offset, int whence) {
	static char garbage[4096];
	size_t fread_ret, bytes_to_read;
	off_t newoffset, lseek_ret;
	off_t newlength;
	int newfd;

	if (!stream) {
		return(-1);
	}
	if (stream->fd < 0) {
		return(-1);
	}

	switch (whence) {
		case SEEK_CUR:
			newoffset = stream->pos + offset;
			break;
		case SEEK_END:
			newoffset = stream->length + offset;
			break;
		case SEEK_SET:
			newoffset = offset;
			break;
		default:
			CHECKPOINT;
			return(-1);
			break;
	}

	if (newoffset < 0) {
		return(-1);
	}

	lseek_ret = lseek(stream->fd, newoffset, SEEK_SET);
	if (lseek_ret != ((off_t) -1)) {
		stream->pos = lseek_ret;
		stream->buf = stream->buf_s;
		stream->bufused = 0;
		stream->bufsize = stream->bufsize_s;

		return(0);
	}

	/*
	 * Read up to 4K of data to avoid opening a new connection
	 */
	if (newoffset >= stream->pos && (newoffset - stream->pos < sizeof(garbage))) {
		bytes_to_read = newoffset - stream->pos;
		fread_ret = fread_net(garbage, sizeof(garbage[0]), bytes_to_read, stream);

		if (fread_ret == bytes_to_read) {
			return(0);
		}
	}

	newfd = open_net_internal(stream->url, O_RDONLY, 0666, NULL, newoffset, &newlength);

	if (newfd < 0) {
		return(-1);
	}

	net_close(stream->fd);
	stream->fd = newfd;
	stream->pos = newoffset;
	stream->buf = stream->buf_s;
	stream->bufused = 0;
	stream->bufsize = stream->bufsize_s;
	stream->eof = 0;

	return(0);
}

int fseek_net(NETFILE *stream, long offset, int whence) {
	return(fseek_net(stream, offset, whence));
}

off_t ftello_net(NETFILE *stream) {
	if (!stream) {
		return(-1);
	}
	if (stream->fd < 0) {
		return(-1);
	}

	return(stream->pos);
}

long ftell_net(NETFILE *stream) {
	return(ftello_net(stream));
}

off_t flength_net(NETFILE *stream) {
	if (!stream) {
		return(-1);
	}
	if (stream->fd < 0) {
		return(-1);
	}

	return(stream->length);
}
