/* mz_os_win32.c -- System functions for Windows
   part of the minizip-ng project

   Copyright (C) Nathan Moinvaziri
     https://github.com/zlib-ng/minizip-ng

   This program is distributed under the terms of the same license as zlib.
   See the accompanying LICENSE file for the full text of the license.
*/

#include "mz.h"
#include "mz_os.h"
#include "mz_strm_os.h"

#include <windows.h>
#include <winioctl.h>

/***************************************************************************/

#ifndef SYMBOLIC_LINK_FLAG_DIRECTORY
#  define SYMBOLIC_LINK_FLAG_DIRECTORY 0x1
#endif

#ifndef _WIN32_WINNT_WIN8
#  define _WIN32_WINNT_WIN8 0x0602
#endif

/***************************************************************************/

typedef struct DIR_int_s {
    void *find_handle;
    WIN32_FIND_DATAW find_data;
    struct dirent entry;
    uint8_t end;
} DIR_int;

/***************************************************************************/

wchar_t *mz_os_unicode_string_create(const char *string, int32_t encoding) {
    wchar_t *string_wide = NULL;
    uint32_t string_wide_size = 0;

    string_wide_size = MultiByteToWideChar(encoding, 0, string, -1, NULL, 0);
    if (string_wide_size == 0)
        return NULL;
    string_wide = (wchar_t *)calloc(string_wide_size + 1, sizeof(wchar_t));
    if (!string_wide)
        return NULL;

    MultiByteToWideChar(encoding, 0, string, -1, string_wide, string_wide_size);
    return string_wide;
}

void mz_os_unicode_string_delete(wchar_t **string) {
    if (string) {
        free(*string);
        *string = NULL;
    }
}

char *mz_os_utf8_string_create(const char *string, int32_t encoding) {
    wchar_t *string_wide = NULL;
    char *string_utf8 = NULL;
    uint32_t string_utf8_size = 0;

    string_wide = mz_os_unicode_string_create(string, encoding);
    if (string_wide) {
        string_utf8_size = WideCharToMultiByte(CP_UTF8, 0, string_wide, -1, NULL, 0, NULL, NULL);
        string_utf8 = (char *)calloc(string_utf8_size + 1, sizeof(char));

        if (string_utf8)
            WideCharToMultiByte(CP_UTF8, 0, string_wide, -1, string_utf8, string_utf8_size, NULL, NULL);

        mz_os_unicode_string_delete(&string_wide);
    }

    return string_utf8;
}

char *mz_os_utf8_string_create_from_unicode(const wchar_t *string, int32_t encoding) {
    char *string_utf8 = NULL;
    uint32_t string_utf8_size = 0;

    MZ_UNUSED(encoding);

    string_utf8_size = WideCharToMultiByte(CP_UTF8, 0, string, -1, NULL, 0, NULL, NULL);
    string_utf8 = (char *)calloc(string_utf8_size + 1, sizeof(char));

    if (string_utf8)
        WideCharToMultiByte(CP_UTF8, 0, string, -1, string_utf8, string_utf8_size, NULL, NULL);

    return string_utf8;
}

void mz_os_utf8_string_delete(char **string) {
    if (string) {
        free(*string);
        *string = NULL;
    }
}

/***************************************************************************/

int32_t mz_os_rand(uint8_t *buf, int32_t size) {
    unsigned __int64 pentium_tsc[1];
    int32_t len = 0;

    for (len = 0; len < (int)size; len += 1) {
        if (len % 8 == 0)
            QueryPerformanceCounter((LARGE_INTEGER *)pentium_tsc);
        buf[len] = ((unsigned char *)pentium_tsc)[len % 8];
    }

    return len;
}

