The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#define _XOPEN_SOURCE  // fdopen

#include "tickit.h"

#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>  // getpid

#define streq(a,b) (!strcmp(a,b))

static void (*debug_func)(const char *str, void *data);
static void *debug_func_data;

static FILE *debug_fh;

/* This needs to default true on startup so the first debugging call gets
 * made, which then lets us inspect the env. vars. to set it correctly
 */
bool tickit_debug_enabled = true;

struct Flag {
  struct Flag *next;
  char *name;
};

static struct Flag *enabled_flags;

static bool init_done = false;
void tickit_debug_init(void)
{
  if(init_done)
    return;

  const char *flags_str = getenv("TICKIT_DEBUG_FLAGS");

  while(flags_str) {
    const char *endp = strchr(flags_str, ',');
    if(!endp)
      endp = flags_str + strlen(flags_str);

    struct Flag *newflag = malloc(sizeof *newflag);
    newflag->name = malloc(endp - flags_str + 1);
    strncpy(newflag->name, flags_str, endp - flags_str);
    newflag->name[endp - flags_str] = 0;

    newflag->next = enabled_flags;
    enabled_flags = newflag;

    flags_str = endp;
    if(flags_str[0] == ',')
      flags_str++;
    else
      break;
  }

  if(!debug_func) {
    const char *val;
    if((val = getenv("TICKIT_DEBUG_FD")) && val[0]) {
      int fd;
      if(sscanf(val, "%d", &fd))
        tickit_debug_set_fh(fdopen(fd, "a"));
    }
    else if((val = getenv("TICKIT_DEBUG_FILE")) && val[0]) {
      tickit_debug_open(val);
    }
    else if(enabled_flags) {
      char name[17];
      sprintf(name, "tickit-%d.log", getpid());
      tickit_debug_open(name);
    }
  }

  tickit_debug_enabled = !!enabled_flags && (debug_func || debug_fh);

  init_done = true;
}

static bool flag_enabled(const char *name)
{
  for(struct Flag *f = enabled_flags; f; f = f->next) {
    if(f->name[0] == '*')
      return true;

    if(name[0] != f->name[0])
      continue;

    if(!f->name[1])
      return true;
    if(streq(name+1, f->name+1))
      return true;
  }

  return false;
}

void tickit_debug_set_func(void (*func)(const char *str, void *data), void *data)
{
  debug_func      = func;
  debug_func_data = data;

  if(debug_fh)
    fclose(debug_fh);

  tickit_debug_enabled = !!enabled_flags && (debug_func || debug_fh);
}

void tickit_debug_set_fh(FILE *fh)
{
  if(debug_fh)
    fclose(debug_fh);

  debug_fh = fh;
  if(debug_fh)
    setvbuf(debug_fh, NULL, _IONBF, 0);

  if(debug_func)
    debug_func = NULL;

  tickit_debug_enabled = !!enabled_flags && (debug_func || debug_fh);
}

bool tickit_debug_open(const char *path)
{
  FILE *fh = fopen(path, "a");
  if(!fh)
    return false;

  tickit_debug_set_fh(fh);
  return true;
}

void tickit_debug_logf(const char *flag, const char *fmt, ...)
{
  va_list args;
  va_start(args, fmt);

  tickit_debug_vlogf(flag, fmt, args);

  va_end(args);
}

void tickit_debug_vlogf(const char *flag, const char *fmt, va_list args)
{
  if(!init_done)
    tickit_debug_init();

  if(!tickit_debug_enabled)
    return;
  if(!flag_enabled(flag))
    return;

  struct timeval now;
  gettimeofday(&now, NULL);

  char timestamp[9];
  strftime(timestamp, sizeof timestamp, "%H:%M:%S", localtime(&now.tv_sec));

#define LINE_PREFIX "%s.%03d [%-3s]: "

  if(debug_func) {
    size_t len;
    va_list args_copy;
    va_copy(args_copy, args);
    len = snprintf(NULL, 0, LINE_PREFIX, timestamp, 0, flag) +
      vsnprintf(NULL, 0, fmt, args_copy) +
      1;
    va_end(args_copy);

    char *buf = malloc(len + 1);
    {
      char *s = buf;

      s += sprintf(s, LINE_PREFIX, timestamp, (int)(now.tv_usec / 1000), flag);
      s += vsprintf(s, fmt, args);
      s += sprintf(s, "\n");
    }

    (*debug_func)(buf, debug_func_data);

    free(buf);
  }
  else if(debug_fh) {
    fprintf(debug_fh, LINE_PREFIX, timestamp, (int)(now.tv_usec / 1000), flag);
    vfprintf(debug_fh, fmt, args);
    fprintf(debug_fh, "\n");
  }

#undef LINE_PREFIX
}