#include <Core/Common/EnumUtils.h>
#include <Core/Common/String.h>
#include <Core/HAL/IO/IOStream.h>
#include <Core/HAL/Win32.h>
#include <Core/Logging/LogManager.h>
#include <Core/Logging/LogTypes.h>
#include <Core/Memory/Allocator.h>

namespace Juliet::Internal
{
    namespace
    {
        struct Win32IOStreamDataPayload : IOStreamDataPayload
        {
            HANDLE Handle;
            void*  Data;
            size_t Size;
            size_t SizeLeft;
            bool   IsAppending;
            bool   ShouldAutoClose;
        };

        constexpr size_t kFileReadBufferSize = 1024;

        int64 FileSize(NonNullPtr<IOStreamDataPayload> payload)
        {
            auto          win32Payload = static_cast<Win32IOStreamDataPayload*>(payload.Get());
            LARGE_INTEGER size;

            if (!GetFileSizeEx(win32Payload->Handle, &size))
            {
                Log(LogLevel::Error, LogCategory::Core, "IoStream:Size error: %d", GetLastError());
                return 0;
            }

            return size.QuadPart;
        }

        int64 FileSeek(NonNullPtr<IOStreamDataPayload> payload, int64 offset, IOStreamSeekPivot pivot)
        {
            auto win32Payload = static_cast<Win32IOStreamDataPayload*>(payload.Get());
            if ((pivot == IOStreamSeekPivot::Current) && (win32Payload->SizeLeft > 0))
            {
                offset -= static_cast<int64>(win32Payload->SizeLeft);
            }
            win32Payload->SizeLeft = 0;

            DWORD windowsPivot = 0;
            switch (pivot)
            {
            case IOStreamSeekPivot::Begin: windowsPivot = FILE_BEGIN; break;
            case IOStreamSeekPivot::Current: windowsPivot = FILE_CURRENT; break;
            case IOStreamSeekPivot::End: windowsPivot = FILE_END; break;
            case IOStreamSeekPivot::Count:
                Log(LogLevel::Error, LogCategory::Core, "IoStream:Seek: Invalid Pivot value");
                return -1;
            }
            static_assert(ToUnderlying(IOStreamSeekPivot::Count) == 3, "IOStreamSeekPivot::Count must be 3");

            LARGE_INTEGER windowsOffset;
            windowsOffset.QuadPart = offset;
            if (!SetFilePointerEx(win32Payload->Handle, windowsOffset, &windowsOffset, windowsPivot))
            {
                Log(LogLevel::Error, LogCategory::Core, "IoStream:Seek:Error: %d", GetLastError());
                return 0;
            }
            return windowsOffset.QuadPart;
        }

        size_t FileRead(NonNullPtr<IOStreamDataPayload> payload, void* outBuffer, size_t size, NonNullPtr<IOStreamStatus> status)
        {
            auto   win32Payload    = static_cast<Win32IOStreamDataPayload*>(payload.Get());
            size_t totalNeed       = size;
            size_t totalRead       = 0;
            size_t sizeToReadAhead = 0;
            if (win32Payload->SizeLeft > 0)
            {
                uint8* data     = static_cast<uint8*>(win32Payload->Data) + win32Payload->Size - win32Payload->SizeLeft;
                sizeToReadAhead = Min(totalNeed, win32Payload->SizeLeft);
                MemCopy(outBuffer, data, sizeToReadAhead);
                win32Payload->SizeLeft -= sizeToReadAhead;

                if (sizeToReadAhead == totalNeed)
                {
                    return size;
                }
                outBuffer = static_cast<uint8*>(outBuffer) + sizeToReadAhead;
                totalNeed -= sizeToReadAhead;
                totalRead += sizeToReadAhead;
            }

            DWORD bytes = 0;
            if (totalNeed < kFileReadBufferSize)
            {
                if (!ReadFile(win32Payload->Handle, win32Payload->Data, kFileReadBufferSize, &bytes, nullptr))
                {
                    switch (DWORD error = GetLastError())
                    {
                    case ERROR_BROKEN_PIPE:
                    case ERROR_HANDLE_EOF: break;
                    case ERROR_NO_DATA: *status = IOStreamStatus::NotReady; break;
                    default:
                        Log(LogLevel::Error, LogCategory::Core, "IoStream:Win32 Read: Error reading from data stream: %d", error);
                        break;
                    }
                    return 0;
                }
                sizeToReadAhead = Min(totalNeed, static_cast<size_t>(bytes));
                MemCopy(outBuffer, win32Payload->Data, sizeToReadAhead);
                win32Payload->Size     = bytes;
                win32Payload->SizeLeft = bytes - sizeToReadAhead;
                totalRead += sizeToReadAhead;
            }
            else
            {
                if (!ReadFile(win32Payload->Handle, outBuffer, static_cast<DWORD>(totalNeed), &bytes, nullptr))
                {
                    switch (DWORD error = GetLastError())
                    {
                    case ERROR_BROKEN_PIPE:
                    case ERROR_HANDLE_EOF: break;
                    case ERROR_NO_DATA: *status = IOStreamStatus::NotReady; break;
                    default:
                        Log(LogLevel::Error, LogCategory::Core, "IoStream:Win32 Read: Error reading from data stream: %d", error);
                        break;
                    }
                    return 0;
                }
                totalRead += bytes;
            }
            return totalRead;
        }

