/*
  einfo.c
  Informational functions
*/

/*
 * Copyright (c) 2007-2024 The OpenRC Authors.
 * See the Authors file at the top-level directory of this distribution and
 * https://github.com/OpenRC/openrc/blob/HEAD/AUTHORS
 *
 * This file is part of OpenRC. It is subject to the license terms in
 * the LICENSE file found in the top-level directory of this
 * distribution and at https://github.com/OpenRC/openrc/blob/HEAD/LICENSE
 * This file may not be copied, modified, propagated, or distributed
 *    except according to the terms contained in the LICENSE file.
 */

#include <sys/ioctl.h>
#include <errno.h>
#include <inttypes.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <syslog.h>
#include <unistd.h>

#include "einfo.h"
#include "helpers.h"

/* Incase we cannot work out how many columns from ioctl, supply a default */
#define DEFAULT_COLS		 80

#define OK			"ok"
#define NOT_OK			"!!"

/* Number of spaces for an indent */
#define INDENT_WIDTH		2

/* How wide can the indent go? */
#define INDENT_MAX		40

/* Default colours */
#define GOOD                    2
#define WARN                    3
#define BAD                     1
#define HILITE                  6
#define BRACKET                 4

/* ANSI escape codes which mimic termcap */
#define AF "\033[3%dm"
#define CE "\033[K"
#define CH "\033[%dC"
#define MD "\033[1m"
#define ME "\033[m"
#define UP "\033[A"

#define _GET_CAP(_d, _c) strlcpy(_d, tgoto(_c, 0, 0), sizeof(_d));
#define _ASSIGN_CAP(_v) do {						      \
		_v = p;							      \
		p += strlcpy(p, tmp, sizeof(ebuffer) - (p - ebuffer)) + 1;    \
	} while (0)

/* A pointer to a string to prefix to einfo/ewarn/eerror messages */
static const char *_eprefix = NULL;

/* Buffers and structures to hold the final colours */
static char ebuffer[100];
struct ecolor {
	ECOLOR color;
	int def;
	const char *name;
};
static char nullstr = '\0';

static const struct ecolor ecolors[] = {
	{ ECOLOR_GOOD,    GOOD,    "good"    },
	{ ECOLOR_WARN,    WARN,    "warn"    },
	{ ECOLOR_BAD,     BAD,     "bad"     },
	{ ECOLOR_HILITE,  HILITE,  "hilite"  },
	{ ECOLOR_BRACKET, BRACKET, "bracket" },
	{ ECOLOR_NORMAL,  0,       NULL      },
};
static const char *ecolors_str[ARRAY_SIZE(ecolors)];

static char *flush = NULL;
static char *up = NULL;
static char *goto_column = NULL;

static const char *term = NULL;
static bool term_is_cons25 = false;

/* Hardcoded list of colour capable terms
 * Only terminals without "color" in the name need to be explicitly listed */
static const char *const color_terms[] = {
	"Eterm",
	"ansi",
	"con132x25",
	"con132x30",
	"con132x43",
	"con132x60",
	"con80x25",
	"con80x28",
	"con80x30",
	"con80x43",
	"con80x50",
	"con80x60",
	"cons25",
	"console",
	"cygwin",
	"dtterm",
	"foot",
	"gnome",
	"konsole",
	"kterm",
	"linux",
	"linux-c",
	"mlterm",
	"putty",
	"rxvt",
	"rxvt-cygwin",
	"rxvt-cygwin-native",
	"rxvt-unicode",
	"screen",
	"screen-bce",
	"screen-w",
	"screen.linux",
	"vt100",
	"vt220",
	"wsvt25",
	"xterm",
	"xterm-debian",
	"xterm-kitty",
	NULL
};

