/*********************************************************************
 * bfs                                                               *
 * Copyright (C) 2017 Tavian Barnes <tavianator@tavianator.com>      *
 *                                                                   *
 * This program is free software. It comes without any warranty, to  *
 * the extent permitted by applicable law. You can redistribute it   *
 * and/or modify it under the terms of the Do What The Fuck You Want *
 * To Public License, Version 2, as published by Sam Hocevar. See    *
 * the COPYING file or http://www.wtfpl.net/ for more details.       *
 *********************************************************************/

#include "printf.h"
#include "color.h"
#include "dstring.h"
#include "util.h"
#include <assert.h>
#include <errno.h>
#include <grp.h>
#include <pwd.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>

typedef int bfs_printf_fn(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf);

/**
 * A single directive in a printf command.
 */
struct bfs_printf_directive {
	/** The printing function to invoke. */
	bfs_printf_fn *fn;
	/** String data associated with this directive. */
	char *str;
	/** The next printf directive in the chain. */
	struct bfs_printf_directive *next;
};

/** Print some text as-is. */
static int bfs_printf_literal(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	return fprintf(file, "%s", directive->str);
}

/** \c: flush */
static int bfs_printf_flush(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	return fflush(file);
}

/**
 * Print a value to a temporary buffer before formatting it.
 */
#define BFS_PRINTF_BUF(buf, format, ...)				\
	char buf[256];							\
	int ret = snprintf(buf, sizeof(buf), format, __VA_ARGS__);	\
	assert(ret >= 0 && ret < sizeof(buf));				\
	(void)ret

/**
 * Print a ctime()-style string, for %a, %c, and %t.
 */
static int bfs_printf_ctime(FILE *file, const struct bfs_printf_directive *directive, const struct timespec *ts) {
	// Not using ctime() itself because GNU find adds nanoseconds
	static const char *days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
	static const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};

	const struct tm *tm = localtime(&ts->tv_sec);
	BFS_PRINTF_BUF(buf, "%s %s %2d %.2d:%.2d:%.2d.%09ld0 %4d",
		       days[tm->tm_wday],
		       months[tm->tm_mon],
		       tm->tm_mday,
		       tm->tm_hour,
		       tm->tm_min,
		       tm->tm_sec,
		       (long)ts->tv_nsec,
		       1900 + tm->tm_year);

	return fprintf(file, directive->str, buf);
}

/** %a: access time */
static int bfs_printf_a(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	return bfs_printf_ctime(file, directive, &ftwbuf->statbuf->st_atim);
}

/** %b: blocks */
static int bfs_printf_b(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)ftwbuf->statbuf->st_blocks);
	return fprintf(file, directive->str, buf);
}

/** %c: change time */
static int bfs_printf_c(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	return bfs_printf_ctime(file, directive, &ftwbuf->statbuf->st_ctim);
}

/** %d: depth */
static int bfs_printf_d(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	return fprintf(file, directive->str, (intmax_t)ftwbuf->depth);
}

/** %D: device */
static int bfs_printf_D(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)ftwbuf->statbuf->st_dev);
	return fprintf(file, directive->str, buf);
}

/** %f: file name */
static int bfs_printf_f(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	return fprintf(file, directive->str, ftwbuf->path + ftwbuf->nameoff);
}

/** %G: gid */
static int bfs_printf_G(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)ftwbuf->statbuf->st_gid);
	return fprintf(file, directive->str, buf);
}

/** %g: group name */
static int bfs_printf_g(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	struct group *grp = getgrgid(ftwbuf->statbuf->st_gid);
	if (!grp) {
		return bfs_printf_G(file, directive, ftwbuf);
	}

	return fprintf(file, directive->str, grp->gr_name);
}

/** %h: leading directories */
static int bfs_printf_h(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	char *copy = NULL;
	const char *buf;

	if (ftwbuf->nameoff > 0) {
		size_t len = ftwbuf->nameoff;
		if (len > 1) {
			--len;
		}

		buf = copy = strndup(ftwbuf->path, len);
	} else if (ftwbuf->path[0] == '/') {
		buf = "/";
	} else {
		buf = ".";
	}

	int ret = fprintf(file, directive->str, buf);
	free(copy);
	return ret;
}

/** %H: current root */
static int bfs_printf_H(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	return fprintf(file, directive->str, ftwbuf->root);
}

/** %i: inode */
static int bfs_printf_i(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)ftwbuf->statbuf->st_ino);
	return fprintf(file, directive->str, buf);
}

