#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <libpq-fe.h>

#include "reprojection.h"

char *mprintf(__const char *__restrict fmt, ...) {
	va_list ap;
	char *ptr;
	int r;

	va_start(ap, fmt);
	r = vasprintf(&ptr, fmt, ap);
	va_end(ap);

	return ptr;
}

#if 0
static int create_dirs(const char *filename, const mode_t mode)
{
	struct stat st;
	char dir[strlen(filename) + 1];
	const char *prev, *next;
	int err;

	err = stat(filename, &st);
	if (!err && S_ISREG(st.st_mode))
		return 0;

	strcpy(dir, "/");

	for (prev = filename; (next = strchr(prev + 1, '/')); prev = next)
		if (next > prev + 1) {
			strncat(dir, prev + 1, next - prev);

			if (mkdir(dir, mode) && errno != EEXIST)
				return -1;
		}

	return 0;
}
#else
int create_dirs(const char *path, const mode_t mode) {
	struct stat s;
	char tmp[strlen(path) + 1];
	char *p;

	strcpy(tmp, path);

	/* Look for parent directory */
	p = strrchr(tmp, '/');
	if (!p)
		return 0;

	*p = '\0';

	if (!stat(tmp, &s))
		return !S_ISDIR(s.st_mode);
	*p = '/';
	/* Walk up the path making sure each element is a directory */
	p = tmp;
	if (!*p)
        	return 0;
	p++; /* Ignore leading / */
	while (*p) {
		if (*p == '/') {
			*p = '\0';
			if (!stat(tmp, &s)) {
				if (!S_ISDIR(s.st_mode)) {
					fprintf(stderr, "Error, is not a "
							"directory: %s\n", tmp);
					return 1;
				}
			} else if (mkdir(tmp, 0777)) {
				perror(tmp);
				return 1;
			}
			*p = '/';
		}
		p++;
	}
	return 0;
}
#endif

typedef double coord_t;
typedef coord_t pt_t[2];
typedef pt_t bbox_t[2];

struct linestring_s {
	int nodes;
	pt_t *pts;
};

static const char *linestring_parse(const char *wkt, struct linestring_s *ls) {
	const char *c;
	int len;

	ls->nodes = 1;
	for (c = wkt; *c && *c != ')'; c ++)
		ls->nodes += (*c == ',');

	ls->pts = malloc(ls->nodes * sizeof(*ls->pts));

	ls->nodes = 0;
	while (1) {
		if (sscanf(wkt, "%lf %lf%n",
					&ls->pts[ls->nodes][0],
					&ls->pts[ls->nodes][1], &len) < 2) {
			fprintf(stderr, "Couldn't parse Linestring %s\n", wkt);
			exit(-1);
		}
		wkt += len;
		ls->nodes ++;

		if (*wkt == ')')
			break;
		if (*wkt ++ != ',') {
			fprintf(stderr, "Couldn't parse Linestring %s\n", wkt);
			exit(-1);
		}
	}

	return wkt + 1;
}

static void linestring_free(struct linestring_s *ls) {
	free(ls->pts);
}

/* TODO: parametrise */
static void linestring_simplify(struct linestring_s *ls, bbox_t box) {
#if 0
	int i, j, delete, outside, inside, p, r;

	delete = ls->nodes - 96;

	inside = 0;
	for (i = 0; i < ls->nodes; i ++)
		inside += (ls->pts[i][0] >= box[0][0] &&
				ls->pts[i][1] >= box[0][1] &&
				ls->pts[i][0] < box[1][0] &&
				ls->pts[i][1] < box[1][1]);
	outside = ls->nodes - inside;

	if (outside > delete + 50) {
		r = delete;
		p = 0;
	} else if (outside > delete) {
		r = delete * 3 / 4;
		p = delete / 4;
	} else {
		r = delete * outside / ls->nodes;
		p = delete * inside / ls->nodes;
	}

	for (i = j = 1; j < ls->nodes; i ++) {
		delete = 0;
		if (ls->pts[i][0] >= box[0][0] &&
				ls->pts[i][1] >= box[0][1] &&
				ls->pts[i][0] < box[1][0] &&
				ls->pts[i][1] < box[1][1]) {
			if (p)
				if (p >= inside || ((inside + 3) %
							(inside / p) == 0)) {
					delete = 1;
					p --;
				}
			inside --;
		} else {
			if (r)
				if (r >= outside || ((outside + 3) %
							(outside / r) == 0)) {
					delete = 1;
					r --;
				}
			outside --;
		}

		if (delete)
			ls->nodes --;
		else {
			if (i != j) {
				ls->pts[j][0] = ls->pts[i][0];
				ls->pts[j][1] = ls->pts[i][1];
			}
			j ++;
		}
	}
#endif
	int i, j, inside, delete, nodes;

	nodes = ls->nodes;
	delete = ls->nodes - 150;
	if (delete < 5)
		delete = 5;

	for (i = j = 1; j < ls->nodes; i ++) {
		inside = (ls->pts[i][0] >= box[0][0] &&
			ls->pts[i][1] >= box[0][1] &&
			ls->pts[i][0] < box[1][0] &&
			ls->pts[i][1] < box[1][1]) ? 2 : 1;

		if ((random() % (nodes * inside)) < delete)
			ls->nodes --;
		else {
			if (i != j) {
				ls->pts[j][0] = ls->pts[i][0];
				ls->pts[j][1] = ls->pts[i][1];
			}
			j ++;
		}
	}
}

