/*      Copyright (C) 2001, 2002, 2003, 2004 Stijn van Dongen
 *
 * This file is part of Zoem. You can redistribute and/or modify Zoem under the
 * terms of the GNU General Public License;  either version 2 of the License or
 * (at your option) any later  version.  You should have received a copy of the
 * GPL along with Zoem, in the file COPYING.
*/

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/wait.h>

#include <limits.h>
#include <string.h>
#include <ctype.h>
#include <string.h>

#include "filter.h"
#include "read.h"
#include "source.h"
#include "parse.h"
#include "iface.h"
#include "segment.h"
#include "curly.h"
#include "util.h"
#include "key.h"

#include "util/io.h"
#include "util/hash.h"
#include "util/types.h"
#include "util/getpagesize.h"
#include "util/err.h"
#include "util/ding.h"

static   mcxHash*    mskTable_g        =  NULL;    /* masked input files*/


/*
 * This code also finds and stores inline files.
 * It is not error/memory clean and contains hard exits.
 *
*/

mcxstatus  yamReadChunk
(  mcxIO          *xf               /* must be open! */
,  mcxTing        *filetxt
,  mcxbits        flags
,  int            *linecount
,  int            chunk_size        /* if 0, try to read everything */
)
   {  int      lc          =  0
   ;  mcxbool  masking     =  flags & READ_MASKING
   ;  mcxTing* line        =  mcxTingEmpty(NULL, 160)
   ;  int      level       =  0
   ;  size_t   bytes_read  =  0      
   /*  size_t   bytes_offset=  xf->bc */
   ;  const char* me       =  "zoem input"
   ;  mcxstatus stat       =  STATUS_DONE

   ;  filetxt = mcxTingEmpty(filetxt, 0)

   ;  while
      (  STATUS_OK == (stat = mcxIOreadLine(xf, line, MCX_READLINE_DEFAULT)))
      {  int discardcmt = 0, esc = 0
      ;  char* a = line->str, *p = a, *z = a + line->len, *P = NULL

      ;  mcxbool inline_file =  strncmp(line->str, "\\=", 2) ? 0 : 1

      ;  lc++

      ;  if (!inline_file)
         {  while (p<z)
            {  int c = *p
            ;  if (esc)
               {  esc = 0
               ;  if (!discardcmt) /* else we don't recognize comments anymore */
                  {  if (c == ':')
                     {  if ((c = p[1]) == '!')
                        discardcmt = 3
                     ;  else if (c == '/')
                        discardcmt = 2
                     ;  else
                        discardcmt = 1
                  ;  }                 /* note: *p == ':' */
                     if (discardcmt)
                     {  if (discardcmt == 3)
                        P = p  /*   must keep on parsing to account for curlies
                                *   it's a special case hack, too bad
                               */
                     ;  else
                        break
                  ;  }
                  }
               }
               else if (c == '\\')
               esc = 1
            ;  else if (c == '{')
               level++
            ;  else if (c == '}')
               level--

            ;  p++
         ;  }
            if (discardcmt == 3)             /* s/:!//       */
            mcxTingSplice(line, "", P-a, 2, 0)
         ;  else if (discardcmt == 2)        /* s/\\:.*\n//    */
               mcxTingShrink(line, p-a-1)
         ;  else if (discardcmt == 1)        /* s/\\:.*\n/\n/  */
               mcxTingShrink(line, p-a-1)
            ,  mcxTingAppend(line, "\n")

         ;  mcxTingAppend(filetxt, line->str)
         ;  bytes_read += line->len
      ;  }

         if (inline_file && !masking)
         {  mcxKV    *kv
         ;  mcxTing *fname = NULL,  *masked = mcxTingEmpty(NULL, 1000)
         ;  int l, sublc = 0

         ;  fname = yamBlock(line, 2, &l, NULL)

         ;  if (!fname)
            {  yamErr(me, "format err in \\={fname} specification")
            ;  mcxExit(1)
         ;  }

            kv = mcxHashSearch(fname, mskTable_g, MCX_DATUM_INSERT)
         ;  if (kv->key != fname)
            {  yamErr(me, "file <%s> already masked", fname->str)
            ;  mcxExit(1)
         ;  }

            mcxTell(me, "=== reading inline file <%s>", fname->str)

         ;  if (STATUS_DONE == yamReadChunk(xf, masked, 1, &sublc, 0))
            {  kv->val = masked
            ;  mcxTingPrintAfter(filetxt, "\\___jump_lc___{%d}\n", sublc)
            ;  continue
         ;  }
            else
            {  yamErr(me, "error reading inline file <%s>", fname->str)
            ;  mcxExit(1)
         ;  }
         }

         else if (inline_file && masking)
         {  mcxTell(me, "=== done reading inline file")

         ;  if (line->str[2] != '=')
            {  yamErr(me, "expecting closing '\\==' at line <%d>",xf->lc)
            ;  mcxExit(1)
         ;  }

            if (linecount)
            *linecount = lc
         ;  else
            {  yamErr("zoem PBD", "should have linecount pointer")
            ;  mcxExit(1)
         ;  }

            mcxTingFree(&line)
         ;  return STATUS_DONE   /* fixme; move this return to end */
      ;  }

         if (level < 0)
            mcxErr
            (  me
            ,  "Curly underflow in file <%s> at line <%ld>"
            ,  xf->fn->str
            ,  (long) xf->lc
            )
         ,  level = 0

      ;  if
         (  !masking
         && chunk_size
         && bytes_read >= chunk_size
         && level <= 0
         )
         {  stat = STATUS_OK
         ;  break
      ;  }
      }
      mcxTingFree(&line)

   ;  if (0)
      fprintf(stderr, "read <%ld> bytes\n", (long) xf->bc)

   ;  if (masking)
      {  yamErr(me, "masking scope not closed!\n")
      ;  mcxExit(1)
   ;  }

      if (linecount)
      *linecount = lc

   ;  return stat
;  }


