/************************************************************/ /**
*
*	@file: win32_io.c
*	@author: Martin Fouilleul
*	@date: 25/05/2023
*
*****************************************************************/

#include <errno.h>
#include <limits.h>
#include <shlwapi.h>
#include <winioctl.h>

#include "platform_io_common.c"
#include "platform_io_internal.c"
#include "win32_string_helpers.h"

oc_io_error oc_io_raw_last_error()
{
    oc_io_error error = 0;

    int winError = GetLastError();
    switch(winError)
    {
        case ERROR_SUCCESS:
            error = OC_IO_OK;
            break;

        case ERROR_ACCESS_DENIED:
            error = OC_IO_ERR_PERM;
            break;

        case ERROR_FILE_NOT_FOUND:
        case ERROR_PATH_NOT_FOUND:
        case ERROR_INVALID_DRIVE:
        case ERROR_DIRECTORY:
            error = OC_IO_ERR_NO_ENTRY;
            break;

        case ERROR_TOO_MANY_OPEN_FILES:
            error = OC_IO_ERR_MAX_FILES;
            break;

        case ERROR_NOT_ENOUGH_MEMORY:
        case ERROR_OUTOFMEMORY:
            error = OC_IO_ERR_MEM;
            break;

        case ERROR_DEV_NOT_EXIST:
            error = OC_IO_ERR_NO_DEVICE;
            break;

        case ERROR_FILE_EXISTS:
        case ERROR_ALREADY_EXISTS:
            error = OC_IO_ERR_EXISTS;
            break;

        case ERROR_BUFFER_OVERFLOW:
        case ERROR_FILENAME_EXCED_RANGE:
            error = OC_IO_ERR_PATH_LENGTH;
            break;

        case ERROR_FILE_TOO_LARGE:
            error = OC_IO_ERR_FILE_SIZE;
            break;

            //TODO: complete

        default:
            error = OC_IO_ERR_UNKNOWN;
            break;
    }
    return (error);
}

static oc_str16 win32_path_from_handle_null_terminated(oc_arena* arena, HANDLE handle)
{
    oc_str16 res = { 0 };

    res.len = GetFinalPathNameByHandleW(handle, NULL, 0, FILE_NAME_NORMALIZED);
    if(res.len)
    {
        res.ptr = oc_arena_push_array(arena, u16, res.len);
        if(!GetFinalPathNameByHandleW(handle, res.ptr, res.len, FILE_NAME_NORMALIZED))
        {
            res.len = 0;
            res.ptr = 0;
        }
    }
    return (res);
}

typedef HANDLE oc_file_desc;

oc_file_desc oc_file_desc_nil()
{
    return (INVALID_HANDLE_VALUE);
}

bool oc_file_desc_is_nil(oc_file_desc fd)
{
    return (fd == NULL || fd == INVALID_HANDLE_VALUE);
}

static oc_str16 win32_get_path_at_null_terminated(oc_arena* arena, oc_file_desc dirFd, oc_str8 path)
{
    oc_str16 result = { 0 };
    oc_arena_scope scratch = oc_scratch_begin_next(arena);

    oc_str16 dirPathW = win32_path_from_handle_null_terminated(scratch.arena, dirFd);
    oc_str16 pathW = oc_win32_utf8_to_wide_null_terminated(scratch.arena, path);

    if(dirPathW.len && pathW.len)
    {
        u64 fullPathWSize = dirPathW.len + pathW.len;
        LPWSTR fullPathW = oc_arena_push_array(scratch.arena, u16, fullPathWSize);
        memcpy(fullPathW, dirPathW.ptr, (dirPathW.len - 1) * sizeof(u16));
        fullPathW[dirPathW.len - 1] = '\\';
        memcpy(fullPathW + dirPathW.len, pathW.ptr, pathW.len * sizeof(u16));

        result.len = fullPathWSize;
        result.ptr = oc_arena_push_array(arena, wchar_t, result.len);

        if(PathCanonicalizeW(result.ptr, fullPathW))
        {
            result.len = lstrlenW(result.ptr);
        }
        else
        {
            result.ptr = 0;
            result.len = 0;
        }
    }
    oc_scratch_end(scratch);

    return (result);
}

