/*
 * Copyright © 2017 Google, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice (including the next
 * paragraph) shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "c11/threads.h"
#include "util/detect_os.h"
#include "util/log.h"
#include "util/ralloc.h"
#include "util/u_debug.h"

#if DETECT_OS_UNIX
#include <syslog.h>
#include "util/u_process.h"
#endif

#if DETECT_OS_ANDROID
#include <android/log.h>
#endif

#if DETECT_OS_WINDOWS
#include <windows.h>
#endif

enum mesa_log_control {
   MESA_LOG_CONTROL_NULL = 1 << 0,
   MESA_LOG_CONTROL_FILE = 1 << 1,
   MESA_LOG_CONTROL_SYSLOG = 1 << 2,
   MESA_LOG_CONTROL_ANDROID = 1 << 3,
   MESA_LOG_CONTROL_WINDBG = 1 << 4,
   MESA_LOG_CONTROL_LOGGER_MASK = 0xff,

   MESA_LOG_CONTROL_WAIT = 1 << 8,
};

static const struct debug_control mesa_log_control_options[] = {
   /* loggers */
   { "null", MESA_LOG_CONTROL_NULL },
   { "file", MESA_LOG_CONTROL_FILE },
   { "syslog", MESA_LOG_CONTROL_SYSLOG },
   { "android", MESA_LOG_CONTROL_ANDROID },
   { "windbg", MESA_LOG_CONTROL_WINDBG },
   /* flags */
   { "wait", MESA_LOG_CONTROL_WAIT },
   { NULL, 0 },
};

static uint32_t mesa_log_control;
static FILE *mesa_log_file;

static void
mesa_log_init_once(void)
{
   mesa_log_control = parse_debug_string(os_get_option("MESA_LOG"),
         mesa_log_control_options);

   if (!(mesa_log_control & MESA_LOG_CONTROL_LOGGER_MASK)) {
      /* pick the default loggers */
#if DETECT_OS_ANDROID
      mesa_log_control |= MESA_LOG_CONTROL_ANDROID;
#else
      mesa_log_control |= MESA_LOG_CONTROL_FILE;
#endif

#if DETECT_OS_WINDOWS
      /* stderr from windows applications without console is not usually
       * visible, so communicate with the debugger instead */
      mesa_log_control |= MESA_LOG_CONTROL_WINDBG;
#endif
   }

   mesa_log_file = stderr;

#if !DETECT_OS_WINDOWS
   if (geteuid() == getuid()) {
      const char *log_file = os_get_option("MESA_LOG_FILE");
      if (log_file) {
         FILE *fp = fopen(log_file, "w");
         if (fp) {
            mesa_log_file = fp;
            mesa_log_control |= MESA_LOG_CONTROL_FILE;
         }
      }
   }
#endif

#if DETECT_OS_UNIX
   if (mesa_log_control & MESA_LOG_CONTROL_SYSLOG)
      openlog(util_get_process_name(), LOG_NDELAY | LOG_PID, LOG_USER);
#endif
}

static void
mesa_log_init(void)
{
   static once_flag once = ONCE_FLAG_INIT;
   call_once(&once, mesa_log_init_once);
}

static inline const char *
level_to_str(enum mesa_log_level l)
{
   switch (l) {
   case MESA_LOG_ERROR: return "error";
   case MESA_LOG_WARN: return "warning";
   case MESA_LOG_INFO: return "info";
   case MESA_LOG_DEBUG: return "debug";
   }

   unreachable("bad mesa_log_level");
}

enum logger_vasnprintf_affix {
   LOGGER_VASNPRINTF_AFFIX_TAG = 1 << 0,
   LOGGER_VASNPRINTF_AFFIX_LEVEL = 1 << 1,
   LOGGER_VASNPRINTF_AFFIX_NEWLINE = 1 << 2,
};

/* Try vsnprintf first and fall back to vasprintf if buf is too small.  This
 * function handles all errors and never fails.
 */
