#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <libgen.h>

#ifdef _WIN32
# include <direct.h>
# include <evil_private.h> /* mkdir realpath */
#endif

#ifdef HAVE_FEATURES_H
# include <features.h>
#endif
#include <ctype.h>
#include <errno.h>

#ifdef HAVE_ATFILE_SOURCE
# include <dirent.h>
#endif

#include "ecore_file_private.h"

/*
 * FIXME: the following functions will certainly not work on Windows:
 * ecore_file_app_exe_get()
 * ecore_file_escape_name()
 */

int _ecore_file_log_dom = -1;
static int _ecore_file_init_count = 0;

static Eina_Bool
_ecore_file_stat(const char *file,
                 long long *mtime,
                 long long *size,
                 mode_t *mode,
                 Eina_Bool *is_dir,
                 Eina_Bool *is_reg)
{
   struct stat st;
#ifdef _WIN32
   /*
    * On Windows, stat() returns -1 is file is a path finishing with
    * a slash or blackslash
    * see https://msdn.microsoft.com/en-us/library/14h5k7ff.aspx
    * ("Return Value" section)
    *
    * so we ensure that file never finishes with \ or /
    */
   char f[MAX_PATH];
   size_t len;

   len = strlen(file);
   if ((len + 1) > MAX_PATH)
     return EINA_FALSE;

   memcpy(f, file, len + 1);
   if ((f[len - 1] == '/') || (f[len - 1] == '\\'))
     f[len - 1] = '\0';

   if (stat(f, &st) < 0)
     return EINA_FALSE;
#else
   if (stat(file, &st) < 0)
     return EINA_FALSE;
#endif

   if (mtime) *mtime = st.st_mtime;
   if (size) *size = st.st_size;
   if (mode) *mode = st.st_mode;
   if (is_dir) *is_dir = S_ISDIR(st.st_mode);
   if (is_reg) *is_reg = S_ISREG(st.st_mode);

   return EINA_TRUE;
}

EAPI int
ecore_file_init()
{
   if (++_ecore_file_init_count != 1)
     return _ecore_file_init_count;

   if (!ecore_init())
     return --_ecore_file_init_count;

   _ecore_file_log_dom = eina_log_domain_register
     ("ecore_file", ECORE_FILE_DEFAULT_LOG_COLOR);
   if(_ecore_file_log_dom < 0)
     {
       EINA_LOG_ERR("Impossible to create a log domain for the ecore file module.");
       return --_ecore_file_init_count;
     }
   ecore_file_path_init();
   ecore_file_monitor_init();
   ecore_file_download_init();

   /* FIXME: were the tests disabled for a good reason ? */

   /*
   if (!ecore_file_monitor_init())
     goto shutdown_ecore_file_path;

   if (!ecore_file_download_init())
     goto shutdown_ecore_file_monitor;
   */

   return _ecore_file_init_count;

   /*
 shutdown_ecore_file_monitor:
   ecore_file_monitor_shutdown();
 shutdown_ecore_file_path:
   ecore_file_path_shutdown();

   return --_ecore_file_init_count;
   */
}

EAPI int
ecore_file_shutdown()
{
   if (--_ecore_file_init_count != 0)
     return _ecore_file_init_count;

   ecore_file_download_shutdown();
   ecore_file_monitor_shutdown();
   ecore_file_path_shutdown();

   eina_log_domain_unregister(_ecore_file_log_dom);
   _ecore_file_log_dom = -1;

   ecore_shutdown();

   return _ecore_file_init_count;
}

EAPI long long
ecore_file_mod_time(const char *file)
{
   long long time;

   if (!_ecore_file_stat(file, &time, NULL, NULL, NULL, NULL))
     return 0;

   return time;
}

EAPI long long
ecore_file_size(const char *file)
{
   long long size;

   if (!_ecore_file_stat(file, NULL, &size, NULL, NULL, NULL))
     return 0;

   return size;
}

