/*
 * zFile.c
 * 
 * The Zen API 
 * Copyright (C) 2008-2009 by LMI Technologies Inc.
 * 
 * Licensed under The MIT License.
 * Redistributions of files must retain the above copyright notice.
 */ 
#include <z/zzFile.h>
#include <stdlib.h>
#include <memory.h>
#include <string.h>

ZAPI_EXPORT(zStatus) zFile_Load(const zChar* path, zByte** data, zUIntPtr* size)
{
    zFile file = zNIL; 
    zByte* buffer = zNULL; 
    z64u fileSize; 
    zStatus exception; 

    zEXCEPT_TRY
    {
        zEXCEPT_CHECK(zFile_Construct(&file, path, zFILE_MODE_READ)); 

        fileSize = zFile_Length(file); 

        zEXCEPT_CHECK(fileSize <= zAPI_UINTPTR_MAX); 

        zEXCEPT_CHECK(zMalloc(&buffer, (zUIntPtr) fileSize)); 
        zEXCEPT_CHECK(zFile_Read(file, buffer, (zUIntPtr) fileSize)); 

        *data = buffer; 
        *size = (zUIntPtr) fileSize; 
    }
    zEXCEPT_CATCH(&exception)     
    {
        zFree(buffer); 
        zEXCEPT_SET(exception); 
    }
    zEXCEPT_FINALLY
    {
        if (!zIS_NIL(file))
        {
            zEXCEPT_SET(zFile_Destroy(file)); 
        }

        zEXCEPT_CLOSE(); 
    }

    return zOK; 
}

ZAPI_EXPORT(zStatus) zFile_Save(const zChar* path, const zByte* data, zUIntPtr size)
{
    zFile file = zNIL; 

    zEXCEPT_TRY
    {
        zEXCEPT_CHECK(zFile_Construct(&file, path, zFILE_MODE_WRITE)); 
        zEXCEPT_CHECK(zFile_Write(file, data, size)); 
    }
    zEXCEPT_CATCH(zNULL) {}
    zEXCEPT_FINALLY
    {
        if (!zIS_NIL(file))
        {
            zEXCEPT_SET(zFile_Destroy(file)); 
        }

        zEXCEPT_CLOSE(); 
    }

    return zOK; 
}

ZAPI_EXPORT(zStatus) zFile_Construct(zFile* file, const zChar* path, zFileMode mode)
{
    zFileClass *obj = zNULL; 
    zFile handle = zNIL; 
    zStatus exception = zOK;  
    zBool opened = zFALSE; 
    
    zEXCEPT_TRY
    {       
        zEXCEPT_CHECK(zCalloc(&obj, 1, sizeof(zFileClass))); 
        zHANDLE(&handle, obj); 
       
        zEXCEPT_CHECK(zFile_OsOpen(handle, path,  mode)); 
        opened = zTRUE; 

        obj->streamLength = zFile_OsSeek(handle, 0, zSEEK_ORIGIN_END); 

        zEXCEPT_CHECK(zFile_OsSeek(handle, 0, zSEEK_ORIGIN_BEGIN) == 0); 
        zEXCEPT_CHECK(zFile_SetBuffer(handle, zFILE_DEFAULT_BUFFER_CAPACITY)); 

        *file = handle; 
    }
    zEXCEPT_CATCH(&exception) 
    {
        if (opened)
        {
            zFile_OsClose(handle); 
        }
        zFree(obj); 

        zEXCEPT_SET(exception); 
    }
    zEXCEPT_FINALLY
    {
        zEXCEPT_CLOSE(); 
    }
       
    return zOK;
}

ZAPI_EXPORT(zStatus) zFile_Destroy(zFile file)
{
    zFileClass *obj = zHANDLE_POINTER(file); 

    if (!zIS_NULL(obj))
    {
        zCHECK(zFile_Flush(file)); 

        zCHECK(zFile_OsClose(file)); 

        zCHECK(zFree(obj->buffer)); 

        zCHECK(zFree(obj)); 
    }

    return zOK; 
}

ZAPI_EXPORT(zStatus) zFile_SetBuffer(zFile file, zUIntPtr size)
{
    zFileClass *obj = zHANDLE_POINTER(file); 

    zCHECK(zFile_Flush(file)); 

    zCHECK(zFree(obj->buffer)); 

    obj->buffer = zNULL; 
    obj->bufferCapacity = 0; 
    obj->bufferBegin = 0; 
    obj->bufferEnd = 0; 

    if (size > 0)
    {
        zCHECK(zMalloc(&obj->buffer, size)); 
        obj->bufferCapacity = size; 
    }
    
    return zOK;
}

