/* $Id: folder.c,v 1.48 2015/09/29 13:17:42 onoe Exp $ */

/*-
 * Copyright (c) 1998-2001 Atsushi Onoe
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/types.h>
#include <sys/stat.h>

#include <dirent.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "cue.h"
#include "tarfile.h"

static struct folder *folders;
static int nfolder, szfolder;

static struct folder *
folder_alloc(const char *path)
{
	struct folder *fl, *p;

	if (nfolder == szfolder) {
		p = realloc(folders, sizeof(*folders) * (szfolder + LINEBLOCK));
		if (!p)
			abort();
		folders = p;
		szfolder += LINEBLOCK;
	}
	fl = &folders[nfolder++];
	memset(fl, 0, sizeof(*fl));
	if (asprintf(&fl->name.ptr, "+%s", path) < 0)
		abort();
	fl->name.len = strlen(fl->name.ptr) + 1;
	if (strcmp(fl->name.ptr, DRAFT_FOLDER) == 0)
		fl->flags |= FOL_DRAFT;
	return fl;
}

static void
folder_list(char *path, size_t pathlen, int nlink)
{
	struct stat stbuf;
	DIR *dirp;
	struct dirent *dp;
	char *p, *ep;
	struct folder *fl;
	int namlen;
	int ret;

	/*
	 * XXX: symlink (folder) directory is not fully supported.
	 *	symlink not followed by directory will be ignored.
	 */
	if (nlink == 0) {
		/* top directory */
		if (stat(".", &stbuf) < 0
		||  (nlink = stbuf.st_nlink) <= 2
		||  (dirp = opendir(".")) == NULL)
			return;
		p = path;
		ep = path + pathlen;
	} else {
		if ((dirp = opendir(path)) == NULL)
			return;
		p = path + strlen(path);
		ep = path + pathlen;
		*p++ = '/';
	}
	while ((dp = readdir(dirp)) != NULL) {
		namlen = strlen(dp->d_name) - 4;
		if ((namlen > 0 && strcmp(dp->d_name + namlen, ".tar") == 0) ||
		    (namlen > 0 && strcmp(dp->d_name + namlen, ".taz") == 0) ||
		    ((namlen -= 3) > 0 &&
		    strcmp(dp->d_name + namlen, ".tar.gz") == 0)) {
			strlcpy(p, dp->d_name, ep - p);
			ret = tar_addtgz(path);
			if (ret < 0)
				continue;
			p[namlen] = '\0';
			fl = folder_alloc(path);
			fl->flags |= FOL_TAR;
			if (ret == 2)
				fl->flags |= FOL_SORT;
			continue;
		}
#ifdef DT_REG
		if (dp->d_type == DT_REG)
			continue;
#endif /* DT_REG */
		if (nlink == 0)
			continue;
		strlcpy(p, dp->d_name, ep - p);
		/* do not decrement nlink if path is SYMLINK */
		if (lstat(path, &stbuf) < 0)
			continue;
		if (S_ISDIR(stbuf.st_mode))
			nlink--;
		else if (!S_ISLNK(stbuf.st_mode)
		||	 stat(path, &stbuf) < 0
		||	 !S_ISDIR(stbuf.st_mode))
			continue;
		if (*dp->d_name == '.')
			continue;
		(void)folder_alloc(path);
		if (stbuf.st_nlink > 2)
			folder_list(path, pathlen, stbuf.st_nlink);
	}
	closedir(dirp);
}

#ifdef notused
int
cbufcmp(cbuf_t *c1, cbuf_t *c2)
{
	int n, l;

	l = c1->len < c2->len ? c1->len : c2->len;
	if ((n = memcmp(c1->ptr, c2->ptr, l)))
		return n;
	return c1->len - c2->len;
}
#endif

static int
folnamecmp(const void *p1, const void *p2)
{
	const struct folder *f1 = p1, *f2 = p2;

	return strcmp(f1->name.ptr, f2->name.ptr);
}

static void
folder_makelist(void)
{
	char path[MAXPATH];

	path[0] = '\0';
	folder_list(path, sizeof(path), 0);
	qsort(folders, nfolder, sizeof(*folders), folnamecmp);
}

