The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#include "tickit.h"
#include "hooklists.h"

#include <stdarg.h>
#include <stdio.h>   /* sscanf */
#include <stdlib.h>
#include <string.h>

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

struct TickitPen {
  signed   int fg      : 9, /* 0 - 255 or -1 */
               bg      : 9; /* 0 - 255 or -1 */

  unsigned int bold    : 1,
               under   : 1,
               italic  : 1,
               reverse : 1,
               strike  : 1,
               blink   : 1;

  signed   int altfont : 5; /* 1 - 10 or -1 */

  struct {
    unsigned int fg      : 1,
                 bg      : 1,
                 bold    : 1,
                 under   : 1,
                 italic  : 1,
                 reverse : 1,
                 strike  : 1,
                 altfont : 1,
                 blink   : 1;
  } valid;

  int refcount;
  struct TickitEventHook *hooks;
};

DEFINE_HOOKLIST_FUNCS(pen,TickitPen,TickitPenEventFn)

TickitPen *tickit_pen_new(void)
{
  TickitPen *pen = malloc(sizeof(TickitPen));
  if(!pen)
    return NULL;

  pen->refcount = 1;
  pen->hooks = NULL;

  tickit_pen_clear(pen);

  return pen;
}

TickitPen *tickit_pen_new_attrs(TickitPenAttr attr, ...)
{
  TickitPen *pen = tickit_pen_new();
  if(!pen)
    return NULL;

  va_list args;
  va_start(args, attr);
  int first = 1;
  int val;

  while(1) {
    int a = first ? attr : va_arg(args, TickitPenAttr);
    first = 0;
    if(a < 0)
      break;

    // TODO: accept colour descs
    switch(tickit_pen_attrtype(a)) {
    case TICKIT_PENTYPE_BOOL:
      val = va_arg(args, int);
      tickit_pen_set_bool_attr(pen, a, val);
      break;
    case TICKIT_PENTYPE_INT:
      val = va_arg(args, int);
      tickit_pen_set_int_attr(pen, a, val);
      break;
    case TICKIT_PENTYPE_COLOUR:
      val = va_arg(args, int);
      tickit_pen_set_colour_attr(pen, a, val);
      break;
    }
  }

  va_end(args);

  return pen;
}

TickitPen *tickit_pen_clone(const TickitPen *orig)
{
  TickitPen *pen = tickit_pen_new();
  tickit_pen_copy(pen, orig, true);
  return pen;
}

static void destroy(TickitPen *pen)
{
  tickit_hooklist_unbind_and_destroy(pen->hooks, pen);
  free(pen);
}

TickitPen *tickit_pen_ref(TickitPen *pen)
{
  pen->refcount++;
  return pen;
}

void tickit_pen_unref(TickitPen *pen)
{
  pen->refcount--;
  if(!pen->refcount)
    destroy(pen);
}

bool tickit_pen_has_attr(const TickitPen *pen, TickitPenAttr attr)
{
  switch(attr) {
    case TICKIT_PEN_FG:      return pen->valid.fg;
    case TICKIT_PEN_BG:      return pen->valid.bg;
    case TICKIT_PEN_BOLD:    return pen->valid.bold;
    case TICKIT_PEN_UNDER:   return pen->valid.under;
    case TICKIT_PEN_ITALIC:  return pen->valid.italic;
    case TICKIT_PEN_REVERSE: return pen->valid.reverse;
    case TICKIT_PEN_STRIKE:  return pen->valid.strike;
    case TICKIT_PEN_ALTFONT: return pen->valid.altfont;
    case TICKIT_PEN_BLINK:   return pen->valid.blink;

    case TICKIT_N_PEN_ATTRS:
      return false;
  }

  return false;
}

bool tickit_pen_nondefault_attr(const TickitPen *pen, TickitPenAttr attr)
{
  if(!tickit_pen_has_attr(pen, attr))
    return false;

  switch(tickit_pen_attrtype(attr)) {
  case TICKIT_PENTYPE_BOOL:
    if(tickit_pen_get_bool_attr(pen, attr))
      return true;
    break;
  case TICKIT_PENTYPE_INT:
    if(tickit_pen_get_int_attr(pen, attr) > -1)
      return true;
    break;
  case TICKIT_PENTYPE_COLOUR:
    if(tickit_pen_get_colour_attr(pen, attr) > -1)
      return true;
    break;
  }

  return false;
}

