#ifndef _EEL2_PREPROC_H_
#define _EEL2_PREPROC_H_
#include "ns-eel-int.h"
#include "../win32_utf8.h"

#define EEL2_PREPROCESS_OPEN_TOKEN "<?"

class EEL2_PreProcessor
{
  enum { LITERAL_BASE = 100000 };
public:
  EEL2_PreProcessor(int max_sz = 64<<20, int max_include_depth=20)
  {
    m_max_sz = max_sz;
    m_fsout = NULL;
    m_vm = NSEEL_VM_alloc();
    m_max_include_depth = max_include_depth;
    m_output_linecnt = 0;
    m_cur_depth = 0;
    NSEEL_VM_SetCustomFuncThis(m_vm, this);
    NSEEL_VM_SetStringFunc(m_vm, addStringCallback, NULL);
    if (!m_ftab.list_size)
    {
      NSEEL_addfunc_varparm_ex("printf",1,0,NSEEL_PProc_THIS,&pp_printf,&m_ftab);
      NSEEL_addfunc_varparm_ex("include",1,1,NSEEL_PProc_THIS,&pp_include,&m_ftab);
    }
    NSEEL_VM_SetFunctionTable(m_vm, &m_ftab);
    m_suppress = NSEEL_VM_regvar(m_vm, "_suppress");
  }

  void define(const char *name, double val)
  {
    EEL_F *v = NSEEL_VM_regvar(m_vm,name);
    if (v) *v = val;
  }

  ~EEL2_PreProcessor()
  {
    for (int x = 0; x < m_code_handles.GetSize(); x ++)
      NSEEL_code_free((NSEEL_CODEHANDLE) m_code_handles.Get(x));
    m_literal_strings.Empty(true,free);
    if (m_vm) NSEEL_VM_free(m_vm);
    m_suppress = NULL;
  }

  void clear_line_info()
  {
    m_line_tab.Resize(0);
  }
  const char *preprocess(const char *str, WDL_FastString *fs)
  {
    if (!m_vm || !m_suppress)
      return "preprocessor: memory error";
    if (!m_cur_depth)
    {
      m_line_tab.Resize(0);
      m_output_linecnt = 0;
      *m_suppress = 0.0;
    }
    int input_linecnt = 0;
    for (;;)
    {
      const bool suppress = m_suppress && *m_suppress > 0.0;

      int lc = 0;
      const char *tag = str;
      while (*tag && strncmp(tag,EEL2_PREPROCESS_OPEN_TOKEN,2)) if (*tag++ == '\n') lc++;

      if (lc)
      {
        input_linecnt += lc;
        if (suppress)
          add_line_inf(m_output_linecnt,lc);
        else
          m_output_linecnt += lc;
      }

      if (!*tag)
      {
        if (!suppress) fs->Append(str);
        return NULL;
      }
      if (!suppress && tag > str) fs->Append(str,(int)(tag-str));

      tag += 2;
      while (*tag == ' ' || *tag == '\t') tag++;
      str = tag;
      lc = 0;
      while (*str && strncmp(str,"?>",2)) if (*str++ == '\n') lc++;

      if (!*str)
      {
        m_tmp.SetFormatted(512, "%d: unterminated preprocessor " EEL2_PREPROCESS_OPEN_TOKEN " block", input_linecnt+1);
        return m_tmp.Get();
      }

      if (lc)
      {
        input_linecnt += lc;
        add_line_inf(m_output_linecnt,lc);
      }
      if (str > tag)
      {
        m_tmp.Set(tag,(int)(str-tag));
        NSEEL_CODEHANDLE ch = NSEEL_code_compile_ex(m_vm, m_tmp.Get(), 0, NSEEL_CODE_COMPILE_FLAG_COMMONFUNCS);
        if (!ch)
        {
          const char *err = NSEEL_code_getcodeerror(m_vm);
          if (err)
          {
            const int line_ref = atoi(err);
            while (*err >= '0' && *err <= '9') err++;
            m_tmp.SetFormatted(512,"%d: preprocessor%s%s",input_linecnt+line_ref,*err && *err != ':' ? ": ":"",err);
            return m_tmp.Get();
          }
        }
        else
        {
          lc = 0;
          const int oldlen = fs->GetLength();
          m_fsout = fs;
          NSEEL_code_execute(ch);
          m_fsout = NULL;
          m_code_handles.Add(ch);
          for (int x = oldlen; x < fs->GetLength(); x ++) if (fs->Get()[x] == '\n') lc++;
          if (lc)
          {
            add_line_inf(m_output_linecnt,-lc);
            m_output_linecnt += lc;
          }
        }
      }
      str += 2;
    }
  }

