/**
    debug and error logging

    Copyright (c) 2020-2022 The Creators of Simphone

    See the file COPYING.LESSER.txt for copying permission.
**/

#include "config.h"

#include "logger.h"
#include "table.h"
#include "error.h"
#include "utils.h"
#include "file.h"
#include "proto.h"

#ifndef _BSD_SOURCE
#define _BSD_SOURCE /* snprintf */
#endif

#ifndef _GNU_SOURCE
#define _GNU_SOURCE /* PTHREAD_MUTEX_RECURSIVE */
#endif

#include <string.h>
#include <stdio.h>
#include <time.h>
#include <errno.h>

#ifndef _WIN32
#include <unistd.h>
#else
#include <windows.h>
#include <io.h>

#define dup _dup
#endif

static const char *log_level_names[] = { " XTRA", "DEBUG", " INFO", " NOTE", " WARN", "ERROR", "FATAL", NULL };
#define LOG_LENGTH_LEVEL 5 /* all levels of equal length */
#define LOG_LENGTH_TIME (SIM_SIZE_TIME + LOG_LENGTH_LEVEL + 1)

struct log_buffer_entry {
  int level;
  unsigned time;
  char text[1]; /* variable size */
};

#define LOG_SIZE_ENTRY (sizeof (int) + sizeof (unsigned)) /* sizeof (struct log_buffer_entry) */

static int log_fd = -1;
static simtype log_temp_string;
static unsigned log_temp_length = 0, log_temp_index;
static int log_temp_level;
simbool log_recursed = false;

static simtype log_buffer_array;
static simtype log_level_table; /* keyed by module name, value is log level number */
static simtype event_log_table; /* preallocated SIM_EVENT_LOG */
static int log_default_level = SIM_LOG_FATAL;
static simbool log_initialized_flag = false;
static int log_file_error = SIM_OK;

static int log_get_level (const char *module) {
  int level = log_default_level;
  if (module && log_level_table.typ == SIMTABLE) {
    simtype number = _sim_table_get_key (log_level_table, pointer_new (module), __FUNCTION__, 0);
    if (number.typ == SIMNUMBER)
      level = (int) number.num;
  }
  return level;
}

static void log_uninit_buffer_ (void) {
  if (log_buffer_array.typ != SIMNIL && log_buffer_array.typ != SIMNUMBER) {
    simtype buf = log_buffer_array;
    log_buffer_array = number_new (0);
    array_free (buf);
  }
  log_buffer_array = nil ();
}

static void log_init_buffer_ (unsigned lines) {
  log_buffer_array = array_new_strings (lines); /* avoid mad reallocation in the beginning */
  log_buffer_array.len = 0;
}

static void log_init_event_ (void) {
  log_temp_string = nil ();
  table_add_number (event_log_table = table_new_const_name (2, SIM_EVENT_LOG, NULL), SIM_EVENT_LOG, 0);
  table_add_pointer (event_log_table, SIM_CMD_MSG_TEXT, "");
  log_init_buffer_ (100);
  log_initialized_flag = true;
}

#ifdef HAVE_LIBPTH
int log_init_ (void) {
  if (! log_initialized_flag)
    log_init_event_ ();
  return SIM_OK;
}

simbool log_protect_ (const char *module, int level) {
  return log_initialized_flag && (level >= SIM_LOG_FATAL || level >= log_get_level (module));
}

void log_unprotect_ (void) {
}

#define log_uninit_()
#elif ! defined(_WIN32)
#include <pthread.h>

static pthread_mutex_t log_lock;

int log_init_ (void) {
  int err = SIM_OK;
  pthread_mutexattr_t attr;
  if (! log_initialized_flag && (err = pthread_mutexattr_init (&attr)) == 0) {
    if ((err = pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE)) == 0)
      if ((err = pthread_mutex_init (&log_lock, &attr)) == 0)
        log_init_event_ ();
    pthread_mutexattr_destroy (&attr);
  }
  return err;
}

