// Copyright © Tavian Barnes <tavianator@tavianator.com>
// SPDX-License-Identifier: 0BSD

#include "bfstd.h"
#include "sanity.h"
#include "xtime.h"

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

/** Parsed xtouch arguments. */
struct args {
	/** Simple flags. */
	enum {
		/** Don't create nonexistent files (-c). */
		NO_CREATE = 1 << 0,
		/** Don't follow symlinks (-h). */
		NO_FOLLOW = 1 << 1,
		/** Create any missing parent directories (-p). */
		CREATE_PARENTS = 1 << 2,
	} flags;

	/** Timestamps (-r|-t|-d). */
	struct timespec times[2];

	/** File creation mode (-M; default 0666 & ~umask). */
	mode_t fmode;
	/** Directory creation mode (-M; default 0777 & ~umask). */
	mode_t dmode;
	/** Parent directory creation mode (0777 & ~umask). */
	mode_t pmode;
};

/** Open (and maybe create) a single directory. */
static int open_dir(const struct args *args, int dfd, const char *path) {
	int ret = openat(dfd, path, O_SEARCH | O_DIRECTORY);

	if (ret < 0 && errno == ENOENT && (args->flags & CREATE_PARENTS)) {
		if (mkdirat(dfd, path, args->pmode) == 0 || errno == EEXIST) {
			ret = openat(dfd, path, O_SEARCH | O_DIRECTORY);
		}
	}

	return ret;
}

/** Open (and maybe create) the parent directory of the path. */
static int open_parent(const struct args *args, const char **path) {
	size_t max = xbaseoff(*path);
	if (max == 0) {
		return AT_FDCWD;
	}

	char *dir = strndup(*path, max);
	if (!dir) {
		return -1;
	}

	// Optimistically try the whole path first
	int dfd = open_dir(args, AT_FDCWD, dir);
	if (dfd >= 0) {
		goto done;
	}

	if (errno == ENOENT) {
		if (!(args->flags & CREATE_PARENTS)) {
			goto err;
		}
	} else if (!errno_is_like(ENAMETOOLONG)) {
		goto err;
	}

	// Open the parents one-at-a-time
	dfd = AT_FDCWD;
	char *cur = dir;
	while (*cur) {
		char *next = cur;
		next += strcspn(next, "/");
		next += strspn(next, "/");

		char c = *next;
		*next = '\0';

		int parent = dfd;
		dfd = open_dir(args, parent, cur);
		if (parent >= 0) {
			close_quietly(parent);
		}
		if (dfd < 0) {
			goto err;
		}

		*next = c;
		cur = next;
	}

done:
	*path += max;
err:
	free(dir);
	return dfd;
}

/** Compute flags for fstatat()/utimensat(). */
static int at_flags(const struct args *args) {
	if (args->flags & NO_FOLLOW) {
		return AT_SYMLINK_NOFOLLOW;
	} else {
		return 0;
	}
}

/** Touch one path. */
static int xtouch(const struct args *args, const char *path) {
	int dfd = open_parent(args, &path);
	if (dfd < 0 && dfd != (int)AT_FDCWD) {
		return -1;
	}

	int ret = utimensat(dfd, path, args->times, at_flags(args));
	if (ret == 0 || errno != ENOENT) {
		goto done;
	}

	if (args->flags & NO_CREATE) {
		ret = 0;
		goto done;
	}

	size_t len = strlen(path);
	if (len > 0 && path[len - 1] == '/') {
		if (mkdirat(dfd, path, args->dmode) == 0) {
			ret = utimensat(dfd, path, args->times, at_flags(args));
		}
	} else {
		int fd = openat(dfd, path, O_WRONLY | O_CREAT, args->fmode);
		if (fd >= 0) {
			if (futimens(fd, args->times) == 0) {
				ret = xclose(fd);
			} else {
				close_quietly(fd);
			}
		}
	}

done:
	if (dfd >= 0) {
		close_quietly(dfd);
	}
	return ret;
}

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

	mode_t mask = umask(0);

	struct args args = {
		.flags = 0,
		.times = {
			{ .tv_nsec = UTIME_OMIT },
			{ .tv_nsec = UTIME_OMIT },
		},
		.fmode = 0666 & ~mask,
		.dmode = 0777 & ~mask,
		.pmode = 0777 & ~mask,
	};

	bool atime = false, mtime = false;
	const char *darg = NULL;
	const char *marg = NULL;
	const char *rarg = NULL;

	const char *cmd = argc > 0 ? argv[0] : "xtouch";
	int c;
	while (c = getopt(argc, argv, ":M:acd:hmpr:t:"), c != -1) {
		switch (c) {
		case 'M':
			marg = optarg;
			break;
		case 'a':
			atime = true;
			break;
		case 'c':
			args.flags |= NO_CREATE;
			break;
		case 'd':
		case 't':
			darg = optarg;
			break;
		case 'h':
			args.flags |= NO_FOLLOW;
			break;
		case 'm':
			mtime = true;
			break;
		case 'p':
			args.flags |= CREATE_PARENTS;
			break;
		case 'r':
			rarg = optarg;
			break;
		case ':':
			fprintf(stderr, "%s: Missing argument to -%c\n", cmd, optopt);
			return EXIT_FAILURE;
		case '?':
			fprintf(stderr, "%s: Unrecognized option -%c\n", cmd, optopt);
			return EXIT_FAILURE;
		}
	}

	if (marg) {
		unsigned int mode;
		if (xstrtoui(marg, NULL, 8, &mode) == 0 && mode < 01000) {
			args.fmode = args.dmode = mode;
		} else {
			fprintf(stderr, "%s: Invalid mode '%s'\n", cmd, marg);
			return EXIT_FAILURE;
		}
	}

	struct timespec times[2];

	if (rarg) {
		struct stat buf;
		if (fstatat(AT_FDCWD, rarg, &buf, at_flags(&args)) != 0) {
			fprintf(stderr, "%s: '%s': %s\n", cmd, rarg, xstrerror(errno));
			return EXIT_FAILURE;
		}
		times[0] = ST_ATIM(buf);
		times[1] = ST_MTIM(buf);
	} else if (darg) {
		if (xgetdate(darg, &times[0]) != 0) {
			fprintf(stderr, "%s: Parsing time '%s' failed: %s\n", cmd, darg, xstrerror(errno));
			return EXIT_FAILURE;
		}
		times[1] = times[0];
	} else {
		// Don't use UTIME_NOW, so that multiple paths all get the same timestamp
		if (clock_gettime(CLOCK_REALTIME, &times[0]) != 0) {
			perror("clock_gettime()");
			return EXIT_FAILURE;
		}
		times[1] = times[0];
	}

	if (!atime && !mtime) {
		atime = true;
		mtime = true;
	}
	if (atime) {
		args.times[0] = times[0];
	}
	if (mtime) {
		args.times[1] = times[1];
	}

	if (optind >= argc) {
		fprintf(stderr, "%s: No files to touch\n", cmd);
		return EXIT_FAILURE;
	}

	int ret = EXIT_SUCCESS;
	for (; optind < argc; ++optind) {
		const char *path = argv[optind];
		if (xtouch(&args, path) != 0) {
			fprintf(stderr, "%s: '%s': %s\n", cmd, path, xstrerror(errno));
			ret = EXIT_FAILURE;
		}
	}
	return ret;
}
