/* Copyright 2022-2026 Kurt Nienhaus
 *
 * This file is part of VgaGames.
 * VgaGames is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 * VgaGames is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with VgaGames.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include "mlang.h"

void init_mlang(void);
void dest_mlang(void);

static void mlang_fb_locale(const char *);
static void mlang_clear(void);
static const char * mlang_getlocale(VG_BOOL);
static VG_BOOL mlang_add(const char *);
static const char * mlang_get(const char *, const char *);
static void mlang_dump(FILE *);

static VG_BOOL cbkf_add(const char *, void *);


/* set functions */
void
init_mlang(void)
{
  vg4data.lists.mlang.s = SML3_calloc(1, sizeof(*vg4data.lists.mlang.s));
  vg4data.lists.mlang.s->hdir = SML3_hashnew(NULL, 0);
  vg4data.lists.mlang.s->hs = SML3_hashnew(NULL, 0);
  { const char *loc = getenv("LANG");
    if (loc == NULL || *loc == '\0') { loc = getenv("LC_ALL"); }
    if (loc == NULL || *loc == '\0') { loc = getenv("LC_MESSAGES"); }
    if (loc == NULL || *loc == '\0') { loc = getenv("LC_CTYPE"); }
    if (loc != NULL && *loc == '\0') { loc = NULL; }
    if (loc != NULL && (strcmp(loc, "C") == 0 || strncmp(loc, "C.", 2) == 0)) { loc = NULL; }
    if (loc != NULL && (strcmp(loc, "POSIX") == 0 || strncmp(loc, "POSIX.", 6) == 0)) { loc = NULL; }
    if (loc != NULL) {
      size_t clen = strcspn(loc, "._");
      if (clen > 0) {
        snprintf(vg4data.lists.mlang.s->locdfl, sizeof(vg4data.lists.mlang.s->locdfl), "%.*s", (int)clen, loc);
        SML3_tolower(vg4data.lists.mlang.s->locdfl, 0);
      }
    } else {
      snprintf(vg4data.lists.mlang.s->locdfl, sizeof(vg4data.lists.mlang.s->locdfl), "en");
    }
  }
  vg4->mlang->fb_locale = mlang_fb_locale;
  vg4->mlang->clear = mlang_clear;
  vg4->mlang->getlocale = mlang_getlocale;
  vg4->mlang->add = mlang_add;
  vg4->mlang->get = mlang_get;
  vg4->mlang->dump = mlang_dump;
} /* Ende init_mlang */

void
dest_mlang(void)
{
  SML3_hashfree(&vg4data.lists.mlang.s->hs);
  SML3_hashfree(&vg4data.lists.mlang.s->hdir);
  free(vg4data.lists.mlang.s);
  vg4data.lists.mlang.s = NULL;
} /* Ende dest_mlang */


/* mlang_fb_locale:
 * set fallback locale
 * @param locale  locale
 *                (e.g. "en", "en_US", but relevant is only language before underscore and period)
 */
static void
mlang_fb_locale(const char *locale)
{
  if (locale != NULL && *locale != '\0') {
    size_t clen = strcspn(locale, "._");
    if (clen > 0) {
      snprintf(vg4data.lists.mlang.s->locfb, sizeof(vg4data.lists.mlang.s->locfb), "%.*s", (int)clen, locale);
      SML3_tolower(vg4data.lists.mlang.s->locfb, 0);
    }
  }
} /* Ende mlang_fb_locale */


/* mlang_clear:
 * clear all loaded multilanguage entries (but not the locales)
 */
static void
mlang_clear(void)
{
  SML3_hashclear(vg4data.lists.mlang.s->hs);
  SML3_hashclear(vg4data.lists.mlang.s->hdir);
} /* Ende mlang_clear */


/* mlang_getlocale:
 * return used locale
 * @param fb  whether return fallback locale instead main locale
 * @return  locale (or empty = not set)
 */
static const char *
mlang_getlocale(VG_BOOL fb)
{
  if (fb) { return (const char *)vg4data.lists.mlang.s->locfb; }
  return (const char *)vg4data.lists.mlang.s->locdfl;
} /* Ende mlang_getlocale */


/* mlang_add:
 * add multilanguage entries from files beneath a directory
 * @param pdir  directory containing multilanguage files
 * @return  VG_TRUE = added or VG_FALSE = not added
 *
 * The directory pdir must contain
 *  - subdirectories with name of locales (lowercase), e.g. "en", "de"
 *    these must contain
 *     - subdirectories with name of scope
 *       these must contain
 *        - files with multilanguage text for this locale and scope
 *
 * A multilanguage file must contain for each multilanguage text
 *  - a line with a key, enclosed in double brackets: "[[" <key> "]]"
 *  - one or more lines with text (may contain control commands)
 *
 * Example:
 *  - mlangdir/en/myscope/file1.mlang:
 *    [[key1]]
 *    an english text belonging to myscope+key1
 *
 *    [[key2]]
 *    another text
 *    with two lines.
 *  - calling with: vg4->mlang->add("mlangdir")
 */
