/*
 * to generate a template language pack, use:
 * build_sample_langpack --template *.rc *.cpp etc > file.langpack
 *
 */
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#ifndef _WIN32
#define stricmp strcasecmp
#endif

#include "../wdltypes.h"
#include "../assocarray.h"
#include "../ptrlist.h"
#include "../wdlstring.h"
#include "../fnv64.h"

static bool isblank(char c)
{
  return c==' ' || c== '\t' || c=='\r' || c=='\n';
}
static bool isblankorz(char c)
{
  return !c || isblank(c);
}

WDL_StringKeyedArray<char *> section_descs;

WDL_StringKeyedArray< WDL_PtrList<char> * > translations;
WDL_StringKeyedArray< WDL_StringKeyedArray<bool> * > translations_indexed;

void gotString(const char *str, int len, const char *secname, bool isRC, const char *fn, int line)
{
  WDL_PtrList<char> *sec=translations.Get(secname);
  if (!sec)
  {
    sec=new WDL_PtrList<char>;
    translations.Insert(secname,sec);
  }
  WDL_StringKeyedArray<bool> *sec2 = translations_indexed.Get(secname);
  if (!sec2)
  {
    sec2 = new WDL_StringKeyedArray<bool>(true);
    translations_indexed.Insert(secname,sec2);
  }
  if (len > (int)strlen(str))
  {
    fprintf(stderr,"gotString got len>strlen(str) on %s:%d\n",fn,line);
    exit(1);
  }

  WDL_FastString buf;
  if (!isRC)
  {
    int st = 0;
    for (int x = 0; x < len; x ++)
    {
      if (!st)
      {
        if (str[x] == '\\' && str[x+1])
        {
          buf.Append(str+x,2);
          x++;
        }
        else if (str[x] == '\"') st=1;
        else buf.Append(str+x,1);
      }
      else if (str[x] == '\"') st=0;
      else if (!isblank(str[x]))
      {
        fprintf(stderr,"gotString has junk between concatenated strings on %s:%d\n",fn,line);
        exit(1);
      }
    }
  }
  else
  {
    buf.Set(str,len);
  }
  if (sec2->Get(buf.Get())) return; //already in list

  sec2->Insert(buf.Get(),true);
  sec->Add(strdup(buf.Get()));
}
const char *g_last_file;
int g_last_linecnt;
int length_of_quoted_string(char *p, bool convertRCquotesToSlash)
{
  int l=0;
  while (p[l])
  {
    if (convertRCquotesToSlash && p[l] == '\"' && p[l+1] == '\"')  p[l]='\\';

    if (p[l] == '\"')
    {
      if (convertRCquotesToSlash) return l;

      // scan over whitespace to see if another string begins, concat them
      int l2 = l+1;
      while (isblank(p[l2])) l2++;
      if (p[l2] != '\"') return l;
      l = l2;
    }
    if (p[l] == '\\')
    {
      l++;
    }
    if (!p[l] || p[l] == '\r' || p[l] == '\n') break;
    l++;
  }
  fprintf(stderr,"ERROR: mismatched quotes in file %s:%d, check input!\n",g_last_file,g_last_linecnt);
  exit(1);
  return -1;
}

#define HACK_WILDCARD_ENTRY 2
static int isLocalizeCall(const char *p)
{
  int rv = 0;
  if (!strncmp(p,"__LOCALIZE",10)) { p+=10; rv = 1; }
  else if (!strncmp(p,"ADD_WILDCARD_ENTRY",18)) { p+=18; rv = HACK_WILDCARD_ENTRY; }
  else return 0;

  if (*p == '_') while (*p == '_' || (*p >= 'A'  && *p <= 'Z')||(*p>='0' && *p <='9')) p++;
  while (isblank(*p)) p++;
  return *p == '(' ? rv : 0;
}