  const char *translate_error_line(const char *err_line)
  {
    if (m_line_tab.GetSize()<2) return err_line;
    int l = atoi(err_line)-1;
    if (l<0) return err_line;

    // tab is a list of pairs
    // [<output position>, delta]
    // delta>0 if input lines were skipped
    // delta<0 if output lines were added

    int nl = l;
    for (int x = m_line_tab.GetSize()-2; x >= 0; x -= 2)
    {
      int p = m_line_tab.Get()[x];
      if (nl > p)
      {
        int delta = m_line_tab.Get()[x+1];
        nl += delta;
        if (nl < p) nl = p;
      }
    }

    if (l == nl) return err_line;

    while (*err_line >= '0' && *err_line <= '9') err_line++;
    if (*err_line == ':') err_line++;
    m_tmp.SetFormatted(512,"%d:%s",1 + nl,err_line);
    return m_tmp.Get();
  }

  NSEEL_VMCTX m_vm;
  WDL_PtrList<char> m_literal_strings;
  WDL_FastString m_tmp, *m_fsout;
  WDL_TypedBuf<int> m_line_tab; // expose this in case the caller wants to keep copies around
  EEL_F *m_suppress;
  int m_max_sz;
  int m_cur_depth, m_max_include_depth;
  int m_output_linecnt;
  static eel_function_table m_ftab;
  WDL_PtrList<void> m_code_handles;
  WDL_PtrList<const char> m_include_paths;

  void add_line_inf(int output_linecnt, int lc)
  {
    if (!m_cur_depth)
    {
      m_line_tab.Add(output_linecnt); // log lc lines of input skipped
      m_line_tab.Add(lc);
    }
  }

  static EEL_F addStringCallback(void *opaque, struct eelStringSegmentRec *list)
  {
    EEL2_PreProcessor *_this = (EEL2_PreProcessor*)opaque;
    if (!_this) return -1.0;

    const int sz = nseel_stringsegments_tobuf(NULL,0,list);
    char *ns = (char *)malloc(sz+1);
    if (WDL_NOT_NORMALLY(!ns)) return -1.0;
    nseel_stringsegments_tobuf(ns,sz,list);

    const int nstr = _this->m_literal_strings.GetSize();
    for (int x=0;x<nstr;x++)
    {
      char *s = _this->m_literal_strings.Get(x);
      if (!strcmp(s,ns))
      {
        free(ns);
        return x + LITERAL_BASE;
      }
    }
    _this->m_literal_strings.Add(ns);
    return nstr + LITERAL_BASE;
  }

  const char *GetString(EEL_F v)
  {
    if (v >= LITERAL_BASE && v < LITERAL_BASE + m_literal_strings.GetSize())
      return m_literal_strings.Get((int) (v - LITERAL_BASE));
    return NULL;
  }

  static int eel_validate_format_specifier(const char *fmt_in, char *typeOut,
                                           char *fmtOut, int fmtOut_sz,
                                           char *varOut, int varOut_sz,
                                           int *varOut_used
                                          )
  {
    const char *fmt = fmt_in+1;
    int state=0;
    if (fmt_in[0] != '%') return 0; // ugh passed a non-specifier

    *varOut_used = 0;
    *varOut = 0;

    if (fmtOut_sz-- < 2) return 0;
    *fmtOut++ = '%';

    while (*fmt)
    {
      const char c = *fmt++;
      if (fmtOut_sz < 2) return 0;

      if (c == 'f'|| c=='e' || c=='E' || c=='g' || c=='G' || c == 'd' || c == 'u' ||
          c == 'x' || c == 'X' || c == 'c' || c == 'C' || c =='s' || c=='S' || c=='i')
      {
        *typeOut = c;
        fmtOut[0] = c;
        fmtOut[1] = 0;
        return (int) (fmt - fmt_in);
      }
      else if (c == '.')
      {
        *fmtOut++ = c; fmtOut_sz--;
        if (state&(2)) break;
        state |= 2;
      }
      else if (c == '+')
      {
        *fmtOut++ = c; fmtOut_sz--;
        if (state&(32|16|8|4)) break;
        state |= 8;
      }
      else if (c == '-' || c == ' ')
      {
        *fmtOut++ = c; fmtOut_sz--;
        if (state&(32|16|8|4)) break;
        state |= 16;
      }
      else if (c >= '0' && c <= '9')
      {
        *fmtOut++ = c; fmtOut_sz--;
        state|=4;
      }
      else if (c == '{')
      {
        if (state & 64) break;
        state|=64;
        if (*fmt == '.' || (*fmt >= '0' && *fmt <= '9')) return 0; // symbol name can't start with 0-9 or .

        while (*fmt != '}')
        {
          if ((*fmt >= 'a' && *fmt <= 'z') ||
              (*fmt >= 'A' && *fmt <= 'Z') ||
              (*fmt >= '0' && *fmt <= '9') ||
              *fmt == '_' || *fmt == '.' || *fmt == '#')
          {
            if (varOut_sz < 2) return 0;
            *varOut++ = *fmt++;
            varOut_sz -- ;
          }
          else
          {
            return 0; // bad character in variable name
          }
        }
        fmt++;
        *varOut = 0;
        *varOut_used=1;
      }
      else
      {
        break;
      }
    }
    return 0;
  }