static int bbox_contains(bbox_t outer, bbox_t inner) {
	return inner[0][0] >= outer[0][0] &&
		inner[0][1] >= outer[0][1] &&
		inner[1][0] < outer[1][0] &&
		inner[1][1] < outer[1][1];
}

int srid = 900913;
bbox_t world = { { -20037508.34, -20037508.34 }, { 20037508.34, 20037508.34 } };
/* For the moment remove the US from tile generation because PostGIS returns
 * too many TopologyError exceptions for that area.  */
bbox_t banned_bbox = { { -13991033.65, 2661231.57 }, { -7758664.11, 6276397.26 } };

/* Area covered by a single tile in Spherical Mercator */
#define Z18_AREA "93481.908689808115"
#define Z17_AREA "373927.63475923246"
#define Z16_AREA "1495710.5390369298"
#define Z15_AREA "5982842.1561477194"
#define Z14_AREA "23931368.624590877"
#define Z13_AREA "95725474.49836351"
#define Z12_AREA "382901897.99345404"
#define Z11_AREA "1531607591.9738162"
#define Z10_AREA "6126430367.8952646"
#define Z9_AREA "24505721471.581059"
#define Z8_AREA "98022885886.324234"
#define Z7_AREA "392091543545.29694"
#define Z6_AREA "1568366174181.1877"
#define Z5_AREA "6273464696724.751"

#define GENERIC_CONDITION	\
	"way && bb and " \
	"ST_Intersects(way, bb) and not " \
	"ST_Contains(way, bb)"

const char *generic_condition = GENERIC_CONDITION;
const char *z_condition[] = {
#include "wikistyle.c"
};

#define MAXZOOM (sizeof(z_condition) / sizeof(*z_condition))

static PGconn *conn;
void db_init(void) {
	conn = PQconnectdb("dbname='gis' port='5432'");

	if (PQstatus(conn) != CONNECTION_OK) {
		fprintf(stderr, "Connection to database "
				"failed: %s\n", PQerrorMessage(conn));
		exit(-1);
	}
}

void db_done(void) {
	PQfinish(conn);
}

static int tilefd;
static char *tile_path;
void tile_init(bbox_t bbox, int z) {
	int x = (bbox[0][0] - world[0][0]) /
		(world[1][0] - world[0][0]) * (1 << z) + 0.5;
	int y = (world[1][1] - bbox[1][1]) /
		(world[1][1] - world[0][1]) * (1 << z) + 0.5;

	tilefd = -1;
	tile_path = mprintf("%i/%i/%i.txt", z, x, y);
}

void tile_open(void) {
	if (create_dirs(tile_path, 0755)) {
		fprintf(stderr, "Couldn't mkdir %s\n", tile_path);
		exit(-1);
	}

	tilefd = open(tile_path, O_CREAT | O_TRUNC | O_WRONLY, 0644);
	if (tilefd == -1) {
		fprintf(stderr, "Couldn't open %s: %s\n", tile_path,
				strerror(errno));
		exit(-1);
	}

#ifdef JSONP
	dprintf(tilefd, "c([");
#else
	dprintf(tilefd, "[");
#endif
}

void tile_done(void) {
	if (tilefd != -1) {
		/* TODO: sort entries, make "wikipedia" field unique */
#ifdef JSONP
		dprintf(tilefd, "])");
#else
		dprintf(tilefd, "]");
#endif
		close(tilefd);
	}

	free(tile_path);
}

#define startswith(s, p) !strncmp(s, p, strlen(p))

static void tile_json_field_string(const char *key, const char *value) {
	char buf[strlen(value) * 2 + 1], *dp = buf;
	const char *endp;

	if (value[0] == '\0')
		return;

	while ((endp = strchr(value, '\"'))) {
		memcpy(dp, value, endp - value);
		dp += endp - value;
		*dp ++= '\\';
		*dp ++= '\"';
		value += endp - value + 1;
	}
	*dp = 0;
	dprintf(tilefd, "\"%s\":\"%s%s\",", key, buf, value);
}