WDL_UINT64 outputLine(const char *strv, int casemode)
{
  WDL_UINT64 h = WDL_FNV64_IV;
  const char *p=strv;
  while (*p)
  {
    char c = *p++;
    if (c == '\\')
    {
      if (*p == '\\'||*p == '"' || *p == '\'') h=WDL_FNV64(h,(unsigned char *)p,1);
      else if (*p == 'n') h=WDL_FNV64(h,(unsigned char *)"\n",1);
      else if (*p == 'r') h=WDL_FNV64(h,(unsigned char *)"\r",1);
      else if (*p == 't') h=WDL_FNV64(h,(unsigned char *)"\t",1);
      else if (*p == '0') h=WDL_FNV64(h,(unsigned char *)"",1);
      else if (*p == 'x' && p[1] == 'e' && p[2] == '9')
      {
        h=WDL_FNV64(h,(unsigned char *)"\xe9",1);
        p+=2;
      }
      else
      {
        fprintf(stderr,"ERROR: unknown escape seq in '%s' at '%s'\n",strv,p);
        exit(1);
      }
      p++;
    }
    else h=WDL_FNV64(h,(unsigned char *)&c,1);
  }
  h=WDL_FNV64(h,(unsigned char *)"",1);

  printf("%08X%08X=",(int)(h>>32),(int)(h&0xffffffff));
  int lc = 0;
  while (*strv)
  {
    int c = *strv++;
    if (lc == '%' || lc == '\\') { /* hacky*/ }
    else if (c == '\\' && strv[0] == 'x' && strv[1] == 'e' && strv[2] == '9')
    {
      strv+=3;
      c = 0xe9;
    }
    else if (casemode == 2)
    {
      switch (tolower_safe(c))
      {
        case 'o': c='0'; break;
        case 'i': c='1'; break;
        case 'e': c='3'; break;
        case 'a': c='4'; break;
        case 's': c='5'; break;
      }
    }
    else if (casemode==-1) c=tolower_safe(c);
    else if (casemode==1) c=toupper_safe(c);
    else if (casemode==4)
    {
      switch (c)
      {
        case 'E': c=0xc494; break;
        case 'A': c=0xc381; break;
        case 'I': c=0xc38f; break;
        case 'N': c=0xc391; break;
        case 'O': c=0xc395; break;
        case 'U': c=0xc39a; break;
        case 'B': c=0xc39f; break;
        case 'C': c=0xc486; break;
        case 'G': c=0xc4a0; break;
        case 'e': c=0xc497; break;
        case 'a': c=0xc3a4; break;
        case 'i': c=0xc4ad; break;
        case 'n': c=0xc584; break;
        case 'o': c=0xc58d; break;
        case 'u': c=0xc5af; break;
        case 'b': c=0xc39e; break;
        case 'c': c=0xc48d; break;
        case 'g': c=0xc49f; break;
      }
      if (c >= 256)
      {
        printf("%c",(c>>8));
        c&=0xff;
      }
    }
    printf("%c",c);
    if (lc == '%' && (c == '.' || c=='l' || (c>='0' && c<='9')))
    {
      // ignore .xyz and l between format spec (hacky)
    }
    else if (lc == '%' && c == '%') lc = 0;
    else if (lc == '\\' && c == '\\') lc = 0;
    else lc = c;
  }
  printf("\n");
  return h;
}


WDL_StringKeyedArray<int> g_resdefs;
const char *getResourceDefinesFromHeader(const char *fn)
{
  g_resdefs.DeleteAll();

  FILE *fp=fopen(fn,"rb");
  if (!fp) return "error opening header";
  for (;;)
  {
    char buf[8192];
    g_last_linecnt++;
    if (!fgets(buf,sizeof(buf),fp)) break;
    char *p = buf;
    while (*p) p++;
    while (p>buf && (p[-1] == '\r'|| p[-1] == '\n' || p[-1] == ' ')) p--;
    *p=0;

    if (!strncmp(buf,"#define",7))
    {
      p=buf;
      while (*p && *p != ' '&& *p != '\t') p++;
      while (*p == ' ' || *p == '\t') p++;
      char *n1 = p;
      while (*p && *p != ' '&& *p != '\t') p++;
      if (*p) *p++=0;
      while (*p == ' ' || *p == '\t') p++;
      int a = atoi(p);
      if (a && *n1)
      {
        g_resdefs.Insert(n1,a);
      }
    }
  }

  fclose(fp);
  return NULL;
}

