/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2014 by 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 <errno.h>
#include <ctype.h>
#include <stdlib.h>
#include "config.h"
#include "system.h"
#include "file.h"
#include "dir.h"
#include "debug.h"
#include "pathfuncs.h"
#include "string-extra.h"
#include "mv.h"

#define SAME_FILE_INFO(lpInfo1, lpInfo2) \
    ((lpInfo1)->dwVolumeSerialNumber == (lpInfo2)->dwVolumeSerialNumber && \
     (lpInfo1)->nFileIndexHigh == (lpInfo2)->nFileIndexHigh &&             \
     (lpInfo1)->nFileIndexLow == (lpInfo2)->nFileIndexLow)

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

static void win32_last_error_errno(void)
{
    switch (GetLastError())
    {
    case ERROR_FILE_NOT_FOUND:
    case ERROR_PATH_NOT_FOUND:
        errno = ENOENT;
        break;
    case ERROR_DIR_NOT_EMPTY:
        errno = ENOTEMPTY;
        break;
    default:
        errno = EIO;
    }
}

#ifdef __MINGW32__
#include <wchar.h>
#include "rbunicode.h"

static HANDLE win32_open(const char *ospath);
static int win32_stat(const char *ospath, LPBY_HANDLE_FILE_INFORMATION lpInfo);

static unsigned short * strcpy_utf8utf16(unsigned short *buffer,
                                         const unsigned char *utf8)
{
    ucschar_t cp = 0xffffffff;
    for (wchar_t *ucs = buffer; cp ; ucs++) {
        utf8 = utf8decode(utf8, &cp);
#ifdef UNICODE32
        if (cp > 0x10000) {
            cp -= 0x10000;
            *ucs++ = 0xd800 | (cp >> 10);
            cp = 0xdc00 | (cp & 0x3ff);
        }
#endif
        *ucs = cp;
    }
    return buffer;
}

#if 0 /* Unused in current code */
static unsigned char * strcpy_utf16utf8(unsigned char *buffer,
                                        const unsigned short *utf16buf)
{
    unsigned char *utf8 = buffer;

    /* windows is always LE */
    const int le = 1;

    while (*utf16buf) {
        const unsigned char *utf16 = (const unsigned char *)utf16buf;
        unsigned long ucs;
        /* Check for a surrogate pair */
        if (*(utf16 + le) >= 0xD8 && *(utf16 + le) < 0xE0) {
            ucs = 0x10000 + ((utf16[1 - le] << 10) | ((utf16[le] - 0xD8) << 18)
                  | utf16[2 + (1 - le)] | ((utf16[2 + le] - 0xDC) << 8));
            utf16buf += 2;
        } else {
            ucs = utf16[le] << 8 | utf16[1 - le];
            utf16buf++;
        }
        utf8 = utf8encode(ucs, utf8);
    }
    return buffer;
}
static size_t strlen_utf16utf8(const unsigned short *utf16buf)
{
    size_t length = 0;
    unsigned char utf8char[4];

    /* windows is always LE */
    const int le = 1;

    while (*utf16buf) {
        const unsigned char *utf16 = (const unsigned char *)utf16buf;
        unsigned long ucs;
        /* Check for a surrogate pair */
        if (*(utf16 + le) >= 0xD8 && *(utf16 + le) < 0xE0) {
            ucs = 0x10000 + ((utf16[1 - le] << 10) | ((utf16[le] - 0xD8) << 18)
                  | utf16[2 + (1 - le)] | ((utf16[2 + le] - 0xDC) << 8));
            utf16buf += 2;
        } else {
            ucs = utf16[le] << 8 | utf16[1 - le];
            utf16buf++;
        }
        length += utf8encode(ucs, utf8char) - utf8char;
    }
    return length;
}
#endif

/* Note: Must be exported */
size_t strlcpy_utf16utf8(char *buffer, const unsigned short *utf16,
                         size_t bufsize)
{
    if (!buffer)
        bufsize = 0;

    size_t length = 0;
    unsigned char utf8char[4];

    unsigned long ucc;
    while(*utf16)
    {
        /* Check for a surrogate UTF16 pair */
        if (*utf16 >= 0xd800 && *utf16 < 0xdc00 &&
            *(utf16+1) >= 0xdc00 && *(utf16+1) < 0xe000) {
            ucc = 0x10000 + (((*utf16 & 0x3ff) << 10) | (*(utf16+1) & 0x3ff));
            utf16++;
        } else {
            ucc = *utf16;
        }

        /* If the last character won't fit, this won't split it */
        size_t utf8size = utf8encode(ucc, utf8char) - utf8char;
        if ((length += utf8size) < bufsize)
            buffer = mempcpy(buffer, utf8char, utf8size);

        utf16++;
    }

    /* Above won't ever copy to very end */
    if (bufsize)
        *buffer = '\0';

    return length;
}