void mod_read_init
(  int n
)
   {  mskTable_g = yamHashNew(n)
;  }


void mod_read_exit
(  void
)
   {  if (mskTable_g)
      mcxHashFree(&mskTable_g, mcxTingFree_v, mcxTingFree_v)
;  }


const mcxTing* yamInlineFile
(  mcxTing* fname
)
   {  mcxKV* kv =    mskTable_g
                  ?  mcxHashSearch(fname, mskTable_g, MCX_DATUM_FIND)
                  :  NULL
   ;  return kv ? (mcxTing*)kv->val : NULL
;  }


void yamUnprotect
(  mcxTing* txt
)
   {  char* p  =  txt->str
   ;  char* q  =  txt->str
   ;  int   esc=  0  
   
   ;  while(p<txt->str+txt->len)
      {
         if (esc)
         {  if (*p == '\\' || *p == '{' || *p == '}')
            *(q++) = *p
         ;  else
            {  *(q++) = '\\'
            ;  *(q++) = *p
         ;  }
            esc = 0
      ;  }
         else if (*p == '\\')
         esc = 1
      ;  else
         *(q++) = *p

      ;  p++
   ;  }

      *q = '\0'
   ;  txt->len = q-txt->str
;  }


mcxTing*  yamProtect
(  mcxTing* data
)
   {  int      i     =   0
   ;  int      j     =   0
   ;  int      n     =   data->len + (data->len/8)

   ;  mcxTing* protected   =  mcxTingEmpty(NULL, n)

   ;  for(i=0;i<data->len;i++)
      {  char c      =  *(data->str+i)
      ;  int esc     =  (c == '\\' || c == '{' || c == '}') ? 1 : 0

      ;  if (j+esc >= protected->mxl)
         mcxTingEnsure(protected, protected->mxl + (protected->mxl/3) + 2)

      ;  if (esc)
         *(protected->str+j++) = '\\'

      ;  *(protected->str+j++) = c
   ;  }

      protected->len =  j
   ;  *(protected->str+j) = '\0'
   ;  return protected
;  }


mcxTing*  yamReadData
(  mcxIO          *xf
,  mcxTing        *filetxt
)
#define BUFLEN 1024
   {  char     cbuf[BUFLEN]
   ;  short    c
   ;  int      i        =  0

   ;  if (!xf->fp)
         mcxIOerr(xf, "yamReadData PBD", "is not open")
      ,  mcxExit(1)

   ;  filetxt = mcxTingEmpty(filetxt, 0)

   ;  while
      (  c = mcxIOstep(xf)
      ,  c != EOF
      )
      {  if (c == '\\' || c == '{' || c == '}')
         cbuf[i++] =  (char) '\\'

      ;  cbuf[i] = (char) c

      ;  if (i+2 >= BUFLEN)      /* if not, no room for double bs */
         {  mcxTingNAppend(filetxt, cbuf, i+1)
         ;  i = 0
         ;  continue
      ;  }
         i++
   ;  }

      if (i)               /* nothing written at this position */
      mcxTingNAppend(filetxt, cbuf, i)

   ;  xf->ateof   =  1
   ;  return filetxt
#undef BUFLEN
;  }


mcxIO* yamRead__open_file__
(  const char* name
)
   {  mcxIO *xf = mcxIOnew(name, "r")
   ;  if (mcxIOopen(xf, RETURN_ON_FAIL) == STATUS_OK)
      return xf
   ;  else
      mcxIOfree(&xf)
   ;  return NULL
;  }


