/*
   djgpp_sgr_support.c
   Copyright (C) 2010 DJ Delorie, see COPYING.DJ for details.
*/


/* Messy DOS/DJGPP-specific code for emulating a Posix terminal driver
   wrt SGR (a.k.a. ANSI) color escape sequences.

   This has several aspects:

     * Redirecting output with ANSI color commands to direct screen
       writes.

*/

#include <config.h>

/*  Screen write redirection.  We need this to support colorization
    without requiring ANSI.SYS driver (or its work-alike) to be loaded.

    This function uses the DJGPP filesystem extensions mechanism.  It is
    installed as a handler for handle-based functions (read/write/close)
    for the standard output (but actually only handles writes, only if
    the standard output is connected to the terminal, and only if user
    asked for colorization).  When a buffer is written to the screen by
    low-level functions of the DJGPP C library, our handler will be
    called.  For any request that doesn't require colored screen writes
    we return a zero to the caller, in which case the caller will handle
    the output in the usual way (by eventually calling DOS).

    When colorization *is* required, the buffer is written directly to
    the screen while converting the ANSI escape sequences into calls to
    DJGPP conio functions which change text attributes.  A non-zero value is
    then returned to the caller to signal that the output has been handled.
*/

#include <stdio.h>
#include <stdarg.h>
#include <errno.h>
#include <unistd.h>
#include <conio.h>
#include <sys/fsext.h>
#include <go32.h> /* for `_dos_ds' */
#include <sys/farptr.h>

static int norm_blink = -1, cur_blink = -1;
static unsigned char norm_attr = 0, cur_attr = 0;
static int isatty_stdout = -1;
static size_t leftover = 0;

/* Restore the BIOS blinking bit to its original value.  Called at exit.  */
static void
restore_blink_bit(void)
{
  if (cur_blink != norm_blink)
  {
    if (norm_blink > 0)
      blinkvideo ();
    else
      intensevideo ();
  }
}

/* Write a buffer to the screen video memory.  This expands the TAB
   characters to the appropriate number of spaces, and also does TRT
   with null characters and other non-printable characters, if
   any.  */
static void
screen_puts(char *buf, char *buf_end)
{
  register char *p = buf, *q = p;
  int row, col;
  unsigned char c;

  while (p < buf_end)
  {
    if (*p < ' ')
    {
      switch (*p)
      {
      case '\b':
      case '\r':
      case '\n':
        /* nothing: cputs already does TRT with these */
        break;
      case '\t':
        *p = '\0';
        cputs (q);
        ScreenGetCursor(&row, &col);
        for (cputs (" "), col += 1; col % 8; col++)
          cputs (" ");
        q = p + 1;
        *p = '\t';
        break;
      default:
        c = *p;
        *p = '\0';
        cputs (q);
        cputs ("^");
        putch (c | 0x40);
        q = p + 1;
        *p = c;
        break;
      }
    }
    p++;
  }
  /* Output whatever is left.  */
  cputs (q);
}

#define ESC       '\033'
#define IS_SGR(s) (((s)[1] == '[') && ((s)[2] == 'm'))
#define IS_EL(s)  (((s)[1] == '[') && ((s)[2] == 'K'))