#ifndef HAVE_STRLCPY
static size_t
strlcat(char *dst, const char *src, size_t size)
{
	char *d = dst;
	const char *s = src;
	size_t src_n = size;
	size_t dst_n;

	while (src_n-- != 0 && *d != '\0')
		d++;
	dst_n = d - dst;
	src_n = size - dst_n;

	if (src_n == 0)
		return dst_n + strlen(src);

	while (*s != '\0') {
		if (src_n != 1) {
			*d++ = *s;
			src_n--;
		}
		s++;
	}
	*d = '\0';

	return dst_n + (s - src);
}
#endif

static bool
yesno(const char *value)
{
	if (!value) {
		errno = ENOENT;
		return false;
	}

	if (strcasecmp(value, "yes") == 0 ||
	    strcasecmp(value, "y") == 0 ||
	    strcasecmp(value, "true") == 0 ||
	    strcasecmp(value, "on") == 0 ||
	    strcasecmp(value, "1") == 0)
		return true;

	if (strcasecmp(value, "no") != 0 &&
	    strcasecmp(value, "n") != 0 &&
	    strcasecmp(value, "false") != 0 &&
	    strcasecmp(value, "off") != 0 &&
	    strcasecmp(value, "0") != 0)
		errno = EINVAL;

	return false;
}

static bool
noyes(const char *value)
{
	int serrno = errno;
	bool retval;

	errno = 0;
	retval = yesno(value);
	if (errno == 0) {
		retval = !retval;
		errno = serrno;
	}

	return retval;
}

static bool
is_quiet(void)
{
	return yesno(getenv("EINFO_QUIET"));
}

static bool
is_really_quiet(void)
{
	return yesno(getenv("EERROR_QUIET"));
}

static bool
is_verbose(void)
{
	return yesno(getenv ("EINFO_VERBOSE"));
}

/* Fake tgoto call - very crapy, but works for our needs */
static char *
tgoto(const char *cap, int col, int line)
{
	static char buf[20];
	char *p, *e, c, dbuf[6];
	int oncol = 0, which = line, i;

	p = buf;
	e = p + sizeof(buf);
	while ((c = *cap++)) {
		if (c != '%' || ((c = *cap++) == '%')) {
			*p++ = c;
			if (p >= e) {
				errno = E2BIG;
				return NULL;
			}
			continue;
		}
		switch (c) {
		case '3':
		case '2':
		case 'd':
			i = 0;
			do
				dbuf[i++] = which % 10 | '0';
			while ((which /= 10));
			if (c != 'd') {
				c -= '0';
				if (i > c) {
					errno = EINVAL;
					return NULL;
				}
				while (i < c)
					dbuf[i++] = '0';
			}
			if (p + i >= e) {
				errno = E2BIG;
				return NULL;
			}
			do
				*p++ = dbuf[--i];
			while (i);
			break;
		case 'r':
			oncol = 0;
			break;
		case 'i':
			col++;
			line++;
			which++;
			continue;
		default:
			errno = EINVAL;
			return NULL;
		}

		oncol = 1 - oncol;
		which = oncol ? col : line;
	}
	*p = '\0';
	return buf;
}

