/*
 * Copyright (C) Tristan Franssen, <tristanfranssen@strawhats.nl>.
 *
 * This software is licensed under the Apache License, Version 2.0 (the "License");
 * You may not use this file except in compliance with the License.
 * You may obtain a copy of the License in the file LICENSE or at
 * http://www.apache.org/licenses/LICENSE-2.0
 */
#include "cosms-core/file.h"

#include <stdlib.h>
#include <stdint.h>
#include <limits.h>

#if defined(__GNUC__)
#define _FILE_OFFSET_BITS_ 64

#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <errno.h>
#endif

CosmsCoreFileError cosms_core_file_open(struct cosms_core_file *file, const char *path, int mode) {
    file->mode = mode;

    #if defined(__GNUC__)
    int flags = 0;
    if (mode & (COSMS_CORE_FILE_MODE_READ | COSMS_CORE_FILE_MODE_WRITE)) {
        flags |= O_RDWR;
    } else if (mode & COSMS_CORE_FILE_MODE_READ) {
        flags |= O_RDONLY;
    } else if (mode & COSMS_CORE_FILE_MODE_WRITE) {
        flags |= O_WRONLY;
    } 
    
    if (mode & COSMS_CORE_FILE_MODE_APPEND) {
        flags |= O_APPEND;
    } else {
        flags |= O_TRUNC;
        if (mode & COSMS_CORE_FILE_MODE_CREATE) {
            flags |= O_CREAT;
        }
    }

    struct stat file_stat;
    if (stat(path, &file_stat) != 0) {
        return COSMS_CORE_FILE_COULD_NOT_READ_SIZE;
    }

    if (file_stat.st_size > COSMS_CORE_FILE_LARGE) {
        flags |= O_LARGEFILE;
    }

    file->native_file = open(path, flags);
    if (file->native_file == -1) {
        switch(errno) {
            case ENOENT: 
                return COSMS_CORE_FILE_NOT_FOUND;

            case EACCES:
                return COSMS_CORE_FILE_NO_ACCESS;

            case EMFILE:
            case ENFILE:
                return COSMS_CORE_FILE_LIMIT_REACHED;

            default:
                return COSMS_CORE_FILE_UNKOWN_ERROR;
        }
    }
    #elif defined(_WIN32)
    DWORD access_flags = 0;
    DWORD create_flag = OPEN_EXISTING;

    if (mode & COSMS_CORE_FILE_MODE_READ) {
        access_flags |= GENERIC_READ;
    } 
    
    if (mode & COSMS_CORE_FILE_MODE_WRITE) {
        access_flags |= GENERIC_WRITE;
    }

    if (mode & COSMS_CORE_FILE_MODE_APPEND) {
        create_flag = OPEN_ALWAYS;
    } else if (mode & COSMS_CORE_FILE_MODE_CREATE) {
        create_flag = CREATE_ALWAYS;
    }

    file->native_file = CreateFile(path, access_flags, 0, NULL, create_flag, FILE_ATTRIBUTE_NORMAL, NULL);
    if (file->native_file == INVALID_HANDLE_VALUE) {
        switch(GetLastError()) {
            case ERROR_FILE_NOT_FOUND:
            case ERROR_PATH_NOT_FOUND:
                return COSMS_CORE_FILE_NOT_FOUND;

            case ERROR_ACCESS_DENIED:
            case ERROR_SHARING_VIOLATION:
            case ERROR_LOCK_VIOLATION:
                return COSMS_CORE_FILE_NO_ACCESS;

            case ERROR_TOO_MANY_OPEN_FILES:
                return COSMS_CORE_FILE_LIMIT_REACHED;

            default:
                return COSMS_CORE_FILE_UNKOWN_ERROR;
        }
    }
    #endif

    return COSMS_CORE_FILE_OK;
}

CosmsCoreFileError cosms_core_file_close(struct cosms_core_file *file) {
    #if defined(__GNUC__)
    int error = close(file->native_file);
    if (error == -1) {
        if (errno == EBADF) {
            return COSMS_CORE_FILE_INVALID_FILE;
        }

        return COSMS_CORE_FILE_UNKOWN_ERROR;
    }
    #elif defined(_WIN32)
    BOOL error = CloseHandle(file->native_file);
    if (error == 0) {
        if (GetLastError() == ERROR_INVALID_HANDLE) {
            return COSMS_CORE_FILE_INVALID_FILE;
        }

        return COSMS_CORE_FILE_UNKOWN_ERROR;
    }
    #endif

    return COSMS_CORE_FILE_OK;
}

CosmsCoreFileError cosms_core_file_get_size(struct cosms_core_file *file, unsigned long long *size) {
    #if defined(__GNUC__)
    struct stat file_stat;
    if (fstat(file->native_file, &file_stat) != 0) {
        if (errno == EBADF) {
            return COSMS_CORE_FILE_INVALID_FILE;
        }

        return COSMS_CORE_FILE_COULD_NOT_READ_SIZE;
    }

    (*size) = file_stat.st_size;
    #elif defined(_WIN32)
    DWORD high_size;
    DWORD low_size = GetFileSize(file->native_file, &high_size);
    if (low_size == INVALID_FILE_SIZE) {
        if (GetLastError() == ERROR_INVALID_HANDLE) {
            return COSMS_CORE_FILE_INVALID_FILE;
        }

        return COSMS_CORE_FILE_COULD_NOT_READ_SIZE;
    }

    (*size) = ((unsigned long long)high_size << 32) | low_size;
    #endif

    return COSMS_CORE_FILE_OK;
}