simbool log_protect_ (const char *module, int level) {
  if (log_initialized_flag)
    return (level >= SIM_LOG_FATAL || level >= log_get_level (module)) && ! pthread_mutex_lock (&log_lock);
  return false;
}

void log_unprotect_ (void) {
  pthread_mutex_unlock (&log_lock);
}

#define log_uninit_() pthread_mutex_destroy (&log_lock)
#else /* _WIN32 */
static CRITICAL_SECTION log_lock;

int log_init_ (void) {
  if (! log_initialized_flag) {
    if (! InitializeCriticalSectionAndSpinCount (&log_lock, 0))
      return ENOMEM;
    log_init_event_ ();
  }
  return SIM_OK;
}

simbool log_protect_ (const char *module, int level) {
  if (! log_initialized_flag || (level < SIM_LOG_FATAL && level < log_get_level (module)))
    return false;
  EnterCriticalSection (&log_lock);
  return true;
}

void log_unprotect_ (void) {
  LeaveCriticalSection (&log_lock);
}

#define log_uninit_() DeleteCriticalSection (&log_lock)
#endif

void log_set_level_ (const char *module, int level) {
  int minlevel = module ? SIM_LOG_LEVEL : SIM_LOG_XTRA;
  simbool ok = log_protect_ (NULL, SIM_LOG_FATAL);
  if (! log_level_table.ptr)
    log_level_table = table_new_const (73, NULL);
  if (level > SIM_LOG_FATAL) {
    level = SIM_LOG_FATAL;
  } else if (level < minlevel)
    level = minlevel;
  if (module) {
    table_set_key_number (log_level_table, string_copy (module), level);
  } else
    log_default_level = level;
  if (ok)
    log_unprotect_ ();
}

int log_get_level_ (const char *module) {
  int level = SIM_LOG_FATAL;
  if (log_protect_ (NULL, SIM_LOG_FATAL)) {
    level = log_get_level (module);
    log_unprotect_ ();
  }
  return level;
}

int sim_log_get_level (simtype *line) {
  int level;
  if (line->len >= SIM_SIZE_TIME && line->str[SIM_SIZE_TIME - 1] == ' ')
    if (line->len > LOG_LENGTH_TIME && line->str[LOG_LENGTH_TIME - 1] == ' ')
      for (level = 0; level < SIM_LOG_UNKNOWN; level++)
        if (! memcmp (log_level_names[level], line->str + SIM_SIZE_TIME, LOG_LENGTH_LEVEL)) {
          line->str += LOG_LENGTH_TIME;
          line->len -= LOG_LENGTH_TIME;
          return level;
        }
  return SIM_LOG_UNKNOWN; /* last index of log_level_names, value is NULL */
}

int log_get_error_ (void) {
  simbool ok = log_protect_ (NULL, SIM_LOG_FATAL);
  int err = log_file_error;
  log_file_error = SIM_OK;
  if (ok)
    log_unprotect_ ();
  return err;
}

simtype log_get_string_ (simunsigned idx, simunsigned *datetime, const char **loglevel) {
  simbool ok = log_protect_ (NULL, SIM_LOG_FATAL);
  simtype line = nil ();
  if (idx && idx <= log_buffer_array.len) {
    struct log_buffer_entry *entry = log_buffer_array.arr[idx].ptr;
    line = pointer_new_len (entry->text, log_buffer_array.arr[idx].len - LOG_SIZE_ENTRY);
    if (loglevel)
      *loglevel = log_level_names[entry->level];
    if (datetime)
      *datetime = entry->time;
  } else
    LOG_API_WARN_ (SIM_MSG_BAD_INDEX);
  if (ok)
    log_unprotect_ ();
  return line;
}

simtype log_put_string_ (int level, const simtype line, simunsigned datetime) {
  simtype tmp = string_new (line.len + LOG_SIZE_ENTRY);
  struct log_buffer_entry *entry = tmp.ptr;
  entry->level = level;
  entry->time = (unsigned) datetime;
  memcpy (entry->text, line.str, line.len);
  entry->text[line.len] = 0;
  array_append (&log_buffer_array, tmp);
#if SIM_LOG_LEVEL <= SIM_LOG_FATAL
  if (log_buffer_array.typ == SIMNIL)
    log_fatal_ (SIM_MODULE_TABLE, SIM_OK, "MEMORY OVERFLOW\n");
#endif
  return tmp;
}

