/* genblog.c - run like:
 * 	genblog atom dirname > feed.xml
 *	genblog index dirname > index.html
 *      genblog lint dirname
 *
 * The files in the blog directory are all named like yyyy-mm-dd-*, which encode
 * their initial publication dates. Their content is also scanned for lines
 * which contain special tags, which are extracted to supply more metadata about
 * the posts:
 *
 *   $#t <title string>
 *   $#o <comma-separated list>
 *   $#s <summary string>
 *   $#u <unique id string>
 *
 * The atom feed generator also computes ids for the posts, which are the md5sum
 * of the post encoded as a uuid.
 */

#include <assert.h>
#include <ctype.h>
#include <dirent.h>
#include <err.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>

/* Change these if you aren't me. BLOG_UUID can be random if you want. */
const char BLOG_ATOMURL[] = "https://elly.town/feed.xml";
const char BLOG_AUTHOR[] = "elly";
const char BLOG_CSS[] = "/c/site.css";
const char BLOG_DESC[] = "Index page of my blog";
const char BLOG_ROOT[] = "https://elly.town/d/blog/";
const char BLOG_TITLE[] = "~elly/blog";
const char BLOG_UUID[] = "58e1c539-4056-4502-9445-29d725e4df97";

enum {
	NAMELEN = 64,
	TITLELEN = 64,
	SUMLEN = 256,
	TOPICLEN = 32,
	TOPICNUM = 8,
	IDLEN = 32 + 4 + 1,
};

typedef struct Post Post;
typedef struct Posts Posts;

struct Post {
	unsigned int year;
	unsigned int month;
	unsigned int day;
	int ishtml;

	char name[NAMELEN];
	char title[TITLELEN];
	char summary[SUMLEN];
	char topics[TOPICNUM][TOPICLEN];
	char id[IDLEN];
};

struct Posts {
	size_t n;
	Post *ps;
};

int scandate(const char *name, Post *p) {
	int y, m, d;
	if (sscanf(name, "%4d-%2d-%2d-", &y, &m, &d) != 3)
		return 1;
	if (p) {
		p->year = y;
		p->month = m;
		p->day = d;
	}
	return 0;
}

size_t countposts(DIR *d) {
	size_t n = 0;
	struct dirent *de;
	for (de = readdir(d); de; de = readdir(d))
		if (!scandate(de->d_name, NULL))
			n++;
	rewinddir(d);
	return n;
}

char *findtag(char *buf, const char *t) {
	char *e = strstr(buf, t);
	if (e)
		return e + strlen(t);
	return NULL;
}

void scantag(char *buf, Post *p) {
	char *m;
	char *t;
	size_t ti = 0;

	if ((m = findtag(buf, "$#t ")))
		strlcpy(p->title, m, sizeof p->title);
	if ((m = findtag(buf, "$#s "))) {
		if (p->summary[0]) {
			strlcat(p->summary, " ", sizeof p->summary);
			strlcat(p->summary, m, sizeof p->summary);
		} else {
			strlcpy(p->summary, m, sizeof p->summary);
		}
	}
	if ((m = findtag(buf, "$#o "))) {
		for (t = strtok(m, ", "); t; t = strtok(NULL, ", ")) {
			if (ti >= TOPICNUM)
				errx(3, "too many topics in '%s'", p->name);
			strlcpy(p->topics[ti++], t, sizeof p->topics[0]);
		}
		qsort(p->topics, ti, sizeof p->topics[0],
		      (int (*)(const void*, const void *))strcmp);
	}
	if ((m = findtag(buf, "$#u "))) {
		strlcpy(p->id, m, sizeof p->id);
	}
}

void loadpost(const char *dir, const char *name, Post *p) {
	char pathbuf[_POSIX_PATH_MAX];
	char linebuf[512];
	FILE *f;

	memset(p, 0, sizeof *p);

	scandate(name, p);

	p->ishtml = strstr(name, ".html") != NULL;

	strlcpy(p->name, name, sizeof p->name - 1);

	memset(pathbuf, 0, sizeof pathbuf);
	snprintf(pathbuf, sizeof(pathbuf), "%s/%s", dir, name);

	f = fopen(pathbuf, "rb");
	if (!f)
		err(2, "fopen(%s)", pathbuf);
	while (fgets(linebuf, sizeof(linebuf), f)) {
		char *e = strchr(linebuf, '\n');
		if (e)
			*e = '\0';
		scantag(linebuf, p);
	}

	fclose(f);
}

int postcmp(const void *_pa, const void *_pb) {
	const Post *pa = _pa;
	const Post *pb = _pb;

	/* Note backward comparison to make the posts sort newest-first */
	return strcmp(pb->name, pa->name);
}

void load(const char *dir, Posts *posts) {
	DIR *d = NULL;
	struct dirent *de = NULL;
	size_t i = 0;

	d = opendir(dir);
	if (!d)
		err(1, "opendir(%s)", dir);

	posts->n = countposts(d);
	posts->ps = calloc(posts->n, sizeof *posts->ps);
	if (!posts->ps)
		err(1, "calloc()");

	for (de = readdir(d); de; de = readdir(d)) {
		if (!scandate(de->d_name, NULL)) {
			assert(i < posts->n);
			loadpost(dir, de->d_name, &posts->ps[i++]);
		}
	}
	closedir(d);

	qsort(posts->ps, posts->n, sizeof *posts->ps, postcmp);
}

