/* ISC license. */

#include <skalibs/bytestr.h>
#include <skalibs/stralloc.h>
#include <skalibs/genalloc.h>
#include <execline/execline.h>

typedef struct elsubsu_s elsubsu_t, *elsubsu_t_ref ;
struct elsubsu_s
{
  elsubst_t const *subst ;
  unsigned int pos ;
} ;

typedef struct subsuinfo_s subsuinfo_t, *subsuinfo_t_ref ;
struct subsuinfo_s
{
  stralloc dst ;
  stralloc sa ;
  genalloc list ; /* array of elsubsu_t */
  char const *values ;
} ;

#define SUBSUINFO_ZERO { .dst = STRALLOC_ZERO, .sa = STRALLOC_ZERO, .list = GENALLOC_ZERO, .values = 0 }

#define TEST 0x80
#define MARK 0x40
#define KEEPESC 0x20
#define INCRESC 0x10

#define STATE 0x07
#define INWORD 0x00
#define INDOLL 0x01
#define INDBR 0x02
#define INVAR 0x03
#define INVARBR 0x04
#define ACCEPT 0x05

static int parseword (stralloc *sa, genalloc *list, char const *s, char const *vars, elsubst_t const *substs, unsigned int nsubst)
{
  static char const class[5] = "\0\\${}" ;
  static unsigned char const table[6][5] =
  {
    { ACCEPT, ACCEPT, ACCEPT, ACCEPT | TEST, ACCEPT },
    { INWORD | KEEPESC | INCRESC, INWORD | INCRESC, INWORD | INCRESC, INWORD | TEST | INCRESC, INWORD | INCRESC },
    { INDOLL | KEEPESC, INDOLL, INDOLL, INDOLL | TEST, INDOLL },
    { INWORD, INDBR | KEEPESC, INWORD, INWORD | TEST, INWORD },
    { INWORD, INWORD, INWORD, INWORD | TEST, INWORD | TEST },
    { INWORD, INVAR | MARK | KEEPESC, INVARBR | MARK | KEEPESC, INVAR | KEEPESC, INVARBR | KEEPESC }
  } ;

  unsigned int mark = 0, pos = 0, offset = 0, esc = 0, salen = sa->len, listlen = genalloc_len(elsubsu_t, list) ;
  unsigned char state = INWORD ;

  while (state != ACCEPT)
  {
    int nopush = 0 ;
    unsigned char c = table[byte_chr(class, 5, s[pos])][state] ;

    if (c & TEST)
    {
      unsigned int supp = (state == INVARBR) ;
      unsigned int i = 0 ;
      for (; i < nsubst ; i++)
      {
        if (!str_diffn(vars + substs[i].var, s + mark, pos - mark) && !vars[substs[i].var + pos - mark])
        {
          sa->len -= esc >> 1 ; offset += esc >> 1 ;
          if (esc & 1)
          {
            byte_copy(sa->s + mark - offset - 2 - supp, pos - mark + 1 + supp, sa->s + mark - offset + (esc>>1) - 1 - supp) ;
            sa->len-- ; offset++ ;
          }
          else
          {
            elsubsu_t cur ;
            cur.subst = substs + i ;
            cur.pos = mark - offset - 1 - supp ;
            if (!genalloc_append(elsubsu_t, list, &cur)) goto err ;
            offset += sa->len - cur.pos ;
            sa->len = cur.pos ;
            if (supp) nopush = 1 ;
          }
          break ;
        }
      }
    }
    if (nopush) offset++ ;
    else if (!stralloc_catb(sa, s+pos, 1)) goto err ;
    if (c & MARK) mark = pos ;
    if (!(c & KEEPESC)) esc = 0 ;
    if (c & INCRESC) esc++ ;
    state = c & STATE ; pos++ ;
  }
  sa->len-- ;
  return (int)pos ;

err:
  sa->len = salen ;
  list->len = listlen ;
  return -1 ;
}

static int substword (subsuinfo_t *info, unsigned int wordstart, unsigned int wordlen, unsigned int n, unsigned int offset)
{
  if (n < genalloc_len(elsubsu_t, &info->list))
  {
    elsubsu_t *list = genalloc_s(elsubsu_t, &info->list) ;
    char const *p = info->values + list[n].subst->value ;
    unsigned int l = list[n].pos + offset ;
    unsigned int dstbase = info->dst.len ;
    unsigned int sabase = info->sa.len ;
    unsigned int i = 0 ;
    int nc = 0 ;
    if (!stralloc_readyplus(&info->sa, l)) return -1 ;
    stralloc_catb(&info->sa, info->sa.s + wordstart, l) ;
    for ( ; i < list[n].subst->n ; i++)
    {
      int r ;
      unsigned int plen = str_len(p) ;
      info->sa.len = sabase + l ;
      if (!stralloc_readyplus(&info->sa, plen + wordlen - l)) goto err ;
      stralloc_catb(&info->sa, p, plen) ;
      stralloc_catb(&info->sa, info->sa.s + wordstart + l, wordlen - l) ;
      r = substword(info, sabase, info->sa.len - sabase, n+1, offset + plen) ;
      if (r < 0) goto err ;
      nc += r ;
      p += plen+1 ;
    }
    return nc ;
   err:
    info->sa.len = sabase ;
    info->dst.len = dstbase ;
    return -1 ;
  }
  else
  {
    if (!stralloc_readyplus(&info->dst, wordlen+1)) return -1 ;
    stralloc_catb(&info->dst, info->sa.s + wordstart, wordlen) ;
    stralloc_0(&info->dst) ;
    return 1 ;
  }
}

int el_substitute (stralloc *dst, char const *src, unsigned int len, char const *vars, char const *values, elsubst_t const *substs, unsigned int nsubst)
{
  subsuinfo_t info = SUBSUINFO_ZERO ;
  unsigned int nc = 0 ;
  unsigned int i = 0 ;
  unsigned int dstbase = dst->len ;
  int wasnull = !dst->s ;
  info.dst = *dst ;
  info.values = values ;

  while (i < len)
  {
    int r ;
    genalloc_setlen(elsubsu_t, &info.list, 0) ;
    info.sa.len = 0 ;
    r = parseword(&info.sa, &info.list, src + i, vars, substs, nsubst) ;
    if (r < 0) goto err ;
    i += r ;
    r = substword(&info, 0, info.sa.len, 0, 0) ;
    if (r < 0) goto err ;
    nc += r ;
  }
  genalloc_free(elsubsu_t, &info.list) ;
  stralloc_free(&info.sa) ;
  if (!wasnull) stralloc_free(dst) ;
  *dst = info.dst ;
  return (int)nc ;

err :
  genalloc_free(elsubsu_t, &info.list) ;
  stralloc_free(&info.sa) ;
  if (wasnull) stralloc_free(&info.dst) ; else info.dst.len = dstbase ;
  *dst = info.dst ;
  return -1 ;
}