simunsigned log_count_ (void) {
  unsigned count = 0;
  if (log_protect_ (NULL, SIM_LOG_FATAL)) {
    count = log_buffer_array.len;
    log_unprotect_ ();
  }
  return count;
}

int log_load_ (simnumber lines, unsigned maxsize) {
  int err = SIM_OK;
  if (! log_protect_ (NULL, SIM_LOG_FATAL))
    return EINVAL;
  switch (lines) {
    case LOG_NO_BUFFER:
      log_uninit_buffer_ ();
      log_buffer_array = number_new (0);
      break;
    case LOG_NO_EVENT:
      log_uninit_buffer_ ();
      break;
    default:
      if (lines >= 0) {
        log_uninit_buffer_ ();
        log_init_buffer_ (lines > 1000 ? 1000 : (unsigned) lines);
        err = sim_file_load_log (lines, maxsize);
      } else
        err = SIM_MSG_BAD_INDEX;
  }
  log_unprotect_ ();
  return err;
}

int log_init_file_ (const char *filename, simbool append) {
  int err = SIM_OK;
  if (! log_protect_ (NULL, SIM_LOG_FATAL))
    return EPERM;
  if (log_fd >= 0)
    sim_file_close (log_fd), log_fd = -1;
  if (filename) {
    int bits = FILE_BIT_CREATE | (append ? FILE_BIT_APPEND : 0) | FILE_BIT_WRITE | FILE_BIT_TEXT;
    if ((log_fd = sim_file_create (filename, bits)) < 0 && errno) {
      sim_error_set_text (" [", filename, "]", err = errno);
      log_error_ (NULL, "open %s error %d\n", filename, err);
    }
  }
  log_unprotect_ ();
  return err;
}

int log_init_fd_ (int fd) {
  int err = SIM_OK;
  if (! log_protect_ (NULL, SIM_LOG_FATAL))
    return EPERM;
  if ((fd = dup (fd)) >= 0) {
    if (log_fd >= 0)
      sim_file_close (log_fd);
    log_fd = fd;
  } else
    err = errno ? errno : EINVAL;
  log_unprotect_ ();
  return err;
}

void uninit_log (void) {
  if (log_temp_string.len)
    string_buffer_free (log_temp_string), log_temp_string = nil ();
  log_temp_length = 0;
  log_uninit_buffer_ ();
  if (log_initialized_flag)
    table_free (event_log_table), event_log_table = nil ();
  if (log_level_table.ptr) {
    simbool ok = true;
    int level = log_get_level_ (SIM_MODULE_API);
    simtype tmp = log_level_table;
    log_level_table = nil (); /* make sure log output from table_free won't use log_level_table */
    log_default_level = SIM_LOG_ERROR;
    table_free (tmp);
    log_default_level = level;
    LOG_CODE_ (SIM_MODULE_API, SIM_LOG_NOTE, memory_log_tables (SIM_MODULE_API, SIM_LOG_NOTE));
    LOG_CODE_ (SIM_MODULE_API, SIM_LOG_NOTE, ok = pth_log_threads (SIM_MODULE_API, SIM_LOG_NOTE));
    if (! ok) {
#if defined(__GNUC__) && (defined(__x86_64__) || defined(__i386__))
      __asm__("int $3");
#endif
    }
    log_default_level = SIM_LOG_FATAL;
  }
  if (log_fd >= 0)
    sim_file_close (log_fd), log_fd = -1;
  if (log_initialized_flag) {
    log_uninit_ ();
    log_initialized_flag = false;
  }
}