static bool
colour_terminal(FILE * EINFO_RESTRICT f)
{
	static int in_colour = -1;
	char *e, *ee, *end, *d, *p;
	int c;
	const char *_af = NULL, *_ce = NULL, *_ch = NULL;
	const char *_md = NULL, *_me = NULL, *_up = NULL;
	const char *bold;
	char tmp[100];
	unsigned int i = 0;

	if (f && !isatty(fileno(f)))
		return false;

	if (noyes(getenv("EINFO_COLOR")))
		return false;

	if (in_colour == 0)
		return false;
	if (in_colour == 1)
		return true;

	term_is_cons25 = false;
	if (!term) {
		term = getenv("TERM");
		if (!term)
			return false;
	}
	if (strcmp(term, "cons25") == 0)
		term_is_cons25 = true;

	if (strstr(term, "color"))
		in_colour = 1;

	while (color_terms[i] && in_colour != 1) {
		if (strcmp(color_terms[i], term) == 0) {
			in_colour = 1;
		}
		i++;
	}

	if (in_colour != 1) {
		in_colour = 0;
		return false;
	}

	if (!_af)
		_af = AF;
	if (!_ce)
		_ce = CE;
	if (!_ch)
		_ch = CH;
	if (!_md)
		_md = MD;
	if (!_me)
		_me = ME;
	if (!_up)
		_up = UP;

	/* Now setup our colours */
	p = ebuffer;
	for (i = 0; i < ARRAY_SIZE(ecolors); ++i) {
		tmp[0] = '\0';
		if (ecolors[i].name) {
			bold = _md;
			c = ecolors[i].def;

			/* See if the user wants to override the colour
			 * We use a :col;bold: format like 2;1: for bold green
			 * and 1;0: for a normal red */
			if ((e = getenv("EINFO_COLOR"))) {
				ee = strstr(e, ecolors[i].name);
				if (ee)
					ee += strlen(ecolors[i].name);

				if (ee && *ee == '=') {
					d = strdup(ee + 1);
					if (d) {
						end = strchr(d, ':');
						if (end)
							*end = '\0';
						c = atoi(d);
						end = strchr(d, ';');
						if (end && *++end == '0')
							bold = _me;
						free(d);
					}
				}
			}
			strlcpy(tmp, tgoto(bold, 0, 0), sizeof(tmp));
			strlcat(tmp, tgoto(_af, 0, c & 0x07), sizeof(tmp));
		} else
			_GET_CAP(tmp, _me);

		if (tmp[0])
			_ASSIGN_CAP(ecolors_str[i]);
		else
			ecolors_str[i] = &nullstr;
	}

	_GET_CAP(tmp, _ce);
	_ASSIGN_CAP(flush);
	_GET_CAP(tmp, _up);
	_ASSIGN_CAP(up);
	strlcpy(tmp, _ch, sizeof(tmp));
	_ASSIGN_CAP(goto_column);

	in_colour = 1;
	return true;
}

static int
get_term_columns(FILE * EINFO_RESTRICT stream)
{
	struct winsize ws;
	char *env = getenv("COLUMNS");
	char *p;
	int i;

	if (env) {
		i = strtoimax(env, &p, 10);
		if (!*p)
			return i;
	}

	if (ioctl(fileno(stream), TIOCGWINSZ, &ws) == 0)
		return ws.ws_col;

	return DEFAULT_COLS;
}

void
eprefix(const char *EINFO_RESTRICT prefix)
{
	_eprefix = prefix;
}

static void EINFO_PRINTF(2, 0)
elogv(int level, const char *EINFO_RESTRICT fmt, va_list ap)
{
	char *e = getenv("EINFO_LOG");
	va_list apc;

	if (fmt && e) {
		closelog();
		openlog(e, LOG_PID, LOG_DAEMON);
		va_copy(apc, ap);
		vsyslog(level, fmt, apc);
		va_end(apc);
		closelog();
	}
}

void
elog(int level, const char *EINFO_RESTRICT fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	elogv(level, fmt, ap);
	va_end(ap);
}

static int
_eindent(FILE * EINFO_RESTRICT stream)
{
	char *env = getenv("EINFO_INDENT");
	int amount = 0;
	char indent[INDENT_MAX];

	if (env) {
		errno = 0;
		amount = strtoimax(env, NULL, 0);
		if (errno != 0 || amount < 0)
			amount = 0;
		else if (amount > INDENT_MAX)
			amount = INDENT_MAX;

		if (amount > 0)
			memset(indent, ' ', (size_t)amount);
	}

	/* Terminate it */
	memset(indent + amount, 0, 1);
	return fprintf(stream, "%s", indent);
}