void processRCfile(FILE *fp, const char *dirprefix, const char *filename)
{
  char sname[512];
  sname[0]=0;
  int depth=0;
  for (;;)
  {
    char buf[8192];
    g_last_linecnt++;
    if (!fgets(buf,sizeof(buf),fp)) break;
    char *p = buf;
    while (*p) p++;
    while (p>buf && (p[-1] == '\r'|| p[-1] == '\n' || p[-1] == ' ')) p--;
    *p=0;

    p=buf;
    if (sname[0]) while (*p == ' ' || *p == '\t') p++;
    char *first_tok = p;
    if (!strncmp(first_tok,"CLASS",5) && isblank(first_tok[5]) && first_tok[6]=='\"') continue;

    while (*p && *p != '\t' && *p != ' ') p++;
    if (*p) *p++=0;
    while (*p == '\t' || *p == ' ') p++;
    char *second_tok = p;

    if ((!strncmp(second_tok,"DIALOG",6) && isblankorz(second_tok[6])) ||
        (!strncmp(second_tok,"DIALOGEX",8) && isblankorz(second_tok[8]))||
        (!strncmp(second_tok,"MENU",4) && isblankorz(second_tok[4])))
    {
      if (sname[0])
      {
        fprintf(stderr,"got %s inside a block\n",second_tok);
        exit(1);
      }
      int sec = g_resdefs.Get(first_tok);
      if (!sec)
      {
        fprintf(stderr, "unknown dialog %s\n",first_tok);
        exit(1);
      }
      sprintf(sname,"%s%s%s_%d",dirprefix?dirprefix:"",dirprefix?"_":"",second_tok[0] == 'M' ? "MENU" : "DLG",sec);
      section_descs.Insert(sname,strdup(first_tok));
    }
    else if (sname[0] && *second_tok && *first_tok)
    {
      if (*second_tok == '"')
      {
        int l = length_of_quoted_string(second_tok+1,true);
        if (l>0)
        {
          gotString(second_tok+1,l,sname, true, filename, g_last_linecnt);

          // OSX menu support: store a 2nd string w/o \tshortcuts, strip '&' too
          // note: relies on length_of_quoted_string() pre-conversion above
          if (depth && strstr(sname, "MENU_"))
          {
            int j=0;
            char* m=second_tok+1;
            for(;;)
            {
              if (!*m || (*m == '\\' && *(m+1)=='t') || (*m=='\"' && *(m-1)!='\\')) { buf[j]=0; break; }
              if (*m != '&') buf[j++] = *m;
              m++;
            }
            if (j!=l) gotString(buf,j,sname,true, filename, g_last_linecnt);
          }
        }
      }
    }
    else if (!strcmp(first_tok,"BEGIN"))
    {
      depth++;
    }
    else if (!strcmp(first_tok,"END"))
    {
      depth--;
      if (depth<0)
      {
        fprintf(stderr,"extra END\n");
        exit(1);
      }
      if (!depth) sname[0]=0;
    }

  }
  if (depth!=0)
  {
    fprintf(stderr,"missing some ENDs at end of rc file\n");
    exit(1);
  }
}

