#include "config.h"

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include "nls.h"
#include "keymap/findfile.h"

void lk_fpclose(lkfile_t *fp)
{
	if (!fp || !fp->fd)
		return;
	if (fp->pipe)
		pclose(fp->fd);
	else
		fclose(fp->fd);
	fp->fd = NULL;
}

#define SIZE(a) (sizeof(a) / sizeof(a[0]))

static struct decompressor {
	const char *ext; /* starts with `.', has no other dots */
	const char *cmd;
} decompressors[] = {
	{ ".gz", "gzip -d -c" },
	{ ".bz2", "bzip2 -d -c" },
	{ 0, 0 }
};

static int
pipe_open(const struct decompressor *dc, lkfile_t *fp)
{
	char *pipe_cmd;

	pipe_cmd = malloc(strlen(dc->cmd) + strlen(fp->pathname) + 2);
	if (pipe_cmd == NULL)
		return -1;

	sprintf(pipe_cmd, "%s %s", dc->cmd, fp->pathname);

	fp->fd   = popen(pipe_cmd, "r");
	fp->pipe = 1;

	free(pipe_cmd);

	if (fp->fd == NULL)
		return -1;
	return 0;
}

/* If a file PATHNAME exists, then open it.
   If is has a `compressed' extension, then open a pipe reading it */
static int
maybe_pipe_open(lkfile_t *fp)
{
	char *t;
	struct stat st;
	struct decompressor *dc;

	if (stat(fp->pathname, &st) == -1 || !S_ISREG(st.st_mode) ||
	    access(fp->pathname, R_OK) == -1)
		return -1;

	t = strrchr(fp->pathname, '.');
	if (t) {
		for (dc = &decompressors[0]; dc->cmd; dc++) {
			if (strcmp(t, dc->ext) == 0)
				return pipe_open(dc, fp);
		}
	}
	fp->fd   = fopen(fp->pathname, "r");
	fp->pipe = 0;

	if (fp->fd == NULL)
		return -1;

	return 0;
}

static int
findfile_by_fullname(const char *fnam, const char *const *suffixes, lkfile_t *fp)
{
	int i;
	struct stat st;
	struct decompressor *dc;
	size_t fnam_len, sp_len;

	fp->pipe = 0;
	fnam_len = strlen(fnam);

	for (i = 0; suffixes[i]; i++) {
		if (suffixes[i] == 0)
			continue; /* we tried it already */

		sp_len = strlen(suffixes[i]);

		if (fnam_len + sp_len + 1 > sizeof(fp->pathname))
			continue;

		sprintf(fp->pathname, "%s%s", fnam, suffixes[i]);

		if (stat(fp->pathname, &st) == 0 && S_ISREG(st.st_mode) && (fp->fd = fopen(fp->pathname, "r")) != NULL)
			return 0;

		for (dc = &decompressors[0]; dc->cmd; dc++) {
			if (fnam_len + sp_len + strlen(dc->ext) + 1 > sizeof(fp->pathname))
				continue;

			sprintf(fp->pathname, "%s%s%s", fnam, suffixes[i], dc->ext);

			if (stat(fp->pathname, &st) == 0 && S_ISREG(st.st_mode) && access(fp->pathname, R_OK) == 0)
				return pipe_open(dc, fp);
		}
	}

	return -1;
}