static const char *
_ecolor(FILE * EINFO_RESTRICT f, ECOLOR color)
{
	unsigned int i;

	if (!colour_terminal(f))
		return "";

	for (i = 0; i < ARRAY_SIZE(ecolors); ++i)
		if (ecolors[i].color == color)
			return ecolors_str[i];
	return "";
}

const char *
ecolor(ECOLOR color)
{
	FILE *f = stdout;

	/* Try and guess a valid tty */
	if (!isatty(fileno(f))) {
		f = stderr;
		if (!isatty(fileno(f))) {
			f = stdin;
			if (!isatty(fileno(f)))
				f = NULL;
		}
	}

	return _ecolor(f, color);
}

#define LASTCMD(_cmd) {							      \
		unsetenv("EINFO_LASTCMD");				      \
		setenv("EINFO_LASTCMD", _cmd, 1);			      \
	}

static int EINFO_PRINTF(3, 0)
	_einfo(FILE *f, ECOLOR color, const char *EINFO_RESTRICT fmt, va_list va)
{
	int retval = 0;
	char *last = getenv("EINFO_LASTCMD");
	va_list ap;

	if (last &&
	    !colour_terminal(f) &&
	    strcmp(last, "ewarn") != 0 &&
	    last[strlen(last) - 1] == 'n')
		fprintf(f, "\n");
	if (_eprefix)
		fprintf(f, "%s%s%s|", _ecolor(f, color), _eprefix, _ecolor(f, ECOLOR_NORMAL));
	fprintf(f, " %s*%s ", _ecolor(f, color), _ecolor(f, ECOLOR_NORMAL));
	retval += _eindent(f);
	va_copy(ap, va);
	retval += vfprintf(f, fmt, ap) + 3;
	va_end(ap); \
	if (colour_terminal(f))
		fprintf(f, "%s", flush);
	return retval;
}

#define _einfovn(fmt, ap) _einfo(stdout, ECOLOR_GOOD, fmt, ap)
#define _ewarnvn(fmt, ap) _einfo(stderr, ECOLOR_WARN, fmt, ap)
#define _eerrorvn(fmt, ap) _einfo(stderr, ECOLOR_BAD, fmt, ap)

int
einfon(const char *EINFO_RESTRICT fmt, ...)
{
	int retval;
	va_list ap;

	if (!fmt || is_quiet())
		return 0;
	va_start(ap, fmt);
	retval = _einfovn(fmt, ap);
	va_end(ap);
	LASTCMD("einfon");
	return retval;
}

int
ewarnn(const char *EINFO_RESTRICT fmt, ...)
{
	int retval;
	va_list ap;

	if (!fmt || is_quiet())
		return 0;
	va_start(ap, fmt);
	retval = _ewarnvn(fmt, ap);
	va_end(ap);
	LASTCMD("ewarnn");
	return retval;
}

int
eerrorn(const char *EINFO_RESTRICT fmt, ...)
{
	int retval;
	va_list ap;

	if (!fmt || is_really_quiet())
		return 0;
	va_start(ap, fmt);
	retval = _eerrorvn(fmt, ap);
	va_end(ap);
	LASTCMD("errorn");
	return retval;
}

int
einfo(const char *EINFO_RESTRICT fmt, ...)
{
	int retval;
	va_list ap;

	if (!fmt || is_quiet())
		return 0;
	va_start(ap, fmt);
	retval = _einfovn(fmt, ap);
	retval += printf("\n");
	va_end(ap);
	LASTCMD("einfo");
	return retval;
}

int
ewarn(const char *EINFO_RESTRICT fmt, ...)
{
	int retval;
	va_list ap;

	if (!fmt || is_quiet())
		return 0;
	va_start(ap, fmt);
	elogv(LOG_WARNING, fmt, ap);
	retval = _ewarnvn(fmt, ap);
	retval += fprintf(stderr, "\n");
	va_end(ap);
	LASTCMD("ewarn");
	return retval;
}