        size_t FileWrite(NonNullPtr<IOStreamDataPayload> payload, ByteBuffer inBuffer, NonNullPtr<IOStreamStatus> status)
        {
            auto  win32Payload = static_cast<Win32IOStreamDataPayload*>(payload.Get());
            DWORD bytes;

            if (win32Payload->SizeLeft)
            {
                if (!SetFilePointer(win32Payload->Handle, -static_cast<LONG>(win32Payload->SizeLeft), nullptr, FILE_CURRENT))
                {
                    Log(LogLevel::Error, LogCategory::Core, "IoStream:FileWrite:Error seeking in datastream: %d", GetLastError());
                    return 0;
                }
                win32Payload->SizeLeft = 0;
            }

            // if in append mode, we must go to the EOF before write
            if (win32Payload->IsAppending)
            {
                LARGE_INTEGER windowsOffset;
                windowsOffset.QuadPart = 0;
                if (!SetFilePointerEx(win32Payload->Handle, windowsOffset, &windowsOffset, FILE_END))
                {
                    Log(LogLevel::Error, LogCategory::Core, "IoStream:FileWrite:Error seeking in datastream: %d", GetLastError());
                    return 0;
                }
            }

            if (!WriteFile(win32Payload->Handle, inBuffer.Data, static_cast<DWORD>(inBuffer.Size), &bytes, nullptr))
            {
                Log(LogLevel::Error, LogCategory::Core, "IoStream:FileWrite:Error writing to datastream: %d", GetLastError());
                return 0;
            }
            if (bytes == 0 && inBuffer.Size > 0)
            {
                *status = IOStreamStatus::NotReady;
            }
            return bytes;
        }

        bool FileClose(NonNullPtr<IOStreamDataPayload> payload)
        {
            auto win32Payload = static_cast<Win32IOStreamDataPayload*>(payload.Get());
            if (win32Payload->Handle != INVALID_HANDLE_VALUE)
            {
                if (win32Payload->ShouldAutoClose)
                {
                    CloseHandle(win32Payload->Handle);
                }
                win32Payload->Handle = INVALID_HANDLE_VALUE;
            }
            SafeFree(win32Payload->Data);
            SafeFree(win32Payload);
            return true;
        }

    } // namespace

    IOStream* IOFromFile(String filename, String mode)
    {
        // "r" = reading, file must exist
        // "w" = writing, truncate existing, file may not exist
        // "r+"= reading or writing, file must exist
        // "a" = writing, append file may not exist
        // "a+"= append + read, file may not exist
        // "w+" = read, write, truncate. file may not exist

#if JULIET_DEBUG
        // Making sure the mode is valid
        size_t modeLength = StringLength(mode);
        Assert((modeLength <= 2) &&
               "Mode should have at most 2 characters, one being either r,w or a and the other can only be +");
        if (modeLength == 1)
        {
            Assert((mode.Data[0] == 'r' || mode.Data[0] == 'w' || mode.Data[0] == 'a') &&
                   "Invalid Mode. First char is not r,w or a");
        }
        else
        {
            Assert((mode.Data[1] == '+' || mode.Data[1] == 'b') && "Invalid Mode. Second char is not +");
        }
#endif

        DWORD      openExisting = ContainsChar(mode, 'r') ? OPEN_EXISTING : 0;
        DWORD      createAlways = ContainsChar(mode, 'w') ? CREATE_ALWAYS : 0;
        const bool isAppending  = ContainsChar(mode, 'a');
        DWORD      openAlways   = isAppending ? OPEN_ALWAYS : 0;

        bool  hasPlus  = ContainsChar(mode, '+');
        DWORD canRead  = openExisting || hasPlus ? GENERIC_READ : 0;
        DWORD canWrite = createAlways || openAlways || hasPlus ? GENERIC_WRITE : 0;

        if (!canRead && !canWrite)
        {
            Log(LogLevel::Error, LogCategory::Core, "IOFromFile: Invalid Mode (cannot read nor write)");
            return nullptr;
        }

        // TODO: Detect when folder is missing and create it if in w or a mode.
        HANDLE hFile = CreateFileA(CStr(filename), (canWrite | canRead), (canWrite) ? 0 : FILE_SHARE_READ, nullptr,
                                   (openExisting | createAlways | openAlways), FILE_ATTRIBUTE_NORMAL, nullptr);
        if (hFile == INVALID_HANDLE_VALUE)
        {
            Log(LogLevel::Error, LogCategory::Core, "IOFromFile: couldn't open %s. Error: %d", CStr(filename), GetLastError());
            return nullptr;
        }

        constexpr bool autoClose = true;
        auto           payload   = static_cast<Win32IOStreamDataPayload*>(Calloc(1, sizeof(Win32IOStreamDataPayload)));
        if (!payload)
        {
            if (autoClose)
            {
                CloseHandle(hFile);
            }
            return nullptr;
        }

        IOStreamInterface iface = {};
        iface.Version           = sizeof(iface);
        if (GetFileType(hFile) == FILE_TYPE_DISK)
        {
            iface.Size = FileSize;
            iface.Seek = FileSeek;
        }
        iface.Read  = FileRead;
        iface.Write = FileWrite;
        iface.Close = FileClose;

        payload->Handle          = hFile;
        payload->IsAppending     = isAppending;
        payload->ShouldAutoClose = autoClose;

        payload->Data = static_cast<char*>(Malloc(kFileReadBufferSize));
        if (!payload->Data)
        {
            iface.Close(payload);
            return nullptr;
        }

        IOStream* stream = IOFromInterface(&iface, payload);
        if (!stream)
        {
            iface.Close(payload);
        }
        return stream;
    }
} // namespace Juliet::Internal