/** %k: 1K blocks */
static int bfs_printf_k(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)(ftwbuf->statbuf->st_blocks + 1)/2);
	return fprintf(file, directive->str, buf);
}

/** %l: link target */
static int bfs_printf_l(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	if (ftwbuf->typeflag != BFTW_LNK) {
		return 0;
	}

	char *target = xreadlinkat(ftwbuf->at_fd, ftwbuf->at_path, 0);
	if (!target) {
		return -1;
	}

	int ret = fprintf(file, directive->str, target);
	free(target);
	return ret;
}

/** %m: mode */
static int bfs_printf_m(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	return fprintf(file, directive->str, (unsigned int)(ftwbuf->statbuf->st_mode & 07777));
}

/** %M: symbolic mode */
static int bfs_printf_M(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	char buf[] = "----------";

	switch (ftwbuf->typeflag) {
	case BFTW_BLK:
		buf[0] = 'b';
		break;
	case BFTW_CHR:
		buf[0] = 'c';
		break;
	case BFTW_DIR:
		buf[0] = 'd';
		break;
	case BFTW_DOOR:
		buf[0] = 'D';
		break;
	case BFTW_FIFO:
		buf[0] = 'p';
		break;
	case BFTW_LNK:
		buf[0] = 'l';
		break;
	case BFTW_SOCK:
		buf[0] = 's';
		break;
	default:
		break;
	}

	mode_t mode = ftwbuf->statbuf->st_mode;

	if (mode & 00400) {
		buf[1] = 'r';
	}
	if (mode & 00200) {
		buf[2] = 'w';
	}
	if ((mode & 04100) == 04000) {
		buf[3] = 'S';
	} else if (mode & 04000) {
		buf[3] = 's';
	} else if (mode & 00100) {
		buf[3] = 'x';
	}

	if (mode & 00040) {
		buf[4] = 'r';
	}
	if (mode & 00020) {
		buf[5] = 'w';
	}
	if ((mode & 02010) == 02000) {
		buf[6] = 'S';
	} else if (mode & 02000) {
		buf[6] = 's';
	} else if (mode & 00010) {
		buf[6] = 'x';
	}

	if (mode & 00004) {
		buf[7] = 'r';
	}
	if (mode & 00002) {
		buf[8] = 'w';
	}
	if ((mode & 01001) == 01000) {
		buf[9] = 'T';
	} else if (mode & 01000) {
		buf[9] = 't';
	} else if (mode & 00001) {
		buf[9] = 'x';
	}

	return fprintf(file, directive->str, buf);
}

/** %n: link count */
static int bfs_printf_n(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)ftwbuf->statbuf->st_nlink);
	return fprintf(file, directive->str, buf);
}

/** %p: full path */
static int bfs_printf_p(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	return fprintf(file, directive->str, ftwbuf->path);
}

/** %P: path after root */
static int bfs_printf_P(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	const char *path = ftwbuf->path + strlen(ftwbuf->root);
	if (path[0] == '/') {
		++path;
	}
	return fprintf(file, directive->str, path);
}

/** %s: size */
static int bfs_printf_s(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)ftwbuf->statbuf->st_size);
	return fprintf(file, directive->str, buf);
}

/** %S: sparseness */
static int bfs_printf_S(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	double sparsity = 512.0 * ftwbuf->statbuf->st_blocks / ftwbuf->statbuf->st_size;
	return fprintf(file, directive->str, sparsity);
}

/** %t: modification time */
static int bfs_printf_t(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	return bfs_printf_ctime(file, directive, &ftwbuf->statbuf->st_mtim);
}

/** %U: uid */
static int bfs_printf_U(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)ftwbuf->statbuf->st_uid);
	return fprintf(file, directive->str, buf);
}

/** %u: user name */
static int bfs_printf_u(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	struct passwd *pwd = getpwuid(ftwbuf->statbuf->st_uid);
	if (!pwd) {
		return bfs_printf_U(file, directive, ftwbuf);
	}

	return fprintf(file, directive->str, pwd->pw_name);
}

static const char *bfs_printf_type(enum bftw_typeflag typeflag) {
	switch (typeflag) {
	case BFTW_BLK:
		return "b";
	case BFTW_CHR:
		return "c";
	case BFTW_DIR:
		return "d";
	case BFTW_DOOR:
		return "D";
	case BFTW_FIFO:
		return "p";
	case BFTW_LNK:
		return "l";
	case BFTW_REG:
		return "f";
	case BFTW_SOCK:
		return "s";
	default:
		return "U";
	}
}