void
ewarnx(const char *EINFO_RESTRICT fmt, ...)
{
	va_list ap;

	if (fmt && !is_quiet()) {
		va_start(ap, fmt);
		elogv(LOG_WARNING, fmt, ap);
		_ewarnvn(fmt, ap);
		va_end(ap);
		fprintf(stderr, "\n");
	}
	exit(EXIT_FAILURE);
}

int
eerror(const char *EINFO_RESTRICT fmt, ...)
{
	int retval;
	va_list ap;

	if (!fmt || is_really_quiet())
		return 0;
	va_start(ap, fmt);
	elogv(LOG_ERR, fmt, ap);
	retval = _eerrorvn(fmt, ap);
	va_end(ap);
	retval += fprintf(stderr, "\n");
	LASTCMD("eerror");
	return retval;
}

void
eerrorx(const char *EINFO_RESTRICT fmt, ...)
{
	va_list ap;

	if (fmt && !is_really_quiet()) {
		va_start(ap, fmt);
		elogv(LOG_ERR, fmt, ap);
		_eerrorvn(fmt, ap);
		va_end(ap);
		fprintf(stderr, "\n");
	}
	exit(EXIT_FAILURE);
}

int
ebegin(const char *EINFO_RESTRICT fmt, ...)
{
	int retval;
	va_list ap;

	if (!fmt || is_quiet())
		return 0;
	va_start(ap, fmt);
	retval = _einfovn(fmt, ap);
	va_end(ap);
	retval += printf(" ...");
	if (colour_terminal(stdout))
		retval += printf("\n");
	LASTCMD("ebegin");
	return retval;
}

static void
_eend(FILE * EINFO_RESTRICT fp, int col, ECOLOR color, const char *msg)
{
	int i;
	int cols;

	if (!msg)
		return;

	cols = get_term_columns(fp) - (strlen(msg) + 5);

	/* cons25 is special - we need to remove one char, otherwise things
	 * do not align properly at all. */
	if (!term) {
		term = getenv("TERM");
		if (term && strcmp(term, "cons25") == 0)
			term_is_cons25 = true;
		else
			term_is_cons25 = false;
	}
	if (term_is_cons25)
		cols--;

	if (cols > 0 && colour_terminal(fp)) {
		fprintf(fp, "%s%s %s[%s %s %s]%s\n", up, tgoto(goto_column, 0, cols),
		    ecolor(ECOLOR_BRACKET), ecolor(color), msg,
		    ecolor(ECOLOR_BRACKET), ecolor(ECOLOR_NORMAL));
	} else {
		if (col > 0)
			for (i = 0; i < cols - col; i++)
				fprintf(fp, " ");
		fprintf(fp, " [ %s ]\n", msg);
	}
}

static int EINFO_PRINTF(3, 0)
_do_eend(const char *cmd, int retval,
    const char *EINFO_RESTRICT fmt, va_list ap)
{
	int col = 0;
	FILE *fp = stdout;
	va_list apc;

	if (fmt && *fmt != '\0' && retval != 0) {
		fp = stderr;
		va_copy(apc, ap);
		if (strcmp(cmd, "ewend") == 0)
			col = _ewarnvn(fmt, apc);
		else
			col = _eerrorvn(fmt, apc);
		col += fprintf(fp, "\n");
		va_end(apc);
	}
	_eend(fp, col,
	    retval == 0 ? ECOLOR_GOOD : ECOLOR_BAD,
	    retval == 0 ? OK : NOT_OK);
	return retval;
}

int
eend(int retval, const char *EINFO_RESTRICT fmt, ...)
{
	va_list ap;

	if (is_quiet())
		return retval;
	va_start(ap, fmt);
	_do_eend("eend", retval, fmt, ap);
	va_end(ap);
	LASTCMD("eend");
	return retval;
}

int
ewend(int retval, const char *EINFO_RESTRICT fmt, ...)
{
	va_list ap;

	if (is_quiet())
		return retval;
	va_start(ap, fmt);
	_do_eend("ewend", retval, fmt, ap);
	va_end(ap);
	LASTCMD("ewend");
	return retval;
}