static VG_BOOL
mlang_add(const char *pdir)
{
  char ldir[VGI_PATHSIZE];
  VG_BOOL retb;
  int inot;

  if (pdir == NULL || *pdir == '\0') { return VG_FALSE; }

  /* already added? */
  if (SML3_hashget(vg4data.lists.mlang.s->hdir, pdir, strlen(pdir) + 1) != NULL) { return VG_TRUE; }

  retb = VG_FALSE;
  inot = 0;

  if (*vg4data.lists.mlang.s->locdfl != '\0') {
    snprintf(ldir, sizeof(ldir), "%s/%s", pdir, vg4data.lists.mlang.s->locdfl);
    if (access(ldir, F_OK) == 0) { retb = vg4_intern_recurs_dir(ldir, cbkf_add, ldir); } else { inot++; }
  } else {
    inot++;
  }

  if (retb == VG_FALSE && *vg4data.lists.mlang.s->locfb != '\0') {
    snprintf(ldir, sizeof(ldir), "%s/%s", pdir, vg4data.lists.mlang.s->locfb);
    if (access(ldir, F_OK) == 0) { retb = vg4_intern_recurs_dir(ldir, cbkf_add, ldir); } else { inot++; }
  } else {
    inot++;
  }

  if (retb == VG_FALSE && inot == 2) {
    outerr("no relevant locale directories found for \"%s\"", pdir);
  }

  /* add to already read directories */
  SML3_hashset(vg4data.lists.mlang.s->hdir, pdir, strlen(pdir) + 1);

  return retb;
} /* Ende mlang_add */


/* cbkf_add: callback function for mlang_add() */
static VG_BOOL
cbkf_add(const char *filename, void *vdata)
{
  char *finh, scope[128];
  size_t fsize, frlen;
  const char *fileroot = (const char *)vdata;

  if (filename == NULL || *filename == '\0' || fileroot == NULL) { outerr("No file passed"); return VG_FALSE; }
  frlen = strlen(fileroot);

  /* get scope from file-directory */
  { const char *kpt1, *kpt2;
    *scope = '\0';
    if ((kpt1 = strrchr(filename, '/')) != NULL) {
      do { kpt1--; } while (kpt1 >= filename && *kpt1 == '/');
      kpt1++;
      kpt2 = kpt1;
      do { kpt2--; } while (kpt2 >= filename && *kpt2 != '/');
      kpt2++;
      if (kpt2 < kpt1 && frlen < (size_t)(kpt2 - filename)) {
        snprintf(scope, sizeof(scope), "%.*s", (int)(size_t)(kpt1 - kpt2), kpt2);
      }
    }
    if (*scope == '\0') { return VG_TRUE; }
    if (strchr(scope, ':') != NULL) { pwarn("mlang-scope must not contain a hyphen (:): %s\n", scope); return VG_TRUE; }
  }

  /* read file */
  { FILE *ffp;
    struct stat sbuf;
    ffp = fopen(filename, "r");
    if (ffp == NULL) { outerr("opening file \"%s\": %s", filename, strerror(errno)); return VG_FALSE; }

    if (fstat(fileno(ffp), &sbuf) < 0) {
      outerr("adding \"%s\": fstat: %s", filename, strerror(errno));
      fclose(ffp);
      return VG_FALSE;
    }
    fsize = (size_t)sbuf.st_size;

    finh = SML3_malloc(fsize + 1);
    if (fread(finh, 1, fsize, ffp) != fsize) {
      outerr("adding \"%s\": file too short", filename);
      fclose(ffp);
      return VG_FALSE;
    }
    finh[fsize] = '\0';
    fclose(ffp);
  }

  /* read keys and values */
  { char *pt1, *pt2, *ptt, key[128], c1;
    struct SML3_hash *hsptr;
    struct SML3_hashelem *heptr;

    for (pt1 = finh;; ) {

      /* search for: "[[" <key> "]]" */
      if (pt1 == finh && strncmp(pt1, "[[", 2) == 0) {
        pt2 = pt1 - 1;
      } else {
        pt2 = strstr(pt1, "\n[[");
        if (pt2 == NULL) { break; }
      }
      pt1 = pt2 + 3;
      pt1 += strspn(pt1, " \t");  /* skip first whitespaces in same line */
      if (*pt1 == '\0') { break; }
      pt2 = pt1 + strcspn(pt1, "]\n");
      if (*pt2 == '\0') { break; }
      if (*pt2 != ']') { pt1 = pt2; continue; }
      if (pt2[1] == '\0') { break; }
      if (pt2[1] != ']') { pt1 = pt2 + 1; continue; }
      ptt = pt2 - 1;
      while (ptt > pt1) {  /* skip last whitespaces in same line */
        if (*ptt != ' ' && *ptt != '\t') { break; }
        ptt--;
      }
      if (ptt < pt1) { pt1 = pt2 + 2; continue; }
      snprintf(key, sizeof(key), "%.*s", (int)(size_t)(ptt - pt1 + 1), pt1);
      pt1 = pt2 + 2;
      pt1 += strcspn(pt1, "\n");  /* skip to newline */
      if (*pt1 == '\0') { break; }
      if (strchr(key, ':') != NULL) { pwarn("mlang-key must not contain a hyphen (:): %s\n", key); continue; }
      pt1++;

      /* read text */
      pt1 += strspn(pt1, VGI_WSPACE);  /* skip whitespaces */
      if (*pt1 == '\0') { break; }
      pt2 = strstr(pt1 - 1, "\n[[");
      if (pt2 == NULL) { pt2 = pt1 + strlen(pt1); }
      ptt = pt2 - 1;
      while (ptt > pt1) {  /* skip last whitespaces */
        if (strchr(VGI_WSPACE, (int)*ptt) == NULL) { break; }
        ptt--;
      }
      if (ptt < pt1) { pt1 = pt2; continue; }
      c1 = ptt[1];
      ptt[1] = '\0';
      /* insert into hash */
      hsptr = SML3_hashsubset(vg4data.lists.mlang.s->hs, scope, strlen(scope) + 1);
      heptr = SML3_hashget(hsptr, key, strlen(key) + 1);
      if (heptr != NULL) {
        pwarn("Add multilanguage file \"%s\": Warning: Key \"%s\" redefined.\n", filename, key);
      }
      heptr = SML3_hashset(hsptr, key, strlen(key) + 1);
      SML3_hashelem_valset(heptr, pt1, strlen(pt1) + 1);
      ptt[1] = c1;
      pt1 = pt2;
    }
  }

  (void)vdata;
  free(finh);
  return VG_TRUE;
} /* Ende cbkf_add */