static simtype log_print (const char *module, int level, const char *format, va_list args) {
  int fd = module ? log_fd : -1;
  if (fd >= 0 || log_buffer_array.typ != SIMNIL || level == SIM_LOG_FATAL) {
    unsigned len = 0, idx = 0, oldlen;
    int err = errno;
    static char log_lines[2][33000];
    static simbool log_newline_flags[2][2] = { { true, true }, { true, true } };
    simbool *nl = &log_newline_flags[fd < 0][log_recursed];
    char *buf = log_lines[log_recursed];
#ifdef _WIN32
    static char log_formats[2][128];
    const char *s = format;
    unsigned l;
    for (l = 0; l < sizeof (*log_formats) && s[0]; l++) {
      if ((log_formats[log_recursed][l] = s[0]) == '%' && s[1]) {
        if (s[1] == '%') {
          if (++l >= sizeof (*log_formats))
            break;
          log_formats[log_recursed][l] = *++s;
        } else {
          while (s[1] >= '0' && s[1] <= '9' && l < sizeof (*log_formats))
            log_formats[log_recursed][++l] = *++s;
          if (s[1] == 'l' && s[2] == 'l') {
            s += 2;
            if (l + 3 >= sizeof (*log_formats))
              break;
            log_formats[log_recursed][++l] = 'I';
            log_formats[log_recursed][++l] = '6';
            log_formats[log_recursed][++l] = '4';
          }
        }
      }
      s++;
    }
    if (l < sizeof (*log_formats)) {
      log_formats[log_recursed][l] = 0;
      format = log_formats[log_recursed];
    } else
      format = "*** format string too long ***\n";
#endif
    if (module && *module && *nl) { /* at line start, add timestamp */
      len = sim_convert_time_to_string (time (NULL), buf);
      buf[len++] = ' ';
      strcpy (buf + len, log_level_names[level - SIM_LOG_XTRA]);
      len += LOG_LENGTH_LEVEL;
      buf[len++] = ' ';
      buf[idx = len] = 0;
      len += strlen (strcat (buf + len, module));
      buf[len++] = ':';
      buf[len++] = ' ';
    }
    len = vsnprintf (buf + (oldlen = len), sizeof (*log_lines) - 1 - len, format, args);
    if (len >= sizeof (*log_lines) - oldlen) { /* buffer is full */
      len = sizeof (*log_lines) - 1;
      *nl = true;
    } else {
      len += oldlen;
      *nl = len && buf[len - 1] == '\n';
    }
    if (! log_recursed) {
      log_recursed = true; /* log to memory buffer (if not nil) line by line and send event */
      if (log_buffer_array.typ != SIMNIL)
        if (level != SIM_LOG_FATAL && (! module || level >= log_get_level ("console"))) {
          char *oldbuf = buf;
          int lvl = module ? level : SIM_LOG_UNKNOWN; /* so that log_get_string_ returns NULL level */
          simtype line = nil ();
          if (log_temp_length || ! *nl) {
            if (! log_temp_length) {
              log_temp_index = idx;
              log_temp_level = lvl;
            } else {
              idx = log_temp_index;
              lvl = log_temp_level;
            }
            string_buffer_append (&log_temp_string, log_temp_length, len);
            oldbuf = log_temp_string.ptr;
            memcpy (log_temp_string.str + log_temp_length, buf, len);
            log_temp_length += len;
            if (*nl) {
              line = pointer_new_len (log_temp_string.str, log_temp_length);
              log_temp_length = 0;
            }
          } else
            line = pointer_new_len (buf, len);
          if (line.typ != SIMNIL) {
            if (line.len && line.str[line.len - 1] == '\n')
              line.len--;
            if (log_buffer_array.typ != SIMNUMBER)
              log_put_string_ (lvl, pointer_new_len (oldbuf + idx, line.len - idx), time (NULL));
            table_set_number (event_log_table, SIM_EVENT_LOG, log_buffer_array.len);
            table_set_pointer_len (event_log_table, SIM_CMD_MSG_TEXT, line.str, line.len);
            event_send_fast (0, event_log_table);
          }
        }
      if (fd >= 0) {
        int ret = sim_file_write (fd, buf, len);
        log_file_error = ret == (int) len ? SIM_OK : FILE_CASE_ERROR (ret, SIM_FILE_NO_SPACE);
      }
      log_recursed = false;
    }
    errno = err;
    return pointer_new_len (buf, len);
  }
  return nil ();
}