static char *
logger_vasnprintf(char *buf,
                  int size,
                  int affixes,
                  enum mesa_log_level level,
                  const char *tag,
                  const char *format,
                  va_list in_va)
{
   struct {
      char *cur;
      int rem;
      int total;
      bool invalid;
   } state = {
      .cur = buf,
      .rem = size,
   };

   va_list va;
   va_copy(va, in_va);

#define APPEND(state, func, ...)                                     \
   do {                                                              \
      int ret = func(state.cur, state.rem, __VA_ARGS__);             \
      if (ret < 0) {                                                 \
         state.invalid = true;                                       \
      }  else {                                                      \
         state.total += ret;                                         \
         if (ret >= state.rem)                                       \
            ret = state.rem;                                         \
         state.cur += ret;                                           \
         state.rem -= ret;                                           \
      }                                                              \
   } while (false)

   if (affixes & LOGGER_VASNPRINTF_AFFIX_TAG)
      APPEND(state, snprintf, "%s: ", tag);
   if (affixes & LOGGER_VASNPRINTF_AFFIX_LEVEL)
      APPEND(state, snprintf, "%s: ", level_to_str(level));

   APPEND(state, vsnprintf, format, va);

   if (affixes & LOGGER_VASNPRINTF_AFFIX_NEWLINE) {
      if (state.cur == buf || state.cur[-1] != '\n')
         APPEND(state, snprintf, "\n");
   }
#undef APPEND

   assert(size >= 64);
   if (state.invalid) {
      strncpy(buf, "invalid message format", size);
   } else if (state.total >= size) {
      /* print again into alloc to avoid truncation */
      void *alloc = malloc(state.total + 1);
      if (alloc) {
         buf = logger_vasnprintf(alloc, state.total + 1, affixes, level, tag,
               format, in_va);
         assert(buf == alloc);
      } else {
         /* pretty-truncate the message */
         strncpy(buf + size - 4, "...", 4);
      }
   }

   va_end(va);

   return buf;
}

static void
logger_file(enum mesa_log_level level,
            const char *tag,
            const char *format,
            va_list va)
{
   FILE *fp = mesa_log_file;
   char local_msg[1024];
   char *msg = logger_vasnprintf(local_msg, sizeof(local_msg),
         LOGGER_VASNPRINTF_AFFIX_TAG |
         LOGGER_VASNPRINTF_AFFIX_LEVEL |
         LOGGER_VASNPRINTF_AFFIX_NEWLINE,
         level, tag, format, va);

   fprintf(fp, "%s", msg);
   fflush(fp);

   if (msg != local_msg)
      free(msg);
}

#if DETECT_OS_UNIX

static inline int
level_to_syslog(enum mesa_log_level l)
{
   switch (l) {
   case MESA_LOG_ERROR: return LOG_ERR;
   case MESA_LOG_WARN: return LOG_WARNING;
   case MESA_LOG_INFO: return LOG_INFO;
   case MESA_LOG_DEBUG: return LOG_DEBUG;
   }

   unreachable("bad mesa_log_level");
}

static void
logger_syslog(enum mesa_log_level level,
              const char *tag,
              const char *format,
              va_list va)
{
   char local_msg[1024];
   char *msg = logger_vasnprintf(local_msg, sizeof(local_msg),
         LOGGER_VASNPRINTF_AFFIX_TAG, level, tag, format, va);

   syslog(level_to_syslog(level), "%s", msg);

   if (msg != local_msg)
      free(msg);
}

#endif /* DETECT_OS_UNIX */

#if DETECT_OS_ANDROID

static inline android_LogPriority
level_to_android(enum mesa_log_level l)
{
   switch (l) {
   case MESA_LOG_ERROR: return ANDROID_LOG_ERROR;
   case MESA_LOG_WARN: return ANDROID_LOG_WARN;
   case MESA_LOG_INFO: return ANDROID_LOG_INFO;
   case MESA_LOG_DEBUG: return ANDROID_LOG_DEBUG;
   }

   unreachable("bad mesa_log_level");
}

static void
logger_android(enum mesa_log_level level,
               const char *tag,
               const char *format,
               va_list va)
{
   /* Android can truncate/drop messages
    *
    *  - the internal buffer for vsnprintf has a fixed size (usually 1024)
    *  - the socket to logd is non-blocking
    *
    * and provides no way to detect.  Try our best.
    */
   char local_msg[1024];
   char *msg = logger_vasnprintf(local_msg, sizeof(local_msg), 0, level, tag,
         format, va);

   __android_log_write(level_to_android(level), tag, msg);

   if (msg != local_msg)
      free(msg);

   /* increase the chance of logd doing its part */
   if (mesa_log_control & MESA_LOG_CONTROL_WAIT)
      thrd_yield();
}