#define _toutf16(utf8) \
    ({ const char *_utf8 = (utf8);         \
       size_t _l = utf16len_utf8(_utf8); \
       void *_buffer = alloca((_l + 1)*2); \
       strcpy_utf8utf16(_buffer, _utf8); })

#define _toutf8(utf16) \
    ({ const char *_ucs = (utf16);         \
       size_t _l = strlen_utf16utf8(_ucs); \
       void *_buffer = alloca(_l + 1);     \
       strcpy_utf16utf8(_buffer, _ucs); })

int os_open(const char *ospath, int oflag, ...)
{
    return _wopen(_toutf16(ospath), oflag __OPEN_MODE_ARG);
}

int os_creat(const char *ospath, mode_t mode)
{
    return _wcreat(_toutf16(ospath), mode);
}

int os_stat(const char *ospath, struct _stat *s)
{
    return _wstat(_toutf16(ospath), s);
}

int os_remove(const char *ospath)
{
    return _wremove(_toutf16(ospath));
}

int os_rename(const char *osold, const char *osnew)
{
    int errnum = errno;

    const wchar_t *wchosold = _toutf16(osold);
    const wchar_t *wchosnew = _toutf16(osnew);

    int rc = _wrename(wchosold, wchosnew);
    if (rc < 0 && errno == EEXIST)
    {
        /* That didn't work; do cheap POSIX mimic */
        BY_HANDLE_FILE_INFORMATION info;
        if (win32_stat(osold, &info))
            return -1;

        if ((info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
            !RemoveDirectoryW(wchosnew))
        {
            win32_last_error_errno();
            return -1;
        }

        if (MoveFileExW(wchosold, wchosnew, MOVEFILE_REPLACE_EXISTING |
                                            MOVEFILE_WRITE_THROUGH))
        {
            errno = errnum;
            return 0;
        }

        errno = EIO;
    }

    return rc;
}

bool os_file_exists(const char *ospath)
{
    HANDLE h = win32_open(ospath);
    if (h == INVALID_HANDLE_VALUE)
        return false;

    CloseHandle(h);
    return true;
}

_WDIR * os_opendir(const char *osdirname)
{
    return _wopendir(_toutf16(osdirname));
}

int os_mkdir(const char *ospath, mode_t mode)
{
    return _wmkdir(_toutf16(ospath));
    (void)mode;
}

int os_rmdir(const char *ospath)
{
    return _wrmdir(_toutf16(ospath));
}

int os_dirfd(_WDIR *osdirp)
{
#ifdef ENOTSUP
    errno = ENOTSUP;
#else
    errno = ENOSYS;
#endif
    return -1;
    (void)osdirp;
}

int os_opendirfd(const char *osdirname)
{
    HANDLE h = win32_open(osdirname);
    if (h == INVALID_HANDLE_VALUE)
        return -1;

    BY_HANDLE_FILE_INFORMATION info;
    if (!GetFileInformationByHandle(h, &info))
        errno = EIO;
    else if (!(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
        errno = ENOTDIR;
    else
    {
        /* Convert OS handle to fd; the fd now owns it */
        int osfd = _open_osfhandle((intptr_t)h, O_RDONLY);
        if (osfd >= 0)
            return osfd;
    }

    CloseHandle(h);
    return -2;
}
#endif /* __MINGW32__ */

static size_t win32_path_strip_root(const char *ospath)
{
    const char *p = ospath;
    int c = toupper(*p);

    if (c >= 'A' && c <= 'Z')
    {
        /* drive */
        if ((c = *++p) == ':')
            return 2;
    }

    if (c == '\\' && *++p == '\\')
    {
        /* UNC */
        while ((c = *++p) && c != '/' && c != '\\');
        return p - ospath;
    }

    return 0;
}

static HANDLE win32_open(const char *ospath)
{
    /* FILE_FLAG_BACKUP_SEMANTICS is required for this to succeed at opening
       a directory */
    HANDLE h = CreateFileW(_toutf16(ospath), GENERIC_READ,
                           FILE_SHARE_READ | FILE_SHARE_WRITE |
                           FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
                           FILE_FLAG_BACKUP_SEMANTICS, NULL);

    if (h == INVALID_HANDLE_VALUE)
        win32_last_error_errno();

    return h;
}

static int win32_fstat(int osfd, HANDLE hFile,
                       LPBY_HANDLE_FILE_INFORMATION lpInfo)
{
    /* The file descriptor takes precedence over the win32 file handle */
    if (osfd >= 0)
        hFile = (HANDLE)_get_osfhandle(osfd);

    int rc = GetFileInformationByHandle(hFile, lpInfo) ? 0 : -1;
    if (rc < 0)
        win32_last_error_errno();

    return rc;
}

static int win32_stat(const char *ospath, LPBY_HANDLE_FILE_INFORMATION lpInfo)
{
    HANDLE h = win32_open(ospath);
    if (h == INVALID_HANDLE_VALUE)
        return -1;

    int rc = win32_fstat(-1, h, lpInfo);

    CloseHandle(h);

    return rc;
}

int os_opendir_and_fd(const char *osdirname, _WDIR **osdirpp,
                      int *osfdp)
{
    /* another possible way is to use open() then fdopendir() */
    *osdirpp = NULL;
    *osfdp = -1;

    _WDIR *dirp = os_opendir(osdirname);
    if (!dirp)
        return -1;

    int rc = 0;
    int errnum = errno;

    int fd = os_dirfd(dirp);
    if (fd < 0)
    {
        fd = os_opendirfd(osdirname);
        rc = 1;
    }

    if (fd < 0)
    {
        os_closedir(dirp);
        return -2;
    }

    errno = errnum;

    *osdirpp = dirp;
    *osfdp = fd;

    return rc;
}

int os_fsamefile(int osfd1, int osfd2)
{
    BY_HANDLE_FILE_INFORMATION info1, info2;

    if (!win32_fstat(osfd1, INVALID_HANDLE_VALUE, &info1) ||
        !win32_fstat(osfd2, INVALID_HANDLE_VALUE, &info2))
        return -1;

    return SAME_FILE_INFO(&info1, &info2) ? 1 : 0;
}

int os_relate(const char *ospath1, const char *ospath2)
{
    DEBUGF("\"%s\" : \"%s\"\n", ospath1, ospath2);

    if (!ospath2 || !*ospath2)
    {
        errno = ospath2 ? ENOENT : EFAULT;
        return -1;
    }

    /* First file must stay open for duration so that its stats don't change */
    HANDLE h1 = win32_open(ospath1);
    if (h1 == INVALID_HANDLE_VALUE)
        return -2;

    BY_HANDLE_FILE_INFORMATION info1;
    if (win32_fstat(-1, h1, &info1))
    {
        CloseHandle(h1);
        return -3;
    }

    char path2buf[strlen(ospath2) + 1];
    *path2buf = 0;

    ssize_t len = 0;
    const char *p = ospath2;
    size_t rootlen = win32_path_strip_root(ospath2);
    const char *sepmo = PA_SEP_SOFT;

    if (rootlen)
    {
        strmemcpy(path2buf, ospath2, rootlen);
        ospath2 += rootlen;
        sepmo = PA_SEP_HARD;
    }

    int rc = RELATE_DIFFERENT;

    while (1)
    {
        if (sepmo != PA_SEP_HARD &&
            !(len = parse_path_component(&ospath2, &p)))
        {
            break;
        }

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

        path_append(path2buf, sepmo, compname, sizeof (path2buf));
        sepmo = PA_SEP_SOFT;

        int errnum = errno; /* save and restore if not actually failing */
        BY_HANDLE_FILE_INFORMATION info2;

        if (!win32_stat(path2buf, &info2))
        {
            if (SAME_FILE_INFO(&info1, &info2))
            {
                rc = RELATE_SAME;
            }
            else if (rc == RELATE_SAME)
            {
                if (name_is_dot_dot(compname))
                    rc = RELATE_DIFFERENT;
                else if (!name_is_dot(compname))
                    rc = RELATE_PREFIX;
            }
        }
        else if (errno == ENOENT && !*GOBBLE_PATH_SEPCH(ospath2) &&
                 !name_is_dot_dot(compname))
        {
            if (rc == RELATE_SAME)
                rc = RELATE_PREFIX;

            errno = errnum;
            break;
        }
        else
        {
            rc = -4;
            break;
        }
    }

    CloseHandle(h1);

    return rc;
}

int os_modtime(const char *path, time_t modtime)
{
    (void)path;
    (void)modtime;
    return 0;
}

int os_volume_path(IF_MV(int volume, ) char *buffer, size_t bufsize);

void volume_size(IF_MV(int volume,) sector_t *sizep, sector_t *freep)
{
    ULARGE_INTEGER free = { .QuadPart = 0 },
                   size = { .QuadPart = 0 };

    char volpath[MAX_PATH];
    if (os_volume_path(IF_MV(volume, ) volpath, sizeof (volpath)) >= 0)
        GetDiskFreeSpaceExW(_toutf16(volpath), &free, &size, NULL);

    if (sizep)
        *sizep = size.QuadPart / 1024;

    if (freep)
        *freep = free.QuadPart / 1024;
}