ZAPI_EXPORT(zStatus) zFile_Read(zFile file, void* buffer, zUIntPtr size)
{
    zFileClass *obj = zHANDLE_POINTER(file); 
    zByte* dest = buffer;  
    zUIntPtr destEnd = 0; 
    zUIntPtr destCapacity = size; 
    zUIntPtr copyCount, bytesRead; 

    if (obj->lastMode == zFILE_MODE_WRITE)
    {
        zCHECK(zFile_Flush(file)); 
    }

    if ((zFile_Position(file) + size) > zFile_Length(file))
    {
        return zERROR_PARAMETER; 
    }
  
    copyCount = zMATH_MIN(destCapacity, obj->bufferEnd - obj->bufferBegin);
    memcpy(&dest[destEnd], &obj->buffer[obj->bufferBegin], copyCount);     
    obj->bufferBegin += copyCount; 
    destEnd += copyCount; 

    if ((destCapacity - destEnd) > 0)
    {
        if ((destCapacity - destEnd) >= obj->bufferCapacity)
        {            
            zCHECK(zFile_ReadAtLeast(file, &dest[destEnd], destCapacity-destEnd, destCapacity-destEnd, &bytesRead));
        }
        else
        {
            obj->bufferBegin = 0; 
            obj->bufferEnd = 0; 

            zCHECK(zFile_ReadAtLeast(file, &obj->buffer[obj->bufferBegin], destCapacity-destEnd, obj->bufferCapacity-obj->bufferBegin, &bytesRead));                
            obj->bufferEnd += bytesRead; 

            memcpy(&dest[destEnd], &obj->buffer[obj->bufferBegin], destCapacity-destEnd);     
            obj->bufferBegin += destCapacity-destEnd; 
        }
    }

    obj->lastMode = zFILE_MODE_READ; 

    return zOK; 
}

ZAPI_EXPORT(zStatus) zFile_Write(zFile file, const void* buffer, zUIntPtr size)
{
    zFileClass *obj = zHANDLE_POINTER(file); 
    const zByte* src = buffer; 
    zUIntPtr srcBegin = 0; 
    zUIntPtr srcCapacity = size; 
    zUIntPtr copyCount; 

    if (obj->lastMode == zFILE_MODE_READ)
    {
        zCHECK(zFile_Flush(file)); 
    }
    
    if (obj->bufferEnd > 0)
    {
        copyCount = zMATH_MIN(srcCapacity-srcBegin, obj->bufferCapacity-obj->bufferEnd); 
        memcpy(&obj->buffer[obj->bufferEnd], &src[srcBegin], copyCount); 
        srcBegin += copyCount; 
        obj->bufferEnd += copyCount; 

        if (obj->bufferEnd == obj->bufferCapacity)
        {
            zCHECK(zFile_Flush(file)); 
        }
    }

    if ((srcCapacity - srcBegin) > 0)
    {
        if ((srcCapacity - srcBegin) >= obj->bufferCapacity)
        {
            zCHECK(zFile_WriteAll(file, &src[srcBegin], srcCapacity-srcBegin)); 
        }
        else
        {
            memcpy(&obj->buffer[obj->bufferEnd], &src[srcBegin], srcCapacity-srcBegin);  
            obj->bufferEnd += srcCapacity-srcBegin; 
        }
    }

    obj->lastMode = zFILE_MODE_WRITE; 
  
    return zOK; 
}

ZAPI_EXPORT(zStatus) zFile_Flush(zFile file)
{
    zFileClass *obj = zHANDLE_POINTER(file); 
        
    if ((obj->lastMode == zFILE_MODE_WRITE) && (obj->bufferEnd > 0))
    {
        zCHECK(zFile_WriteAll(file, obj->buffer, obj->bufferEnd));
    }

    obj->bufferBegin = 0; 
    obj->bufferEnd = 0; 

    zCHECK(zFile_OsFlush(file)); 
            
    return zOK;
}

ZAPI_EXPORT(zStatus) zFile_Seek(zFile file, z64s offset, zSeekOrigin origin)
{
    zFileClass *obj = zHANDLE_POINTER(file); 
    
    zCHECK(zFile_Flush(file)); 
    
    obj->streamPosition = zFile_OsSeek(file, offset, origin); 

    return zOK; 
}

ZAPI_EXPORT(z64u) zFile_Length(zFile file)
{
    zFileClass *obj = zHANDLE_POINTER(file);

    if (obj->lastMode == zFILE_MODE_READ)
    {
        return obj->streamLength; 
    }
    else
    {
        return zMATH_MAX(obj->streamLength, obj->streamPosition + obj->bufferEnd); 
    }
}