static void tile_json_field_number(const char *key, const char *value) {
	if (value[0] == '\0')
		return;

	dprintf(tilefd, "\"%s\":%s,", key, value);
}

static void tile_json_field_geojson(const char *wkt, bbox_t bbox) {
	pt_t pt;
	int i;
	struct linestring_s linestring;

	dprintf(tilefd, "\"geometry\":{");

	if (sscanf(wkt, "POINT(%lf %lf)", &pt[0], &pt[1]) == 2) {
		dprintf(tilefd, "\"type\":\"Point\",");

		unproject(pt);
		dprintf(tilefd, "\"coordinates\":[%.12g,%.12g]", pt[0], pt[1]);
	} else if (startswith(wkt, "LINESTRING(")) {
		wkt += 11;

		wkt = linestring_parse(wkt, &linestring);
		if (linestring.nodes > 96)
			linestring_simplify(&linestring, bbox);

		dprintf(tilefd, "\"type\":\"LineString\",");
		dprintf(tilefd, "\"coordinates\":[");

		for (i = 0; i < linestring.nodes; i ++) {
			unproject(linestring.pts[i]);

			dprintf(tilefd, "%s[%.12g,%.12g]", i ? "," : "",
					linestring.pts[i][0],
					linestring.pts[i][1]);
		}

		dprintf(tilefd, "]");

		linestring_free(&linestring);
	} else if (startswith(wkt, "POLYGON((")) {
		wkt += 9;

		dprintf(tilefd, "\"type\":\"Polygon\",");
		dprintf(tilefd, "\"coordinates\":[[");

		while (1) {
			wkt = linestring_parse(wkt, &linestring);
			if (linestring.nodes > 96)
				linestring_simplify(&linestring, bbox);

			for (i = 0; i < linestring.nodes; i ++) {
				unproject(linestring.pts[i]);

				dprintf(tilefd, "%s[%.12g,%.12g]", i ? "," : "",
						linestring.pts[i][0],
						linestring.pts[i][1]);
			}

			linestring_free(&linestring);

			if (*wkt == ')')
				break;
			if (*wkt ++ != ',') {
				fprintf(stderr, "Couldn't parse "
						"WKT polygon %s\n", wkt);
				exit(-1);
			}
			if (*wkt ++ != '(') {
				fprintf(stderr, "Couldn't parse "
						"WKT polygon %s\n", wkt);
				exit(-1);
			}
			dprintf(tilefd, "],[");
		}

		dprintf(tilefd, "]]");
	} else {
		fprintf(stderr, "unsupported WKT feature: %s\n", wkt);
		exit(-1);
	}

	dprintf(tilefd, "}"); /* This is the last field: no comma */
}

static long long int url_raw = 0;
static long long int url_raw_wp = 0;
static long long int url_wp = 0;
static long long int url_wp_ne = 0;

static void tile_append(bbox_t bbox, const char *wkt,
		const char *wikipedia, const char *url, const char *website,
		const char *name, const char *highway, const char *place,
		const char *id, const char *flickr,
		const char *foursquare, const char *dopplr,
		const char *lastfm, const char *upcoming,
		const char *openplaques, const char *openlibrary) {
	if (tilefd == -1)
		tile_open();

	/* TODO: Currently we dump objets directly to the file but we really
	 * need to store them in a dictionary indexed by "wikipedia" and join
	 * objects.  We could also preprocess the different wikipedia link
	 * formats and URLs and reject if unparseable. */
	/* TODO: Sanitize stuff for JSON! */

	srandom(strtol(id, 0, 0));

	dprintf(tilefd, "{");
	tile_json_field_string("wikipedia", wikipedia);
	tile_json_field_string("url", url);
	tile_json_field_string("website", website);
	tile_json_field_string("name", name);
	tile_json_field_number("id", id);
	tile_json_field_number("flickr", flickr);
	tile_json_field_number("foursquare", foursquare);
	tile_json_field_string("dopplr", dopplr);
	tile_json_field_number("lastfm", lastfm);
	tile_json_field_number("upcoming", upcoming);
	tile_json_field_number("openplaques", openplaques);
	tile_json_field_number("openlibrary", openlibrary);
	tile_json_field_geojson(wkt, bbox);
	dprintf(tilefd, "},");

	if (url[0]) {
		url_raw ++;
		if (strstr(url, "wikipedia"))
			url_raw_wp ++;
	}
	if (wikipedia[0]) {
		url_wp ++;
		if (strchr(wikipedia, ':'))
			url_wp_ne ++;
	}
}

