/* gophl - line-mode gopher client
 * See LICENSE */
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <err.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <libgen.h>

#define MAXLABEL 128
#define MAXPATH 128
#define MAXHOST 128
#define MAXLINKS 1024
#define MAXHIST 1024

struct link {
	char type;
	char label[MAXLABEL];
	char path[MAXPATH];
	char host[MAXHOST];
	char port[6];
};

struct link hist[MAXHIST]; /* history, including current page */
size_t nhist;
struct link llinks[MAXLINKS]; /*loaded links */
size_t nlinks; /* number of loaded links */

void handlelink(struct link *l);
void loadmenu(int sockfd);
struct link *poph();
struct link *pushh(struct link *l);

int
main(int argc, char *argv[])
{
	char *buf, *path, *endptr;
	struct link tlink, *ptlink;
	long longn;
	size_t n;

	buf = NULL;
	nlinks = nhist = 0;
	while (getline(&buf, &n, stdin) > -1) {
		longn = strtol(buf, &endptr, 10);
		if (*endptr == '\n') {
			if (longn >= nlinks) {
				fprintf(stderr, "?\n");
			} else {
				printf("%s\n", llinks[longn].label);
				handlelink(&llinks[longn]);
			}
		} else switch (buf[0]) {
		case 't': /* type link */
			if (strlen(buf) == 2) {
				if ((ptlink = poph()) != NULL) {
					printf("%c %s %s %s %s\n", ptlink->type,
						ptlink->label,ptlink->path,
						ptlink->host,ptlink->port);
					pushh(ptlink);
				} else fprintf(stderr, "?\n");
			} else {
				longn = strtol(buf+1, NULL, 10);
				if (longn > nlinks-1 || nlinks == 0) {
					fprintf(stderr, "?\n");
				} else {
					printf("%c %s %s %s %s\n", llinks[longn].type,
						llinks[longn].label,llinks[longn].path,
						llinks[longn].host,llinks[longn].port);
				}
			}
			break;
		case 'r': /* reload */
			if ((ptlink = poph()) != NULL)
				handlelink(ptlink);
			else fprintf(stderr, "?\n");
			break;
		case 'b':
			poph();
			if ((ptlink = poph()) != NULL)
				handlelink(ptlink);
			else fprintf(stderr, "?\n");
			break;
		case 'f':
			if (nhist < MAXHIST-1) {
				nhist++;
				tlink = *poph();
				handlelink(&tlink);
			} else fprintf(stderr, "?\n");
			break;
		case 'q':
			goto MAINDONE;
			break;
		default:
			buf[strlen(buf)-1] = '\0';
			if ((path = strchr(buf, '/')) != NULL) {
				snprintf(tlink.path, MAXPATH, "%s", path);
				*path = '\0';
			} else {
				strcpy(tlink.path, "/");
			}
			tlink.type = buf[0];
			snprintf(tlink.host, MAXHOST, "%s", buf+1);
			strcpy(tlink.port, "70");
			handlelink(&tlink);
		}
	}

MAINDONE: /* <- to escape the while loop */
	free(buf);
	return 0;
}

struct link *
poph()
{
	if (nhist <= 0) {
		return NULL;
	} else {
		return &hist[nhist--];
	}
}
struct link *
pushh(struct link *l)
{
	if (nhist >= MAXHIST-1) {
		memmove(hist, hist+1,
			(MAXHIST-1)*sizeof(hist[0]));
		nhist--;
	}
	hist[++nhist] = *l;
	return &hist[nhist];
}

/* either spawns the user's pager and returns its fp, or returns stdout */
FILE *
pager(void)
{
	if (!isatty(STDOUT_FILENO))
		return stdout;
	char *pager = getenv("PAGER");
	if (!pager || *pager == '\0')
		return stdout;
	FILE *fp = popen(pager, "w");
	if (!fp)
		return stdout;
	setvbuf(fp, NULL, _IOLBF, 0);
	return fp;
}
/* creates a socket fd, and connects to the given host and port,
 * then returns the socket fd */
int
createandconnect(char host[MAXHOST], char port[6])
{
	int fd, s;
	struct addrinfo hints, *addrs;

	memset(&hints, 0, sizeof(hints));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	if ((s = getaddrinfo(host, port, &hints, &addrs)) != 0) {
		warnx("nothing done; invalid host: %s",
				gai_strerror(s));
		return -1;
	}
	/* just use the first and ignore the rest */
	if ((fd = socket(addrs->ai_family, addrs->ai_socktype,
				addrs->ai_protocol)) < 0) {
		freeaddrinfo(addrs);
		err(1, "failed to create socket");
	}

	if (connect(fd, addrs->ai_addr, addrs->ai_addrlen)) {
		warn("nothing done: could not connect");
		freeaddrinfo(addrs);
		close(fd);
		return -1;
	}
	freeaddrinfo(addrs);
	return fd;
}
/* either fetches a link itself, or passes the request onto loadmenu,
 * depending on the link type */
