/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2002 Daniel Stenberg
 * Copyright (C) 2014 Michael Sevakis
 *
 * This program 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.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/
#define RB_FILESYSTEM_OS
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <time.h>
#include <errno.h>
#include <limits.h>
#include "config.h"
#include "system.h"
#include "file.h"
#include "dir.h"
#include "file_internal.h"
#include "pathfuncs.h"
#include "string-extra.h"
#include "debug.h"

#ifndef os_fstatat
#define USE_OSDIRNAME /* we need to remember the open directory path */
#endif

extern const char *sim_root_dir;

/* Windows (and potentially other OSes) distinguish binary and text files.
 * Define a dummy for the others. */
#ifndef O_BINARY
#define O_BINARY 0
#endif

struct filestr_desc
{
    int  osfd;              /* The host OS file descriptor */
    bool mounted;           /* Is host volume still mounted? */
#ifdef HAVE_MULTIVOLUME
    int  volume;            /* The virtual volume number */
#endif
} openfiles[MAX_OPEN_FILES] =
{
    [0 ... MAX_OPEN_FILES-1] = { .osfd = -1 }
};

static struct filestr_desc * alloc_filestr(int *fildesp)
{
    for (unsigned int i = 0; i < MAX_OPEN_FILES; i++)
    {
        struct filestr_desc *filestr = &openfiles[i];
        if (filestr->osfd < 0)
        {
            *fildesp = i;
            return filestr;
        }
    }

    return NULL;
}

static struct filestr_desc * get_filestr(int fildes)
{
    struct filestr_desc *filestr = &openfiles[fildes];

    if ((unsigned int)fildes >= MAX_OPEN_FILES || filestr->osfd < 0)
        filestr = NULL;
    else if (filestr->mounted)
        return filestr;

    errno = filestr ? ENXIO : EBADF;
    DEBUGF("fildes %d: %s\n", fildes, strerror(errno));
    return NULL;
}

struct dirstr_desc
{
    int               osfd;          /* Host OS directory file descriptor */
    bool              osfd_opened;   /* Host fd is another open file */
    OS_DIR_T          *osdirp;       /* Host OS directory stream */
#ifdef USE_OSDIRNAME
    char              *osdirname;    /* Host OS directory path */
#endif
    struct sim_dirent entry;         /* Rockbox directory entry */
#ifdef HAVE_MULTIVOLUME
    int               volume;        /* Virtual volume number */
    int               volumecounter; /* Counter for root volume entries */
#endif
    bool              mounted;       /* Is the virtual volume still mounted? */
} opendirs[MAX_OPEN_DIRS];

static struct dirstr_desc * alloc_dirstr(void)
{
    for (unsigned int i = 0; i < MAX_OPEN_DIRS; i++)
    {
        struct dirstr_desc *dirstr = &opendirs[i];
        if (dirstr->osdirp == NULL)
            return dirstr;
    }

    return NULL;
}

static struct dirstr_desc * get_dirstr(DIR *dirp)
{
    struct dirstr_desc *dirstr = (struct dirstr_desc *)dirp;

    if (!PTR_IN_ARRAY(opendirs, dirstr, MAX_OPEN_DIRS) || !dirstr->osdirp)
        dirstr = NULL;
    else if (dirstr->mounted)
        return dirstr;

    errno = dirstr ? ENXIO : EBADF;
    DEBUGF("dir #%d: %s\n", (int)(dirstr - opendirs), strerror(errno));
    return NULL;
}

static int close_dirstr(struct dirstr_desc *dirstr)
{
    OS_DIR_T *osdirp = dirstr->osdirp;

    dirstr->mounted = false;

#ifdef USE_OSDIRNAME
    free(dirstr->osdirname);
#endif
    if (dirstr->osfd_opened)
    {
        os_close(dirstr->osfd);
        dirstr->osfd_opened = false;
    }

    int rc = os_closedir(osdirp);
    dirstr->osdirp = NULL;

    return rc;
}

#ifdef HAVE_MULTIVOLUME
static int readdir_volume_inner(struct dirstr_desc *dirstr,
                                struct sim_dirent *entry)
{
    /* Volumes (secondary file systems) get inserted into the system root
     * directory. If the path specified volume 0, enumeration will not
     * include other volumes, but just its own files and directories.
     *
     * Fake special directories, which don't really exist, that will get
     * redirected upon opendir()
     */
    while (++dirstr->volumecounter < NUM_VOLUMES)
    {
        /* on the system root */
        if (!volume_present(dirstr->volumecounter))
            continue;

        get_volume_name(dirstr->volumecounter, entry->d_name);
        return 1;
    }

