/* Copyright © Charliecloud contributors. */

#define _GNU_SOURCE
#include "config.h"

#include <string.h>
#include <unistd.h>

#include "all.h"

/** Constants **/

/* Text colors. Note leading escape characters (U+001B), which don’t always
   show up depending on your viewer.

   In principle, we should be using a library for this, e.g. terminfo(5).
   However, moderately thorough web searching suggests that pretty much any
   modern terminal will support 256-color ANSI codes, and this is way
   simpler [1]. Probably should coordinate these colors with the Python code
   somehow.

   [1]: https://stackoverflow.com/a/3219471 */
static const char COLOUR_CYAN_DARK[] =  "[0;38;5;6m";
static const char COLOUR_CYAN_LIGHT[] = "[0;38;5;14m";
//static const char COLOUR_GRAY[] =       "[0;90m";
static const char COLOUR_RED[] =        "[0;31m";
static const char COLOUR_RED_BOLD[] =   "[1;31m";
static const char COLOUR_RESET[] =      "[0m";
static const char COLOUR_YELLOW[] =     "[0;33m";
static const char *_LL_COLOURS[] = { COLOUR_RED_BOLD,     // fatal
                                     COLOUR_RED_BOLD,     // stderr
                                     COLOUR_RED,          // warning
                                     COLOUR_YELLOW,       // info
                                     COLOUR_CYAN_LIGHT,   // verbose
                                     COLOUR_CYAN_DARK,    // debug
                                     COLOUR_CYAN_DARK };  // trace
/* This lets us index by verbosity, which can be negative. */
static const char **LL_COLOURS = _LL_COLOURS + 3;

/* Number of supplemental GIDs we can deal with. */
static const size_t SUPP_GIDS_MAX = 128;


/** External variables **/

/* If true, exit abnormally on fatal error. Set in ch-run.c during argument
   parsing, so will always be default value before that. */
bool abort_fatal = false;

/* If true, use coloured logging. Set in ch-run.c. */
bool log_color_p = false;

/* Level of chatter on stderr. */
enum log_level verbose;

/* List of warnings to be re-printed on exit. This is a buffer of shared memory
   allocated by mmap(2), structured as a sequence of null-terminated character
   strings. Warnings that do not fit in this buffer will be lost, though we
   allocate enough memory that this is unlikely. See “string_append()” for
   more details. */
char *warnings;

/* Current byte offset from start of “warnings” buffer. This gives the address
   where the next appended string will start. This means that the null
   terminator of the previous string is warnings_offset - 1. */
size_t warnings_offset = 0;


/** Function prototypes (private) **/

void msgv(enum log_level level, const char *file, int line, int errno_,
          const char *fmt, va_list ap)
         __attribute__ ((format (printf, 5, 0)));


/** Functions **/

/* If verbose enough, print uids and gids on stderr prefixed with where.

   FIXME: Should change to DEBUG(), but that will give the file/line within
   this function, which we don’t want. */
void log_ids(const char *func, int line)
{
   uid_t ruid, euid, suid;
   gid_t rgid, egid, sgid;
   gid_t supp_gids[SUPP_GIDS_MAX];
   int supp_gid_ct;

   if (verbose >= LL_TRACE + 1) {  // don’t bother b/c haven’t needed in ages
      Z_e (getresuid(&ruid, &euid, &suid));
      Z_e (getresgid(&rgid, &egid, &sgid));
      if (log_color_p)
         T_e (EOF != fputs(LL_COLOURS[LL_TRACE], stderr));
      fprintf(stderr, "%s %d: uids=%d,%d,%d, gids=%d,%d,%d + ", func, line,
              ruid, euid, suid, rgid, egid, sgid);
      supp_gid_ct = getgroups(SUPP_GIDS_MAX, supp_gids);
      if (supp_gid_ct == -1) {
         T_e (errno == EINVAL);
         Tf_ (0, "more than %zu groups", SUPP_GIDS_MAX);
      }
      for (int i = 0; i < supp_gid_ct; i++) {
         if (i > 0)
            fprintf(stderr, ",");
         fprintf(stderr, "%d", supp_gids[i]);
      }
      fprintf(stderr, "\n");
      if (log_color_p)
         T_e (EOF != fputs(COLOUR_RESET, stderr));
      Z_e (fflush(stderr));
   }
}

/* Set up logging. Note ch-run(1) specifies a bunch of color synonyms; this
   translation happens during argument parsing.*/