void
handlelink(struct link *l)
{
	int sockfd, savefd;
	char c, *buf;
	FILE *outfp;
	size_t n;

	switch (l->type) {
	case '0': /* text file */
		if ((sockfd = createandconnect(l->host, l->port)) < 0) return;
		outfp = pager();
		dprintf(sockfd, "%s\r\n", l->path);
		while (read(sockfd, &c, 1) > 0) {
			if (c != '\r') fputc(c, outfp);
		}
		if (outfp != stdout) pclose(outfp);
		break;
	case '1': /* menu */
	case '7': /* FTS */
	case '+': /* mirror server */
		pushh(l);
		if ((sockfd = createandconnect(l->host, l->port)) < 0) {
			poph();
			return;
		}
		if (l->type == '7') {
			buf = NULL;
			getline(&buf, &n, stdin);
			buf[strlen(buf)-1] = '\0';
			dprintf(sockfd, "%s\t%s\r\n", l->path, buf);
			free(buf);
		} else dprintf(sockfd, "%s\r\n", l->path);
		loadmenu(sockfd);
		break;
	case '2': /* CCSO */
		break;
	case '4': /* BinHex-encoded file */
	case '5': /* DOS file */
	case '6': /* uuencoded file */
	case '9': /* binary file */
	case 'g': /* GIF image */
	case 'I': /* image */
	default:
		printf("save to \"%s\"?\n", basename(l->path));
		buf = NULL;
		getline(&buf, &n, stdin);
		buf[strlen(buf)-1] = '\0';
#define flags O_WRONLY|O_CREAT|O_TRUNC
		if (strlen(buf) == 0) {
			savefd = open(basename(l->path), flags, 0644);
		} else savefd = open(buf, flags, 0644);
		buf = realloc(buf, 8192);
		if ((sockfd = createandconnect(l->host, l->port)) < 0) return;
		dprintf(sockfd, "%s\r\n", l->path);
		while ((n = read(sockfd, buf, 8192)) > 0) {
			write(savefd, buf, n);
			write(STDOUT_FILENO, ".", 1);
		}
		write(STDOUT_FILENO, "\n", 1);
		free(buf);
		break;
	case '8': /* telnet */
	case 'T': /* telnet 3270 */
		execlp("telnet", "telnet", l->host, l->port, NULL);
		break;
	case 'h': /* URI */
		break;
	}
}
/* reads from sockfd and parses it as a menu */
void
loadmenu(int sockfd)
{
	struct link clink; /* current link */
	int i; /* link index */
	FILE *sockfp, *outfp; /* socket file pointer, output fp */
	/* TCP buffer, slice of buf, character pointer */
	char *buf, *slice, *cptr;
	size_t n; /* allocated size of buf */
	
	outfp = pager();
	sockfp = fdopen(sockfd, "r");
	if (!sockfp) {
		close(sockfd);
		return;
	}
	buf = NULL;
	i = 0;
	
	while (getline(&buf, &n, sockfp) > -1) {
		if (strcmp(buf, ".\r\n") == 0) {
			fprintf(outfp, ".\n");
			break;
		}
		if (strlen(buf) < 3) goto INVALIDLINE;
		buf[strlen(buf)-2] = '\0'; /* Get rid of that pesky CRLF */
		clink.type = buf[0];
		slice = buf+1;
		cptr = strchr(slice, '\t');
		if (cptr == NULL) goto INVALIDLINE;
		*cptr = '\0';
		snprintf(clink.label, MAXLABEL, "%s", slice);
		slice = cptr+1;
		cptr = strchr(slice, '\t');
		if (cptr == NULL) goto INVALIDLINE;
		*cptr = '\0';
		snprintf(clink.path, MAXPATH, "%s", slice);
		slice = cptr+1;
		cptr = strchr(slice, '\t');
		if (cptr == NULL) goto INVALIDLINE;
		*cptr = '\0';
		snprintf(clink.host, MAXHOST, "%s", slice);
		snprintf(clink.port, 6, "%s", cptr+1);
		if (clink.type == 'i' || clink.type == '3' || i >= MAXLINKS) {
			fprintf(outfp, "%s\n", clink.label);
		} else {
			fprintf(outfp, "%d:%c %s\n", i, clink.type, clink.label);
			memcpy(&llinks[i++], &clink, sizeof(clink));
		}
INVALIDLINE:
	}
	if (outfp != stdout) pclose(outfp);
	if (i == MAXLINKS) {
		warnx("There are more links than %d (MAXLINKS)",
			MAXLINKS);
	}
	nlinks = i;
	fclose(sockfp);
	close(sockfd);
	free(buf);
}