int32_t mz_os_rename(const char *source_path, const char *target_path) {
    wchar_t *source_path_wide = NULL;
    wchar_t *target_path_wide = NULL;
    int32_t result = 0;
    int32_t err = MZ_OK;

    if (!source_path || !target_path)
        return MZ_PARAM_ERROR;

    source_path_wide = mz_os_unicode_string_create(source_path, MZ_ENCODING_UTF8);
    if (!source_path_wide) {
        err = MZ_PARAM_ERROR;
    } else {
        target_path_wide = mz_os_unicode_string_create(target_path, MZ_ENCODING_UTF8);
        if (!target_path_wide)
            err = MZ_PARAM_ERROR;
    }

    if (err == MZ_OK) {
#if _WIN32_WINNT >= _WIN32_WINNT_WINXP
        result = MoveFileExW(source_path_wide, target_path_wide, MOVEFILE_WRITE_THROUGH);
#else
        result = MoveFileW(source_path_wide, target_path_wide);
#endif
        if (result == 0)
            err = MZ_EXIST_ERROR;
    }

    if (target_path_wide)
        mz_os_unicode_string_delete(&target_path_wide);
    if (source_path_wide)
        mz_os_unicode_string_delete(&source_path_wide);

    return err;
}

int32_t mz_os_unlink(const char *path) {
    wchar_t *path_wide = NULL;
    int32_t result = 0;

    if (!path)
        return MZ_PARAM_ERROR;
    path_wide = mz_os_unicode_string_create(path, MZ_ENCODING_UTF8);
    if (!path_wide)
        return MZ_PARAM_ERROR;

    if (mz_os_is_dir(path) == MZ_OK)
        result = RemoveDirectoryW(path_wide);
    else
        result = DeleteFileW(path_wide);

    mz_os_unicode_string_delete(&path_wide);

    if (result == 0)
        return MZ_EXIST_ERROR;

    return MZ_OK;
}

int32_t mz_os_file_exists(const char *path) {
    wchar_t *path_wide = NULL;
    DWORD attribs = 0;

    if (!path)
        return MZ_PARAM_ERROR;
    path_wide = mz_os_unicode_string_create(path, MZ_ENCODING_UTF8);
    if (!path_wide)
        return MZ_PARAM_ERROR;

    attribs = GetFileAttributesW(path_wide);
    mz_os_unicode_string_delete(&path_wide);

    if (attribs == 0xFFFFFFFF)
        return MZ_EXIST_ERROR;

    return MZ_OK;
}