CosmsCoreFileError cosms_core_file_read(struct cosms_core_file *file, char *buffer, unsigned int bytes_to_read, unsigned int *bytes_read) {
    if ((file->mode & COSMS_CORE_FILE_MODE_READ) == 0) {
        return COSMS_CORE_FILE_INVALID_OPERATION;
    }

    #if defined(__GNUC__)
    int read_bytes = read(file->native_file, buffer, bytes_to_read);
    if (read_bytes == -1) {
        return COSMS_CORE_FILE_FAILED_TO_READ;
    }

    if (bytes_read != NULL) {
        (*bytes_read) = read_bytes;
    }
    #elif defined(_WIN32)
    if (ReadFile(file->native_file, buffer, bytes_to_read, (LPDWORD)bytes_read, NULL) == 0) {
        return COSMS_CORE_FILE_FAILED_TO_READ;
    }
    #endif

    buffer[bytes_to_read] = '\0';
    return COSMS_CORE_FILE_OK;
}

CosmsCoreFileError cosms_core_file_read_all(struct cosms_core_file *file, char **buffer, unsigned long long *bytes_read) {
    if ((file->mode & COSMS_CORE_FILE_MODE_READ) == 0) {
        return COSMS_CORE_FILE_INVALID_OPERATION;
    }

    #if defined(__GNUC__)
    struct stat file_stat;
    if (fstat(file->native_file, &file_stat) != 0) {
        return COSMS_CORE_FILE_COULD_NOT_READ_SIZE;
    }

    if (bytes_read != NULL) {
        (*bytes_read) = file_stat.st_size;
    }

    (*buffer) = (char*)malloc((file_stat.st_size + 1) * sizeof(char));
    if ((*buffer) == NULL) {
        return COSMS_CORE_FILE_FAILED_TO_ALLOCATE;
    }

    unsigned long long remaining_bytes = file_stat.st_size;
    while (remaining_bytes != 0) {
        unsigned int current_bytes_to_read;
        if (remaining_bytes > UINT_MAX) {
            current_bytes_to_read = UINT_MAX;
        } else {
            current_bytes_to_read = (unsigned int)remaining_bytes;
        }

        int read_bytes = read(file, (*buffer) + (file_stat.st_size - remaining_bytes), current_bytes_to_read);
        if (read_bytes == -1) {
            free(*buffer);
            return COSMS_CORE_FILE_FAILED_TO_READ;
        }

        remaining_bytes -= read_bytes;
    }

    (*buffer)[file_stat.st_size] = '\0';
    #elif defined(_WIN32)
    DWORD high_size;
    DWORD low_size = GetFileSize(file->native_file, &high_size);
    if (low_size == INVALID_FILE_SIZE && GetLastError() != NO_ERROR) {
        return COSMS_CORE_FILE_COULD_NOT_READ_SIZE;
    }

    unsigned long long file_size = ((unsigned long long)high_size << 32) | low_size;
    if (bytes_read != NULL) {
        (*bytes_read) = file_size;
    }

    (*buffer) = (char*)malloc((file_size + 1) * sizeof(char));
    if ((*buffer) == NULL) {
        return COSMS_CORE_FILE_FAILED_TO_ALLOCATE;
    }

    unsigned long long remaining_bytes = file_size;
    while (remaining_bytes != 0) {
        DWORD current_bytes_to_read, read_bytes;
        if (remaining_bytes > UINT_MAX) {
            current_bytes_to_read = UINT_MAX;
        } else {
            current_bytes_to_read = (DWORD)remaining_bytes;
        }

        if (ReadFile(file->native_file, (*buffer) + (file_size - remaining_bytes), current_bytes_to_read, &read_bytes, NULL) == 0) {
            free(*buffer);
            return COSMS_CORE_FILE_FAILED_TO_READ;
        }

        remaining_bytes -= read_bytes;
    }

    (*buffer)[file_size] = '\0';
    #endif
    
    return COSMS_CORE_FILE_OK;
}

CosmsCoreFileError cosms_core_file_write(struct cosms_core_file *file, char *buffer, unsigned int bytes_to_write, unsigned int *bytes_written) {
    if ((file->mode & COSMS_CORE_FILE_MODE_WRITE) == 0) {
        return COSMS_CORE_FILE_INVALID_OPERATION;
    }

    #if defined(__GNUC__)
    int written_bytes = write(file->native_file, buffer, bytes_to_write);
    if (written_bytes == -1) {
        return COSMS_CORE_FILE_FAILED_TO_WRITE;
    }

    if (bytes_written != NULL) {
        (*bytes_written) = written_bytes;
    }
    #elif defined(_WIN32)
    if (file->mode & COSMS_CORE_FILE_MODE_APPEND) {
        SetFilePointer(file->native_file, 0, NULL, FILE_END);
    }

    if (WriteFile(file->native_file, buffer, bytes_to_write, (LPDWORD)bytes_written, NULL) == 0) {
        return COSMS_CORE_FILE_FAILED_TO_WRITE;
    }
    #endif
    
    return COSMS_CORE_FILE_OK;
}