void processCPPfile(FILE *fp, const char *filename)
{
  char clocsec[512];
  clocsec[0]=0;
  WDL_FastString fs;
  for (;;)
  {
    char buf[8192];
    if (!fgets(buf,sizeof(buf),fp)) break;
    fs.Append(buf);
  }

  char *p = (char*)fs.Get();
  char *comment_state = NULL;
  g_last_linecnt++;
  while (*p)
  {
    if (!strncmp(p,"//",2))
    {
      comment_state = p;
      p+=2;
    }
    else if (*p == '\n')
    {
      g_last_linecnt++;
      comment_state = NULL;
      p++;
    }
    else if (!comment_state)
    {
      int hm;
      if (*p == '\\') { p++; if (*p) p++; }
      else if (*p == '\'') { p++; if (*p == '"') p++; }
      else if (*p == '"')
      {
        int l = length_of_quoted_string(p+1,false);
        if (clocsec[0])
        {
          gotString(p+1,l,clocsec,false, filename, g_last_linecnt);
        }
        p += l+2;
      }
      else if ((p==(char*)fs.Get() || (!isalnum_safe(p[-1]) && p[-1] != '_')) && (hm=isLocalizeCall(p)))
      {
        while (*p != '(') p++;
        p++;
        while (isblank(*p)) p++;
        if (*p++ != '"')
        {
          fprintf(stderr,"Error: missing \" on %s:%d\n",filename,g_last_linecnt);
          exit(1);
        }
        int l = length_of_quoted_string(p,false);
        char *sp = p;
        p+=l+1;
        while (isblank(*p)) p++;
        if (*p++ != ',')
        {
          fprintf(stderr,"Error: missing , on %s:%d\n",filename,g_last_linecnt);
          exit(1);
        }
        while (isblank(*p)) p++;
        if (*p++ != '"')
        {
          fprintf(stderr,"Error: missing second \" on %s:%d\n",filename,g_last_linecnt);
          exit(1);
        }
        int l2 = length_of_quoted_string(p,false);

        if (hm == HACK_WILDCARD_ENTRY)
        {
          gotString(p,l2,"render_wildcard",false, filename, g_last_linecnt);
          p += l2;
        }
        else
        {
          char sec[512];
          memcpy(sec,p,l2);
          sec[l2]=0;
          p+=l2;
          gotString(sp,l,sec,false, filename, g_last_linecnt);
        }
        p++;
      }
      else
        p++;
    }
    else if (p > comment_state && p < comment_state + 4)
    {
      if (!strncmp(p,"!WANT_LOCALIZE_STRINGS_BEGIN:",29))
      {
        p += 29;
        if (clocsec[0])
        {
          fprintf(stderr,"Error: !WANT_LOCALIZE_STRINGS_BEGIN: before WANT_LOCALIZE_STRINGS_END on  %s:%d\n",filename,g_last_linecnt);
          exit(1);
        }
        int a = 0;
        while (*p && !isblank(*p) && a < sizeof(clocsec)) clocsec[a++] = *p++;
        if (a >= sizeof(clocsec))
        {
          fprintf(stderr,"Error: !WANT_LOCALIZE_STRINGS_BEGIN: too long on %s:%d\n",filename,g_last_linecnt);
          exit(1);
        }
        clocsec[a]=0;
      }
      else
      {
        if (!strncmp(p,"!WANT_LOCALIZE_STRINGS_END",26))
        {
          if (!clocsec[0])
          {
            fprintf(stderr,"Error: mismatched !WANT_LOCALIZE_STRINGS_END on %s:%d\n",filename,g_last_linecnt);
            exit(1);
          }
          clocsec[0]=0;
        }
        p++;
      }
    }
    else p++;
  }
  if (clocsec[0])
  {
    fprintf(stderr,"Error: missing !WANT_LOCALIZE_STRINGS_END at eof %s:%d\n",filename,g_last_linecnt);
    exit(1);
  }
}