int
folder_completion(char *candidate, void (*callback)(void *arg, const char *entry), void *arg)
{
	struct folder *fl;
	cbuf_t *mname;
	int nmatch = 0;
	int matchlen, len;
	int n, i;
	int l;
	char *p;

	if (folders == NULL)
		folder_makelist();
	if ((p = strrchr(candidate, ',')))
		candidate = p + 1;
	len = strlen(candidate);
	matchlen = 0;
	mname = NULL;
	l = 0;
	for (fl = folders, n = 0; n < nfolder; fl++, n++) {
		if (fl->name.len >= len
		&&  memcmp(fl->name.ptr, candidate, len) == 0) {
			if (nmatch && mname) {
				for (i = len; i < matchlen; i++) {
					if (i >= fl->name.len
					||  fl->name.ptr[i] != mname->ptr[i]) {
						matchlen = i;
						break;
					}
				}
			} else {
				matchlen = fl->name.len;
			}
			nmatch++;
			if (callback)
				callback(arg, fl->name.ptr);
			mname = &fl->name;
		}
	}
	if (nmatch && matchlen > len) {
		memcpy(candidate + len, mname->ptr + len, matchlen - len);
		candidate[matchlen] = '\0';
	}
	return nmatch;
}

static int
msgnumcmp(const void *p1, const void *p2)
{
	const struct msginfo *m1 = p1, *m2 = p2;

	return m1->num - m2->num;
}

static void
folder_numscan(struct folder *fl, int ignore)
{
	char path[MAXPATH];
	DIR *dirp;
	struct dirent *dp;
	char *p, *m, *ep;
	int n;
	struct msginfo *msg, *q;
	DIR *(*func_opendir)();
	int (*func_closedir)();
	struct dirent *(*func_readdir)();

	if (fl->flags & FOL_TAR) {
		func_opendir = tar_opendir;
		func_closedir = tar_closedir;
		func_readdir = tar_readdir;
	} else {
		func_opendir = opendir;
		func_closedir = closedir;
		func_readdir = readdir;
	}

	if ((dirp = func_opendir(fl->name.ptr + 1)) == NULL)
		return;

	strlcpy(path, fl->name.ptr + 1, sizeof(path));
	m = path + fl->name.len - 1;
	ep = path + sizeof(path);
	*m++ = '/';
	while ((dp = func_readdir(dirp)) != NULL) {
#ifdef DT_DIR
		if (dp->d_type == DT_DIR)
			continue;
#endif /* DT_DIR */
		if (*dp->d_name == '.')
			continue;
		n = strtoul(dp->d_name, &p, 10);
		if (n == 0 || *p != '\0')
			continue;
		if (n <= ignore)
			continue;
		strlcpy(m, dp->d_name, ep - m);
		if (fl->nmsg >= fl->szmsg) {
			q = realloc(fl->msg,
			    sizeof(*msg) * (fl->szmsg + LINEBLOCK));
			if (!q)
				abort();
			fl->msg = q;
			fl->szmsg += LINEBLOCK;
		}
		msg = &fl->msg[fl->nmsg++];
		memset(msg, 0, sizeof(*msg));
		msg->num = n;
	}
	func_closedir(dirp);
	if (fl->nmsg && !(fl->flags & FOL_SORT))
		qsort(fl->msg, fl->nmsg, sizeof(*msg), msgnumcmp);
}

void
folder_update(struct folder *fl, int force)
{
	int n;
	struct msginfo *msg;
	struct stat stbuf;

	if (fl->flags & FOL_TAR)
		goto skip;	/*XXX: TODO*/
	if (stat(fl->name.ptr + 1, &stbuf) < 0 || !S_ISDIR(stbuf.st_mode)) {
		force = 1;
		fl->flags &= ~FOL_POSAVAIL;
		memset(&stbuf, 0, sizeof(stbuf));
	}
	if (!force
	&&  stbuf.st_ino == fl->stbuf.st_ino
	&&  stbuf.st_mtime == fl->stbuf.st_mtime
	&&  stbuf.st_size == fl->stbuf.st_size)
		return;
  skip:
	if (force) {
		for (n = 0, msg = fl->msg; n < fl->nmsg; n++, msg++) {
			if (msg->scan.ptr)
				free(msg->scan.ptr);
		}
		fl->nmsg = 0;
		fl->nmark = 0;
		memset(&fl->stbuf, 0, sizeof(fl->stbuf));
	}

	if (fl->nmsg == 0)
		folder_numscan(fl, 0);
	else
		folder_numscan(fl, fl->msg[fl->nmsg - 1].num);
	if (!(fl->flags & FOL_POSAVAIL) || fl->pos >= fl->nmsg) {
		fl->flags |= FOL_POSAVAIL;
		fl->pos = fl->nmsg ? fl->nmsg - 1 : 0;
	}
	fl->stbuf = stbuf;
}

