/* 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 <dirent.h>
#include "dialog.h"

#ifndef S_ISDIR
# define S_ISDIR(m)  (((m) & S_IFMT) == S_IFDIR)
#endif

#ifndef S_ISREG
# define S_ISREG(m)  (((m) & S_IFMT) == S_IFREG)
#endif

void init_dialog_fsel(void);

static VG_BOOL dialog_file_select(char *, size_t, const char *, const struct VG_Position *, struct VG_Hash *, const char *, VG_BOOL, VG_BOOL);

struct d_inh {
  char name[256];
  size_t size;
  VG_BOOL isdir;
};

static VG_BOOL pathclean(char *, size_t);
static int cmp_dinh(const void *, const void *);
static VG_BOOL test_chpath(const char *, const char *, size_t, VG_BOOL);
static VG_BOOL make_chpath(char *, size_t, const char *, size_t, const char *, const char *);


/* set functions */
void
init_dialog_fsel(void)
{
  vg4->dialog->file_select = dialog_file_select;
} /* Ende init_dialog_fsel */


/* pathclean:
 * clean path (must not exist)
 * e.g.: /a/b/../c  -> /a/c  (return: VG_TRUE)
 *       /a/../../c -> /c    (return: VG_FALSE)
 *       a/b/../c   -> a/c   (return: VG_TRUE)
 *       a/../../c  -> c     (return: VG_FALSE)
 * @param path   path to clean
 * @param nsize  sizeof path
 * @return  VG_TRUE: OK, VG_FALSE: path would exceed top directory
 */
static VG_BOOL
pathclean(char *path, size_t nsize)
{
  char *pt0, *pt1, *pt2;
  size_t vzlen, glen;
  VG_BOOL noflow;

  if (path == NULL || nsize == 0 || *path == '\0') { return VG_TRUE; }

  noflow = VG_TRUE;
  if (*path == '/') { pt0 = path; } else { pt0 = path - 1; }

  for (pt1 = pt0;;) {
    if (pt1[1] == '\0') { pt1[0] = '\0'; break; }  /* Ende-/ */
    pt2 = strchr(pt1 + 1, '/');
    if (pt2 == NULL) {
      pt2 = pt1 + 1 + strlen(pt1 + 1);
      vzlen = (size_t)(pt2 - (pt1 + 1));
    } else {
      pt2++;
      vzlen = (size_t)(pt2 - 1 - (pt1 + 1));
    }
    glen = strlen(pt2) + 1;
    if (vzlen == 0) {
      memmove(pt1 + 1, pt2, glen);
    } else if (vzlen == 1 && strncmp(pt1 + 1, ".", 1) == 0) {
      memmove(pt1 + 1, pt2, glen);
    } else if (vzlen == 2 && strncmp(pt1 + 1, "..", 2) == 0) {
      char *ptv = NULL, *ptt = pt0;
      while (ptt < pt1) { ptv = ptt; ptt = strchr(ptt + 1, '/'); }
      if (ptv != NULL) { pt1 = ptv; } else { noflow = VG_FALSE; }
      memmove(pt1 + 1, pt2, glen);
    } else {
      if (*(pt2 - 1) != '/') { break; }
      pt1 = pt2 - 1;
    }
  }
  if (*path == '\0') { snprintf(path, nsize, "%s", (pt0 == path ? "/" : ".")); }

  return noflow;
} /* Ende pathclean */


/* compare function for qsort() */
static int
cmp_dinh(const void *v1, const void *v2)
{
  struct d_inh *d1 = (struct d_inh *)v1;
  struct d_inh *d2 = (struct d_inh *)v2;
  int erg;
  char s1[sizeof(d1->name) + 1];
  char s2[sizeof(d2->name) + 1];

  vg4->misc->strcpy(s1 + 1, sizeof(s1) - 1, d1->name);
  if (d1->isdir) { s1[0] = '0'; } else { s1[0] = '1'; }
  vg4->misc->strcpy(s2 + 1, sizeof(s2) - 1, d2->name);
  if (d2->isdir) { s2[0] = '0'; } else { s2[0] = '1'; }

  erg = strcmp(s1, s2);
  if (erg < 0) { return -1; }
  if (erg > 0) { return 1; }
  return 0;
} /* Ende cmp_dinh */