/** %y: type */
static int bfs_printf_y(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	const char *type = bfs_printf_type(ftwbuf->typeflag);
	return fprintf(file, directive->str, type);
}

/** %Y: target type */
static int bfs_printf_Y(FILE *file, const struct bfs_printf_directive *directive, const struct BFTW *ftwbuf) {
	if (ftwbuf->typeflag != BFTW_LNK) {
		return bfs_printf_y(file, directive, ftwbuf);
	}

	const char *type = "U";

	struct stat sb;
	if (fstatat(ftwbuf->at_fd, ftwbuf->at_path, &sb, 0) == 0) {
		type = bfs_printf_type(bftw_mode_to_typeflag(sb.st_mode));
	} else {
		switch (errno) {
		case ELOOP:
			type = "L";
			break;
		case ENOENT:
			type = "N";
			break;
		}
	}

	return fprintf(file, directive->str, type);
}

/**
 * Append a printf directive to the chain.
 */
static int append_directive(struct bfs_printf_directive ***tail, bfs_printf_fn *fn, char *str) {
	struct bfs_printf_directive *directive = malloc(sizeof(*directive));
	if (!directive) {
		perror("malloc()");
		return -1;
	}

	directive->fn = fn;
	directive->str = str;
	directive->next = NULL;
	**tail = directive;
	*tail = &directive->next;
	return 0;
}

/**
 * Append a literal string to the chain.
 */
static int append_literal(struct bfs_printf_directive ***tail, char **literal, bool last) {
	if (!*literal || dstrlen(*literal) == 0) {
		return 0;
	}

	if (append_directive(tail, bfs_printf_literal, *literal) != 0) {
		return -1;
	}

	if (last) {
		*literal = NULL;
	} else {
		*literal = dstralloc(0);
		if (!*literal) {
			perror("dstralloc()");
			return -1;
		}
	}

	return 0;
}

struct bfs_printf *parse_bfs_printf(const char *format, const struct colors *stderr_colors) {
	struct bfs_printf *command = malloc(sizeof(*command));
	if (!command) {
		perror("malloc()");
		return NULL;
	}

	command->directives = NULL;
	command->needs_stat = false;
	struct bfs_printf_directive **tail = &command->directives;

	char *literal = dstralloc(0);
	if (!literal) {
		perror("dstralloc()");
		goto error;
	}