/*
 *    I had some trouble getting the details right, and perhaps the stuff below
 *    is stupid in places. I had problems getting rid of duplicate output; The
 *    fflush(NULL) solves that.  These are the different cases that once caused
 *    trouble: Assume a file t.azm containing
 *
 *    \write{bar}{txt}{1 2 3 4 hoedje van papier}
 *    \system{date}{}{}
 *    \system{date}{}{}
 *    \system{date}{}{}
 *    \system{date}{}{}
 *
 *    and consider the command lines
 *
 *    zoem -i t -o -
 *    zoem -i t -o foo
 *    zoem -i t -o - > foo
 *
 *    All these cases were buggy under different circumstances; pipe ends need
 *    to be closed, data needs to be flushed, and open file descriptors
 *    need to be closed.
*/

mcxTing* yamSystem
(  char* cmd
,  char* argv[]
,  mcxTing* data
)
#define LDB (tracing_g & ZOEM_TRACE_SYSTEM)
#define  RB_SIZE 4096
   {  int   stat1, stat2, xstat, k
   ;  int   fx[2]
   ;  int   fy[2]
   ;  pid_t cp1, cp2
   ;  char  rb[RB_SIZE]

   ;  int statusx =  pipe(fx)
   ;  int statusy =  pipe(fy)

   ;  if (statusx && statusy)
      ;  /* elude compiler warning */

   ;  yamUnprotect(data)
   ;  if (fflush(NULL))
      perror("flush")

   ;  cp1 = fork()

   ;  if (cp1 == -1)       /* error */
      {  yamErr("\\system#3", "fork error")
      ;  perror("\\system#3")
      ;  return NULL
   ;  }
      else if (cp1 == 0)  /* child */
      {  if (LDB)
         printf
         (" sys| CHILD closing x1=%d and y0=%d\n", (int) fx[1], (int) fy[0])

      ;  if (close(fx[1])<0)
         perror("close fx[1]")  /* close parent output */
      ;  if (close(fy[0])<0)
         perror("close fy[0]")  /* close parent input  */

      ;  if (LDB)
         {  printf
            (" sys| CHILD mapping %d to %d\n",(int)fx[0], (int) fileno(stdin))
         ;  printf
            (" sys| CHILD mapping %d to %d\n",(int)fy[1], (int) fileno(stdout))
      ;  }

         dup2(fx[0], fileno(stdin))
      ;  dup2(fy[1], fileno(stdout))

      ;  if (close(fx[0])<0)
         perror("close fx[0]")
      ;  if (close(fy[1])<0)
         perror("close fy[1]")

      ;  switch ((cp2 = fork()))
         {  case -1
         :  perror("fork")
         ;  exit(1)
         ;

            case 0               /* grandchild */
         :  execvp(cmd, argv)    /* do the actual stuff */
         ;  yamErr("\\system#3", "exec <%s> failed (bummer)", cmd)
         ;  if (0)
            perror("exec failed (bummer)")
         ;  exit(1)
         ;

            default              /* child */
         :  if (waitpid(cp2, &stat2, 0)< 0 || !WIFEXITED(stat2))
            {  if (0)
               perror("child")
         ;  }

            while (1 && (k = read(fileno(stdin), rb, RB_SIZE)))
            {  if (LDB)
               fprintf
               (  stderr
               ," sys| CHILD discarding %d bytes\n", (int) k
               )
         ;  }
           /* The discard loop is for this:
            * \system{cat}{{-#}}{abcdef} (i.e. failing command)
            * we need someone to read the stdin, lest we get a broken pipe.
            * (and we want \system never to fail in zoem).
           */

           /*  Do I need the close below? e.g. wrt 'zoem -i t -o - > foo'.  I'm
            *  not too clear on these issues, but it certainly seems best
            *  practice to close all pipe ends when done.
           */
         ;  if (close(fileno(stdin))<0)
            perror("close fileno(stdin)")
         ;  if (close(fileno(stdout))<0)
            perror("close fileno(stdout)")
         ;  if (stat2)
            yamErr("\\system#3", "child <%s> failed", cmd)
         ;  exit(stat2 ? 1 : 0)
      ;  }
      }
      else /* parent */
      {  mcxTing* buf   =  mcxTingEmpty(NULL, 80)
      ;  mcxTing* res   =  NULL
      ;  static int ct = 0

      ;  if (LDB)
         printf
         (" sys| PARENT closing x0=%d and y1=%d\n", (int) fx[0], (int) fy[1])

      ;  close(fy[1])                  /* close child output     */
      ;  close(fx[0])                  /* close child input      */
      ;  if (data->len)                /* mq, must not exceed buffer size */
         {  fprintf(stderr, "<%d> bytes written\n", (int) data->len)
         ;  write(fx[1], data->str, data->len)
      ;  }
         close(fx[1])

      ;  ct++

      ;  while ((k = read(fy[0], rb, RB_SIZE)))
         {  if (k<0)
            {  perror("read")
            ;  break
         ;  }
            if (buf->len + k > buf->mxl)
            mcxTingEnsure(buf, buf->mxl  + buf->mxl/2)
         ;  mcxTingNAppend(buf, rb, k)
      ;  }
         close(fy[0])
      ;  if ((waitpid(cp1, &stat1, 0)) < 0 || !WIFEXITED(stat1))
         perror("waiting parent")
      ;  xstat = WIFEXITED(stat1) ? WEXITSTATUS(stat1) : 1
      ;  yamKeySet("__sysval__", xstat ? "1" : "0")

      ;  if (!xstat)
         res = yamProtect(buf)
      ;  else
         res = NULL

      ;  mcxTingFree(&buf)
      ;  return res
   ;  }
      return NULL
;  }
#undef LDB
#undef RB_SIZE