/* return whether aktpath is in startpath */
static VG_BOOL
test_chpath(const char *aktpath, const char *startpath, size_t splen, VG_BOOL plussub)
{
  if (aktpath == NULL) { return VG_FALSE; }

  if (splen == 0 && strcmp(aktpath, "/") != 0) { return VG_TRUE; }
  if (splen == 1 && *startpath == '.') {
    if (plussub) {  /* must have sub-directory */
      if (aktpath[splen] != '\0') { return VG_TRUE; }
    } else {
      return VG_TRUE;
    }
  } else if (strncmp(aktpath, startpath, splen) == 0) {
    if (plussub) {  /* must have sub-directory */
      if (aktpath[splen] == '/') { return VG_TRUE; }
    } else {
      return VG_TRUE;
    }
  }

  return VG_FALSE;
} /* Ende test_chpath */


/* append directory-name to actual path */
static VG_BOOL
make_chpath(char *aktpath, size_t asize, const char *startpath, size_t splen, const char *vorpath, const char *cdir)
{
  VG_BOOL retw;
  size_t alen;

  if (aktpath == NULL || asize == 0 || vorpath == NULL || cdir == NULL || *cdir == '\0') { return VG_FALSE; }
  if (startpath != NULL && splen == 0) { startpath = NULL; }

  retw = VG_TRUE;
  alen = strlen(aktpath);

  vg4->misc->strccat(aktpath + alen, asize - alen, "/", cdir, NULL);
  if (!pathclean(aktpath, sizeof(aktpath))) {
    vg4->misc->strcpy(aktpath, sizeof(aktpath), vorpath);
    retw = VG_FALSE;
  } else if (!test_chpath(aktpath, startpath, splen, VG_FALSE)) {
    vg4->misc->strcpy(aktpath, sizeof(aktpath), vorpath);
    retw = VG_FALSE;
  }

  return retw;
} /* Ende make_chpath */


/* dialog_file_select:
 * open a canvas-dialog to select a filename
 * @param filename    for returning selected path+file
 * @param fsize       sizeof(filename)
 * @param cvasdir     directory to load canvasses from, or NULL = system-canvas
 * @param posdst      canvas-position on window, or NULL = centered
 * @param hvar        hash with variable-values for text control-commands "var", or NULL
 *                      hash-format:
 *                       - key:   variable name
 *                       - value: variable value
 *                    (see below)
 * @param startdir    topmost directory to start selecting filename from
 *                     - beginning with "/":      absolute path
 *                     - not beginning with "/":  relative path from current directory
 *                     - beginning with "~":      path beginning from the HOME-directory
 *                     - NULL:                    no topmost directory, starting in HOME-directory
 * @param textinput   whether creating a new filename or directory is allowed
 * @param showhidden  whether show hidden files and directories
 * @return  VG_TRUE = OK or VG_FALSE = exit-request
 *
 * If the returned filename is empty, the function was cancelled.
 *
 * The canvas-names must be:
 *  - file_select.top.cvas: for selecting a file
 *    - with items:
 *      - [CV-TEXT]   with name "title": text for title
 *      - [CV-TEXT]   with name "current-path": current path
 *      - [CV-BUTTON] with name "done": for returning without selecting a file
 *      - [CV-LIST]   with name "filelist": for contents of directory
 *      - [CV-BUTTON] with name "makefile": for creating a new file
 *      - [CV-BUTTON] with name "makedir": for creating a new directory
 *    - uses following variables:
 *      - key: "title", value: <title of canvas>
 *      - key: "current-path", value: <current path>
 *    - hvar should contain:
 *      - key: "top:title", value: <title of canvas>
 *  - file_select.input.cvas: for creating a new directory or file
 *    - with items:
 *      - [CV-TEXT]  with name "title": text for title
 *      - [CV-INPUT] with name "input": for typing a directory-/file-name
 *    - uses following variables:
 *      - key: "title", value: <title of canvas>
 *    - hvar should contain:
 *      - key: "input:title-dir", value: <title of canvas for creating directory>
 *      - key: "input:title-file", value: <title of canvas for creating file>
 *  - file_select.overwrite.cvas: for asking for overwriting
 *    - with items:
 *      - [CV-TEXT]   with name "title": text for title
 *      - [CV-TEXT]   with name "overwrite-file": filename
 *      - [CV-BUTTON] with name "yes": yes
 *      - [CV-BUTTON] as a cancel-button
 *    - uses following variables:
 *      - key: "title", value: <title of canvas>
 *      - key: "overwrite-file", value: <filename to be overwritten>
 *    - hvar should contain:
 *      - key: "overwrite:title", value: <title of canvas>
 */