struct folder *
folder_open(const char *name, int doupdate)
{
	struct folder *fl;
	struct stat stbuf;
	int n;

	if (folders == NULL)
		folder_makelist();
	if (*name != '+')
		return NULL;
	for (n = 0, fl = folders; n < nfolder; n++, fl++) {
		if (strcmp(fl->name.ptr, name) == 0) {
			if (doupdate)
				folder_update(fl, 0);
			return fl;
		}
	}
	if (stat(name + 1, &stbuf) == 0 && S_ISDIR(stbuf.st_mode)) {
		fl = folder_alloc(name + 1);
		if (doupdate)
			folder_update(fl, 0);
		return fl;
	}
	if (strcmp(name, DEF_FOLDER) == 0)
		return folders;
	return NULL;
}

ccbuf_t *
folder_read(struct state *state, int n)
{
	struct msginfo *msg;
	struct folder *fl = state->folder;

	if (n < 0 || n >= fl->nmsg)
		return NULL;
	msg = &fl->msg[n];
	if (msg->scan.ptr == NULL)
		message_scan(state, msg);
	msg->scan.ptr[state->config.numwidth] = msg->mark ? msg->mark : ' ';
	return (ccbuf_t *)&msg->scan;
}

void
folder_purge(struct state *state, int num)
{
	struct msginfo *msg;
	struct folder *fl = state->folder;
	int i;

	for (i = 0, msg = fl->msg; i < fl->nmsg; i++, msg++) {
		if (msg->num == num) {
			if (msg->scan.ptr != NULL) {
				free(msg->scan.ptr);
				msg->scan.ptr = NULL;
			}
			msg->scan.len = 0;
			break;
		}
	}
}

struct folder *
folder_modified(void)
{
	int i;
	struct folder *fl;

	for (i = 0, fl = folders; i < nfolder; i++, fl++) {
		if (fl->nmark)
			return fl;
	}
	return NULL;
}

void
folder_change(struct state *state)
{
	char buf[LINE_WIDTH];
	struct folder *fl;

	snprintf(state->status, sizeof(state->status),
	    "Folder name (%s): ", state->config.folder);
	buf[0] = '+';
	buf[1] = '\0';
	if (edit_stline(state, buf, sizeof(buf), folder_completion) == NULL) {
		strlcpy(state->status, "Quit", sizeof(state->status));
		return;
	}
	if (buf[0] == '+' && buf[1] == '\0')
		strlcpy(buf, state->config.folder, sizeof(buf));
	if (buf[0] != '+') {
		snprintf(state->status, sizeof(state->status),
		    "No such folder: %s", buf);
		return;
	}
	fl = folder_open(buf, 1);
	if (fl) {
		state->folder = fl;
		state->message = NULL;
	}
	state->status[0] = '\0';
}

static FILE *
open_statfile(struct state *state, const char *mode)
{
	FILE *fp;
	char path[BUFSIZ];
	char *p;

	/* open statfile */
	for (p = state->config.statfile; *p != '\0' && *p != '/'; p++)
		;
	if (*p == '\0') {
		snprintf(path, sizeof(path), "%s/%s",
		    getenv("HOME"), state->config.statfile);
	} else
		strlcpy(path, state->config.statfile, sizeof(path));

	if ((fp = fopen(path, mode)) == NULL) {
		strlcpy(state->status, "Fail to open file: ",
		    sizeof(state->status));
		strlcat(state->status, path, sizeof(state->status));
		return NULL;
	}

	return fp;
}