oc_file_desc oc_io_raw_open_at(oc_file_desc dirFd, oc_str8 path, oc_file_access accessRights, oc_file_open_flags openFlags)
{
    HANDLE handle = INVALID_HANDLE_VALUE;

    // convert flags
    DWORD win32AccessFlags = 0;
    DWORD win32ShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE;
    DWORD win32CreateFlags = 0;
    DWORD win32AttributeFlags = FILE_ATTRIBUTE_NORMAL
                              | FILE_FLAG_BACKUP_SEMANTICS;

    if(accessRights & OC_FILE_ACCESS_READ)
    {
        win32AccessFlags |= GENERIC_READ;
    }
    if(accessRights & OC_FILE_ACCESS_WRITE)
    {
        if(accessRights & OC_FILE_OPEN_APPEND)
        {
            win32AccessFlags |= FILE_APPEND_DATA;
        }
        else
        {
            win32AccessFlags |= GENERIC_WRITE;
        }
    }

    if(openFlags & OC_FILE_OPEN_TRUNCATE)
    {
        if(openFlags & OC_FILE_OPEN_CREATE)
        {
            win32CreateFlags |= CREATE_ALWAYS;
        }
        else
        {
            win32CreateFlags |= TRUNCATE_EXISTING;
        }
    }
    if(openFlags & OC_FILE_OPEN_CREATE)
    {
        if(!(win32CreateFlags & CREATE_ALWAYS))
        {
            win32CreateFlags |= OPEN_ALWAYS;
        }
    }
    if(!(win32CreateFlags & OPEN_ALWAYS)
       && !(win32CreateFlags & CREATE_ALWAYS)
       && !(win32CreateFlags & TRUNCATE_EXISTING))
    {
        win32CreateFlags |= OPEN_EXISTING;
    }

    if(openFlags & OC_FILE_OPEN_SYMLINK)
    {
        win32AttributeFlags |= FILE_FLAG_OPEN_REPARSE_POINT;
    }

    oc_arena_scope scratch = oc_scratch_begin();
    oc_str16 pathW = oc_win32_utf8_to_wide_null_terminated(scratch.arena, path);

    if(dirFd == NULL || dirFd == INVALID_HANDLE_VALUE)
    {
        handle = CreateFileW(pathW.ptr, win32AccessFlags, win32ShareMode, NULL, win32CreateFlags, win32AttributeFlags, NULL);
    }
    else
    {
        oc_str16 fullPathW = win32_get_path_at_null_terminated(scratch.arena, dirFd, path);
        if(fullPathW.len)
        {
            handle = CreateFileW(fullPathW.ptr, win32AccessFlags, win32ShareMode, NULL, win32CreateFlags, win32AttributeFlags, NULL);
        }
    }
    oc_scratch_end(scratch);
    return (handle);
}

void oc_io_raw_close(oc_file_desc fd)
{
    CloseHandle(fd);
}

bool oc_io_raw_file_exists_at(oc_file_desc dirFd, oc_str8 path, oc_file_open_flags openFlags)
{
    bool result = false;
    oc_file_desc fd = oc_io_raw_open_at(dirFd, path, OC_FILE_ACCESS_NONE, (openFlags & OC_FILE_OPEN_SYMLINK));
    if(!oc_file_desc_is_nil(fd))
    {
        result = true;
        oc_io_raw_close(fd);
    }
    return (result);
}