CosmsCoreFileError cosms_core_file_write_all(struct cosms_core_file *file, char *buffer, unsigned long long bytes_to_write) {
    if ((file->mode & COSMS_CORE_FILE_MODE_WRITE) == 0) {
        return COSMS_CORE_FILE_INVALID_OPERATION;
    }

    #if defined(__GNUC__)
    unsigned long long remaining_bytes = bytes_to_write;
    while (remaining_bytes != 0) {
        unsigned int current_bytes_to_write;
        if (remaining_bytes > UINT_MAX) {
            current_bytes_to_write = UINT_MAX;
        } else {
            current_bytes_to_write = (unsigned int)remaining_bytes;
        }

        int written_bytes = write(file->native_file, content + (bytes_to_write - remaining_bytes), current_bytes_to_write);
        if (written_bytes == -1) {
            return COSMS_CORE_FILE_FAILED_TO_WRITE;
        }

        remaining_bytes -= written_bytes;
    }
    #elif defined(_WIN32)
    if (file->mode & COSMS_CORE_FILE_MODE_APPEND) {
        LARGE_INTEGER zero;
        zero.QuadPart = 0;

        SetFilePointerEx(file->native_file, zero, NULL, FILE_END);
    }

    unsigned long long remaining_bytes = bytes_to_write;
    while (remaining_bytes != 0) {
        DWORD current_bytes_to_write, written_bytes;
        if (remaining_bytes > UINT_MAX) {
            current_bytes_to_write = UINT_MAX;
        } else {
            current_bytes_to_write = (DWORD)remaining_bytes;
        }

        if (WriteFile(file->native_file, buffer + (bytes_to_write - remaining_bytes), current_bytes_to_write, &written_bytes, NULL) == 0) {
            return COSMS_CORE_FILE_FAILED_TO_WRITE;
        }

        remaining_bytes -= written_bytes;
    }
    #endif
    
    return COSMS_CORE_FILE_OK;
}

CosmsCoreFileError cosms_core_file_delete(const char *path) {
    #if defined(__GNUC__)
    if (unlink(path) == -1) {
        switch (errno) {
            case ENOENT:
                return COSMS_CORE_FILE_NOT_FOUND;

            case EACCES:
                return COSMS_CORE_FILE_NO_ACCESS;

            case EBUSY:
                return COSMS_CORE_FILE_STILL_OPEN;

            default:
                return COSMS_CORE_FILE_UNKOWN_ERROR;
        }
    }
    #elif defined(_WIN32)
    if (!DeleteFile(path)) {
        switch(GetLastError()) {
            case ERROR_FILE_NOT_FOUND:
            case ERROR_PATH_NOT_FOUND:
                return COSMS_CORE_FILE_NOT_FOUND;

            case ERROR_ACCESS_DENIED:
                return COSMS_CORE_FILE_NO_ACCESS;

            case ERROR_LOCK_VIOLATION:
            case ERROR_SHARING_VIOLATION:
                return COSMS_CORE_FILE_STILL_OPEN;

            default:
                return COSMS_CORE_FILE_UNKOWN_ERROR;
        }
    }
    #endif

    return COSMS_CORE_FILE_OK;
}

const char *cosms_core_file_error_string(CosmsCoreFileError error) {
    switch(error) {
        case COSMS_CORE_FILE_OK:
            return "cosms-file everything ok";

        case COSMS_CORE_FILE_NOT_FOUND:
            return "cosms-file path not found";

        case COSMS_CORE_FILE_NO_ACCESS:
            return "cosms-file no access";

        case COSMS_CORE_FILE_LIMIT_REACHED:
            return "cosms-file to many open files";

        case COSMS_CORE_FILE_COULD_NOT_READ_SIZE:
            return "cosms-file failed to read size";

        case COSMS_CORE_FILE_UNKOWN_ERROR:
            return "cosms-file unkown error occured";

        case COSMS_CORE_FILE_FAILED_TO_ALLOCATE:
            return "cosms-file failed to allocate memory";

        case COSMS_CORE_FILE_FAILED_TO_READ:
            return "cosms-file failed to read content of file";

        case COSMS_CORE_FILE_FAILED_TO_WRITE:
            return "cosms-file failed to write content to file";

        case COSMS_CORE_FILE_STILL_OPEN:
            return "cosms-file still open";

        case COSMS_CORE_FILE_INVALID_FILE:
            return "cosms-file invalid native file given";

        case COSMS_CORE_FILE_INVALID_OPERATION:
            return "cosms-file invalid operation for current file";

        default:
            return NULL;
    }
}