static int
findfile_in_dir(const char *fnam, const char *dir, const int recdepth, const char *const *suf, lkfile_t *fp)
{
	DIR *d;
	struct dirent *de;
	char *ff, *fdir, *p;
	const char *q;
	struct decompressor *dc;
	int i, rc = -1, secondpass = 0;
	size_t dir_len;

	fp->fd   = NULL;
	fp->pipe = 0;

	if ((d = opendir(dir)) == NULL)
		return -1;

	dir_len = strlen(dir);

	fdir = NULL;
	if ((ff = strchr(fnam, '/')) != NULL) {
		if ((fdir = strndup(fnam, ff - fnam)) == NULL) {
			closedir(d);
			return -1;
		}
	}

/* Scan the directory twice: first for files, then
	   for subdirectories, so that we do never search
	   a subdirectory when the directory itself already
	   contains the file we are looking for. */
StartScan:
	while ((de = readdir(d)) != NULL) {
		struct stat st;
		int okdir;
		size_t d_len;

		d_len = strlen(de->d_name);
		if (d_len < 3) {
			if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
				continue;
		}

		if (dir_len + d_len + 2 > sizeof(fp->pathname))
			continue;

		okdir = (ff && strcmp(de->d_name, fdir) == 0);

		if ((secondpass && recdepth) || okdir) {
			char *a;

			if ((a = malloc(dir_len + d_len + 2)) == NULL)
				goto EndScan;

			sprintf(a, "%s/%s", dir, de->d_name);

			if (stat(a, &st) == 0 && S_ISDIR(st.st_mode)) {
				if (okdir)
					rc = findfile_in_dir(ff + 1, a, 0, suf, fp);

				if (rc && recdepth)
					rc = findfile_in_dir(fnam, a, recdepth - 1, suf, fp);

				if (!rc) {
					free(a);
					goto EndScan;
				}
			}
			free(a);
		}

		if (secondpass)
			continue;

		/* Should we be in a subdirectory? */
		if (ff)
			continue;

		/* Does d_name start right? */
		p = &de->d_name[0];
		q = fnam;
		while (*p && *p == *q)
			p++, q++;
		if (*q)
			continue;

		sprintf(fp->pathname, "%s/%s", dir, de->d_name);
		if (stat(fp->pathname, &st) != 0 || !S_ISREG(st.st_mode))
			continue;

		/* Does tail consist of a known suffix and possibly
	       a compression suffix? */
		for (i = 0; suf[i]; i++) {
			size_t l;

			if (!strcmp(p, suf[i])) {
				rc = maybe_pipe_open(fp);
				goto EndScan;
			}

			l = strlen(suf[i]);
			if (!strncmp(p, suf[i], l)) {
				for (dc = &decompressors[0]; dc->cmd; dc++)
					if (strcmp(p + l, dc->ext) == 0) {
						rc = pipe_open(dc, fp);
						goto EndScan;
					}
			}
		}
	}

	if (recdepth > 0 && !secondpass) {
		secondpass = 1;
		seekdir(d, 0);
		goto StartScan;
	}

EndScan:
	if (fdir != NULL)
		free(fdir);
	closedir(d);
	return rc;
}

int lk_findfile(const char *fnam, const char *const *dirpath, const char *const *suffixes, lkfile_t *fp)
{
	char *dir;
	int dl, recdepth, rc, i;

	fp->fd   = NULL;
	fp->pipe = 0;

	/* Try explicitly given name first */
	strcpy(fp->pathname, fnam);

	if (!maybe_pipe_open(fp))
		return 0;

	/* Test for full pathname - opening it failed, so need suffix */
	/* (This is just nonsense, for backwards compatibility.) */
	if (*fnam == '/' &&
	    !findfile_by_fullname(fnam, suffixes, fp))
		return 0;

	/* Search a list of directories and directory hierarchies */
	for (i = 0; dirpath[i]; i++) {
		recdepth = 0;
		dl       = strlen(dirpath[i]);

		/* trailing stars denote recursion */
		while (dl && dirpath[i][dl - 1] == '*')
			dl--, recdepth++;

		/* delete trailing slashes */
		while (dl && dirpath[i][dl - 1] == '/')
			dl--;

		if (dl)
			dir = strndup(dirpath[i], dl);
		else
			dir = strdup(".");

		if (dir == NULL)
			return 1;

		rc = findfile_in_dir(fnam, dir, recdepth, suffixes, fp);
		free(dir);

		if (!rc)
			return 0;
	}
	return 1;
}

lkfile_t *
lk_fpopen(const char *filename)
{
	lkfile_t *fp;

	fp = malloc(sizeof(lkfile_t));
	if (!fp)
		return NULL;

	strcpy(fp->pathname, filename);

	if (maybe_pipe_open(fp) < 0) {
		free(fp);
		return NULL;
	}

	return fp;
}