ZAPI_EXPORT(z64u) zFile_Position(zFile file)
{
    zFileClass *obj = zHANDLE_POINTER(file);

    if (obj->lastMode == zFILE_MODE_READ)
    {
        return obj->streamPosition - (obj->bufferEnd - obj->bufferBegin); 
    }
    else
    {
        return obj->streamPosition + obj->bufferEnd; 
    }
}

ZAPI_EXPORT(zStatus) zFile_ReadAtLeast(zFile file, zByte* buffer, zUIntPtr minCount, zUIntPtr maxCount, zUIntPtr* bytesRead)
{
    zFileClass *obj = zHANDLE_POINTER(file); 

    zCHECK(zFile_OsRead(file, buffer, minCount, maxCount, bytesRead)); 

    obj->streamPosition += *bytesRead;  

    return zOK; 
}

ZAPI_EXPORT(zStatus) zFile_WriteAll(zFile file, const zByte* buffer, zUIntPtr count)
{
    zFileClass *obj = zHANDLE_POINTER(file); 

    zCHECK(zFile_OsWrite(file, buffer, count)); 

    obj->streamPosition += count; 
    obj->streamLength = zMATH_MAX(obj->streamLength, obj->streamPosition); 
    
    return zOK;    
}

#if defined(ZAPI_WINDOWS)

#include <windows.h>

ZAPI_EXPORT(zStatus) zFile_OsOpen(zFile file, const zChar* path, zFileMode mode)
{
    zFileClass *obj = zHANDLE_POINTER(file); 
    DWORD desiredAccess = 0; 
    DWORD shareMode = 0; 
    DWORD creationDisposition = 0; 
    DWORD flags = FILE_ATTRIBUTE_NORMAL;  

    if (mode == zFILE_MODE_READ)
    {
        desiredAccess = GENERIC_READ;        
        shareMode = FILE_SHARE_READ; 
        creationDisposition = OPEN_EXISTING;         
    }
    else if (mode == zFILE_MODE_WRITE)
    {
        desiredAccess = GENERIC_WRITE;        
        creationDisposition = CREATE_ALWAYS;         
    }
    else if (mode == (zFILE_MODE_READ | zFILE_MODE_WRITE))
    {
        desiredAccess = GENERIC_READ | GENERIC_WRITE;        
        creationDisposition = CREATE_ALWAYS;         
    }
    else if (mode == (zFILE_MODE_WRITE | zFILE_MODE_UPDATE))
    {
        desiredAccess = GENERIC_WRITE;        
        creationDisposition = OPEN_ALWAYS;         
    }
    else if (mode == (zFILE_MODE_READ | zFILE_MODE_WRITE | zFILE_MODE_UPDATE))
    {
        desiredAccess = GENERIC_READ | GENERIC_WRITE;        
        creationDisposition = OPEN_ALWAYS;         
    }
    else
    {
        return zERROR_PARAMETER; 
    }
  
    if ((obj->stream = CreateFile(path, desiredAccess, shareMode, zNULL, creationDisposition, flags, zNULL)) == INVALID_HANDLE_VALUE)
    {
        switch (GetLastError())
        {
        case ERROR_FILE_NOT_FOUND:
        case ERROR_PATH_NOT_FOUND:
            return zERROR_NOT_FOUND;
        default:
            return zERROR_STATE; 
        }
    }

    return zOK; 
}

ZAPI_EXPORT(zStatus) zFile_OsClose(zFile file)
{
    zFileClass *obj = zHANDLE_POINTER(file); 

    zCHECK(CloseHandle(obj->stream)); 
    
    return zOK; 
}

ZAPI_EXPORT(zStatus) zFile_OsRead(zFile file, zByte* buffer, zUIntPtr minCount, zUIntPtr maxCount, zUIntPtr* bytesRead)
{
    zFileClass *obj = zHANDLE_POINTER(file); 
    DWORD read; 

    zCHECK(maxCount <= zFILE_MAX_IO_SIZE); 

    zCHECK(ReadFile(obj->stream, buffer, (DWORD)maxCount, &read, zNULL)); 

    zCHECK(read >= minCount); 

    *bytesRead = read;    

    return zOK; 
}

ZAPI_EXPORT(zStatus) zFile_OsWrite(zFile file, const zByte* buffer, zUIntPtr count)
{
    zFileClass *obj = zHANDLE_POINTER(file); 
    DWORD written; 

    zCHECK(count <= zFILE_MAX_IO_SIZE); 

    zCHECK(WriteFile(obj->stream, buffer, (DWORD)count, &written, zNULL));

    zCHECK(written == count); 

    return zOK; 
}