void
ebracket(int col, ECOLOR color, const char *msg)
{
	_eend(stdout, col, color, msg);
}

void
eindent(void)
{
	char *env = getenv("EINFO_INDENT");
	int amount = 0;
	char *num;

	if (env) {
		errno = 0;
		amount = strtoimax(env, NULL, 0);
		if (errno != 0)
			amount = 0;
	}
	amount += INDENT_WIDTH;
	if (amount > INDENT_MAX)
		amount = INDENT_MAX;
	xasprintf(&num, "%08d", amount);
	setenv("EINFO_INDENT", num, 1);
	free(num);
}

void eoutdent(void)
{
	char *env = getenv("EINFO_INDENT");
	int amount = 0;
	char *num = NULL;
	int serrno = errno;

	if (!env)
		return;
	errno = 0;
	amount = strtoimax(env, NULL, 0);
	if (errno != 0)
		amount = 0;
	else
		amount -= INDENT_WIDTH;
	if (amount <= 0)
		unsetenv("EINFO_INDENT");
	else {
		xasprintf(&num, "%08d", amount);
		setenv("EINFO_INDENT", num, 1);
		free(num);
	}
	errno = serrno;
}

int
einfovn(const char *EINFO_RESTRICT fmt, ...)
{
	int retval;
	va_list ap;

	if (!fmt || !is_verbose())
		return 0;
	va_start(ap, fmt);
	retval = _einfovn(fmt, ap);
	va_end(ap);
	LASTCMD("einfovn");
	return retval;
}

int
ewarnvn(const char *EINFO_RESTRICT fmt, ...)
{
	int retval;
	va_list ap;

	if (!fmt || !is_verbose())
		return 0;
	va_start(ap, fmt);
	retval = _ewarnvn(fmt, ap);
	va_end(ap);
	LASTCMD("ewarnvn");
	return retval;
}

int
einfov(const char *EINFO_RESTRICT fmt, ...)
{
	int retval;
	va_list ap;

	if (!fmt || !is_verbose())
		return 0;
	va_start(ap, fmt);
	retval = _einfovn(fmt, ap);
	retval += printf("\n");
	va_end(ap);
	LASTCMD("einfov");
	return retval;
}

int
ewarnv(const char *EINFO_RESTRICT fmt, ...)
{
	int retval;
	va_list ap;

	if (!fmt || !is_verbose())
		return 0;
	va_start(ap, fmt);
	retval = _ewarnvn(fmt, ap);
	retval += printf("\n");
	va_end(ap);
	LASTCMD("ewarnv");
	return retval;
}

int
ebeginv(const char *EINFO_RESTRICT fmt, ...)
{
	int retval;
	va_list ap;

	if (!fmt || !is_verbose())
		return 0;

	va_start(ap, fmt);
	retval = _einfovn(fmt, ap);
	retval += printf(" ...");
	if (colour_terminal(stdout))
		retval += printf("\n");
	va_end(ap);
	LASTCMD("ebeginv");
	return retval;
}

int
eendv(int retval, const char *EINFO_RESTRICT fmt, ...)
{
	va_list ap;

	if (!is_verbose())
		return 0;
	va_start(ap, fmt);
	_do_eend("eendv", retval, fmt, ap);
	va_end(ap);
	LASTCMD("eendv");
	return retval;
}

int
ewendv(int retval, const char *EINFO_RESTRICT fmt, ...)
{
	va_list ap;

	if (!is_verbose())
		return 0;
	va_start(ap, fmt);
	_do_eend("ewendv", retval, fmt, ap);
	va_end(ap);
	LASTCMD("ewendv");
	return retval;
}

void
eindentv(void)
{
	if (is_verbose())
		eindent();
}

void
eoutdentv(void)
{
	if (is_verbose())
		eoutdent();
}