void logging_init(enum log_color_when when, enum log_test test)
{
   // set up colors
   switch (when) {
   case LL_COLOR_AUTO:
      if (isatty(fileno(stderr)))
         log_color_p = true;
      else {
         T_e (errno == ENOTTY);
         log_color_p = false;
      }
      break;
   case LL_COLOR_YES:
      log_color_p = true;
      break;
   case LL_COLOR_NO:
      log_color_p = false;
      break;
   case LL_COLOR_NULL:
      T__ (0);  // unreachable
      break;
   }

   // test logging
   if (test >= LL_TEST_YES) {
      TRACE("trace");
      DEBUG("debug");
      VERBOSE("verbose");
      INFO("info");
      WARNING("warning");
      if (test >= LL_TEST_FATAL)
         FATAL(-1, "the program failed inexplicably (\"log-fail\" specified)");
      exit(0);
   }
}

/* Print a formatted message on stderr if the level warrants it. */
void msg(enum log_level level, const char *file, int line, int errno_,
         const char *fmt, ...)
{
   va_list ap;

   va_start(ap, fmt);
   msgv(level, file, line, errno_, fmt, ap);
   va_end(ap);
}

void msg_error(const char *file, int line, int errno_,
               const char *fmt, ...)
{
   va_list ap;

   va_start(ap, fmt);
   /* We print errors at LL_FATAL because, according to our documentation,
      errors are never suppressed. Perhaps we need to rename this log level
      (see issue #1914). */
   msgv(LL_FATAL, file, line, errno_, fmt, ap);
   va_end(ap);
}

/* Note that msg_fatal doesn’t call msg_error like we do in the Python code
   because the variable number of arguments make it easier to simply define
   separate functions. */
noreturn msg_fatal(const char *file, int line, int errno_,
                        const char *fmt, ...)
{
   va_list ap;

   va_start(ap, fmt);
   msgv(LL_FATAL, file, line, errno_, fmt, ap);
   va_end(ap);

   if (abort_fatal)
      abort();
   else
      exit(EXIT_ERR_MISC);
}

/* va_list form of msg(). */
void msgv(enum log_level level, const char *file, int line, int errno_,
          const char *fmt, va_list ap)
{
   // note: all components contain appropriate leading/trailing space
   char *text_formatted;  // caller’s message, formatted
   char *level_prefix;    // level prefix
   char *errno_code;      // errno code/number
   char *errno_desc;      // errno description
   char *text_full;       // complete text but w/o color codes
   const char * colour;          // ANSI codes for color
   const char * colour_reset;    // ANSI codes to reset color

   if (level > verbose)   // not verbose enough; do nothing
      return;

   // Format caller message.
   if (fmt == NULL)
      text_formatted = "assertion failure: please report this bug";
   else
      text_formatted = vasprintf_ch(fmt, ap);

   // Prefix some of the levels.
   switch (level) {
   case LL_FATAL:
      level_prefix = "error: ";   // "fatal" too morbid for users
      break;
   case LL_WARNING:
      level_prefix = "warning: ";
      break;
   default:
      level_prefix = "";
      break;
   }

   // errno.
   if (errno_ < 0) {
      errno_code = "";
      errno_desc = "";
   } else {
      errno_code = cat(" ", errno_nerd_str(errno_));
      errno_desc = asprintf_ch(": %s", strerror(errno_));
   }

   // Color.
   if (log_color_p) {
      colour = LL_COLOURS[level];
      colour_reset = COLOUR_RESET;
   } else {
      colour = "";
      colour_reset = "";
   };

   // Format and print.
   text_full = asprintf_ch("%s[%d]: %s%s%s (%s:%d%s)",
                           program_invocation_short_name, getpid(),
                           level_prefix, text_formatted, errno_desc,
                           file, line, errno_code);
   fprintf(stderr, "%s%s%s\n", colour, text_full, colour_reset);
   if (fflush(stderr))
      abort();  // can’t print an error b/c already trying to do that
   if (level == LL_WARNING)
      warnings_offset += string_append(warnings, text_full,
                                       WARNINGS_SIZE, warnings_offset);
}

/* Reprint messages stored in “warnings” memory buffer. */
void warnings_reprint(void)
{
   size_t offset = 0;
   int warn_ct = buf_strings_count(warnings, WARNINGS_SIZE);

   if (warn_ct > 0) {
      if (log_color_p)
         T_e (EOF != fputs(LL_COLOURS[LL_WARNING], stderr));
      T_e (1 <= fprintf(stderr, "%s[%d]: reprinting first %d warning(s)\n",
                       program_invocation_short_name, getpid(), warn_ct));
      while (   warnings[offset] != 0
             || (offset < (WARNINGS_SIZE - 1) && warnings[offset+1] != 0)) {
         T_e (EOF != fputs(warnings + offset, stderr));
         T_e (EOF != fputc('\n', stderr));
         offset += strlen(warnings + offset) + 1;
      }
      if (log_color_p)
         T_e (EOF != fputs(COLOUR_RESET, stderr));
      if (fflush(stderr))
         abort();  // can't print an error b/c already trying to do that
   }
}
