/*
 * Copyright (c) 2024 Jonas 'Sortie' Termansen.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * wall.c
 * Write message to all users.
 */

#include <dirent.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <pthread.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#ifdef __sortix__
#include <sortix/limits.h>
#endif

static char* message;
static size_t message_size;

void on_alarm(int sig)
{
	(void) sig;
	_exit(0);
}

void* wall(void* arg)
{
	int fd = (int) (uintptr_t) arg;
	size_t done = 0;
	while ( done < message_size )
	{
		ssize_t amount = write(fd, message + done, message_size - done);
		if ( amount < 0 )
			break;
		done += amount;
	}
	return NULL;
}

void wall_dir(const char* path)
{
	pthread_attr_t attr;
	pthread_attr_init(&attr);
	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
	DIR* dir = opendir(path);
	if ( !dir )
	{
		warn("opendir: %s", path);
		return;
	}
	struct dirent* dirent;
	while ( (errno = 0, dirent = readdir(dir)) )
	{
		if ( !strcmp(dirent->d_name, ".") ||
		     !strcmp(dirent->d_name, "..") ||
		     !strcmp(dirent->d_name, "tty") )
			continue;
		int fd = openat(dirfd(dir), dirent->d_name, O_WRONLY);
		if ( fd < 0 )
			continue;
		if ( !isatty(fd) )
		{
			close(fd);
			continue;
		}
		pthread_t pth;
		int errnum = pthread_create(&pth, &attr, wall, (void*) (uintptr_t) fd);
		if ( errnum )
		{
			errno = errnum;
			warn("pthread_create: %s/%s", path, dirent->d_name);
		}
	}
	if ( errno )
		warn("readdir: %s", path);
	closedir(dir);
	pthread_attr_destroy(&attr);
}

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

	const char* msg = NULL;

	int opt;
	while ( (opt = getopt(argc, argv, "m:")) != -1 )
	{
		switch ( opt )
		{
		case 'm': msg = optarg; break;
		default: return 1;
		}
	}

	const char* stdin_path = "stdin";
	if ( 2 <= argc - optind )
		errx(1, "extra operand: %s", argv[optind + 1]);
	else if ( 1 <= argc - optind )
	{
		stdin_path = argv[optind];
		if ( !freopen(stdin_path, "r", stdin) )
			err(1, "%s", stdin_path);
	}

	char* login = getlogin();
	char hostname[HOST_NAME_MAX] = "?";
	gethostname(hostname, sizeof(hostname));
	char tty[TTY_NAME_MAX] = "";
	int tty_fd = open("/dev/tty", O_RDONLY);
	if ( 0 <= tty_fd )
	{
		ttyname_r(tty_fd, tty, sizeof(tty));
		close(tty_fd);
	}
	struct timespec now;
	clock_gettime(CLOCK_REALTIME, &now);
	struct tm tm;
	localtime_r(&now.tv_sec, &tm);
	char datetime[64];
	strftime(datetime, sizeof(datetime), "%Y-%m-%d %H:%M:%S %Z", &tm);

	FILE* fp = open_memstream(&message, &message_size);
	if ( !fp )
		err(1, "malloc");
	fprintf(fp, "\r\nBroadcast message from %s@%s%s%s (%s):\r\n\r\n",
	        login ? login : "?", hostname, tty[0] ? " on " : "", tty, datetime);
	bool cr = false;
	bool nl = false;
	size_t i = 0;
	while ( true )
	{
		int c = msg ? (unsigned char) msg[i++] : getchar();
		if ( !c || c == EOF )
		{
			if ( c == EOF && ferror(stdin) )
				err(1, "%s", stdin_path);
			break;
		}
		if ( c != '\t' && c != '\r' && c != '\n' && c < 32 )
			c = '?';
		if ( c == '\r' )
			cr = true, nl = false;
		else if ( c == '\n' )
		{
			if ( !cr )
				fputc('\r', fp);
			cr = true;
			nl = true;
		}
		else
			cr = false, nl = false;
		fputc(c, fp);
	}
	if ( !cr )
		fputc('\r', fp);
	if ( !nl )
		fputc('\n', fp);
	fputs("\r\n", fp);
	if ( feof(fp) || fflush(fp) == EOF )
		err(1, "malloc");
	fclose(fp);

	wall_dir("/dev");
	wall_dir("/dev/pts");

	sigset_t set;
	sigemptyset(&set);
	sigaddset(&set, SIGALRM);
	sigprocmask(SIG_UNBLOCK, &set, NULL);
	signal(SIGALRM, SIG_DFL);
	alarm(5);

	pthread_exit(NULL);
}