static int
backup_statfile(struct state *state)
{
	FILE *fpfrom, *fpto;
	char path[BUFSIZ], buf[BUFSIZ];
	char *p;
	int len, res;

	if ((fpfrom = open_statfile(state, "r")) == NULL)
		return 0;

	/* open backup file */
	for (p = state->config.statfile; *p != '\0' && *p != '/'; p++)
		;
	if (*p == '\0') {
		snprintf(path, sizeof(path), "%s/%s~",
			getenv("HOME"), state->config.statfile);
	} else
		snprintf(path, sizeof(path), "%s~", state->config.statfile);
	if ((fpto = fopen(path, "w")) == NULL) {
		snprintf(state->status, sizeof(state->status),
		    "Fail to open file: %s", path);
		return -1;
	}

	while ((len = fread(buf, sizeof(buf[0]), sizeof(buf), fpfrom)) > 0) {
		res = fwrite(buf, sizeof(buf[0]), len, fpto);
                if (len != res ) {
			snprintf(state->status, sizeof(state->status),
			    "Fail to open file: %s", path);
			fclose(fpfrom);
			fclose(fpto);
			return -1;
                }
	}
	fclose(fpfrom);
	fclose(fpto);

	return 0;
}

int
import_mark(struct state *state)
{
	FILE *fp;
	struct folder *fl;
	struct msginfo *msg;
	char *p, buf[64], *dmy;
	int mnum, newm = 0;
	int i;

	if ((fp = open_statfile(state, "r")) == NULL)
		return -1;

	state->status[0] = '\0';
	fl = NULL;

	while (fgets(buf, sizeof(buf), fp) != NULL) {
		if ((p = strchr(buf, '\n')) != NULL)
			*p = '\0';

		/* get folder name */
		if (!strncmp(buf, "folder:", strlen("folder:"))) {
			p = buf + strlen("folder:");
			while (*p == ' ' || *p == '\t')
				p++;
			for (i = 0, fl = folders; i < nfolder; i++, fl++) {
				if (strcmp(fl->name.ptr, p) == 0)
					break;
			}
			if (i == nfolder) {
				/* warning */
				snprintf(state->status, sizeof(state->status),
				    "No such folder \"%s\"", p);
				fl = NULL;
			} else {
				if (fl->nmsg == 0)
					folder_numscan(fl, 0);
			}
			continue;
		}

		if (fl == NULL)
			continue;

		/* get the number of marked */
		mnum = strtol(buf, &dmy, 10);
		if (*dmy != '\0') {
			snprintf(state->status, sizeof(state->status),
			    "Invalid subject number: %s", buf);
			continue;
		}
		for (i = 0, msg = fl->msg; i < fl->nmsg; i++, msg++) {
			if (msg->num != mnum)
				continue;
			if (msg->mark == MARK_MARK)
				break;
			msg->mark = MARK_MARK;
			fl->nmark++;
			newm++;
			break;
		}
		if (i == fl->nmsg) {
			/* warning */
			snprintf(state->status, sizeof(state->status),
			    "No such message: %s", buf);
		}
	}
	fclose(fp);

	if (state->status[0] == '\0')
		snprintf(state->status, sizeof(state->status),
		    "Import %d marks successfully.", newm);

	return 0;
}

int
export_mark(struct state *state)
{
	FILE *fp;
	struct folder *fl;
	struct msginfo *msg;
	int newm = 0;
	int i, j;

	if (state->config.statbackup && backup_statfile(state) < 0)
		return -1;
		
	if ((fp = open_statfile(state, "w")) == NULL)
		return -1;

	for (i = 0, fl = folders; i < nfolder; i++, fl++) {
		if (fl->nmark == 0)
			continue;

		fprintf(fp, "folder:%s\n", fl->name.ptr);
		for (j = 0, msg = fl->msg; j < fl->nmsg; j++, msg++) {
			if (msg->mark == MARK_MARK) {
				fprintf(fp, "%d\n", msg->num);
				msg->mark = 0;
				newm++;
			}
		}
		fl->nmark = 0;
	}
	fclose(fp);

	snprintf(state->status, sizeof(state->status),
	    "Export %d marks successfully.", newm);

	return 0;
}

#ifdef TEST_MAIN1
main()
{
	struct folder *fl;
	int n;

	folder_makelist();
	for (n = 0, fl = folders; n < nfolder; n++, fl++)
		printf("%s\n", fl->name.ptr);
}
#endif