/* mlang_get:
 * get multilanguage entry for a scope and a key
 * @param scope  scope
 * @param key    key
 * @return  multilanguage text, (or "scope:key" if not found)
 */
static const char *
mlang_get(const char *scope, const char *key)
{
  struct SML3_hash *hsptr;
  struct SML3_hashelem *heptr;
  const char *pval;

  if (scope == NULL || *scope == '\0') { return ""; }
  if (key == NULL || *key == '\0') { return ""; }

  hsptr = SML3_hashsubset(vg4data.lists.mlang.s->hs, scope, strlen(scope) + 1);
  heptr = SML3_hashset(hsptr, key, strlen(key) + 1);

  pval = SML3_hashelem_valget(heptr, NULL);
  if (pval == NULL) {
    char buf[128];
    snprintf(buf, sizeof(buf), "%s:%s", scope, key);
    SML3_hashelem_valset(heptr, buf, strlen(buf) + 1);
    pval = SML3_hashelem_valget(heptr, NULL);
  }

  return pval;
} /* Ende mlang_get */


/* mlang_dump:
 * dump multilanguage entries
 * @param outfp  filepointer to dump to, or NULL = stdout
 */
static void
mlang_dump(FILE *outfp)
{
  struct SML3_hash *hsptr, *hs1;
  struct SML3_hashelem *he1;
  const void *vptr;

  if (outfp == NULL) { outfp = stdout; }

  fprintf(outfp, "\nDump of multilanguage entries\n");
  fprintf(outfp, "=============================\n\n");

  hsptr = vg4data.lists.mlang.s->hs;
  for (he1 = SML3_hashlist(hsptr, NULL, 1); he1 != NULL; he1 = SML3_hashlist(hsptr, he1, 1)) {
    hs1 = SML3_hashtophash(hsptr);
    while (SML3_hashelem_keypath(&hs1, he1, &vptr, NULL)) {
      fprintf(outfp, "[%s]", (const char *)vptr);
    }
    fprintf(outfp, ":\n%s\n\n", (char *)SML3_hashelem_valget(he1, NULL));
  }
} /* Ende mlang_dump */