mcxIO* yamTryOpen
(  mcxTing*  fname
,  const mcxTing** inline_txt_ptr
,  mcxbool useSearchPath
)
#define LDB (tracing_g & ZOEM_TRACE_IO)
   {  mcxIO *xf = NULL
   ;  mcxTing* file = NULL
   ;  char* envp

   ;  if (fname->len > 1024)
      {  yamErr("yamTryOpen", "input file name expansion too long (>1024)")
      ;  *inline_txt_ptr = NULL
      ;  return NULL
   ;  }

   /* .  first check inline file
    * .  then check file name as is.
    * .  then check $ZOEMSEARCHPATH
    * .  then check __searchpath__
    * .  then check sourceGetPath (last file openened)
   */

                           /* .. then we may look for inline files */
      if (inline_txt_ptr)
      {  const mcxTing* filetxt = yamInlineFile(fname)
      ;  if (filetxt)
         {  *inline_txt_ptr = filetxt
         ;  if (LDB)
            mcxTell(NULL, "=== using inline file <%s>", fname->str)
         ;  return mcxIOnew(fname->str, "r")
      ;  }
      }

      if (LDB)
      printf("<IO>| trying (cwd) <%s>\n", fname->str)
   ;  xf = yamRead__open_file__(fname->str)

   /*
    * Try to find a readable file in a few more places.
   */

   ;  if (!xf && useSearchPath && (envp = getenv("ZOEMSEARCHPATH")))
      {  mcxTing* zsp = mcxTingNew(envp)
      ;  char *q, *p = zsp->str
      ;  mcxTingTr(zsp, ": \n\t\f\r", "      ", TR_SQUASH)
      ;  file  = mcxTingEmpty(file, 30)
      ;  while (p)
         {  while (isspace((unsigned char) *p))
            p++
         ;  q = mcxStrChrIs(p, isspace, -1)
         ;  if (q)
            mcxTingNWrite(file, p, q-p)
         ;  else
            mcxTingWrite(file, p)

         ;  mcxTingAppend(file, fname->str)
         ;  if (LDB)
            printf("<IO>| trying (env) <%s>\n", file->str)
         ;  if ((xf = yamRead__open_file__(file->str)))
            {  mcxTingWrite(fname, file->str)
            ;  break
         ;  }
            p = q
      ;  }
         mcxTingFree(&zsp)
   ;  }

      if (!xf && useSearchPath)
      {  mcxTing* tmp = mcxTingNew("__searchpath__")
      ;  mcxTing* sp  = yamKeyGet(tmp)
      ;  yamSeg*  spseg = sp ? yamStackPushTmp(sp) : NULL

      ;  file  = mcxTingEmpty(file, 30)

      ;  while (sp && yamParseScopes(spseg, 1, 0) == 1)
         {  mcxTingPrint(file, "%s/%s", arg1_g->str, fname->str)
         ;  if (LDB)
            printf("<IO>| trying (var) <%s>\n", file->str)
         ;  if ((xf = yamRead__open_file__(file->str)))
            {  mcxTingWrite(fname, file->str)
            ;  break
         ;  }
         }
         mcxTingFree(&tmp)
      ;  yamStackFreeTmp(&spseg)
   ;  }

      if (!xf)
      {  mcxTingFree(&file)      /* ugly hack ensuring memcleannes */
      ;  file = sourceGetPath()
      ;  if (file)
         {  mcxTingAppend(file, fname->str)
         ;  if (LDB)
            printf
            ("<IO>| trying (his) <%s> path stripped from last include\n"
            ,  file->str
            )
         ;  if ((xf = yamRead__open_file__(file->str)))
            mcxTingWrite(fname, file->str)
      ;  }
      }

      mcxTingFree(&file)
   ;  return xf
#undef LDB
;  }