  static int eel_format_strings(void *opaque, const char *fmt, const char *fmt_end, char *buf, int buf_sz, int num_fmt_parms, EEL_F **fmt_parms)
  {
    EEL2_PreProcessor *_this = (EEL2_PreProcessor*)opaque;
    int fmt_parmpos = 0;
    char *op = buf;
    while ((fmt_end ? fmt < fmt_end : *fmt) && op < buf+buf_sz-128)
    {
      if (fmt[0] == '%' && fmt[1] == '%')
      {
        *op++ = '%';
        fmt+=2;
      }
      else if (fmt[0] == '%')
      {
        char ct=0;
        char fs[128];
        char varname[128];
        int varname_used=0;
        const int l=eel_validate_format_specifier(fmt,&ct,fs,sizeof(fs),varname,sizeof(varname),&varname_used);
        if (!l || !ct)
        {
          *op=0;
          return -1;
        }

        const EEL_F *varptr = NULL;
        if (!varname_used)
        {
          if (fmt_parmpos < num_fmt_parms) varptr = fmt_parms[fmt_parmpos];
          fmt_parmpos++;
        }
        double v = varptr ? (double)*varptr : 0.0;

        if (ct == 's' || ct=='S')
        {
          const char *str = _this->GetString(v);
          const int maxl=(int) (buf+buf_sz - 2 - op);
          snprintf(op,maxl,fs,str ? str : "");
        }
        else
        {
          if (ct == 'x' || ct == 'X' || ct == 'd' || ct == 'u' || ct=='i')
          {
            snprintf(op,64,fs,(int) (v));
          }
          else if (ct == 'c')
          {
            *op++=(char) (int)v;
            *op=0;
          }
          else if (ct == 'C')
          {
            const unsigned int iv = (unsigned int) v;
            int bs = 0;
            if (iv &      0xff000000) bs=24;
            else if (iv & 0x00ff0000) bs=16;
            else if (iv & 0x0000ff00) bs=8;
            while (bs>=0)
            {
              const char c=(char) (iv>>bs);
              *op++=c?c:' ';
              bs-=8;
            }
            *op=0;
          }
          else
          {
            snprintf(op,64,fs,v);
          }
        }

        while (*op) op++;

        fmt += l;
      }
      else
      {
        *op++ = *fmt++;
      }

    }
    *op=0;
    return (int) (op - buf);
  }

  static EEL_F NSEEL_CGEN_CALL pp_printf(void *opaque, INT_PTR num_param, EEL_F **parms)
  {
    if (num_param>0 && opaque)
    {
      EEL2_PreProcessor *_this = (EEL2_PreProcessor*)opaque;
      const char *fmt = _this->GetString(parms[0][0]);
      if (fmt)
      {
        char buf[16384];
        const int len = eel_format_strings(opaque,fmt,NULL,buf,(int)sizeof(buf), (int)num_param-1, parms+1);

        if (len >= 0)
        {
          if (_this->m_fsout && _this->m_fsout->GetLength() < _this->m_max_sz)
          {
            _this->m_fsout->Append(buf,len);
          }
          return 1.0;
        }
      }
    }
    return 0.0;
  }

  static EEL_F NSEEL_CGEN_CALL pp_include(void *opaque, INT_PTR num_param, EEL_F **parms)
  {
    if (num_param>0 && opaque)
    {
      EEL2_PreProcessor *_this = (EEL2_PreProcessor*)opaque;
      if (_this->m_cur_depth >= _this->m_max_include_depth) return -1.0;

      const char *fn = _this->GetString(parms[0][0]);
      if (!fn || !*fn) return -2.0;

      WDL_FastString fullfn;
      for (int x = _this->m_include_paths.GetSize()-1; x >= 0; x--)
      {
        const char *p = _this->m_include_paths.Get(x);
        if (p && *p)
        {
          fullfn.Set(p);
          fullfn.Append(WDL_DIRCHAR_STR);
          fullfn.Append(fn);
          FILE *fp = fopenUTF8(fullfn.Get(),"rb");
          if (fp)
          {
            double rv = 0.0;
            _this->m_cur_depth++;
            fullfn.Set("");
            while (fullfn.GetLength() < (4<<20))
            {
              char buf[512];
              if (!fgets(buf,sizeof(buf),fp)) break;
              fullfn.Append(buf);
            }
            fclose(fp);

            WDL_FastString *outp = _this->m_fsout;
            if (_this->preprocess(fullfn.Get(),outp))
            {
              rv = -3.0;
            }
            _this->m_fsout = outp;

            _this->m_cur_depth--;
            return rv;
          }
        }
      }
      return -4.0;
    }
    return 0.0;
  }

};

eel_function_table EEL2_PreProcessor::m_ftab;

#endif