static VG_BOOL
dialog_file_select(char *filename, size_t fsize,
                   const char *cvasdir, const struct VG_Position *posdst,
                   struct VG_Hash *hvar, const char *startdir,
                   VG_BOOL textinput, VG_BOOL showhidden)
{
  const char *cvprefix = "file_select";
  const char *cvfile_top = "file_select.top.cvas";
  const char *cvfile_input = "file_select.input.cvas";
  const char *cvfile_overwrite = "file_select.overwrite.cvas";
  const char *cvname_done = "done";
  const char *cvname_filelist = "filelist";
  const char *cvname_makefile = "makefile";
  const char *cvname_makedir = "makedir";
  const char *cvname_input = "input";
  const char *cvname_yes = "yes";
  const char *cvvar_title = "title";
  const char *cvvar_current_path = "current-path";
  const char *cvvar_overwrite_file = "overwrite-file";
  const char *cvparm_input_title_dir = "input:title-dir";
  const char *cvparm_input_title_file = "input:title-file";
  struct VG_Hash *hvar_top, *hvar_input, *hvar_overwrite;
  DIR *dirp;
  struct dirent *direntp;
  char startpath[512], aktpath[512], vorpath[512], wrkpath[512], bidx[16], *cptr;
  size_t aktpos, splen;
  struct d_inh *dinh;
  int dinhmax, dno;
  struct stat sbuf;
  VG_BOOL isdir, retw;
  struct VG_Canvas *cvas_top, *cvas_input, *cvas_overwrite;
  const char *selname;

  if (filename == NULL || fsize == 0) { return VG_TRUE; }
  *filename = '\0';
  hvar_top = hvar_input = hvar_overwrite = NULL;


  /* get topmost directory */

  if (startdir == NULL || *startdir == '~') {
    char *ehome = getenv("HOME");
    if (ehome != NULL && *ehome != '\0') {
      vg4->misc->strcpy(aktpath, sizeof(aktpath), ehome);
    } else {
      vg4->misc->strcpy(aktpath, sizeof(aktpath), "/");
    }
    aktpos = strlen(aktpath);
    if (startdir != NULL) {
      vg4->misc->strcpy(aktpath + aktpos, sizeof(aktpath) - aktpos, startdir + 1);
    }
  } else if (*startdir == '/') {
    vg4->misc->strcpy(aktpath, sizeof(aktpath), startdir);
  } else {
    vg4->misc->strcpy(aktpath, sizeof(aktpath), startdir);
  }

  if (!pathclean(aktpath, sizeof(aktpath))) {
    pwarn("file_select: topmost directory \"%s\" exceeded\n", (startdir == NULL ? "/" : startdir));
    return VG_TRUE;
  }
  *vorpath = '\0';

  splen = 0; *startpath = '\0';
  if (startdir != NULL) {
    vg4->misc->strcpy(startpath, sizeof(startpath), aktpath);
    splen = strlen(startpath);
  }


  /* set variables and load canvasses */

  /* top-canvas */
  hvar_top = vg4->hash->create();
  dialog_transfer_variables(hvar, hvar_top, "top");
  vg4->hash->setstr(hvar_top, cvvar_current_path, aktpath);
  dialog_cvasfile(cvprefix, cvfile_top, cvasdir, wrkpath, sizeof(wrkpath));
  cvas_top = vg4->canvas->load(wrkpath, hvar_top);
  if (cvas_top == NULL) {
    outerr("%s [at cvasdir=%s] not found", cvfile_top, (cvasdir == NULL ? "(null)" : cvasdir));
    return VG_TRUE;
  }

  if (textinput) {
    /* input-canvas */
    hvar_input = vg4->hash->create();
    vg4->hash->setstr(hvar_input, cvvar_title, "");
    dialog_cvasfile(cvprefix, cvfile_input, cvasdir, wrkpath, sizeof(wrkpath));
    cvas_input = vg4->canvas->load(wrkpath, hvar_input);
    if (cvas_input == NULL) {
      outerr("%s [at cvasdir=%s] not found", cvfile_input, (cvasdir == NULL ? "(null)" : cvasdir));
      vg4->canvas->destroy(cvas_top);
      return VG_TRUE;
    }

    /* overwrite-canvas */
    hvar_overwrite = vg4->hash->create();
    dialog_transfer_variables(hvar, hvar_overwrite, "overwrite");
    vg4->hash->setstr(hvar_overwrite, cvvar_overwrite_file, "");
    dialog_cvasfile(cvprefix, cvfile_overwrite, cvasdir, wrkpath, sizeof(wrkpath));
    cvas_overwrite = vg4->canvas->load(wrkpath, hvar_overwrite);
    if (cvas_overwrite == NULL) {
      outerr("%s [at cvasdir=%s] not found", cvfile_overwrite, (cvasdir == NULL ? "(null)" : cvasdir));
      vg4->canvas->destroy(cvas_input);
      vg4->canvas->destroy(cvas_top);
      return VG_TRUE;
    }

  } else {
    cvas_input = cvas_overwrite = NULL;
  }


  /* loop for displaying directory contents */

  retw = VG_TRUE;
  dinh = NULL;
  dinhmax = 0;

  for (;;) {
    if (dinh != NULL) { free(dinh); dinh = NULL; dinhmax = 0; }

    /* read directory */

    dirp = opendir(aktpath);
    if (dirp == NULL) {
      pwarn("open directory %s: %s\n", aktpath, strerror(errno));
      if (*vorpath == '\0') { break; }
      vg4->misc->strcpy(aktpath, sizeof(aktpath), vorpath);
      dirp = opendir(aktpath);
      if (dirp == NULL) { outerr("open directory %s: %s", aktpath, strerror(errno)); break; }
    }
    vg4->misc->strcpy(vorpath, sizeof(vorpath), aktpath);

    while ((direntp = readdir(dirp)) != NULL) {
      if (strcmp(direntp->d_name, ".") == 0 || strcmp(direntp->d_name, "..") == 0) { continue; }
      if (!showhidden && *direntp->d_name == '.') { continue; }

      vg4->misc->strccat(wrkpath, sizeof(wrkpath), aktpath, "/", direntp->d_name, NULL);
      if (stat(wrkpath, &sbuf) < 0) {
        pwarn("stat %s: %s\n", wrkpath, strerror(errno));
        continue;
      }

      /* only directories and files */
      if (S_ISDIR(sbuf.st_mode)) {
        isdir = VG_TRUE;
      } else if (S_ISREG(sbuf.st_mode)) {
        isdir = VG_FALSE;
      } else {
        continue;
      }

      /* insert into dinh */

      dinhmax++;
      if (dinh == NULL) {
        dinh = malloc(sizeof(*dinh) * dinhmax);
      } else {
        dinh = realloc(dinh, sizeof(*dinh) * dinhmax);
      }
      if (dinh == NULL) { outerr("malloc/realloc: %s", strerror(errno)); exit(1); }

      vg4->misc->strcpy(dinh[dinhmax - 1].name, sizeof(dinh[0].name), direntp->d_name);
      if (isdir) {
        dinh[dinhmax - 1].size = 0;
      } else {
        dinh[dinhmax - 1].size = (size_t)sbuf.st_size;
      }
      dinh[dinhmax - 1].isdir = isdir;
    }

    closedir(dirp);

    /* sort contents */
    if (dinh != NULL) { qsort(dinh, dinhmax, sizeof(*dinh), cmp_dinh); }

    /* add parent-directory? */
    if (test_chpath(aktpath, startpath, splen, VG_TRUE)) {
      dinhmax++;
      if (dinh == NULL) {
        dinh = malloc(sizeof(*dinh) * dinhmax);
      } else {
        dinh = realloc(dinh, sizeof(*dinh) * dinhmax);
      }
      if (dinh == NULL) { outerr("malloc/realloc: %s", strerror(errno)); exit(1); }

      if (dinhmax > 1) { memmove(dinh + 1, dinh, sizeof(*dinh) * (dinhmax - 1)); }
      vg4->misc->strcpy(dinh[0].name, sizeof(dinh[0].name), "..");
      dinh[0].size = 0;
      dinh[0].isdir = VG_TRUE;
    }

    /* update path-variable and reload top-canvas */
    vg4->hash->setstr(hvar_top, cvvar_current_path, aktpath);
    if (!vg4->canvas->reload(cvas_top, hvar_top)) { break; }

    /* set top-canvas items */
    if (!textinput) {
      vg4->canvas->disable(cvas_top, cvname_makefile, VG_TRUE);
      vg4->canvas->disable(cvas_top, cvname_makedir, VG_TRUE);
    }
    for (dno = 0; dno < dinhmax; dno++) {
      snprintf(bidx, sizeof(bidx), "%d", dno + 1);
      if (dinh[dno].isdir) {
        snprintf(wrkpath, sizeof(wrkpath), "[%s]", dinh[dno].name);
      } else {
        snprintf(wrkpath, sizeof(wrkpath), "%s", dinh[dno].name);
      }
      vg4->canvas->list_add(cvas_top, cvname_filelist, wrkpath, bidx);
    }

    /* execute top-canvas */
    if (!vg4->canvas->exec(cvas_top, posdst, &selname)) { retw = VG_FALSE; break; }
    if (selname == NULL) { break; }  /* cancel */
    if (strcmp(selname, cvname_done) == 0) { break; }

    /* act according to selection */

    if (strcmp(selname, cvname_filelist) == 0) {  /* file selected from list */
      const char *listitem = vg4->canvas->list_get_activated(cvas_top, selname);
      if (listitem != NULL) {
        dno = atoi(listitem);
        if (dno > 0 && dno <= dinhmax) {  /* item-name is index + 1 of dinh */
          if (dinh[dno - 1].isdir) {  /* change to directory */
            make_chpath(aktpath, sizeof(aktpath), startpath, splen, vorpath, dinh[dno - 1].name);
          } else {  /* select file */
            if (vg4->misc->strccat(filename, fsize, aktpath, "/", dinh[dno - 1].name, NULL) >= fsize) {
              outerr("selected filename \"%s/%s\" too long for return-buffer with size %d", aktpath, dinh[dno - 1].name, (int)fsize);
            }
            break;
          }
        }
      }

    } else if (strcmp(selname, cvname_makefile) == 0) {  /* button clicked to create file */
      /* reload input-canvas */
      cptr = (char *)vg4->hash->get(hvar, cvparm_input_title_file, NULL);
      if (cptr == NULL) { cptr = "New file"; }
      vg4->hash->setstr(hvar_input, cvvar_title, cptr);
      if (vg4->canvas->reload(cvas_input, hvar_input)) {
        /* execute input-canvas */
        if (!dialog_exec_subcvas(cvas_top, cvas_input, &selname)) { retw = VG_FALSE; break; }

        if (selname != NULL) {  /* not cancelled */
          if (*selname == '\0') {  /* OK without specific selection */
            selname = cvname_input;  /* act as if cvname_input was selected */
          }

          /* act according to selection */
          if (strcmp(selname, cvname_input) == 0) {  /* filename typed into textline-input */
            const char *text = vg4->canvas->input_get_text(cvas_input, selname);
            if (*text != '\0') {
              vg4->misc->strcpy(wrkpath, sizeof(wrkpath), aktpath);
              if (make_chpath(wrkpath, sizeof(wrkpath), startpath, splen, vorpath, text)) {
                if (stat(wrkpath, &sbuf) == 0) {  /* filename exists */
                  if (S_ISDIR(sbuf.st_mode)) {  /* change to directory */
                    vg4->misc->strcpy(aktpath, sizeof(aktpath), wrkpath);
                  } else if (S_ISREG(sbuf.st_mode)) {  /* ask to overwrite */
                    /* reload overwrite-canvas */
                    vg4->hash->setstr(hvar_overwrite, cvvar_overwrite_file, text);
                    if (vg4->canvas->reload(cvas_overwrite, hvar_overwrite)) {
                      /* execute overwrite-canvas */
                      if (!dialog_exec_subcvas(cvas_top, cvas_overwrite, &selname)) { retw = VG_FALSE; break; }
                      if (selname != NULL) {  /* not cancelled */
                        /* act according to selection */
                        if (strcmp(selname, cvname_yes) == 0) {  /* overwrite */
                          vg4->misc->strcpy(aktpath, sizeof(aktpath), wrkpath);
                          if (vg4->misc->strcpy(filename, fsize, aktpath) >= fsize) {
                            outerr("selected filename \"%s\" too long for return-buffer with size %d", aktpath, (int)fsize);
                          }
                          break;
                        }
                      }
                    }
                  }
                } else {  /* filename does not exist */
                  vg4->misc->strcpy(aktpath, sizeof(aktpath), wrkpath);
                  if (vg4->misc->strcpy(filename, fsize, aktpath) >= fsize) {
                    outerr("selected filename \"%s\" too long for return-buffer with size %d", aktpath, (int)fsize);
                  }
                  break;
                }
              }
            }
          }
        }
      }

    } else if (strcmp(selname, cvname_makedir) == 0) {  /* button clicked to create directory */
      /* reload input-canvas */
      cptr = (char *)vg4->hash->get(hvar, cvparm_input_title_dir, NULL);
      if (cptr == NULL) { cptr = "New directory"; }
      vg4->hash->setstr(hvar_input, cvvar_title, cptr);
      if (vg4->canvas->reload(cvas_input, hvar_input)) {
        /* execute input-canvas */
        if (!dialog_exec_subcvas(cvas_top, cvas_input, &selname)) { retw = VG_FALSE; break; }

        if (selname != NULL) {  /* not cancelled */
          if (*selname == '\0') {  /* OK without specific selection */
            selname = cvname_input;  /* act as if cvname_text was selected */
          }

          /* act according to selection */
          if (strcmp(selname, cvname_input) == 0) {  /* directory name typed into textline-input */
            const char *text = vg4->canvas->input_get_text(cvas_input, selname);
            if (*text != '\0' && strcmp(text, ".") != 0 && strcmp(text, "..") != 0) {
              if (make_chpath(aktpath, sizeof(aktpath), startpath, splen, vorpath, text)) {
                mkdir(aktpath, 0777);  /* create directory */
              }
            }
          }
        }
      }
    }
  }

  if (dinh != NULL) { free(dinh); }
  if (textinput) {
    vg4->canvas->destroy(cvas_overwrite);
    vg4->canvas->destroy(cvas_input);
  }
  vg4->canvas->destroy(cvas_top);
  if (hvar_overwrite != NULL) { vg4->hash->destroy(hvar_overwrite); }
  if (hvar_input != NULL) { vg4->hash->destroy(hvar_input); }
  if (hvar_top != NULL) { vg4->hash->destroy(hvar_top); }

  return retw;
} /* Ende dialog_file_select */