    /* do normal directory entry fetching */
    return 0;
}
#endif /* HAVE_MULTIVOLUME */

static inline int readdir_volume(struct dirstr_desc *dirstr,
                                 struct sim_dirent *entry)
{
#ifdef HAVE_MULTIVOLUME
    if (dirstr->volumecounter < NUM_VOLUMES)
        return readdir_volume_inner(dirstr, entry);
#endif /* HAVE_MULTIVOLUME */

    /* do normal directory entry fetching */
    return 0;
    (void)dirstr; (void)entry;
}


/** Internal functions **/

#ifdef HAVE_MULTIDRIVE
/**
 * Handle drive extraction by pretending the files' volumes no longer exist
 * and invalidating their I/O for the remainder of their lifetimes as would
 * happen on a native target
 */
void sim_ext_extracted(int drive)
{
    for (unsigned int i = 0; i < MAX_OPEN_FILES; i++)
    {
        struct filestr_desc *filestr = &openfiles[i];
        if (filestr->osfd >= 0 && volume_drive(filestr->volume) == drive)
            filestr->mounted = false;
    }

    for (unsigned int i = 0; i < MAX_OPEN_DIRS; i++)
    {
        struct dirstr_desc *dirstr = &opendirs[i];
        if (dirstr->osdirp && volume_drive(dirstr->volume) == drive)
            dirstr->mounted = false;
    }

    (void)drive;
}
#endif /* HAVE_MULTIDRIVE */

/**
 * Provides target-like path parsing behavior with single and multiple volumes
 * while performing minimal transforming of the input.
 *
 * Paths are sandboxed to the simulated namespace:
 *   e.g. "{simdir}/foo/../../../../bar" becomes "{simdir}/foo/../bar"
 */
int sim_get_os_path(char *buffer, const char *path, size_t bufsize)
{
    #define ADVBUF(amt) \
        ({ buffer += (amt); bufsize -= (amt); })

    #define PPP_SHOWPATHS 0

    if (!path_is_absolute(path))
    {
        DEBUGF("ERROR: path is not absolute: \"%s\"\n", path);
        errno = ENOENT;
        return -1;
    }

#if PPP_SHOWPATHS
    const char *const buffer0 = buffer;
    DEBUGF("PPP (pre): \"%s\"\n", path);
#endif

    /* Prepend sim root */
    size_t size = strlcpy(buffer, sim_root_dir, bufsize);
    if (size >= bufsize)
    {
        errno = ENAMETOOLONG;
        return -1;
    }
    ADVBUF(size);

#ifdef HAVE_MULTIVOLUME
    /* Track the last valid volume spec encountered */
    int volume = -1;
    bool sysroot = true;

    /* Basename of sim dir to switch back to simdisk from ext */
    #define DIRBASE_FMT  ".." PATH_SEPSTR "%s"
    ssize_t dirbase_len = 0;
    char dirbase[size + 3 + 1];

    /* Basename of ext directory to switch to alternate volume */
    #define SIMEXT_FMT   ".." PATH_SEPSTR "simext%d"
    char extbuf[sizeof (SIMEXT_FMT) + 20 + 1];
#endif /* HAVE_MULTIVOLUME */

    int level = 0;
    bool done = false;
    while (!done)
    {
        const char *p;
        ssize_t len = parse_path_component(&path, &p);


        switch (len)
        {
        case 0:
            done = true;
            if (path[-1] != PATH_SEPCH)
                continue;

            /* Path ends with a separator; preserve that */
            p = &path[-1];
            len = 1;
            break;

        case 1:
        case 2:
            if (p[0] == '.')
            {
                if (len == 1)
                    break;

                if (p[1] == '.')
                    goto is_dot_dot;
            }

        default:
            level++;

        #ifdef HAVE_MULTIVOLUME
            if (level != 1)
                break; /* Volume spec only valid @ root level */

            const char *next;
            volume = path_strip_volume(p, &next, true);

            if (next > p)
            {
            #ifdef HAVE_MULTIDRIVE
                /* Feign failure if the volume isn't "mounted" */
                if (!volume_present(volume))
                {
                    errno = ENXIO;
                    return -1;
                }
            #endif /* HAVE_MULTIDRIVE */

                sysroot = false;

                if (volume == 0)
                    continue;

                p = extbuf;
                len = snprintf(extbuf, sizeof (extbuf), SIMEXT_FMT, volume);
            }
        #endif /* HAVE_MULTIVOLUME */
            break;

        is_dot_dot:
            if (level <= 0)
                continue; /* Can't go above root; erase */

            level--;

        #ifdef HAVE_MULTIVOLUME
            if (level == 0)
            {
                int v = volume;
                bool sr = sysroot;
                volume = -1;
                sysroot = true;

                if (v <= 0)
                {
                    if (sr)
                        break;

                    continue;
                }

                /* Going up from a volume root and back down to the sys root */
                if (dirbase_len == 0)
                {
                    /* Get the main simdisk directory so it can be reentered */
                    char tmpbuf[sizeof (dirbase)];
                #ifdef WIN32
                    path_correct_separators(tmpbuf, sim_root_dir);
                    path_strip_drive(tmpbuf, &p, false);
                #else
                    p = tmpbuf;
                    strcpy(tmpbuf, sim_root_dir);
                #endif
                    size = path_basename(p, &p);
                    ((char *)p)[size] = '\0';

                    if (size == 0 || is_dotdir_name(p))
                    {
                        /* This is nonsense and won't work */
                        DEBUGF("ERROR: sim root dir basname is dotdir or"
                               " empty: \"%s\"\n", sim_root_dir);
                        errno = ENOENT;
                        return -1;
                    }

                    dirbase_len = snprintf(dirbase, sizeof (dirbase),
                                           DIRBASE_FMT, p);
                }

                p = dirbase;
                len = dirbase_len;
                break;
            }
        #endif /* HAVE_MULTIVOLUME */
            break;
        } /* end switch */

        char compname[len + 1];
        strmemcpy(compname, p, len);

        size = path_append(buffer, PA_SEP_HARD, compname, bufsize);
        if (size >= bufsize)
        {
            errno = ENAMETOOLONG;
            return -1;
        }
        ADVBUF(size);
    }

#if PPP_SHOWPATHS
    DEBUGF("PPP (post): \"%s\"" IF_MV(" vol:%d") "\n",
           buffer0 IF_MV(, volume));
#endif

    return IF_MV(volume) +1;
}