void log_print_ (const char *module, int level, const char *format, va_list args) {
  if (log_protect_ (module, level)) {
    log_print (module, level, format, args);
    log_unprotect_ ();
  }
}

void log_print_function_ (const char *module, int level, const char *function, const char *format, va_list args) {
  if (log_protect_ (module, level)) {
    log_any (module, level, "%s ", function);
    log_print (module, level, format, args);
    log_unprotect_ ();
  }
}

void log_function (const char *module, int level, const char *function,
                   const char *file, unsigned line, const char *format, ...) {
  va_list args;
  if (! file) {
    log_any (module, level, "%s ", function);
  } else if ((int) line != -SIMPOINTER) {
    log_any (module, level, "%s %s:%d ", function, file, line);
  } else
    log_any (module, level, "%s %p ", function, file);
  va_start (args, format);
  log_print (module, level, format, args);
  va_end (args);
}

void log_any (const char *module, int level, const char *format, ...) {
  va_list args;
  va_start (args, format);
  log_print (module, level, format, args);
  va_end (args);
}

void log_any_ (const char *module, int level, const char *format, ...) {
  va_list args;
  va_start (args, format);
  log_print_ (module, level, format, args);
  va_end (args);
}

void log_xtra_ (const char *module, const char *format, ...) {
  va_list args;
  va_start (args, format);
  log_print_ (module, SIM_LOG_XTRA, format, args);
  va_end (args);
}

void log_debug_ (const char *module, const char *format, ...) {
  va_list args;
  va_start (args, format);
  log_print_ (module, SIM_LOG_DEBUG, format, args);
  va_end (args);
}

void log_info_ (const char *module, const char *format, ...) {
  va_list args;
  va_start (args, format);
  log_print_ (module, SIM_LOG_INFO, format, args);
  va_end (args);
}

void log_note_ (const char *module, const char *format, ...) {
  va_list args;
  va_start (args, format);
  log_print_ (module, SIM_LOG_NOTE, format, args);
  va_end (args);
}

void log_warn_ (const char *module, const char *format, ...) {
  va_list args;
  va_start (args, format);
  log_print_ (module, SIM_LOG_WARN, format, args);
  va_end (args);
}

void log_error_ (const char *module, const char *format, ...) {
  va_list args;
  va_start (args, format);
  log_print_ (module, SIM_LOG_ERROR, format, args);
  va_end (args);
}

void log_fatal_ (const char *module, int error, const char *format, ...) {
  simtype line;
  va_list args;
  simbool ok = log_protect_ (NULL, SIM_LOG_FATAL);
  va_start (args, format);
  line = log_print (module, SIM_LOG_FATAL, format, args);
  va_end (args);
  if (line.len && line.str[line.len - 1] == '\n')
    line.str[--line.len] = 0;
  sim_event_send_fatal (error, line);
  log_any (module, SIM_LOG_FATAL, "error %d\n", error);
  if (ok)
    log_unprotect_ ();
}