/* Screen writes redirector function.  */
static int
msdos_screen_write(__FSEXT_Fnumber func, int *retval, va_list rest_args)
{
  static char *cbuf = NULL;
  static size_t cbuf_len = 0;
  /* Only dark colors mentioned here, so that bold has visible effect.  */
  static enum COLORS screen_color[] = {
    BLACK,
    RED,
    GREEN,
    BROWN,
    BLUE,
    MAGENTA,
    CYAN,
    LIGHTGRAY
  };
  char *anchor, *p_next;
  unsigned char fg, bg;

  int handle;
  char *buf, *buf_end;
  size_t buflen;

  /* Avoid direct screen writes unless colorization was actually requested.
     Otherwise, we will break programs that catch I/O from their children.  */
  handle = va_arg(rest_args, int);
  if (!do_colour || func != __FSEXT_write ||
      !(handle == STDOUT_FILENO ? isatty_stdout : isatty (handle)))
    return 0;

  buf = va_arg(rest_args, char *);
  if (!buf)
  {
    errno = EINVAL;
    *retval = -1;
    return 1;
  }

  /* Allocate a sufficiently large buffer to hold the output.  */
  buflen = va_arg(rest_args, size_t);
  if (!cbuf)
  {
    struct text_info txtinfo;

    cbuf_len = buflen + 1;
    cbuf = (char *)xmalloc(cbuf_len);
    gettextinfo (&txtinfo);
    norm_attr = txtinfo.attribute; /* save the original text attribute */
    cur_attr = norm_attr;
    /* Does it normally blink when bg has its 3rd bit set?  */
    norm_blink = (_farpeekb(_dos_ds, 0x465) & 0x20) ? 1 : 0;
    cur_blink = norm_blink;
  }
  else if (buflen >= cbuf_len)
  {
    cbuf_len = buflen + 1 + leftover;
    cbuf = (char *)xrealloc(cbuf, cbuf_len);
  }
  memcpy(cbuf + leftover, buf, buflen);
  buf_end = cbuf + buflen + leftover;
  *buf_end = '\0';

  /* Current text attributes are used as baseline.  */
  fg = cur_attr & 15;
  bg = (cur_attr >> 4) & 15;

  /* Walk the buffer, writing text directly to video RAM,
     changing color attributes when an escape sequence is seen.  */
  for (anchor = p_next = cbuf; (p_next = memchr(p_next, ESC, buflen - (p_next - cbuf))) != 0;)
  {
    char *p = p_next;

    /* If some chars seen since the last escape sequence,
       write it out to the screen using current text attributes.  */
    if (p > anchor)
    {
      *p = '\0'; /* `cputs' needs ASCIIZ string */
      screen_puts(anchor, p);
      *p = ESC; /* restore the ESC character */
      anchor = p;
    }

    /* Handle the null escape sequence (ESC-[m), which is used to
       restore the original color. */
    if (IS_SGR(p))
    {
      textattr(norm_attr);
      p += 3;
      anchor = p_next = p;
      continue;
    }

    /* Handle the erase in line to the right escape sequence (ESC-[K). */
    if (IS_EL(p))
    {
      clreol();
      p += 3;
      anchor = p_next = p;
      continue;
    }

    if (p[1] == '[') /* "Esc-[" sequence */
    {
      p += 2; /* get past "Esc-[" sequence */
      p_next = p;
      while (*p != 'm') /* `m' ends the escape sequence */
      {
        char *q;
        long code = strtol(p, &q, 10);

        if (!*q)
        {
          /* Incomplete escape sequence.  Remember the part
             we've seen for the next time.  */
          leftover = q - anchor;
          if (leftover >= cbuf_len)
          {
            cbuf_len += 1 + leftover;
            cbuf = (char *)xrealloc(cbuf, cbuf_len);
          }
          strcpy (cbuf, anchor);
          *retval = buflen; /* that's a lie, but we have to! */
          return 1;
        }

        /* 
           Sanity checks:

           q > p unless p doesn't point to a number;
           SGR codes supported by ANSI.SYS are between 0 and 49;
           Each SGR code ends with a `;' or an `m'.

           If any of the above is violated, we just ignore the bogon.
        */
        if (q == p || code > 49 || code < 0 || (*q != 'm' && *q != ';'))
        {
          p_next = q;
          break;
        }
        if (*q == ';') /* more codes to follow */
        q++;

        /* Convert ANSI codes to color fore- and background.  */
        switch (code)
        {
        case 0: /* all attributes off */
          fg = norm_attr & 15;
          bg = (norm_attr >> 4) & 15;
          break;
        case 1: /* intensity on */
          fg |= 8;
          break;
        case 4: /* underline on */
          fg |= 8; /* we can't, so make it bold instead */
          break;
        case 5: /* blink */
          if (cur_blink != 1)
          {
            blinkvideo(); /* ensure we are'nt in bright bg mode */
            cur_blink = 1;
          }
          bg |= 8;
          break;
        case 7: /* reverse video */
          {
            unsigned char t = fg;
            fg = bg;
            bg = t;

            /* If it was blinking before, let it blink after.  */
            if (fg & 8)
              bg |= 8;

            /* If the fg was bold, let the background be bold.  */
            if ((t & 8) && cur_blink != 0)
            {
              intensevideo();
              cur_blink = 0;
            }
          }
          break;
        case 8: /* concealed on */
          fg = (bg & 7) | 8; /* make fg be like bg, only bright */
          break;
        case 30: case 31: case 32: case 33: /* foreground color */
        case 34: case 35: case 36: case 37:
          fg = (fg & 8) | (screen_color[code - 30] & 15);
          break;
        case 40: case 41: case 42: case 43: /* background color */
        case 44: case 45: case 46: case 47:
          bg = (bg & 8) | (screen_color[code - 40] & 15);
          break;
        case 39: /* default fg */
          fg = norm_attr & 15;
          break;
        case 49:
          bg = (norm_attr >> 4) & 15;
          break;
        default:
          p_next = q; /* ignore unknown codes */
          break;
        }
        p = q;
      } /* while loop */

      if (*p == 'm' && p > p_next)
      {
        /* They don't *really* want it invisible, do they?  */
        if (fg == (bg & 7))
          fg |= 8; /* make it concealed instead */

        /* Construct the text attribute and set it.  */
        cur_attr = (bg << 4) | fg;
        textattr(cur_attr);
        p_next = anchor = p + 1;
      }
      else
        break;
    }
    else
      p_next++;
  }  /* for loop */

  /* Output what's left in the buffer.  */
  screen_puts (anchor, buf_end);
  leftover = 0;
  *retval = buflen;
  return 1;
}

/* This is called before `main' to install our STDOUT redirector.  */
static void __attribute__((constructor))
djgpp_grep_startup(void)
{
  __FSEXT_set_function(STDOUT_FILENO, msdos_screen_write);
  isatty_stdout = isatty(STDOUT_FILENO);
  atexit(restore_blink_bit);
}