bool tickit_pen_is_nonempty(const TickitPen *pen)
{
  for(TickitPenAttr attr = 0; attr < TICKIT_N_PEN_ATTRS; attr++) {
    if(tickit_pen_has_attr(pen, attr))
      return true;
  }
  return false;
}

bool tickit_pen_is_nondefault(const TickitPen *pen)
{
  for(TickitPenAttr attr = 0; attr < TICKIT_N_PEN_ATTRS; attr++) {
    if(tickit_pen_nondefault_attr(pen, attr))
      return true;
  }
  return false;
}

bool tickit_pen_get_bool_attr(const TickitPen *pen, TickitPenAttr attr)
{
  if(!tickit_pen_has_attr(pen, attr))
    return false;

  switch(attr) {
    case TICKIT_PEN_BOLD:    return pen->bold;
    case TICKIT_PEN_UNDER:   return pen->under;
    case TICKIT_PEN_ITALIC:  return pen->italic;
    case TICKIT_PEN_REVERSE: return pen->reverse;
    case TICKIT_PEN_STRIKE:  return pen->strike;
    case TICKIT_PEN_BLINK:   return pen->blink;
    default:
      return false;
  }
}

void tickit_pen_set_bool_attr(TickitPen *pen, TickitPenAttr attr, bool val)
{
  switch(attr) {
    case TICKIT_PEN_BOLD:    pen->bold    = !!val; pen->valid.bold    = 1; break;
    case TICKIT_PEN_UNDER:   pen->under   = !!val; pen->valid.under   = 1; break;
    case TICKIT_PEN_ITALIC:  pen->italic  = !!val; pen->valid.italic  = 1; break;
    case TICKIT_PEN_REVERSE: pen->reverse = !!val; pen->valid.reverse = 1; break;
    case TICKIT_PEN_STRIKE:  pen->strike  = !!val; pen->valid.strike  = 1; break;
    case TICKIT_PEN_BLINK:   pen->blink   = !!val; pen->valid.blink   = 1; break;
    default:
      return;
  }
  run_events(pen, TICKIT_EV_CHANGE, NULL);
}

int tickit_pen_get_int_attr(const TickitPen *pen, TickitPenAttr attr)
{
  if(!tickit_pen_has_attr(pen, attr))
    return -1;

  switch(attr) {
    case TICKIT_PEN_ALTFONT: return pen->altfont;
    default:
      return 0;
  }
}

void tickit_pen_set_int_attr(TickitPen *pen, TickitPenAttr attr, int val)
{
  switch(attr) {
    case TICKIT_PEN_ALTFONT: pen->altfont = val; pen->valid.altfont = 1; break;
    default:
      return;
  }
  run_events(pen, TICKIT_EV_CHANGE, NULL);
}

/* Cheat and pretend the index of a colour attribute is a number attribute */
int tickit_pen_get_colour_attr(const TickitPen *pen, TickitPenAttr attr)
{
  if(!tickit_pen_has_attr(pen, attr))
    return -1;

  switch(attr) {
    case TICKIT_PEN_FG: return pen->fg;
    case TICKIT_PEN_BG: return pen->bg;
    default:
      return 0;
  }
}

void tickit_pen_set_colour_attr(TickitPen *pen, TickitPenAttr attr, int val)
{
  switch(attr) {
    case TICKIT_PEN_FG: pen->fg = val; pen->valid.fg = 1; break;
    case TICKIT_PEN_BG: pen->bg = val; pen->valid.bg = 1; break;
    default:
      return;
  }
  run_events(pen, TICKIT_EV_CHANGE, NULL);
}

static struct { const char *name; int colour; } colournames[] = {
  { "black",   0 },
  { "red",     1 },
  { "green",   2 },
  { "yellow",  3 },
  { "blue",    4 },
  { "magenta", 5 },
  { "cyan",    6 },
  { "white",   7 },
  // 256-colour palette
  { "grey",     8 },
  { "brown",   94 },
  { "orange", 208 },
  { "pink",   212 },
  { "purple", 128 },
};