/** File functions **/

int sim_open(const char *path, int oflag, ...)
{
    int fildes;
    struct filestr_desc *filestr = alloc_filestr(&fildes);
    if (!filestr)
    {
        errno = EMFILE;
        return -1;
    }

    char ospath[SIM_TMPBUF_MAX_PATH];
    int pprc = sim_get_os_path(ospath, path, sizeof (ospath));
    if (pprc < 0)
        return -2;

    filestr->osfd = os_open(ospath, oflag | O_BINARY __OPEN_MODE_ARG);
    if (filestr->osfd < 0)
        return -3;

#ifdef HAVE_MULTIVOLUME
    filestr->volume  = MAX(pprc - 1, 0);
#endif
    filestr->mounted = true;
    return fildes;
}

int sim_creat(const char *path, mode_t mode)
{
    return sim_open(path, O_WRONLY|O_CREAT|O_TRUNC, mode);
}

int sim_close(int fildes)
{
    struct filestr_desc *filestr = &openfiles[fildes];
    if ((unsigned int)fildes >= MAX_OPEN_FILES || filestr->osfd < 0)
    {
        errno = EBADF;
        return -1;
    }

    int osfd = filestr->osfd;
    filestr->osfd = -1;
    return os_close(osfd);
}

int sim_ftruncate(int fildes, off_t length)
{
    struct filestr_desc *filestr = get_filestr(fildes);
    if (!filestr)
        return -1;

    off_t size = os_filesize(filestr->osfd);
    if (size < 0)
        return -1;

    if (length >= size)
        return 0;

    int rc = os_ftruncate(filestr->osfd, length);

#ifdef HAVE_DIRCACHE
    if (rc >= 0)
        dircache_ftruncate(xxxxxx);
#endif

    return rc;
}

int sim_fsync(int fildes)
{
    struct filestr_desc *filestr = get_filestr(fildes);
    if (!filestr)
        return -1;

    int rc = os_fsync(filestr->osfd);

#ifdef HAVE_DIRCACHE
    if (rc >= 0)
        dircache_fsync(xxxxxx);
#endif

    return rc;
}

off_t sim_lseek(int fildes, off_t offset, int whence)
{
    struct filestr_desc *filestr = get_filestr(fildes);
    if (!filestr)
        return -1;

    return os_lseek(filestr->osfd, offset, whence);
}

ssize_t sim_read(int fildes, void *buf, size_t nbyte)
{
    struct filestr_desc *filestr = get_filestr(fildes);
    if (!filestr)
        return -1;

    return os_read(filestr->osfd, buf, nbyte);
}

ssize_t sim_write(int fildes, const void *buf, size_t nbyte)
{
    struct filestr_desc *filestr = get_filestr(fildes);
    if (!filestr)
        return -1;

    return os_write(filestr->osfd, buf, nbyte);
}