	for (const char *i = format; *i; ++i) {
		char c = *i;

		if (c == '\\') {
			c = *++i;

			if (c >= '0' && c < '8') {
				c = 0;
				for (int j = 0; j < 3 && *i >= '0' && *i < '8'; ++i, ++j) {
					c *= 8;
					c += *i - '0';
				}
				--i;
				goto one_char;
			}

			switch (c) {
			case 'a':  c = '\a'; break;
			case 'b':  c = '\b'; break;
			case 'f':  c = '\f'; break;
			case 'n':  c = '\n'; break;
			case 'r':  c = '\r'; break;
			case 't':  c = '\t'; break;
			case 'v':  c = '\v'; break;
			case '\\': c = '\\'; break;

			case 'c':
				if (append_literal(&tail, &literal, true) != 0) {
					goto error;
				}
				if (append_directive(&tail, bfs_printf_flush, NULL) != 0) {
					goto error;
				}
				goto done;

			case '\0':
				pretty_error(stderr_colors,
				             "error: '%s': Incomplete escape sequence '\\'.\n",
				             format);
				goto error;

			default:
				pretty_error(stderr_colors,
				             "error: '%s': Unrecognized escape sequence '\\%c'.\n",
				             format, c);
				goto error;
			}
		} else if (c == '%') {
			bfs_printf_fn *fn;

			char *directive = dstralloc(2);
			if (!directive) {
				perror("dstralloc()");
				goto directive_error;
			}
			dstrncat(&directive, &c, 1);

			const char *specifier = "s";

			// Parse any flags
			bool must_be_int = false;
			while (true) {
				c = *++i;

				switch (c) {
				case '#':
				case '0':
				case '+':
					must_be_int = true;
				case ' ':
				case '-':
					if (strchr(directive, c)) {
						pretty_error(stderr_colors,
						             "error: '%s': Duplicate flag '%c'.\n",
						             format, c);
						goto directive_error;
					}
					if (dstrncat(&directive, &c, 1) != 0) {
						perror("dstrncat()");
						goto directive_error;
					}
					continue;
				}

				break;
			}

			// Parse the field width
			while (c >= '0' && c <= '9') {
				if (dstrncat(&directive, &c, 1) != 0) {
					perror("dstrncat()");
					goto directive_error;
				}
				c = *++i;
			}

			// Parse the precision
			if (c == '.') {
				do {
					if (dstrncat(&directive, &c, 1) != 0) {
						perror("dstrncat()");
						goto directive_error;
					}
					c = *++i;
				} while (c >= '0' && c <= '9');
			}

			switch (c) {
			case '%':
				dstrfree(directive);
				goto one_char;
			case 'a':
				fn = bfs_printf_a;
				command->needs_stat = true;
				break;
			case 'b':
				fn = bfs_printf_b;
				command->needs_stat = true;
				break;
			case 'c':
				fn = bfs_printf_c;
				command->needs_stat = true;
				break;
			case 'd':
				fn = bfs_printf_d;
				specifier = "jd";
				break;
			case 'D':
				fn = bfs_printf_D;
				command->needs_stat = true;
				break;
			case 'f':
				fn = bfs_printf_f;
				break;
			case 'g':
				fn = bfs_printf_g;
				command->needs_stat = true;
				break;
			case 'G':
				fn = bfs_printf_G;
				command->needs_stat = true;
				break;
			case 'h':
				fn = bfs_printf_h;
				break;
			case 'H':
				fn = bfs_printf_H;
				break;
			case 'i':
				fn = bfs_printf_i;
				command->needs_stat = true;
				break;
			case 'k':
				fn = bfs_printf_k;
				command->needs_stat = true;
				break;
			case 'l':
				fn = bfs_printf_l;
				break;
			case 'm':
				fn = bfs_printf_m;
				specifier = "o";
				command->needs_stat = true;
				break;
			case 'M':
				fn = bfs_printf_M;
				command->needs_stat = true;
				break;
			case 'n':
				fn = bfs_printf_n;
				command->needs_stat = true;
				break;
			case 'p':
				fn = bfs_printf_p;
				break;
			case 'P':
				fn = bfs_printf_P;
				break;
			case 's':
				fn = bfs_printf_s;
				command->needs_stat = true;
				break;
			case 'S':
				fn = bfs_printf_S;
				specifier = "g";
				command->needs_stat = true;
				break;
			case 't':
				fn = bfs_printf_t;
				command->needs_stat = true;
				break;
			case 'u':
				fn = bfs_printf_u;
				command->needs_stat = true;
				break;
			case 'U':
				fn = bfs_printf_U;
				command->needs_stat = true;
				break;
			case 'y':
				fn = bfs_printf_y;
				break;
			case 'Y':
				fn = bfs_printf_Y;
				break;

			case '\0':
				pretty_error(stderr_colors,
				             "error: '%s': Incomplete format specifier '%s'.\n",
				             format, directive);
				goto directive_error;

			default:
				pretty_error(stderr_colors,
				             "error: '%s': Unrecognized format specifier '%%%c'.\n",
				             format, c);
				goto directive_error;
			}

			if (must_be_int && strcmp(specifier, "s") == 0) {
				pretty_error(stderr_colors,
				             "error: '%s': Invalid flags '%s' for string format '%%%c'.\n",
				             format, directive + 1, c);
				goto directive_error;
			}

			if (dstrcat(&directive, specifier) != 0) {
				perror("dstrcat()");
				goto directive_error;
			}

			if (append_literal(&tail, &literal, false) != 0) {
				goto directive_error;
			}
			if (append_directive(&tail, fn, directive) != 0) {
				goto directive_error;
			}
			continue;

		directive_error:
			dstrfree(directive);
			goto error;
		}

	one_char:
		if (dstrncat(&literal, &c, 1) != 0) {
			perror("dstrncat()");
			goto error;
		}
	}

done:
	if (append_literal(&tail, &literal, true) != 0) {
		goto error;
	}

	dstrfree(literal);
	return command;

error:
	dstrfree(literal);
	free_bfs_printf(command);
	return NULL;
}

int bfs_printf(FILE *file, const struct bfs_printf *command, const struct BFTW *ftwbuf) {
	int ret = -1;

	for (struct bfs_printf_directive *directive = command->directives; directive; directive = directive->next) {
		if (directive->fn(file, directive, ftwbuf) < 0) {
			goto done;
		}
	}

	ret = 0;
done:
	return ret;
}

void free_bfs_printf(struct bfs_printf *command) {
	if (command) {
		struct bfs_printf_directive *directive = command->directives;
		while (directive) {
			struct bfs_printf_directive *next = directive->next;
			dstrfree(directive->str);
			free(directive);
			directive = next;
		}

		free(command);
	}
}