bool tickit_pen_set_colour_attr_desc(TickitPen *pen, TickitPenAttr attr, const char *desc)
{
  int hi = 0;
  int val;
  if(strncmp(desc, "hi-", 3) == 0) {
    desc += 3;
    hi   = 8;
  }

  if(sscanf(desc, "%d", &val) == 1) {
    if(hi && val > 7)
      return false;

    tickit_pen_set_colour_attr(pen, attr, val + hi);
    return true;
  }

  for(int i = 0; i < sizeof(colournames)/sizeof(colournames[0]); i++) {
    if(strcmp(desc, colournames[i].name) != 0)
      continue;

    val = colournames[i].colour;
    if(val < 8 && hi)
      val += hi;

    tickit_pen_set_colour_attr(pen, attr, val);
    return true;
  }

  return false;
}

void tickit_pen_clear_attr(TickitPen *pen, TickitPenAttr attr)
{
  switch(attr) {
    case TICKIT_PEN_FG:      pen->valid.fg      = 0; break;
    case TICKIT_PEN_BG:      pen->valid.bg      = 0; break;
    case TICKIT_PEN_BOLD:    pen->valid.bold    = 0; break;
    case TICKIT_PEN_UNDER:   pen->valid.under   = 0; break;
    case TICKIT_PEN_ITALIC:  pen->valid.italic  = 0; break;
    case TICKIT_PEN_REVERSE: pen->valid.reverse = 0; break;
    case TICKIT_PEN_STRIKE:  pen->valid.strike  = 0; break;
    case TICKIT_PEN_ALTFONT: pen->valid.altfont = 0; break;
    case TICKIT_PEN_BLINK:   pen->valid.blink   = 0; break;

    case TICKIT_N_PEN_ATTRS:
      return;
  }
  run_events(pen, TICKIT_EV_CHANGE, NULL);
}

void tickit_pen_clear(TickitPen *pen)
{
  for(TickitPenAttr attr = 0; attr < TICKIT_N_PEN_ATTRS; attr++)
    tickit_pen_clear_attr(pen, attr);
}

bool tickit_pen_equiv_attr(const TickitPen *a, const TickitPen *b, TickitPenAttr attr)
{
  switch(tickit_pen_attrtype(attr)) {
  case TICKIT_PENTYPE_BOOL:
    return tickit_pen_get_bool_attr(a, attr) == tickit_pen_get_bool_attr(b, attr);
  case TICKIT_PENTYPE_INT:
    return tickit_pen_get_int_attr(a, attr) == tickit_pen_get_int_attr(b, attr);
  case TICKIT_PENTYPE_COLOUR:
    return tickit_pen_get_colour_attr(a, attr) == tickit_pen_get_colour_attr(b, attr);
  }

  return false;
}

bool tickit_pen_equiv(const TickitPen *a, const TickitPen *b)
{
  if(a == b)
    return true;

  for(TickitPenAttr attr = 0; attr < TICKIT_N_PEN_ATTRS; attr++)
    if(!tickit_pen_equiv_attr(a, b, attr))
      return false;

  return true;
}

void tickit_pen_copy_attr(TickitPen *dst, const TickitPen *src, TickitPenAttr attr)
{
  switch(tickit_pen_attrtype(attr)) {
  case TICKIT_PENTYPE_BOOL:
    tickit_pen_set_bool_attr(dst, attr, tickit_pen_get_bool_attr(src, attr));
    return;
  case TICKIT_PENTYPE_INT:
    tickit_pen_set_int_attr(dst, attr, tickit_pen_get_int_attr(src, attr));
    return;
  case TICKIT_PENTYPE_COLOUR:
    tickit_pen_set_colour_attr(dst, attr, tickit_pen_get_colour_attr(src, attr));
    return;
  }

  return;
}