int sim_remove(const char *path)
{
    char ospath[SIM_TMPBUF_MAX_PATH];
    if (sim_get_os_path(ospath, path, sizeof (ospath)) < 0)
        return -1;

    int rc = os_remove(ospath);

#ifdef HAVE_DIRCACHE
    if (rc >= 0)
        dircache_remove(xxxxxx);
#endif

    return rc;
}

int sim_rename(const char *old, const char *new)
{
    char osold[SIM_TMPBUF_MAX_PATH];
    int pprc1 = sim_get_os_path(osold, old, sizeof (osold));
    if (pprc1 < 0)
        return -1;

    char osnew[SIM_TMPBUF_MAX_PATH];
    int pprc2 = sim_get_os_path(osnew, new, sizeof (osnew));
    if (pprc2 < 0)
        return -1;

    if (MAX(pprc1 - 1, 0) != MAX(pprc2 - 1, 0))
    {
        /* Pretend they're different devices */
        errno = EXDEV;
        return -1;
    }

    int rc = os_rename(osold, osnew);

#ifdef HAVE_DIRCACHE
    if (rc >= 0)
        dircache_rename(xxxxxx);
#endif

    return rc;
}

off_t sim_filesize(int fildes)
{
    struct filestr_desc *filestr = get_filestr(fildes);
    if (!filestr)
        return -1;

    return os_filesize(filestr->osfd);
}

int sim_fsamefile(int fildes1, int fildes2)
{
    struct filestr_desc *filestr1 = get_filestr(fildes1);
    if (!filestr1)
        return -1;

    struct filestr_desc *filestr2 = get_filestr(fildes2);
    if (!filestr2)
        return -1;

    if (filestr1 == filestr2)
        return 1;

    return os_fsamefile(filestr1->osfd, filestr2->osfd);
}

int sim_relate(const char *path1, const char *path2)
{
    char ospath1[SIM_TMPBUF_MAX_PATH];
    if (sim_get_os_path(ospath1, path1, sizeof (ospath1)) < 0)
        return -1;

    char ospath2[SIM_TMPBUF_MAX_PATH];
    if (sim_get_os_path(ospath2, path2, sizeof (ospath2)) < 0)
        return -1;

    return os_relate(ospath1, ospath2);
}

bool sim_file_exists(const char *path)
{
    char ospath[SIM_TMPBUF_MAX_PATH];
    if (sim_get_os_path(ospath, path, sizeof (ospath)) < 0)
        return false;

    return os_file_exists(ospath);
}


/** Directory functions **/
DIR * sim_opendir(const char *dirname)
{
    struct dirstr_desc *dirstr = alloc_dirstr();
    if (!dirstr)
    {
        errno = EMFILE;
        return NULL;
    }

    char osdirname[SIM_TMPBUF_MAX_PATH];
    int pprc = sim_get_os_path(osdirname, dirname, sizeof (osdirname));
    if (pprc < 0)
        return NULL;

    int rc = os_opendir_and_fd(osdirname, &dirstr->osdirp, &dirstr->osfd);
    if (rc < 0)
        return NULL;

    dirstr->osfd_opened = rc > 0;

#ifdef USE_OSDIRNAME
    dirstr->osdirname = strdup(osdirname);
    if (!dirstr->osdirname)
    {
        close_dirstr(dirstr);
        return NULL;
    }
#endif

    dirstr->entry.d_name[0] = 0; /* Mark as invalid */
#ifdef HAVE_MULTIVOLUME
    dirstr->volume = MAX(pprc - 1, 0);
    dirstr->volumecounter = pprc == 0 ? 0 : INT_MAX;
#endif
    dirstr->mounted = true;
    return (DIR *)dirstr; /* A-Okay */
}

int sim_closedir(DIR *dirp)
{
    struct dirstr_desc *dirstr = (struct dirstr_desc *)dirp;
    if (!PTR_IN_ARRAY(opendirs, dirstr, MAX_OPEN_DIRS) || !dirstr->osdirp)
    {
        errno = EBADF;
        return -1;
    }

    return close_dirstr(dirstr);
}

struct sim_dirent * sim_readdir(DIR *dirp)
{
    struct dirstr_desc *dirstr = get_dirstr(dirp);
    if (!dirstr)
        return NULL;

    struct sim_dirent *entry = &dirstr->entry;
    entry->info.osdirent = NULL;

    if (readdir_volume(dirstr, entry))
        return entry;
  
    OS_DIRENT_T *osdirent = os_readdir(dirstr->osdirp);
    if (!osdirent)
        return NULL;