ZAPI_EXPORT(zStatus) zFile_OsFlush(zFile file)
{
    zFileClass *obj = zHANDLE_POINTER(file); 

    if (obj->lastMode == zFILE_MODE_WRITE)
    {
        zCHECK(FlushFileBuffers(obj->stream)); 
    }

    return zOK; 
}

ZAPI_EXPORT(z64u) zFile_OsSeek(zFile file, z64s offset, zSeekOrigin origin)
{
    zFileClass *obj = zHANDLE_POINTER(file); 
    DWORD moveMethod; 
    LARGE_INTEGER offsetLarge; 
    LARGE_INTEGER positionLarge; 

    switch(origin)
    {
    case zSEEK_ORIGIN_BEGIN:        moveMethod = FILE_BEGIN;    break;
    case zSEEK_ORIGIN_CURRENT:      moveMethod = FILE_CURRENT;  break;
    case zSEEK_ORIGIN_END:          moveMethod = FILE_END;      break;
    default:                                                    return 0; 
    }

    offsetLarge.QuadPart = offset; 

    zCHECK(SetFilePointerEx(obj->stream, offsetLarge, &positionLarge, moveMethod)); 

    return (z64u) positionLarge.QuadPart; 
}

#elif defined (ZAPI_LINUX)

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

ZAPI_EXPORT(zStatus) zFile_OsOpen(zFile file, const zChar* path, zFileMode mode)
{
    zFileClass *obj = zHANDLE_POINTER(file); 
    int oflag = 0; 

    if (mode == zFILE_MODE_READ)
    {
        oflag = O_RDONLY;        
    }
    else if (mode == zFILE_MODE_WRITE)
    {
        oflag = O_WRONLY | O_CREAT | O_TRUNC;     
    }
    else if (mode == (zFILE_MODE_READ | zFILE_MODE_WRITE))
    {
        oflag = O_RDWR | O_CREAT | O_TRUNC; 
    }
    else if (mode == (zFILE_MODE_WRITE | zFILE_MODE_UPDATE))
    {
        oflag = O_WRONLY | O_CREAT;     
    }
    else if (mode == (zFILE_MODE_READ | zFILE_MODE_WRITE | zFILE_MODE_UPDATE))
    {
        oflag = O_RDWR | O_CREAT; 
    }
    else
    {
        return zERROR_PARAMETER; 
    }
      
    if ((obj->stream = open(path, oflag)) == -1)
    {
        switch (errno)
        {
        case ENOENT:
        case ENOTDIR:
            return zERROR_NOT_FOUND;
        default:
            return zERROR_STATE; 
        }
    }
    
    return zOK; 
}

ZAPI_EXPORT(zStatus) zFile_OsClose(zFile file)
{
    zFileClass *obj = zHANDLE_POINTER(file); 

    if (close(obj->stream) == -1)
    {
        return zERROR; 
    }

    return zOK;  
}

ZAPI_EXPORT(zStatus) zFile_OsRead(zFile file, zByte* buffer, zUIntPtr minCount, zUIntPtr maxCount, zUIntPtr* bytesRead)
{
    zFileClass *obj = zHANDLE_POINTER(file);
    ssize_t readSize; 

    if ((readSize = read(obj->stream, buffer, maxCount)) < 0)
    {
        return zERROR_STREAM; 
    } 

    if ((zUIntPtr)readSize < minCount)
    {
        return zERROR_STREAM; 
    }

    *bytesRead = (zUIntPtr) readSize; 

    return zOK; 
}

ZAPI_EXPORT(zStatus) zFile_OsWrite(zFile file, const zByte* buffer, zUIntPtr count)
{
    zFileClass *obj = zHANDLE_POINTER(file);
    ssize_t writeSize; 

    if ((writeSize = write(obj->stream, buffer, count)) < 0)
    {
        return zERROR_STREAM; 
    } 

    if ((zUIntPtr)writeSize != count)
    {
        return zERROR_STREAM; 
    }

    return zOK; 
}

ZAPI_EXPORT(zStatus) zFile_OsFlush(zFile file)
{
    return zOK; 
}

ZAPI_EXPORT(z64u) zFile_OsSeek(zFile file, z64s offset, zSeekOrigin origin)
{
    zFileClass *obj = zHANDLE_POINTER(file); 
    off_t position; 
    int whence; 

    switch(origin)
    {
    case zSEEK_ORIGIN_BEGIN:        whence = SEEK_SET;    break;
    case zSEEK_ORIGIN_CURRENT:      whence = SEEK_CUR;    break;
    case zSEEK_ORIGIN_END:          whence = SEEK_END;    break;
    default:                                              return 0; 
    }

    if ((position = lseek(obj->stream, (off_t)offset, whence)) == -1)
    {
        return 0; 
    }

    return (z64u) position; 
}

#endif