#endif /* DETECT_OS_ANDROID */

#if DETECT_OS_WINDOWS

static void
logger_windbg(enum mesa_log_level level,
              const char *tag,
              const char *format,
              va_list va)
{
   char local_msg[1024];
   char *msg = logger_vasnprintf(local_msg, sizeof(local_msg),
         LOGGER_VASNPRINTF_AFFIX_TAG |
         LOGGER_VASNPRINTF_AFFIX_LEVEL |
         LOGGER_VASNPRINTF_AFFIX_NEWLINE,
         level, tag, format, va);

   OutputDebugStringA(msg);

   if (msg != local_msg)
      free(msg);
}

#endif /* DETECT_OS_WINDOWS */

/* This is for use with debug functions that take a FILE, such as
 * nir_print_shader, although switching to nir_log_shader* is preferred.
 */
FILE *
mesa_log_get_file(void)
{
   mesa_log_init();
   return mesa_log_file;
}

void
mesa_log(enum mesa_log_level level, const char *tag, const char *format, ...)
{
   va_list va;

   va_start(va, format);
   mesa_log_v(level, tag, format, va);
   va_end(va);
}

void
mesa_log_v(enum mesa_log_level level, const char *tag, const char *format,
            va_list va)
{
   static const struct {
      enum mesa_log_control bit;
      void (*log)(enum mesa_log_level level,
                  const char *tag,
                  const char *format,
                  va_list va);
   } loggers[] = {
      { MESA_LOG_CONTROL_FILE, logger_file },
#if DETECT_OS_UNIX
      { MESA_LOG_CONTROL_SYSLOG, logger_syslog },
#endif
#if DETECT_OS_ANDROID
      { MESA_LOG_CONTROL_ANDROID, logger_android },
#endif
#if DETECT_OS_WINDOWS
      { MESA_LOG_CONTROL_WINDBG, logger_windbg },
#endif
   };

   mesa_log_init();

   for (uint32_t i = 0; i < ARRAY_SIZE(loggers); i++) {
      if (mesa_log_control & loggers[i].bit) {
         va_list copy;
         va_copy(copy, va);
         loggers[i].log(level, tag, format, copy);
         va_end(copy);
      }
   }
}

struct log_stream *
_mesa_log_stream_create(enum mesa_log_level level, const char *tag)
{
   struct log_stream *stream = ralloc(NULL, struct log_stream);
   stream->level = level;
   stream->tag = tag;
   stream->msg = ralloc_strdup(stream, "");
   stream->pos = 0;
   return stream;
}

void
mesa_log_stream_destroy(struct log_stream *stream)
{
   /* If you left trailing stuff in the log stream, flush it out as a line. */
   if (stream->pos != 0)
      mesa_log(stream->level, stream->tag, "%s", stream->msg);

   ralloc_free(stream);
}

static void
mesa_log_stream_flush(struct log_stream *stream, size_t scan_offset)
{
   char *end;
   char *next = stream->msg;
   while ((end = strchr(stream->msg + scan_offset, '\n'))) {
      *end = 0;
      mesa_log(stream->level, stream->tag, "%s", next);
      next = end + 1;
      scan_offset = next - stream->msg;
   }
   if (next != stream->msg) {
      /* Clear out the lines we printed and move any trailing chars to the start. */
      size_t remaining = stream->msg + stream->pos - next;
      memmove(stream->msg, next, remaining);
      stream->pos = remaining;
   }
}

void mesa_log_stream_printf(struct log_stream *stream, const char *format, ...)
{
   size_t old_pos = stream->pos;

   va_list va;
   va_start(va, format);
   ralloc_vasprintf_rewrite_tail(&stream->msg, &stream->pos, format, va);
   va_end(va);

   mesa_log_stream_flush(stream, old_pos);
}

void
_mesa_log_multiline(enum mesa_log_level level, const char *tag, const char *lines)
{
   struct log_stream tmp = {
      .level = level,
      .tag = tag,
      .msg = strdup(lines),
      .pos = strlen(lines),
   };
   mesa_log_stream_flush(&tmp, 0);
   free(tmp.msg);
}