    size_t size = sizeof (entry->d_name);
    if (strlcpy_from_os(entry->d_name, osdirent->d_name, size) >= size)
        FILE_ERROR_RETURN(ENAMETOOLONG, NULL);

    entry->info.osdirent = osdirent;
    return entry;
}

int sim_mkdir(const char *path)
{
    char ospath[SIM_TMPBUF_MAX_PATH];
    if (sim_get_os_path(ospath, path, sizeof (ospath)) < 0)
        return -1;

    int rc = os_mkdir(ospath __MKDIR_MODE_ARG);

#ifdef HAVE_DIRCACHE
    if (rc >= 0)
        dircache_mkdir(xxxxxx);
#endif

    return rc;
}

int sim_rmdir(const char *path)
{
    char ospath[SIM_TMPBUF_MAX_PATH];
    if (sim_get_os_path(ospath, path, sizeof (ospath)) < 0)
        return -1;

    int rc = os_rmdir(ospath);

#ifdef HAVE_DIRCACHE
    if (rc >= 0)
        dircache_rmdir(xxxxxx);
#endif

    return rc;
}

int sim_samedir(DIR *dirp1, DIR *dirp2)
{
    struct dirstr_desc *dirstr1 = get_dirstr(dirp1);
    if (!dirstr1)
        return -1;

    struct dirstr_desc *dirstr2 = get_dirstr(dirp2);
    if (!dirstr2)
        return -1;

    return os_fsamefile(dirstr1->osfd, dirstr2->osfd);
}

bool sim_dir_exists(const char *dirname)
{
    char osdirname[SIM_TMPBUF_MAX_PATH];
    if (sim_get_os_path(osdirname, dirname, sizeof (osdirname)) < 0)
        return false;

    OS_DIR_T *dirp = os_opendir(osdirname);
    if (!dirp)
        return false;

    os_closedir(dirp);
    return true;
}

struct dirinfo dir_get_info(DIR *dirp, struct sim_dirent *entry)
{
    int rc;
    struct dirstr_desc *dirstr = get_dirstr(dirp);
    if (!dirstr)
        FILE_ERROR(ERRNO, RC);

    if (entry->d_name[0] == 0)
        FILE_ERROR(ENOENT, RC);

    OS_DIRENT_T *osdirent = entry->info.osdirent;
    if (osdirent == NULL)
        return (struct dirinfo){ .attribute = ATTR_MOUNT_POINT };

    struct dirinfo info;
    info.attribute = 0;

    OS_STAT_T s;

#ifdef os_fstatat
    if (os_fstatat(dirstr->osfd, entry->d_name, &s, 0))
#else /* no fstatat; build file name for stat() */
    char statpath[SIM_TMPBUF_MAX_PATH];
    if (path_append(statpath, dirstr->osdirname, entry->d_name,
                    sizeof (statpath)) >= sizeof (statpath))
    {
        FILE_ERROR(ENAMETOOLONG, RC);
    }

    if (os_stat(statpath, &s)) /* get info */
#endif /* os_fstatat */
    {
        /* File size larger than 2 GB? */
    #ifdef EOVERFLOW
        if (errno == EOVERFLOW)
            DEBUGF("stat overflow for \"%s\"\n", entry->d_name);
    #endif
        FILE_ERROR(ERRNO, RC);
    }

    if (S_ISDIR(s.st_mode))
        info.attribute |= ATTR_DIRECTORY;

    info.size = s.st_size;

    struct tm tm;
    if (localtime_r(&s.st_mtime, &tm) == NULL)
        FILE_ERROR(ERRNO, RC);

    info.mtime = mktime(&tm);

    return info;

file_error:
    return (struct dirinfo){ .size = 0 };
}

int os_volume_path(IF_MV(int volume, ) char *buffer, size_t bufsize)
{
    if (!buffer || !bufsize IF_MV( || !volume_present(volume) ))
        return -1;

    char tmpbuf[SIM_TMPBUF_MAX_PATH];
    tmpbuf[0] = '\0';

#ifdef HAVE_MULTIVOLUME
    char volname[VOL_MAX_LEN + 1];
    get_volume_name(volume, volname);

    if (path_append(tmpbuf, PA_SEP_HARD, volname, sizeof (volname))
            >= sizeof (volname))
        return -1;
#endif /* HAVE_MULTIVOLUME */

    if (path_append(tmpbuf, PA_SEP_HARD, ".", sizeof (tmpbuf))
        >= sizeof (tmpbuf))
        return -1;

    return sim_get_os_path(buffer, tmpbuf, bufsize);
}