void genatompost(Post *p) {
	const char *srctype = p->ishtml ? "text/html" : "text/plain";
	char urlbuf[_POSIX_PATH_MAX];
	snprintf(urlbuf, sizeof urlbuf, "%s%s", BLOG_ROOT, p->name);

	printf("  <entry>\n");
	printf("    <content src=\"%s\" type=\"%s\"/>\n", urlbuf, srctype);
	printf("    <id>urn:uuid:%s</id>\n", p->id);
	printf("    <link href=\"%s\"/>\n", urlbuf);
	if (p->summary[0])
		printf("    <summary>%s</summary>\n", p->summary);
	printf("    <title>%s</title>\n", p->title);
	printf("    <updated>%04d-%02d-%02dT00:00:00Z</updated>\n",
	       p->year, p->month, p->day);
	printf("  </entry>\n");
}

void genatom(Posts *ps) {
	size_t i;

	printf("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
	printf("<feed xmlns=\"http://www.w3.org/2005/Atom\">\n");

	printf("  <title>%s</title>\n", BLOG_TITLE);
	printf("  <link href=\"%s\"/>\n", BLOG_ROOT);
	printf("  <link rel=\"self\" href=\"%s\"/>\n", BLOG_ATOMURL);
	printf("  <updated>%04d-%02d-%02dT00:00:00Z</updated>\n",
	       ps->ps[0].year, ps->ps[0].month, ps->ps[0].day);
	printf("  <author><name>%s</name></author>\n", BLOG_AUTHOR);
	printf("  <id>urn:uuid:%s</id>\n", BLOG_UUID);

	for (i = 0; i < ps->n; i++)
		genatompost(&ps->ps[i]);
	
	printf("</feed>\n");
}

void genindexpost(Post *p) {
	size_t i;

	printf("      <li>\n");
	printf("        %04d-%02d-%02d:\n", p->year, p->month, p->day);
	printf("        <a href=\"%s\">\n", p->name);
	printf("        %s</a> (", p->title);

	for (i = 0; p->topics[i][0]; i++) {
		if (i > 0)
			printf(", ");
		printf("%s", p->topics[i]);
	}

	printf(")\n");
	printf("      </li>\n");
}

void genindex(Posts *ps) {
	size_t i;

	printf("<!DOCTYPE html>\n");
	printf("<html lang=\"en\">\n");
	printf("  <head>\n");
	printf("    <title>%s</title>\n", BLOG_TITLE);
	printf("    <link rel=\"stylesheet\" href=\"%s\"></link>\n", BLOG_CSS);
	printf("    <link rel=\"alternate\" type=\"application/atom+xml\"\n");
	printf("          href=\"%s\"></link>\n", BLOG_ATOMURL);
	printf("    <meta name=\"description\" content=\"%s\">\n", BLOG_DESC);
	printf("    <meta name=\"viewport\" "
	                  "content=\"width=device-width, initial-scale=1\">\n");
	printf("  </head>\n");
	printf("  <body>\n");
	printf("    <h1>blog</h1>\n");
	printf("    <ul>\n");

	for (i = 0; i < ps->n; i++)
		genindexpost(&ps->ps[i]);

	printf("    </ul>\n");
	printf("  </body>\n");
	printf("</html>\n");
}

int lintpost(Post *p) {
	int bad = 0;

	if (!p->title[0]) {
		fprintf(stderr, "%s: no title\n", p->name);
		bad = 1;
	}

	if (!p->id[0]) {
		fprintf(stderr, "%s: no id\n", p->name);
		bad = 1;
	}

	if (!p->summary[0]) {
		fprintf(stderr, "%s: no summary\n", p->name);
		bad = 1;
	}

	if (!p->topics[0][0]) {
		fprintf(stderr, "%s: no topics\n", p->name);
		bad = 1;
	}

	/* TODO: check that C sources compile? */
	/* TODO: validate html?? */
	return bad;
}

void lint(Posts *ps) {
	size_t i;
	int bad = 0;

	for (i = 0; i < ps->n; i++) {
		Post *p = &ps->ps[i];
		bad |= lintpost(p);
	}

	exit(bad);
}

void usage(const char *progn) {
	fprintf(stderr, "Usage: %s <atom|index|lint> <dir>\n", progn);
	exit(1);
}

int main(int argc, char *argv[]) {
	Posts ps;

	if (argc != 3)
		usage(argv[0]);
	load(argv[2], &ps);

#if 0
	for (i = 0; i < ps.n; i++) {
		Post *p = &ps.ps[i];
		printf("%s\n", p->name);
		if (p->title[0])
			printf("  t: '%s'\n", p->title);
		if (p->summary[0])
			printf("  s: '%s'\n", p->summary);
		if (p->topics[0][0]) {
			printf("  o:");
			for (j = 0; p->topics[j][0]; j++)
				printf(" '%s'", p->topics[j]);
			printf("\n");
		}
		printf("  h: %s\n", p->hash);
	}
#endif

	if (!strcmp(argv[1], "atom"))
		genatom(&ps);
	else if (!strcmp(argv[1], "index"))
		genindex(&ps);
	else if (!strcmp(argv[1], "lint"))
		lint(&ps);
	else
		usage(argv[0]);

	return 0;
}