int64_t mz_os_get_file_size(const char *path) {
    HANDLE handle = NULL;
    LARGE_INTEGER large_size;
    wchar_t *path_wide = NULL;

    if (!path)
        return MZ_PARAM_ERROR;
    path_wide = mz_os_unicode_string_create(path, MZ_ENCODING_UTF8);
    if (!path_wide)
        return MZ_PARAM_ERROR;
#if _WIN32_WINNT >= _WIN32_WINNT_WIN8
    handle = CreateFile2(path_wide, GENERIC_READ, 0, OPEN_EXISTING, NULL);
#else
    handle = CreateFileW(path_wide, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
#endif
    mz_os_unicode_string_delete(&path_wide);

    large_size.QuadPart = 0;

    if (handle != INVALID_HANDLE_VALUE) {
        GetFileSizeEx(handle, &large_size);
        CloseHandle(handle);
    }

    return large_size.QuadPart;
}

static void mz_os_file_to_unix_time(FILETIME file_time, time_t *unix_time) {
    uint64_t quad_file_time = 0;
    quad_file_time = file_time.dwLowDateTime;
    quad_file_time |= ((uint64_t)file_time.dwHighDateTime << 32);
    *unix_time = (time_t)((quad_file_time - 116444736000000000LL) / 10000000);
}

static void mz_os_unix_to_file_time(time_t unix_time, FILETIME *file_time) {
    uint64_t quad_file_time = 0;
    quad_file_time = ((uint64_t)unix_time * 10000000) + 116444736000000000LL;
    file_time->dwHighDateTime = (quad_file_time >> 32);
    file_time->dwLowDateTime = (uint32_t)(quad_file_time);
}

int32_t mz_os_get_file_date(const char *path, time_t *modified_date, time_t *accessed_date, time_t *creation_date) {
    WIN32_FILE_ATTRIBUTE_DATA wfad;
    wchar_t *path_wide = NULL;
    int32_t err = MZ_INTERNAL_ERROR;

    if (!path)
        return MZ_PARAM_ERROR;
    path_wide = mz_os_unicode_string_create(path, MZ_ENCODING_UTF8);
    if (!path_wide)
        return MZ_PARAM_ERROR;

    if (GetFileAttributesExW(path_wide, GetFileExInfoStandard, &wfad)) {
        if (modified_date)
            mz_os_file_to_unix_time(wfad.ftLastWriteTime, modified_date);
        if (accessed_date)
            mz_os_file_to_unix_time(wfad.ftLastAccessTime, accessed_date);
        if (creation_date)
            mz_os_file_to_unix_time(wfad.ftCreationTime, creation_date);

        err = MZ_OK;
    }

    free(path_wide);

    return err;
}

int32_t mz_os_set_file_date(const char *path, time_t modified_date, time_t accessed_date, time_t creation_date) {
    HANDLE handle = NULL;
    FILETIME ftm_creation, ftm_accessed, ftm_modified;
    wchar_t *path_wide = NULL;
    int32_t err = MZ_OK;

    if (!path)
        return MZ_PARAM_ERROR;
    path_wide = mz_os_unicode_string_create(path, MZ_ENCODING_UTF8);
    if (!path_wide)
        return MZ_PARAM_ERROR;

#if _WIN32_WINNT >= _WIN32_WINNT_WIN8
    handle = CreateFile2(path_wide, GENERIC_READ | GENERIC_WRITE, 0, OPEN_EXISTING, NULL);
#else
    handle = CreateFileW(path_wide, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
#endif
    mz_os_unicode_string_delete(&path_wide);

    if (handle != INVALID_HANDLE_VALUE) {
        GetFileTime(handle, &ftm_creation, &ftm_accessed, &ftm_modified);

        if (modified_date != 0)
            mz_os_unix_to_file_time(modified_date, &ftm_modified);
        if (accessed_date != 0)
            mz_os_unix_to_file_time(accessed_date, &ftm_accessed);
        if (creation_date != 0)
            mz_os_unix_to_file_time(creation_date, &ftm_creation);

        if (SetFileTime(handle, &ftm_creation, &ftm_accessed, &ftm_modified) == 0)
            err = MZ_INTERNAL_ERROR;

        CloseHandle(handle);
    }

    return err;
}

int32_t mz_os_get_file_attribs(const char *path, uint32_t *attributes) {
    wchar_t *path_wide = NULL;
    int32_t err = MZ_OK;

    if (!path || !attributes)
        return MZ_PARAM_ERROR;
    path_wide = mz_os_unicode_string_create(path, MZ_ENCODING_UTF8);
    if (!path_wide)
        return MZ_PARAM_ERROR;

    *attributes = GetFileAttributesW(path_wide);
    mz_os_unicode_string_delete(&path_wide);

    if (*attributes == INVALID_FILE_ATTRIBUTES)
        err = MZ_INTERNAL_ERROR;

    return err;
}

int32_t mz_os_set_file_attribs(const char *path, uint32_t attributes) {
    wchar_t *path_wide = NULL;
    int32_t err = MZ_OK;

    if (!path)
        return MZ_PARAM_ERROR;
    path_wide = mz_os_unicode_string_create(path, MZ_ENCODING_UTF8);
    if (!path_wide)
        return MZ_PARAM_ERROR;

    if (SetFileAttributesW(path_wide, attributes) == 0)
        err = MZ_INTERNAL_ERROR;
    mz_os_unicode_string_delete(&path_wide);

    return err;
}

int32_t mz_os_make_dir(const char *path) {
    wchar_t *path_wide = NULL;
    int32_t err = MZ_OK;

    if (!path)
        return MZ_PARAM_ERROR;

    /* Don't try to create a drive letter */
    if ((path[0] != 0) && (strlen(path) <= 3) && (path[1] == ':'))
        return mz_os_is_dir(path);

    path_wide = mz_os_unicode_string_create(path, MZ_ENCODING_UTF8);
    if (!path_wide)
        return MZ_PARAM_ERROR;

    if (CreateDirectoryW(path_wide, NULL) == 0) {
        if (GetLastError() != ERROR_ALREADY_EXISTS)
            err = MZ_INTERNAL_ERROR;
    }

    mz_os_unicode_string_delete(&path_wide);

    return err;
}

DIR *mz_os_open_dir(const char *path) {
    WIN32_FIND_DATAW find_data;
    DIR_int *dir_int = NULL;
    wchar_t *path_wide = NULL;
    char fixed_path[320];
    void *handle = NULL;

    if (!path)
        return NULL;

    strncpy(fixed_path, path, sizeof(fixed_path) - 1);
    fixed_path[sizeof(fixed_path) - 1] = 0;

    mz_path_append_slash(fixed_path, sizeof(fixed_path), MZ_PATH_SLASH_PLATFORM);
    mz_path_combine(fixed_path, "*", sizeof(fixed_path));

    path_wide = mz_os_unicode_string_create(fixed_path, MZ_ENCODING_UTF8);
    if (!path_wide)
        return NULL;

    handle = FindFirstFileW(path_wide, &find_data);
    mz_os_unicode_string_delete(&path_wide);

    if (handle == INVALID_HANDLE_VALUE)
        return NULL;

    dir_int = (DIR_int *)malloc(sizeof(DIR_int));
    if (!dir_int) {
        FindClose(handle);
        return NULL;
    }
    dir_int->find_handle = handle;
    dir_int->end = 0;

    memcpy(&dir_int->find_data, &find_data, sizeof(dir_int->find_data));

    return (DIR *)dir_int;
}

struct dirent *mz_os_read_dir(DIR *dir) {
    DIR_int *dir_int;

    if (!dir)
        return NULL;

    dir_int = (DIR_int *)dir;
    if (dir_int->end)
        return NULL;

    WideCharToMultiByte(CP_UTF8, 0, dir_int->find_data.cFileName, -1, dir_int->entry.d_name,
                        sizeof(dir_int->entry.d_name), NULL, NULL);

    if (FindNextFileW(dir_int->find_handle, &dir_int->find_data) == 0) {
        if (GetLastError() != ERROR_NO_MORE_FILES)
            return NULL;

        dir_int->end = 1;
    }

    return &dir_int->entry;
}

int32_t mz_os_close_dir(DIR *dir) {
    DIR_int *dir_int;

    if (!dir)
        return MZ_PARAM_ERROR;

    dir_int = (DIR_int *)dir;
    if (dir_int->find_handle != INVALID_HANDLE_VALUE)
        FindClose(dir_int->find_handle);
    free(dir_int);
    return MZ_OK;
}

int32_t mz_os_is_dir(const char *path) {
    wchar_t *path_wide = NULL;
    uint32_t attribs = 0;

    if (!path)
        return MZ_PARAM_ERROR;
    path_wide = mz_os_unicode_string_create(path, MZ_ENCODING_UTF8);
    if (!path_wide)
        return MZ_PARAM_ERROR;

    attribs = GetFileAttributesW(path_wide);
    mz_os_unicode_string_delete(&path_wide);

    if (attribs != 0xFFFFFFFF) {
        if (attribs & FILE_ATTRIBUTE_DIRECTORY)
            return MZ_OK;
    }

    return MZ_EXIST_ERROR;
}

int32_t mz_os_is_symlink(const char *path) {
    wchar_t *path_wide = NULL;
    uint32_t attribs = 0;

    if (!path)
        return MZ_PARAM_ERROR;
    path_wide = mz_os_unicode_string_create(path, MZ_ENCODING_UTF8);
    if (!path_wide)
        return MZ_PARAM_ERROR;

    attribs = GetFileAttributesW(path_wide);
    mz_os_unicode_string_delete(&path_wide);

    if (attribs != 0xFFFFFFFF) {
        if (attribs & FILE_ATTRIBUTE_REPARSE_POINT)
            return MZ_OK;
    }

    return MZ_EXIST_ERROR;
}

int32_t mz_os_make_symlink(const char *path, const char *target_path) {
    typedef BOOLEAN(WINAPI * LPCREATESYMBOLICLINKW)(LPCWSTR, LPCWSTR, DWORD);
    MEMORY_BASIC_INFORMATION mbi;
    LPCREATESYMBOLICLINKW create_symbolic_link_w = NULL;
    HMODULE kernel32_mod = NULL;
    wchar_t *path_wide = NULL;
    wchar_t *target_path_wide = NULL;
    int32_t err = MZ_OK;
    int32_t flags = 0;

    if (!path)
        return MZ_PARAM_ERROR;

    /* Use VirtualQuery instead of GetModuleHandleW for UWP */
    memset(&mbi, 0, sizeof(mbi));
    VirtualQuery(VirtualQuery, &mbi, sizeof(mbi));
    kernel32_mod = (HMODULE)mbi.AllocationBase;

    if (!kernel32_mod)
        return MZ_SUPPORT_ERROR;

    create_symbolic_link_w = (LPCREATESYMBOLICLINKW)GetProcAddress(kernel32_mod, "CreateSymbolicLinkW");
    if (!create_symbolic_link_w) {
        return MZ_SUPPORT_ERROR;
    }

    path_wide = mz_os_unicode_string_create(path, MZ_ENCODING_UTF8);
    if (!path_wide) {
        return MZ_PARAM_ERROR;
    }

    target_path_wide = mz_os_unicode_string_create(target_path, MZ_ENCODING_UTF8);
    if (target_path_wide) {
        if (mz_path_has_slash(target_path) == MZ_OK)
            flags |= SYMBOLIC_LINK_FLAG_DIRECTORY;

        if (create_symbolic_link_w(path_wide, target_path_wide, flags) == FALSE)
            err = MZ_SYMLINK_ERROR;

        mz_os_unicode_string_delete(&target_path_wide);
    } else {
        err = MZ_PARAM_ERROR;
    }

    mz_os_unicode_string_delete(&path_wide);

    return err;
}

int32_t mz_os_read_symlink(const char *path, char *target_path, int32_t max_target_path) {
    typedef struct _REPARSE_DATA_BUFFER {
        ULONG ReparseTag;
        USHORT ReparseDataLength;
        USHORT Reserved;
        union {
            struct {
                USHORT SubstituteNameOffset;
                USHORT SubstituteNameLength;
                USHORT PrintNameOffset;
                USHORT PrintNameLength;
                ULONG Flags;
                WCHAR PathBuffer[1];
            } SymbolicLinkReparseBuffer;
            struct {
                USHORT SubstituteNameOffset;
                USHORT SubstituteNameLength;
                USHORT PrintNameOffset;
                USHORT PrintNameLength;
                WCHAR PathBuffer[1];
            } MountPointReparseBuffer;
            struct {
                UCHAR DataBuffer[1];
            } GenericReparseBuffer;
        };
    } REPARSE_DATA_BUFFER;
    REPARSE_DATA_BUFFER *reparse_data = NULL;
    DWORD length = 0;
    HANDLE handle = NULL;
    wchar_t *path_wide = NULL;
    wchar_t *target_path_wide = NULL;
    uint8_t buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
    int32_t target_path_len = 0;
    int32_t target_path_idx = 0;
    int32_t err = MZ_OK;
    char *target_path_utf8 = NULL;

    if (!path)
        return MZ_PARAM_ERROR;
    path_wide = mz_os_unicode_string_create(path, MZ_ENCODING_UTF8);
    if (!path_wide)
        return MZ_PARAM_ERROR;

#if _WIN32_WINNT >= _WIN32_WINNT_WIN8
    CREATEFILE2_EXTENDED_PARAMETERS extended_params;
    memset(&extended_params, 0, sizeof(extended_params));
    extended_params.dwSize = sizeof(extended_params);
    extended_params.dwFileAttributes = FILE_ATTRIBUTE_NORMAL;
    extended_params.dwFileFlags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT;
    extended_params.dwSecurityQosFlags = SECURITY_ANONYMOUS;
    extended_params.lpSecurityAttributes = NULL;
    extended_params.hTemplateFile = NULL;
    handle = CreateFile2(path_wide, FILE_READ_EA, FILE_SHARE_READ | FILE_SHARE_WRITE, OPEN_EXISTING, &extended_params);
#else
    handle = CreateFileW(path_wide, FILE_READ_EA, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
                         FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
#endif

    if (handle == INVALID_HANDLE_VALUE) {
        mz_os_unicode_string_delete(&path_wide);
        return MZ_OPEN_ERROR;
    }

    if (DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0, buffer, sizeof(buffer), &length, NULL) == TRUE) {
        reparse_data = (REPARSE_DATA_BUFFER *)buffer;
        if ((IsReparseTagMicrosoft(reparse_data->ReparseTag)) && (reparse_data->ReparseTag == IO_REPARSE_TAG_SYMLINK)) {
            target_path_len = max_target_path * sizeof(wchar_t);
            if (target_path_len > reparse_data->SymbolicLinkReparseBuffer.PrintNameLength)
                target_path_len = reparse_data->SymbolicLinkReparseBuffer.PrintNameLength;

            target_path_wide = (wchar_t *)malloc(target_path_len + sizeof(wchar_t));
            if (target_path_wide) {
                target_path_idx = reparse_data->SymbolicLinkReparseBuffer.PrintNameOffset / sizeof(wchar_t);
                memcpy(target_path_wide, &reparse_data->SymbolicLinkReparseBuffer.PathBuffer[target_path_idx],
                       target_path_len);

                target_path_wide[target_path_len / sizeof(wchar_t)] = 0;
                target_path_utf8 = mz_os_utf8_string_create_from_unicode(target_path_wide, MZ_ENCODING_UTF8);

                if (target_path_utf8) {
                    strncpy(target_path, target_path_utf8, max_target_path - 1);
                    target_path[max_target_path - 1] = 0;
                    /* Ensure directories have slash at the end so we can recreate them later */
                    if (mz_os_is_dir(target_path_utf8) == MZ_OK)
                        mz_path_append_slash(target_path, max_target_path, MZ_PATH_SLASH_PLATFORM);
                    mz_os_utf8_string_delete(&target_path_utf8);
                } else {
                    err = MZ_MEM_ERROR;
                }

                free(target_path_wide);
            } else {
                err = MZ_MEM_ERROR;
            }
        }
    } else {
        err = MZ_INTERNAL_ERROR;
    }

    CloseHandle(handle);
    mz_os_unicode_string_delete(&path_wide);
    return err;
}

uint64_t mz_os_ms_time(void) {
    SYSTEMTIME system_time;
    FILETIME file_time;
    uint64_t quad_file_time = 0;

    GetSystemTime(&system_time);
    SystemTimeToFileTime(&system_time, &file_time);

    quad_file_time = file_time.dwLowDateTime;
    quad_file_time |= ((uint64_t)file_time.dwHighDateTime << 32);

    return quad_file_time / 10000 - 11644473600000LL;
}