int main(int argc, char **argv)
{
  int x;
  int casemode=0;
  for (x=1;x<argc;x++)
  {
    if (argv[x][0] == '-')
    {
      if (!strcmp(argv[x],"--lower")) casemode = -1;
      else if (!strcmp(argv[x],"--leet")) casemode = 2;
      else if (!strcmp(argv[x],"--utf8test")) casemode = 4;
      else if (!strcmp(argv[x],"--upper")) casemode = 1;
      else if (!strcmp(argv[x],"--template")) casemode = 3;
      else
      {
        printf("Usage: build_sample_langpack [--leet|--lower|--upper|--template] file.rc file.cpp ...\n");
        exit(1);
      }
      continue;
    }
    FILE *fp = fopen(argv[x],"rb");
    if (!fp)
    {
      fprintf(stderr,"Error opening %s\n",argv[x]);
      return 1;
    }
    g_last_file = argv[x];
    g_last_linecnt=1;
    int alen =strlen(argv[x]);
    if (alen>3 && !stricmp(argv[x]+alen-3,".rc"))
    {
      WDL_String s(argv[x]);
      WDL_String dpre;
      char *p=s.Get();
      while (*p) p++;
      while (p>=s.Get() && *p != '\\' && *p != '/') p--;
      *++p=0;
      if (p>s.Get())
      {
        p-=2;
        while (p>=s.Get() && *p != '\\' && *p != '/') p--;
        dpre.Set(++p); // get dir name portion
        if (dpre.GetLength()) dpre.Get()[dpre.GetLength()-1]=0;
      }
      if (!strcmp(dpre.Get(),"jesusonic")) dpre.Set("jsfx");

      s.Append("resource.h");
      const char *err=getResourceDefinesFromHeader(s.Get());
      if (err)
      {
        fprintf(stderr,"Error reading %s: %s\n",s.Get(),err);
        exit(1);
      }
      processRCfile(fp,dpre.Get()[0]?dpre.Get():NULL, argv[x]);
    }
    else
    {
      processCPPfile(fp,argv[x]);
    }

    fclose(fp);
  }
  if (casemode==4) printf("\xef\xbb\xbf");
  printf("#NAME:%s\n",
       casemode==-1 ? "English (lower case, demo)" :
       casemode==1 ? "English (upper case, demo)" :
       casemode==2 ? "English (leet-speak, demo)" :
       casemode==4 ? "UTF-8 English test (demo)" :
       casemode==3 ? "Template (edit-me)" :
       "English (sample language pack)");
  if (casemode==3)
  {
    printf("; NOTE: this is the best starting point for making a new langpack.\n"
           "; As you translate a string, remove the ; from the beginning of the\n"
           "; line. If the line begins with ;^, then it is an optional string,\n"
           "; and you should only modify that line if the definition in [common]\n"
           "; is not accurate for that context.\n"
           "; You can enlarge windows using 5CA1E00000000000=scale, for example:\n"
           "; [DLG_218] ; IDD_LOCKSETTINGS\n"
           "; 5CA1E00000000000=1.2\n"
           "; This makes the above dialog 1.2x wider than default.\n\n");
  }

  WDL_StringKeyedArray<bool> common_found;
  {
    if (!translations_indexed.GetSize())
    {
      fprintf(stderr,"no sections!\n");
      exit(1);
    }
    else if (translations_indexed.GetSize() > 4096)
    {
      fprintf(stderr,"too many translation sections, check input or adjust code here\n");
      exit(1);
    }
    fprintf(stderr,"%d sections\n",translations_indexed.GetSize());

    int pos[4096]={0,};
    printf("[common]\n");
    WDL_FastString matchlist;
    WDL_KeyedArray<WDL_UINT64, bool> ids;
    int minpos = 0;
    for (;;)
    {
      int matchcnt=0;
      matchlist.Set("");
      const char *str=NULL;
      for(x=minpos;x<translations_indexed.GetSize();x++)
      {
        const char *secname;
        WDL_StringKeyedArray<bool> *l = translations_indexed.Enumerate(x,&secname);
        int sz=l->GetSize();
        if (!str)
        {
          if (x>minpos)
          {
            memset(pos,0,sizeof(pos)); // start over
            minpos=x;
          }
          while (!str && pos[x]<sz)
          {
            l->Enumerate(pos[x]++,&str);
            if (!*str || common_found.Get(str)) str=NULL; // skip if we've already analyzed this string
          }
          if (str) matchlist.Set(secname);
        }
        else
        {
          while (pos[x] < sz)
          {
            const char *tv=NULL;
            l->Enumerate(pos[x],&tv);
            int c = strcmp(tv,str);
            if (c>0) break;
            pos[x]++;
            if (!c)
            {
              matchlist.Append(", ");
              matchlist.Append(secname);
              matchcnt++;
              break;
            }
          }
        }
      }
      if (matchcnt>0)
      {
        common_found.Insert(str,true);
        //printf("; used by: %s\n",matchlist.Get());
        if (casemode==3) printf(";");
        WDL_UINT64 a = outputLine(str,casemode);
        if (ids.Get(a))
        {
          fprintf(stderr,"duplicate hash for strings in common section, hope this is OK.. 64 bit hash fail!\n");
          exit(1);
        }
//        printf("\n");
        ids.Insert(a,true);
      }
      if (minpos == x-1) break;
    }
    printf("\n");
  }

  for(x=0;x<translations.GetSize();x++)
  {
    const char *nm=NULL;
    WDL_PtrList<char> *p = translations.Enumerate(x,&nm);
    if (x) printf("\n");
    char *secinfo = section_descs.Get(nm);
    printf("[%s]%s%s\n",nm,secinfo?" ; ":"", secinfo?secinfo:"");
    int a;
    int y;
    WDL_KeyedArray<WDL_UINT64, bool> ids;
    for (a=0;a<2;a++)
    {
      for (y=0;y<p->GetSize();y++)
      {
        char *strv=p->Get(y);
        if (!*strv) continue;
        if ((common_found.Get(strv)?1:0) != a) continue;

        if (a) printf(";^");
        else if (casemode==3) printf(";");

        WDL_UINT64 a = outputLine(strv,casemode);
        if (ids.Get(a))
        {
          fprintf(stderr,"duplicate hash for strings in section, hope this is OK.. 64 bit hash fail!\n");
          exit(1);
        }
        ids.Insert(a,true);
      }
    }
  }
  return 0;
}