void tickit_pen_copy(TickitPen *dst, const TickitPen *src, bool overwrite)
{
  int changed = 0;
  for(TickitPenAttr attr = 0; attr < TICKIT_N_PEN_ATTRS; attr++) {
    if(!tickit_pen_has_attr(src, attr))
      continue;
    if(tickit_pen_has_attr(dst, attr) &&
       (!overwrite || tickit_pen_equiv_attr(src, dst, attr)))
      continue;

    /* Avoid using copy_attr so it doesn't invoke change events yet */
    switch(attr) {
    case TICKIT_PEN_FG:
      dst->fg = src->fg;
      dst->valid.fg = 1;
      break;
    case TICKIT_PEN_BG:
      dst->bg = src->bg;
      dst->valid.bg = 1;
      break;
    case TICKIT_PEN_BOLD:
      dst->bold = src->bold;
      dst->valid.bold = 1;
      break;
    case TICKIT_PEN_ITALIC:
      dst->italic = src->italic;
      dst->valid.italic = 1;
      break;
    case TICKIT_PEN_UNDER:
      dst->under = src->under;
      dst->valid.under = 1;
      break;
    case TICKIT_PEN_REVERSE:
      dst->reverse = src->reverse;
      dst->valid.reverse = 1;
      break;
    case TICKIT_PEN_STRIKE:
      dst->strike = src->strike;
      dst->valid.strike = 1;
      break;
    case TICKIT_PEN_ALTFONT:
      dst->altfont = src->altfont;
      dst->valid.altfont = 1;
      break;
    case TICKIT_PEN_BLINK:
      dst->blink = src->blink;
      dst->valid.blink = 1;
      break;
    case TICKIT_N_PEN_ATTRS:
      continue;
    }

    changed++;
  }

  if(changed)
    run_events(dst, TICKIT_EV_CHANGE, NULL);
}

TickitPenAttrType tickit_pen_attrtype(TickitPenAttr attr)
{
  switch(attr) {
    case TICKIT_PEN_FG:
    case TICKIT_PEN_BG:
      return TICKIT_PENTYPE_COLOUR;

    case TICKIT_PEN_ALTFONT:
      return TICKIT_PENTYPE_INT;

    case TICKIT_PEN_BOLD:
    case TICKIT_PEN_UNDER:
    case TICKIT_PEN_ITALIC:
    case TICKIT_PEN_REVERSE:
    case TICKIT_PEN_STRIKE:
    case TICKIT_PEN_BLINK:
      return TICKIT_PENTYPE_BOOL;

    case TICKIT_N_PEN_ATTRS:
      return -1;
  }

  return -1;
}

const char *tickit_pen_attrname(TickitPenAttr attr)
{
  switch(attr) {
    case TICKIT_PEN_FG:      return "fg";
    case TICKIT_PEN_BG:      return "bg";
    case TICKIT_PEN_BOLD:    return "b";
    case TICKIT_PEN_UNDER:   return "u";
    case TICKIT_PEN_ITALIC:  return "i";
    case TICKIT_PEN_REVERSE: return "rv";
    case TICKIT_PEN_STRIKE:  return "strike";
    case TICKIT_PEN_ALTFONT: return "af";
    case TICKIT_PEN_BLINK:   return "blink";

    case TICKIT_N_PEN_ATTRS: ;
  }
  return NULL;
}

TickitPenAttr tickit_pen_lookup_attr(const char *name)
{
  switch(name[0]) {
    case 'a':
      return streq(name+1,"f") ? TICKIT_PEN_ALTFONT
                               : -1;
    case 'b':
      return name[1] == 0      ? TICKIT_PEN_BOLD
           : streq(name+1,"g") ? TICKIT_PEN_BG
           : streq(name+1,"link") ? TICKIT_PEN_BLINK
                               : -1;
    case 'f':
      return streq(name+1,"g") ? TICKIT_PEN_FG
                               : -1;
    case 'i':
      return name[1] == 0      ? TICKIT_PEN_ITALIC
                               : -1;
    case 'r':
      return streq(name+1,"v") ? TICKIT_PEN_REVERSE
                               : -1;
    case 's':
      return streq(name+1,"trike") ? TICKIT_PEN_STRIKE
                               : -1;
    case 'u':
      return name[1] == 0      ? TICKIT_PEN_UNDER
                               : -1;
  }
  return -1;
}