static void log_type (const char *module, int level, const simtype value, int tab, unsigned bits) {
  int c;
  simbyte *str;
  simtype key, val, *ptr;
  char ch = '\'';
  simwalker ctx;
  unsigned len = value.len;
  switch (value.typ) {
    default:
      log_any (module, level, value.typ == SIMNIL ? "NIL" : "INVALID");
      break;
    case SIMNUMBER:
      if (bits & LOG_BIT_HEX && (int) value.num == value.num) {
        log_any (module, level, "0x%X", (unsigned) value.num);
      } else
        log_any (module, level, bits & LOG_BIT_HEX ? "0x%llX" : "%lld", value.num);
      break;
    case SIMSTRING:
      ch = '"';
    case SIMPOINTER:
      if (! (bits & LOG_BIT_BIN)) {
        log_any (module, level, "%c", ch);
        for (str = value.str; len--;)
          switch (c = *str++) {
            case '\n':
              log_any (module, level, "\\n");
              break;
            case '\r':
              log_any (module, level, "\\r");
              break;
            case '\t':
              log_any (module, level, "\\t");
              break;
            case 0:
              log_any (module, level, "\\0");
              break;
            default:
              if (c >= ' ' && c <= '~') {
                if (c == ch || c == '\\')
                  log_any (module, level, "\\");
                log_any (module, level, "%c", c);
              } else
                log_any (module, level, "\\%02X", c);
          }
        log_any (module, level, "%c", ch);
      } else {
        case SIMBUFFER:
          log_any (module, level, "0x");
          for (str = value.str; len--;)
            log_any (module, level, "%02X", *str++);
      }
      break;
    case SIMTABLE: /* log_any (module, level, "%s:%u {\n", (char *) table_get_name (value), table_get_line (value)); */
      log_any (module, level, "{\n");
      for (table_walk_first (&ctx, value); (val = table_walk_next (&ctx, &key)).typ != SIMNIL;) {
        log_any (module, level, "%*c", tab + 1, ' ');
        log_type (module, level, key, tab + 1, key.typ != SIMPOINTER ? bits : bits & ~LOG_BIT_BIN);
        log_any (module, level, ": ");
        log_type (module, level, val, tab + 1, bits);
        log_any (module, level, "\n");
      }
      log_any (module, level, "%*c", tab + 1, '}');
      break;
    case SIMARRAY_NUMBER:
    case SIMARRAY_STRING:
    case SIMARRAY_TABLE:
    case SIMARRAY_ARRAY: /* log_any (module, level, "%s:%d [", (char *) array_get_name (value), array_get_line (value)); */
      if (len > array_size (value))
        len = array_size (value);
      log_any (module, level, "[");
      for (ptr = value.ptr; len--;) {
        log_type (module, level, *++ptr, tab + 1, bits);
        if (len)
          log_any (module, level, ", ");
      }
      log_any (module, level, "]");
  }
}

static void log_print_simtype (const char *module, int level, const simtype value, unsigned bits,
                               const char *format, va_list args) {
  log_print (module, level, format, args);
  log_type (module, level, value, 1, bits);
  if (! (bits & LOG_BIT_CONT))
    log_any (module, level, "\n");
}

static void log_print_simtypes (const char *module, int level, const simtype value1, const simtype value2,
                                unsigned bits, const char *format, va_list args) {
  if (bits & LOG_BIT_BIN)
    log_print (module, level, format, args);
  log_type (module, level, value1, 1, bits);
  log_any (module, level, " ");
  if (! (bits & LOG_BIT_BIN))
    log_print (module, level, format, args);
  log_type (module, level, value2, 1, bits);
  if (! (bits & LOG_BIT_CONT))
    log_any (module, level, "\n");
}

void log_simtype (const char *module, int level, const simtype value, unsigned bits, const char *format, ...) {
  va_list args;
  va_start (args, format);
  log_print_simtype (module, level, value, bits, format, args);
  va_end (args);
}

void log_simtype_ (const char *module, int level, const simtype value, unsigned bits, const char *format, ...) {
  if (log_protect_ (module, level)) {
    va_list args;
    va_start (args, format);
    log_print_simtype (module, level, value, bits, format, args);
    va_end (args);
    log_unprotect_ ();
  }
}

void log_simtypes (const char *module, int level, const simtype value1, const simtype value2, unsigned bits,
                   const char *format, ...) {
  va_list args;
  va_start (args, format);
  log_print_simtypes (module, level, value1, value2, bits, format, args);
  va_end (args);
}

void log_simtypes_ (const char *module, int level, const simtype value1, const simtype value2, unsigned bits,
                    const char *format, ...) {
  if (log_protect_ (module, level)) {
    va_list args;
    va_start (args, format);
    log_print_simtypes (module, level, value1, value2, bits, format, args);
    va_end (args);
    log_unprotect_ ();
  }
}
