#include "moar.h"
#ifndef _WIN32
#include <dirent.h>
#endif

#ifdef _WIN32
#  define IS_SLASH(c)     ((c) == L'\\' || (c) == L'/')
#else
#  define IS_SLASH(c)     ((c) == '/')
#endif

#ifndef PATH_MAX
#  define PATH_MAX 2048
#endif

#ifdef _WIN32
static wchar_t * UTF8ToUnicode(char *str)
{
     const int         len = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0);
     wchar_t * const result = (wchar_t *)MVM_malloc(len * sizeof(wchar_t));

     MultiByteToWideChar(CP_UTF8, 0, str, -1, result, len);

     return result;
}

static char * UnicodeToUTF8(const wchar_t *str)
{
     const int       len = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL);
     char * const result = (char *)MVM_malloc(len * sizeof(char));

     WideCharToMultiByte(CP_UTF8, 0, str, -1, result, len, NULL, NULL);

     return result;
}

static int mkdir_p(MVMThreadContext *tc, wchar_t *pathname, MVMint64 mode) {
    wchar_t *p = pathname, ch;
#else
static int mkdir_p(MVMThreadContext *tc, char *pathname, MVMint64 mode) {
    char *p = pathname, ch;
    uv_fs_t req;
    int mkdir_error = 0;
#endif
    int created = 0;

    for (;; ++p)
        if (!*p || IS_SLASH(*p)) {
            ch = *p;
            *p  = '\0';
#ifdef _WIN32
            created = CreateDirectoryW(pathname, NULL);
#else
            created = (mkdir(pathname, mode) == 0
                       || ( (mkdir_error = errno) == EEXIST
                       && uv_fs_stat(NULL, &req, pathname, NULL) == 0
                       && S_ISDIR(req.statbuf.st_mode)));
#endif
            if (!(*p = ch)) break;
        }

#ifndef _WIN32
    errno = mkdir_error; /* error reporting should use errno of latest mkdir */
#endif
    if (!created) return -1;

    return 0;
}

/* Create a directory recursively. */
void MVM_dir_mkdir(MVMThreadContext *tc, MVMString *path, MVMint64 mode) {
    char * const pathname = MVM_string_utf8_c8_encode_C_string(tc, path);

#ifdef _WIN32
    /* Must using UTF8ToUnicode for supporting CJK Windows file name. */
    wchar_t *wpathname = UTF8ToUnicode(pathname);
    int str_len = wcslen(wpathname);
    MVM_free(pathname);

    if (str_len > MAX_PATH) {
        wchar_t  abs_dirname[4096]; /* 4096 should be enough for absolute path */
        wchar_t *lpp_part;

        /* You cannot use the "\\?\" prefix with a relative path,
         * relative paths are always limited to a total of MAX_PATH characters.
         * see http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx */
        if (!GetFullPathNameW(wpathname, 4096, abs_dirname, &lpp_part)) {
            MVM_free(wpathname);
            MVM_exception_throw_adhoc(tc, "Directory path is wrong: %lu", GetLastError());
        }

        MVM_free(wpathname);

        str_len  = wcslen(abs_dirname);
        wpathname = (wchar_t *)MVM_malloc((str_len + 4) * sizeof(wchar_t));
        wcscpy(wpathname, L"\\\\?\\");
        wcscat(wpathname, abs_dirname);
    }

    if (mkdir_p(tc, wpathname, mode) == -1) {
        DWORD error = GetLastError();
        if (error != ERROR_ALREADY_EXISTS) {
            MVM_free(wpathname);
            MVM_exception_throw_adhoc(tc, "Failed to mkdir: %lu", error);
        }
    }
    MVM_free(wpathname);
#else

    if (mkdir_p(tc, pathname, mode) == -1) {
        int mkdir_error = errno;
        MVM_free(pathname);
        MVM_exception_throw_adhoc(tc, "Failed to mkdir: %s", strerror(mkdir_error));
    }

    MVM_free(pathname);
#endif
}

/* Remove a directory recursively. */
void MVM_dir_rmdir(MVMThreadContext *tc, MVMString *path) {
    char * const pathname = MVM_string_utf8_c8_encode_C_string(tc, path);
    uv_fs_t req;

    if(uv_fs_rmdir(NULL, &req, pathname, NULL) < 0 ) {
        MVM_free(pathname);
        MVM_exception_throw_adhoc(tc, "Failed to rmdir: %s", uv_strerror(req.result));
    }

    MVM_free(pathname);
}

/* Get the current working directory. */
MVMString * MVM_dir_cwd(MVMThreadContext *tc) {
#ifdef _WIN32
    char path[MAX_PATH];
    size_t max_path = MAX_PATH;
    int r;
#else
    char path[PATH_MAX];
    size_t max_path = PATH_MAX;
    int r;
#endif

    if ((r = uv_cwd(path, (size_t *)&max_path)) < 0) {
        MVM_exception_throw_adhoc(tc, "Failed to determine cwd: %s", uv_strerror(r));
    }

    return MVM_string_utf8_c8_decode(tc, tc->instance->VMString, path, strlen(path));
}
int MVM_dir_chdir_C_string(MVMThreadContext *tc, const char *dirstring) {
    return uv_chdir(dirstring);
}
/* Change directory. */
void MVM_dir_chdir(MVMThreadContext *tc, MVMString *dir) {
    const char *dirstring = MVM_string_utf8_c8_encode_C_string(tc, dir);
    int chdir_error = MVM_dir_chdir_C_string(tc, dirstring);
    MVM_free((void*)dirstring);
    if (chdir_error) {
        MVM_exception_throw_adhoc(tc, "chdir failed: %s", uv_strerror(chdir_error));
    }
}

/* Structure to keep track of directory iteration state. */
typedef struct {
#ifdef _WIN32
    wchar_t *dir_name;
    HANDLE   dir_handle;
#else
    DIR     *dir_handle;
#endif
} MVMIODirIter;

/* Frees data associated with the directory handle. */
static void gc_free(MVMThreadContext *tc, MVMObject *h, void *d) {
    MVMIODirIter *data = (MVMIODirIter *)d;
    if (data) {
#ifdef _WIN32
        if (data->dir_name)
            MVM_free(data->dir_name);

        if (data->dir_handle)
            FindClose(data->dir_handle);
#else
        if (data->dir_handle)
            closedir(data->dir_handle);
#endif
        MVM_free(data);
    }
}

/* Ops table for directory iterator; it all works off special ops, so no entries. */
static const MVMIOOps op_table = {
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    gc_free
};

/* Open a filehandle, returning a handle. */
MVMObject * MVM_dir_open(MVMThreadContext *tc, MVMString *dirname) {
    MVMOSHandle  * result;
    MVMIODirIter * const data   = MVM_calloc(1, sizeof(MVMIODirIter));
    MVMROOT(tc, dirname, {
        result = (MVMOSHandle *)MVM_repr_alloc_init(tc, tc->instance->boot_types.BOOTIO);
    });
#ifdef _WIN32
    {
        char *name;
        int str_len;
        wchar_t *wname;
        wchar_t *dir_name;

        name  = MVM_string_utf8_c8_encode_C_string(tc, dirname);
        wname = UTF8ToUnicode(name);
        MVM_free(name);

        str_len = wcslen(wname);

        if (str_len > MAX_PATH - 2) { // the length of later appended '\*' is 2
            wchar_t  abs_dirname[4096]; /* 4096 should be enough for absolute path */
            wchar_t *lpp_part;

            /* You cannot use the "\\?\" prefix with a relative path,
             * relative paths are always limited to a total of MAX_PATH characters.
             * see http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx */
            if (!GetFullPathNameW(wname, 4096, abs_dirname, &lpp_part)) {
                MVM_free(data);
                MVM_free(wname);
                MVM_exception_throw_adhoc(tc, "Directory path is wrong: %lu", GetLastError());
            }
            MVM_free(wname);

            str_len  = wcslen(abs_dirname);
            dir_name = (wchar_t *)MVM_malloc((str_len + 7) * sizeof(wchar_t));
            wcscpy(dir_name, L"\\\\?\\");
            wcscat(dir_name, abs_dirname);
        } else {
            dir_name = (wchar_t *)MVM_malloc((str_len + 3) * sizeof(wchar_t));
            wcscpy(dir_name, wname);
            MVM_free(wname);
        }

        wcscat(dir_name, L"\\*");     /* Three characters are for the "\*" plus NULL appended.
                                       * see http://msdn.microsoft.com/en-us/library/windows/desktop/aa365200%28v=vs.85%29.aspx */

        data->dir_name   = dir_name;
        data->dir_handle = INVALID_HANDLE_VALUE;
    }

#else
    {
        char * const dir_name = MVM_string_utf8_c8_encode_C_string(tc, dirname);
        DIR * const dir_handle = opendir(dir_name);
        int opendir_error = errno;
        MVM_free(dir_name);

        if (!dir_handle) {
            MVM_free(data);
            MVM_exception_throw_adhoc(tc, "Failed to open dir: %s", strerror(opendir_error));
        }

        data->dir_handle = dir_handle;
    }
#endif

    result->body.ops  = &op_table;
    result->body.data = data;

    return (MVMObject *)result;
}

/* Casts to a handle, checking it's a directory handle along the way. */
static MVMOSHandle * get_dirhandle(MVMThreadContext *tc, MVMObject *oshandle, const char *msg) {
    MVMOSHandle *handle = (MVMOSHandle *)oshandle;
    if (REPR(oshandle)->ID != MVM_REPR_ID_MVMOSHandle)
        MVM_exception_throw_adhoc(tc, "%s requires an object with REPR MVMOSHandle (got %s with REPR %s)", msg, MVM_6model_get_debug_name(tc, (MVMObject *)handle), REPR(handle)->name);
    if (handle->body.ops != &op_table)
        MVM_exception_throw_adhoc(tc, "%s got incorrect kind of handle", msg);
    return handle;
}

/* Reads a directory entry from a directory. */
MVMString * MVM_dir_read(MVMThreadContext *tc, MVMObject *oshandle) {
    MVMOSHandle  *handle = get_dirhandle(tc, oshandle, "readdir");
    MVMIODirIter *data   = (MVMIODirIter *)handle->body.data;
#ifdef _WIN32
    MVMString *result;
    WIN32_FIND_DATAW ffd;
    char *dir_str;

    if (!data->dir_handle) {
        MVM_exception_throw_adhoc(tc, "Cannot read a closed dir handle.");
    }
    else if (data->dir_handle == INVALID_HANDLE_VALUE) {
        HANDLE hFind = FindFirstFileW(data->dir_name, &ffd);

        if (hFind == INVALID_HANDLE_VALUE) {
            MVM_exception_throw_adhoc(tc, "read from dirhandle failed: %lu", GetLastError());
        }

        data->dir_handle = hFind;
        dir_str = UnicodeToUTF8(ffd.cFileName);
        result = MVM_string_utf8_c8_decode(tc, tc->instance->VMString, dir_str, strlen(dir_str));
        MVM_free(dir_str);
        return result;
    }
    else if (FindNextFileW(data->dir_handle, &ffd) != 0)  {
        dir_str = UnicodeToUTF8(ffd.cFileName);
        result  = MVM_string_decode(tc, tc->instance->VMString, dir_str, strlen(dir_str),
                                    MVM_encoding_type_utf8_c8);
        MVM_free(dir_str);
        return result;
    } else {
        return tc->instance->str_consts.empty;
    }
#else

    struct dirent *entry;
    errno = 0; /* must reset errno so we won't check old errno */

    if (!data->dir_handle) {
        MVM_exception_throw_adhoc(tc, "Cannot read a closed dir handle.");
    }

    entry = readdir(data->dir_handle);

    if (errno == 0) {
        MVMString *ret = (entry == NULL)
                       ? tc->instance->str_consts.empty
                       : MVM_string_decode(tc, tc->instance->VMString, entry->d_name,
                               strlen(entry->d_name), MVM_encoding_type_utf8_c8);
        return ret;
    }

    MVM_exception_throw_adhoc(tc, "Failed to read dirhandle: %s", strerror(errno));
#endif
}

void MVM_dir_close(MVMThreadContext *tc, MVMObject *oshandle) {
    MVMOSHandle  *handle = get_dirhandle(tc, oshandle, "readdir");
    MVMIODirIter *data   = (MVMIODirIter *)handle->body.data;

#ifdef _WIN32
    if (data->dir_name) {
        MVM_free_null(data->dir_name);
    }

    if (data->dir_handle != INVALID_HANDLE_VALUE) {
        if (!FindClose(data->dir_handle))
            MVM_exception_throw_adhoc(tc, "Failed to close dirhandle: %lu", GetLastError());
    }
    data->dir_handle = NULL;
#else
    if (closedir(data->dir_handle) == -1)
        MVM_exception_throw_adhoc(tc, "Failed to close dirhandle: %s", strerror(errno));
    data->dir_handle = NULL;
#endif
}