EAPI Eina_Bool
ecore_file_exists(const char *file)
{
#ifdef _WIN32
   /* I prefer not touching the specific UNIX code... */
   return _ecore_file_stat(file, NULL, NULL, NULL, NULL, NULL);
#else
   struct stat st;
   if (!file) return EINA_FALSE;

   /*Workaround so that "/" returns a true, otherwise we can't monitor "/" in ecore_file_monitor*/
   if (stat(file, &st) < 0 && strcmp(file, "/")) return EINA_FALSE;
   return EINA_TRUE;
#endif
}

EAPI Eina_Bool
ecore_file_is_dir(const char *file)
{
   Eina_Bool is_dir;

   if (!_ecore_file_stat(file, NULL, NULL, NULL, &is_dir, NULL))
     return EINA_FALSE;

   return is_dir;
}

static mode_t default_mode = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH;

EAPI Eina_Bool
ecore_file_mkdir(const char *dir)
{
   return (mkdir(dir, default_mode) == 0);
}

EAPI int
ecore_file_mkdirs(const char **dirs)
{
   int i = 0;

   if (!dirs) return -1;

   for (; *dirs; dirs++)
     if (ecore_file_mkdir(*dirs))
       i++;
   return i;
}

EAPI int
ecore_file_mksubdirs(const char *base, const char **subdirs)
{
#ifndef HAVE_ATFILE_SOURCE
   char buf[PATH_MAX];
   int baselen;
#else
   int fd;
   DIR *dir;
#endif
   int i;

   if (!subdirs) return -1;
   if ((!base) || (base[0] == '\0')) return -1;

   if ((!ecore_file_is_dir(base)) && (!ecore_file_mkpath(base)))
     return 0;

#ifndef HAVE_ATFILE_SOURCE
   baselen = eina_strlcpy(buf, base, sizeof(buf));
   if ((baselen < 1) || (baselen + 1 >= (int)sizeof(buf)))
     return 0;

   if (buf[baselen - 1] != '/')
     {
        buf[baselen] = '/';
        baselen++;
     }
#else
   dir = opendir(base);
   if (!dir)
     return 0;
   fd = dirfd(dir);
#endif

   i = 0;
   for (; *subdirs; subdirs++)
     {
#ifdef HAVE_ATFILE_SOURCE
        struct stat st;
#endif
        Eina_Bool is_dir;

#ifndef HAVE_ATFILE_SOURCE
        eina_strlcpy(buf + baselen, *subdirs, sizeof(buf) - baselen);
        if (_ecore_file_stat(buf, NULL, NULL, NULL, &is_dir, NULL))
          {
#else
        if (fstatat(fd, *subdirs, &st, 0) == 0)
          {
             is_dir = S_ISDIR(st.st_mode);
#endif
             if (is_dir)
               {
                  i++;
                  continue;
               }
          }
        else
          {
             if (errno == ENOENT)
               {
#ifndef HAVE_ATFILE_SOURCE
                  if (ecore_file_mkdir(buf))
#else
                  if (mkdirat(fd, *subdirs, default_mode) == 0)
#endif
                    {
                       i++;
                       continue;
                    }
                 }
            }
     }

#ifdef HAVE_ATFILE_SOURCE
   closedir(dir);
#endif

   return i;
}

EAPI Eina_Bool
ecore_file_rmdir(const char *dir)
{
   if (rmdir(dir) < 0) return EINA_FALSE;
   return EINA_TRUE;
}

EAPI Eina_Bool
ecore_file_unlink(const char *file)
{
   if (unlink(file) < 0) return EINA_FALSE;
   return EINA_TRUE;
}

EAPI Eina_Bool
ecore_file_remove(const char *file)
{
   if (remove(file) < 0) return EINA_FALSE;
   return EINA_TRUE;
}

EAPI Eina_Bool
ecore_file_recursive_rm(const char *dir)
{
#ifndef _WIN32
   struct stat st;
#endif
   Eina_Bool is_dir;

#ifdef _WIN32
   if (!_ecore_file_stat(dir, NULL, NULL, NULL, &is_dir, NULL))
     return EINA_FALSE;
#else
   if (lstat(dir, &st) == -1)
     return EINA_FALSE;
   is_dir = S_ISDIR(st.st_mode);
#endif

   if (is_dir)
     {
        Eina_File_Direct_Info *info;
        Eina_Iterator *it;
        int ret;

        ret = 1;
        it = eina_file_direct_ls(dir);
        EINA_ITERATOR_FOREACH(it, info)
          {
             if (!ecore_file_recursive_rm(info->path))
               ret = 0;
          }
        eina_iterator_free(it);

        if (!ecore_file_rmdir(dir)) ret = 0;
        if (ret)
            return EINA_TRUE;
        else
            return EINA_FALSE;
     }
   else
     {
        return ecore_file_unlink(dir);
     }
}

static inline Eina_Bool
_ecore_file_mkpath_if_not_exists(const char *path)
{
   Eina_Bool is_dir;

   /* Windows: path like C: or D: etc are valid, but stat() returns an error */
#ifdef _WIN32
   if ((strlen(path) == 2) &&
       ((path[0] >= 'a' && path[0] <= 'z') ||
        (path[0] >= 'A' && path[0] <= 'Z')) &&
       (path[1] == ':'))
     return EINA_TRUE;
#endif

   if (!_ecore_file_stat(path, NULL, NULL, NULL, &is_dir, NULL))
     return ecore_file_mkdir(path);
   else if (!is_dir)
     return EINA_FALSE;
   else
     return EINA_TRUE;
}

EAPI Eina_Bool
ecore_file_mkpath(const char *path)
{
   char ss[PATH_MAX];
   unsigned int i;

   EINA_SAFETY_ON_NULL_RETURN_VAL(path, EINA_FALSE);

   if (ecore_file_is_dir(path))
     return EINA_TRUE;

   for (i = 0; path[i] != '\0'; ss[i] = path[i], i++)
     {
        if (i == sizeof(ss) - 1) return EINA_FALSE;
        if (((path[i] == '/') || (path[i] == '\\')) && (i > 0))
          {
             ss[i] = '\0';
             if (!_ecore_file_mkpath_if_not_exists(ss))
               return EINA_FALSE;
          }
     }
   ss[i] = '\0';
   return _ecore_file_mkpath_if_not_exists(ss);
}

EAPI int
ecore_file_mkpaths(const char **paths)
{
   int i = 0;

   if (!paths) return -1;

   for (; *paths; paths++)
     if (ecore_file_mkpath(*paths))
       i++;
   return i;
}

EAPI Eina_Bool
ecore_file_cp(const char *src, const char *dst)
{
   FILE *f1, *f2;
   char buf[16384];
   char realpath1[PATH_MAX], realpath2[PATH_MAX];
   size_t num;
   Eina_Bool ret = EINA_TRUE;

   if (!realpath(src, realpath1)) return EINA_FALSE;
   if (realpath(dst, realpath2) && !strcmp(realpath1, realpath2)) return EINA_FALSE;

   f1 = fopen(src, "rb");
   if (!f1) return EINA_FALSE;
   f2 = fopen(dst, "wb");
   if (!f2)
     {
        fclose(f1);
        return EINA_FALSE;
     }
   while ((num = fread(buf, 1, sizeof(buf), f1)) > 0)
     {
        if (fwrite(buf, 1, num, f2) != num) ret = EINA_FALSE;
     }
   fclose(f1);
   fclose(f2);
   return ret;
}

EAPI Eina_Bool
ecore_file_mv(const char *src, const char *dst)
{
   char buf[PATH_MAX];
   int fd;

   if (rename(src, dst))
     {
        // File cannot be moved directly because
        // it resides on a different mount point.
        if (errno == EXDEV)
          {
             mode_t mode;
             Eina_Bool is_reg;

             // Make sure this is a regular file before
             // we do anything fancy.
             if (!_ecore_file_stat(src, NULL, NULL, &mode, NULL, &is_reg))
                 goto FAIL;
             if (is_reg)
               {
                  char *dir;
                  Eina_Tmpstr *tmpstr = NULL;

                  dir = ecore_file_dir_get(dst);
                  // Since we can't directly rename, try to
                  // copy to temp file in the dst directory
                  // and then rename.
                  snprintf(buf, sizeof(buf), "%s/.%s.tmp.XXXXXX",
                           dir, ecore_file_file_get(dst));
                  free(dir);
                  fd = eina_file_mkstemp(buf, &tmpstr);
                  if (fd < 0) goto FAIL;
                  close(fd);

                  // Copy to temp file
                  if (!ecore_file_cp(src, tmpstr))
                    {
                       eina_tmpstr_del(tmpstr);
                       goto FAIL;
                    }

                  // Set file permissions of temp file to match src
                  if (chmod(tmpstr, mode) == -1)
                    {
                       eina_tmpstr_del(tmpstr);
                       goto FAIL;
                    }

                  // Try to atomically move temp file to dst
                  if (rename(tmpstr, dst))
                    {
                       // If we still cannot atomically move
                       // do a normal copy and hope for the best.
                       if (!ecore_file_cp(tmpstr, dst))
                         {
                            eina_tmpstr_del(tmpstr);
                            goto FAIL;
                         }
                    }

                  // Delete temporary file and src
                  ecore_file_unlink(tmpstr);
                  ecore_file_unlink(src);
                  eina_tmpstr_del(tmpstr);
                  goto PASS;
               }
          }
#ifdef _WIN32
          if (errno == EEXIST)
            {
               struct _stat s;
               _stat(dst, &s);
               if (_S_IFREG & s.st_mode)
               {
                  ecore_file_unlink(dst);
                  if (rename(src, dst))
                    {
                       return EINA_TRUE;
                    }
               }
            }
#endif
        goto FAIL;
     }

PASS:
   return EINA_TRUE;

FAIL:
   return EINA_FALSE;
}

EAPI Eina_Bool
ecore_file_symlink(const char *src, const char *dest)
{
#ifndef _WIN32
   return !symlink(src, dest);
#else
   return EINA_FALSE;
   (void)src;
   (void)dest;
#endif
}

EAPI char *
ecore_file_realpath(const char *file)
{
   char buf[PATH_MAX];

   /*
    * Some implementations of realpath do not conform to the SUS.
    * And as a result we must prevent a null arg from being passed.
    */
   if (!file) return strdup("");
   if (!realpath(file, buf)) return strdup("");

   return strdup(buf);
}

EAPI const char *
ecore_file_file_get(const char *path)
{
   char *result = NULL;

   if (!path) return NULL;

   if ((result = strrchr(path, '/'))) result++;
   else result = (char *)path;

#ifdef _WIN32
   /*
    * Here, we know that there is no more / in the string beginning at
    * 'result'. So just check that there is no more \ from it.
    */
   {
      char *result_backslash;
      if ((result_backslash = strrchr(result, '\\')))
        result = ++result_backslash;
   }
#endif

   return result;
}

EAPI char *
ecore_file_dir_get(const char *file)
{
   char *p;
   char buf[PATH_MAX];

   if (!file) return NULL;
   strncpy(buf, file, PATH_MAX);
   buf[PATH_MAX - 1] = 0;
   p = dirname(buf);
   return strdup(p);
}

EAPI Eina_Bool
ecore_file_can_read(const char *file)
{
   if (!file) return EINA_FALSE;
   if (!access(file, R_OK)) return EINA_TRUE;
   return EINA_FALSE;
}

EAPI Eina_Bool
ecore_file_can_write(const char *file)
{
   if (!file) return EINA_FALSE;
   if (!access(file, W_OK)) return EINA_TRUE;
   return EINA_FALSE;
}

EAPI Eina_Bool
ecore_file_can_exec(const char *file)
{
#ifdef _WIN32
   HANDLE h;
   HANDLE fm;
   char *base;
   char *base_nt;
   LARGE_INTEGER sz;
   WORD characteristics;
#endif

   if (!file || !*file) return EINA_FALSE;

#ifdef _WIN32
   /*
    * we parse the file to check if it is a PE file (EXE or DLL)
    * and we finally check whether it's a DLL or not.
    * Reference :
    * https://docs.microsoft.com/en-us/windows/win32/debug/pe-format
    */
   h = CreateFile(file, GENERIC_READ, FILE_SHARE_READ, NULL,
                  OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
   if (h == INVALID_HANDLE_VALUE)
     goto test_bat;

   if (!GetFileSizeEx(h, &sz))
     goto close_h;

   /* a PE file must have at least the DOS and NT headers */
   if (sz.QuadPart < (LONGLONG)(sizeof(IMAGE_DOS_HEADER) + sizeof(IMAGE_NT_HEADERS)))
     goto close_h;

   fm = CreateFileMapping(h, NULL, PAGE_READONLY, 0, 0, NULL);
   if (fm == NULL)
     goto close_h;

   base = (char *)MapViewOfFile(fm, FILE_MAP_READ, 0, 0, 0);
   CloseHandle(fm);
   if (base == NULL)
     goto close_h;

   /*
    * the PE file begins with the DOS header.
    * First magic number : the DOS header must begin with a DOS magic number,
    * that is "MZ", that is 0x5a4d, stored in a WORD.
    */
   if (*((WORD *)base) != 0x5a4d)
     goto unmap_view;

   /*
    * The position of the NT header is located at the offset 0x3c.
    */
   base_nt = base + *((DWORD *)(base + 0x3c));
   /*
    * The NT header begins with the magic number "PE\0\0", that is
    * 0x00004550, stored in a DWORD.
    */
   if (*((DWORD *)base_nt) != 0x00004550)
     goto unmap_view;

   /*
    * to get informations about executable (EXE or DLL), we look at
    * the 'Characteristics' member of the NT header, located at the offset
    * 22 (4 for the magic number, 18 for the offset) from base_nt.
    * https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#characteristics
    */
   characteristics = *((WORD *)(base_nt + 4 + 18));

   UnmapViewOfFile(base);
   CloseHandle(h);

   /*
    * 0x0002 : if set, EXE or DLL
    * 0x2000 : if set, DLL
    */
   if ((characteristics & 0x0002) && !(characteristics & 0x2000))
     return EINA_TRUE;
 unmap_view:
   UnmapViewOfFile(base);
 close_h:
   CloseHandle(h);
 test_bat:
   /*
    * a .bat file, considered as an executable, is only a text file,
    * so we rely on the extension. Not the best but we cannot do more.
    */
   return eina_str_has_extension(file, ".bat");
#else
   if (!access(file, X_OK)) return EINA_TRUE;
#endif
   return EINA_FALSE;
}

EAPI char *
ecore_file_readlink(const char *link)
{
#ifndef _WIN32
   char buf[PATH_MAX];
   int count;

   if ((count = readlink(link, buf, sizeof(buf) - 1)) < 0) return NULL;
   buf[count] = 0;
   return strdup(buf);
#else
   return NULL;
   (void)link;
#endif
}

EAPI Eina_List *
ecore_file_ls(const char *dir)
{
   Eina_File_Direct_Info *info;
   Eina_Iterator *ls;
   Eina_List *list = NULL;

   ls = eina_file_direct_ls(dir);
   if (!ls) return NULL;

   EINA_ITERATOR_FOREACH(ls, info)
     {
        char *f;

        f = strdup(info->path + info->name_start);
        list = eina_list_append(list, f);
     }
   eina_iterator_free(ls);

   list = eina_list_sort(list, eina_list_count(list), EINA_COMPARE_CB(strcoll));

   return list;
}

EAPI char *
ecore_file_app_exe_get(const char *app)
{
   Eina_Strbuf *buf;
   char *exe;
   const char *p;
   Eina_Bool in_qout_double = EINA_FALSE;
   Eina_Bool in_qout_single = EINA_FALSE;

   if (!app) return NULL;
   buf = eina_strbuf_new();
   if (!buf) return NULL;
   p = app;
   if ((p[0] == '~') && (p[1] == '/'))
     {
        const char *home = eina_environment_home_get();
        if (home) eina_strbuf_append(buf, home);
        p++;
     }
   for (; *p; p++)
     {
        if (in_qout_double)
          {
             if (*p == '\\')
               {
                  if (p[1]) p++;
                  eina_strbuf_append_char(buf, *p);
               }
             else if (*p == '"') in_qout_double = EINA_FALSE;
             else eina_strbuf_append_char(buf, *p);
          }
        else if (in_qout_single)
          {
             if (*p == '\\')
               {
                  if (p[1]) p++;
                  eina_strbuf_append_char(buf, *p);
               }
             else if (*p == '\'') in_qout_single = EINA_FALSE;
             else eina_strbuf_append_char(buf, *p);
          }
        else
          {
             if (*p == '\\')
               {
                  if (p[1]) p++;
                  eina_strbuf_append_char(buf, *p);
               }
             else if (*p == '"') in_qout_double = EINA_TRUE;
             else if (*p == '\'') in_qout_single = EINA_TRUE;
             else
               {
                  if (isspace((unsigned char)(*p))) break;
                  eina_strbuf_append_char(buf, *p);
               }
          }
     }
   exe = eina_strbuf_string_steal(buf);
   eina_strbuf_free(buf);
   return exe;
}

EAPI char *
ecore_file_escape_name(const char *filename)
{
   const char *p;
   char *q;
   char buf[PATH_MAX];

   EINA_SAFETY_ON_NULL_RETURN_VAL(filename, NULL);

   p = filename;
   q = buf;
   while (*p)
     {
        if ((q - buf) > (PATH_MAX - 6)) return NULL;
        if (
            (*p == ' ') || (*p == '\\') || (*p == '\'') ||
            (*p == '\"') || (*p == ';') || (*p == '!') ||
            (*p == '#') || (*p == '$') || (*p == '%') ||
            (*p == '&') || (*p == '*') || (*p == '(') ||
            (*p == ')') || (*p == '[') || (*p == ']') ||
            (*p == '{') || (*p == '}') || (*p == '|') ||
            (*p == '<') || (*p == '>') || (*p == '?')
            )
          {
             *q = '\\';
             q++;
          }
        else if (*p == '\t')
          {
             *q = '\\';
             q++;
             *q = '\\';
             q++;
             *q = 't';
             q++;
             p++;
             continue;
          }
        else if (*p == '\n')
          {
            *q = '\\';
            q++;
            *q = '\\';
            q++;
            *q = 'n';
            q++;
            p++;
            continue;
          }

        *q = *p;
        q++;
        p++;
     }
   *q = 0;
   return strdup(buf);
}

EAPI char *
ecore_file_strip_ext(const char *path)
{
   char *p, *file = NULL;

   if (!path)
     return NULL;

   p = strrchr(path, '.');
   if (!p)
     file = strdup(path);
   else if (p != path)
     {
        file = malloc(((p - path) + 1) * sizeof(char));
        if (file)
          {
             memcpy(file, path, (p - path));
             file[p - path] = 0;
          }
     }

   return file;
}

EAPI int
ecore_file_dir_is_empty(const char *dir)
{
   Eina_File_Direct_Info *info;
   Eina_Iterator *it;

   it = eina_file_direct_ls(dir);
   if (!it) return -1;

   EINA_ITERATOR_FOREACH(it, info)
     {
        eina_iterator_free(it);
        return 0;
     }

   eina_iterator_free(it);
   return 1;
}