static void make_tiles(bbox_t bbox, int level) {
	bbox_t subbox;
	pt_t middle;
	char *query;
	PGresult *res;
	const char **table;
	int i, n;
	static const char *tables[] = {
		"planet_osm_point", "planet_osm_line", "planet_osm_polygon", 0
	};

	if (level >= MAXZOOM)
		return;

	if (bbox_contains(banned_bbox, bbox))
		return;

#if 0
	if (z_condition[level]) {
		struct stat s;
		tile_init(bbox, level);
		if (!stat(tile_path, &s))
			return; /* Already done */
		tile_done();
	}
#endif

	for (table = tables; *table; table ++) {
		query = mprintf("select name from (select "
				"SetSRID('BOX3D(%f %f,%f %f)'::box3d, %i) "
				"as bb) as tmp,%s "
				"where %s "
				"LIMIT 1",
				bbox[0][0], bbox[0][1], bbox[1][0], bbox[1][1],
				srid, *table, generic_condition);
		res = PQexec(conn, query);
		if (!res || PQnfields(res) != 1) {
			fprintf(stderr, "Result is not Nx1 sized (%s)\n",
					query);
			free(query);
			fprintf(stderr, "Assuming area is empty and carrying "
					"on\n");
			PQclear(res);
			continue;
		}
		free(query);
		i = PQntuples(res);
		PQclear(res);

		if (i == 1)
			/* Area is not empty... make a tile, then recurse */
			break;
	}
	if (!*table)
		/* Area is empty, nothing to do here */
		return;

	/* Recurse */
	level ++;
	middle[0] = (bbox[0][0] + bbox[1][0]) * 0.5;
	middle[1] = (bbox[0][1] + bbox[1][1]) * 0.5;
	subbox[0][0] = bbox[0][0];
	subbox[0][1] = bbox[0][1];
	subbox[1][0] = middle[0];
	subbox[1][1] = middle[1];
	make_tiles(subbox, level);
	subbox[0][0] = middle[0];
	subbox[0][1] = bbox[0][1];
	subbox[1][0] = bbox[1][0];
	subbox[1][1] = middle[1];
	make_tiles(subbox, level);
	subbox[0][0] = bbox[0][0];
	subbox[0][1] = middle[1];
	subbox[1][0] = middle[0];
	subbox[1][1] = bbox[1][1];
	make_tiles(subbox, level);
	subbox[0][0] = middle[0];
	subbox[0][1] = middle[1];
	subbox[1][0] = bbox[1][0];
	subbox[1][1] = bbox[1][1];
	make_tiles(subbox, level);

	if (!z_condition[-- level])
		return;

	/* Make a tile */
	tile_init(bbox, level);

	for (table = tables; *table; table ++) {
		query = mprintf("select ST_AsText(way),wikipedia,url,website,"
				"name,highway,place,osm_id,flickr,foursquare,"
				"dopplr,lastfm,upcoming,openplaques,"
				"openlibrary from (select "
				"SetSRID('BOX3D(%f %f,%f %f)'::box3d, %i) "
				"as bb) as tmp,%s "
				"where %s",
				bbox[0][0], bbox[0][1], bbox[1][0], bbox[1][1],
				srid, *table, z_condition[level]);
		res = PQexec(conn, query);
		if (!res || PQnfields(res) != 15) {
			fprintf(stderr, "Result is not Nx15 sized (%s)\n",
					query);
			free(query);
			fprintf(stderr, "Assuming area is empty and carrying "
					"on\n");
			PQclear(res);
			continue;
		}
		free(query);

		n = PQntuples(res);
		for (i = 0; i < n; i ++)
			tile_append(bbox, PQgetvalue(res, i, 0),
					PQgetvalue(res, i, 1),
					PQgetvalue(res, i, 2),
					PQgetvalue(res, i, 3),
					PQgetvalue(res, i, 4),
					PQgetvalue(res, i, 5),
					PQgetvalue(res, i, 6),
					PQgetvalue(res, i, 7),
					PQgetvalue(res, i, 8),
					PQgetvalue(res, i, 9),
					PQgetvalue(res, i, 10),
					PQgetvalue(res, i, 11),
					PQgetvalue(res, i, 12),
					PQgetvalue(res, i, 13),
					PQgetvalue(res, i, 14));

		PQclear(res);
	}

	tile_done();
}

int main(int argc, char *argv[]) {
	project_init(PROJ_SPHERE_MERC);
	db_init();

	make_tiles(world, 0);

	project_exit();
	db_done();

	printf("stats: %lli %lli %lli %lli\n",
			url_raw, url_raw_wp, url_wp, url_wp_ne);

	return 0;
}