oc_io_error oc_io_raw_fstat(oc_file_desc fd, oc_file_status* status)
{
    oc_io_error error = OC_IO_OK;

    BY_HANDLE_FILE_INFORMATION info;
    if(!GetFileInformationByHandle(fd, &info))
    {
        error = oc_io_raw_last_error();
    }
    else
    {
        status->size = (((u64)info.nFileSizeHigh) << 32) | ((u64)info.nFileSizeLow);
        status->uid = ((u64)info.nFileIndexHigh << 32) | ((u64)info.nFileIndexLow);

        DWORD attrRegularSet = FILE_ATTRIBUTE_ARCHIVE
                             | FILE_ATTRIBUTE_COMPRESSED
                             | FILE_ATTRIBUTE_ENCRYPTED
                             | FILE_ATTRIBUTE_HIDDEN
                             | FILE_ATTRIBUTE_NORMAL
                             | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED
                             | FILE_ATTRIBUTE_OFFLINE
                             | FILE_ATTRIBUTE_READONLY
                             | FILE_ATTRIBUTE_SPARSE_FILE
                             | FILE_ATTRIBUTE_SYSTEM
                             | FILE_ATTRIBUTE_TEMPORARY;

        if((info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT))
        {
            FILE_ATTRIBUTE_TAG_INFO tagInfo;
            if(!GetFileInformationByHandleEx(fd, FileAttributeTagInfo, &tagInfo, sizeof(tagInfo)))
            {
                error = oc_io_raw_last_error();
            }
            else if(tagInfo.ReparseTag == IO_REPARSE_TAG_SYMLINK)
            {
                status->type = OC_FILE_SYMLINK;
            }
            else
            {
                status->type = OC_FILE_UNKNOWN;
            }
        }
        else if(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
        {
            status->type = OC_FILE_DIRECTORY;
        }
        else if(info.dwFileAttributes & attrRegularSet)
        {
            status->type = OC_FILE_REGULAR;
        }
        else
        {
            //TODO: might want to check for socket/block/character devices? (otoh MS STL impl. doesn't seem to do it)
            status->type = OC_FILE_UNKNOWN;
        }

        status->perm = OC_FILE_OWNER_READ | OC_FILE_GROUP_READ | OC_FILE_OTHER_READ;
        if(!(info.dwFileAttributes & FILE_ATTRIBUTE_READONLY))
        {
            status->perm = OC_FILE_OWNER_WRITE | OC_FILE_GROUP_WRITE | OC_FILE_OTHER_WRITE;
        }
        //TODO: times
    }
    return (error);
}

oc_io_error oc_io_raw_fstat_at(oc_file_desc dirFd, oc_str8 name, oc_file_open_flags openFlags, oc_file_status* status)
{
    oc_io_error error = OC_IO_OK;
    oc_file_desc fd = oc_io_raw_open_at(dirFd, name, OC_FILE_ACCESS_NONE, OC_FILE_OPEN_SYMLINK);
    if(oc_file_desc_is_nil(fd))
    {
        error = oc_io_raw_last_error();
    }
    else
    {
        error = oc_io_raw_fstat(fd, status);
        oc_io_raw_close(fd);
    }
    return (error);
}

typedef struct
{
    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;
    };
} oc_win32_reparse_data_buffer;

oc_io_raw_read_link_result oc_io_raw_read_link(oc_arena* arena, oc_file_desc fd)
{
    oc_io_raw_read_link_result result = { 0 };

    char buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
    DWORD bytesReturned;

    if(!DeviceIoControl(fd, FSCTL_GET_REPARSE_POINT, NULL, 0, buffer, MAXIMUM_REPARSE_DATA_BUFFER_SIZE, &bytesReturned, 0))
    {
        result.error = oc_io_raw_last_error();
    }
    else
    {
        oc_win32_reparse_data_buffer* reparse = (oc_win32_reparse_data_buffer*)buffer;
        if(reparse->ReparseTag == IO_REPARSE_TAG_SYMLINK)
        {
            oc_str16 nameW = { 0 };
            nameW.len = reparse->SymbolicLinkReparseBuffer.SubstituteNameLength / sizeof(wchar_t);
            nameW.ptr = (u16*)((char*)reparse->SymbolicLinkReparseBuffer.PathBuffer + reparse->SymbolicLinkReparseBuffer.SubstituteNameOffset);
            result.target = oc_win32_wide_to_utf8(arena, nameW);
        }
        else
        {
            result.error = OC_IO_ERR_UNKNOWN;
        }
    }
    return (result);
}

oc_io_raw_read_link_result oc_io_raw_read_link_at(oc_arena* arena, oc_file_desc dirFd, oc_str8 name)
{
    oc_file_desc fd = oc_io_raw_open_at(dirFd, name, OC_FILE_ACCESS_READ, OC_FILE_OPEN_SYMLINK);
    oc_io_raw_read_link_result result = oc_io_raw_read_link(arena, fd);
    oc_io_raw_close(fd);
    return (result);
}

static oc_io_cmp oc_io_close(oc_file_slot* slot, oc_io_req* req, oc_file_table* table)
{
    oc_io_cmp cmp = { 0 };
    if(slot->fd)
    {
        CloseHandle(slot->fd);
    }
    oc_file_slot_recycle(table, slot);
    return (cmp);
}

static oc_io_cmp oc_io_fstat(oc_file_slot* slot, oc_io_req* req)
{
    oc_io_cmp cmp = { 0 };

    if(req->size < sizeof(oc_file_status))
    {
        cmp.error = OC_IO_ERR_ARG;
    }
    else
    {
        slot->error = oc_io_raw_fstat(slot->fd, (oc_file_status*)req->buffer);
        cmp.error = slot->error;
    }
    return (cmp);
}

static oc_io_cmp oc_io_seek(oc_file_slot* slot, oc_io_req* req)
{
    oc_io_cmp cmp = { 0 };

    DWORD whence;
    switch(req->whence)
    {
        case OC_FILE_SEEK_CURRENT:
            whence = FILE_CURRENT;
            break;

        case OC_FILE_SEEK_SET:
            whence = FILE_BEGIN;
            break;

        case OC_FILE_SEEK_END:
            whence = FILE_END;
    }

    LARGE_INTEGER off = { .QuadPart = req->offset };
    LARGE_INTEGER newPos = { 0 };

    if(!SetFilePointerEx(slot->fd, off, &newPos, whence))
    {
        slot->error = oc_io_raw_last_error();
        cmp.error = slot->error;
    }
    else
    {
        cmp.result = newPos.QuadPart;
    }

    return (cmp);
}

static oc_io_cmp oc_io_read(oc_file_slot* slot, oc_io_req* req)
{
    oc_io_cmp cmp = { 0 };

    if(slot->type != OC_FILE_REGULAR)
    {
        slot->error = OC_IO_ERR_PERM;
        cmp.error = slot->error;
    }
    else
    {
        DWORD bytesRead = 0;

        if(!ReadFile(slot->fd, req->buffer, req->size, &bytesRead, NULL))
        {
            slot->error = oc_io_raw_last_error();
            cmp.result = 0;
            cmp.error = slot->error;
        }
        else
        {
            cmp.result = bytesRead;
        }
    }
    return (cmp);
}

static oc_io_cmp oc_io_write(oc_file_slot* slot, oc_io_req* req)
{
    oc_io_cmp cmp = { 0 };

    if(slot->type != OC_FILE_REGULAR)
    {
        slot->error = OC_IO_ERR_PERM;
        cmp.error = slot->error;
    }
    else
    {
        DWORD bytesWritten = 0;

        if(!WriteFile(slot->fd, req->buffer, req->size, &bytesWritten, NULL))
        {
            slot->error = oc_io_raw_last_error();
            cmp.result = 0;
            cmp.error = slot->error;
        }
        else
        {
            cmp.result = bytesWritten;
        }
    }
    return (cmp);
}

static oc_io_cmp oc_io_get_error(oc_file_slot* slot, oc_io_req* req)
{
    oc_io_cmp cmp = { 0 };
    cmp.result = slot->error;
    return (cmp);
}

oc_io_cmp oc_io_wait_single_req_with_table(oc_io_req* req, oc_file_table* table)
{
    oc_io_cmp cmp = { 0 };

    oc_file_slot* slot = oc_file_slot_from_handle(table, req->handle);
    if(!slot)
    {
        if(req->op != OC_IO_OPEN_AT)
        {
            cmp.error = OC_IO_ERR_HANDLE;
        }
    }
    else if(slot->fatal && req->op != OC_IO_CLOSE && req->op != OC_OC_IO_ERROR)
    {
        cmp.error = OC_IO_ERR_PREV;
    }

    if(cmp.error == OC_IO_OK)
    {
        switch(req->op)
        {
            case OC_IO_OPEN_AT:
                cmp = oc_io_open_at(slot, req, table);
                break;

            case OC_IO_FSTAT:
                cmp = oc_io_fstat(slot, req);
                break;

            case OC_IO_CLOSE:
                cmp = oc_io_close(slot, req, table);
                break;

            case OC_IO_READ:
                cmp = oc_io_read(slot, req);
                break;

            case OC_IO_WRITE:
                cmp = oc_io_write(slot, req);
                break;

            case OC_IO_SEEK:
                cmp = oc_io_seek(slot, req);
                break;

            case OC_OC_IO_ERROR:
                cmp = oc_io_get_error(slot, req);
                break;

            default:
                cmp.error = OC_IO_ERR_OP;
                if(slot)
                {
                    slot->error = cmp.error;
                }
                break;
        }
    }
    return (cmp);
}
