// Copyright (c) Wojciech Figat. All rights reserved.

#include "Engine/Platform/FileSystem.h"
#include "Engine/Platform/File.h"
#include "Engine/Core/Types/Guid.h"
#include "Engine/Core/Types/String.h"
#include "Engine/Core/Types/StringView.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Core/Log.h"
#include "Engine/Engine/Globals.h"

bool FileSystemBase::ShowOpenFileDialog(Window* parentWindow, const StringView& initialDirectory, const StringView& filter, bool multiSelect, const StringView& title, Array<String, HeapAllocation>& filenames)
{
    // Not supported
    return true;
}

bool FileSystemBase::ShowSaveFileDialog(Window* parentWindow, const StringView& initialDirectory, const StringView& filter, bool multiSelect, const StringView& title, Array<String, HeapAllocation>& filenames)
{
    // Not supported
    return true;
}

bool FileSystemBase::ShowBrowseFolderDialog(Window* parentWindow, const StringView& initialDirectory, const StringView& title, String& path)
{
    // Not supported
    return true;
}

bool FileSystemBase::ShowFileExplorer(const StringView& path)
{
    // Not supported
    return true;
}

void FileSystemBase::SaveBitmapToFile(byte* data, uint32 width, uint32 height, uint32 bitsPerPixel, const uint32 padding, const String& path)
{
    // Try to open file
    auto file = File::Open(path, FileMode::CreateAlways, FileAccess::Write, FileShare::None);
    if (file == nullptr)
        return;

    const unsigned long headers_size = 54;
    const unsigned long pixel_data_size = height * ((width * bitsPerPixel / 8) + padding);
    const unsigned int filesize = headers_size + pixel_data_size;
    uint8 bmpfileheader[14] = { 'B', 'M', 0, 0, 0, 0, 0, 0, 0, 0, 54, 0, 0, 0 };
    uint8 bmpinfoheader[40] = { 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 24, 0 };
    uint8 bmppad[3] = { 0, 0, 0 };

    // Init the file header
    bmpfileheader[2] = (uint8)(filesize);
    bmpfileheader[3] = (uint8)(filesize >> 8);
    bmpfileheader[4] = (uint8)(filesize >> 16);
    bmpfileheader[5] = (uint8)(filesize >> 24);

    // Init the bitmap info header
    bmpinfoheader[4] = (uint8)(width);
    bmpinfoheader[5] = (uint8)(width >> 8);
    bmpinfoheader[6] = (uint8)(width >> 16);
    bmpinfoheader[7] = (uint8)(width >> 24);
    bmpinfoheader[8] = (uint8)(height);
    bmpinfoheader[9] = (uint8)(height >> 8);
    bmpinfoheader[10] = (uint8)(height >> 16);
    bmpinfoheader[11] = (uint8)(height >> 24);

    // Write the file header
    file->Write(bmpfileheader, sizeof(bmpfileheader));

    // Write the bitmap info header  
    file->Write(bmpinfoheader, sizeof(bmpinfoheader));

    // Write the RGB Data
    file->Write(data, pixel_data_size);

    // Close the file
    Delete(file);
}

bool FileSystemBase::AreFilePathsEqual(const StringView& path1, const StringView& path2)
{
    if (path1.Compare(path2, StringSearchCase::CaseSensitive) == 0)
        return true;

    // Normalize file paths
    String filename1(path1);
    String filename2(path2);
    NormalizePath(filename1);
    NormalizePath(filename2);

    return filename1.Compare(filename2, StringSearchCase::IgnoreCase) == 0;
}

void FileSystemBase::NormalizePath(String& path)
{
    path.Replace('\\', '/');
    if (path.Length() > 2 && StringUtils::IsAlpha(path[0]) && path[1] == ':')
    {
        path[2] = '\\';
    }
}

bool FileSystemBase::IsRelative(const StringView& path)
{
    const bool isRooted =
            (path.Length() >= 2 && StringUtils::IsAlpha(path[0]) && path[1] == ':') ||
            path.StartsWith(StringView(TEXT("\\\\"), 2), StringSearchCase::CaseSensitive) ||
            path.StartsWith('\\') ||
            path.StartsWith('/');
    return !isRooted;
}

String FileSystemBase::GetExtension(const StringView& path)
{
    Char chr;
    int32 length = path.Length();
    int32 num = length;
    do
    {
        num--;
        if (num < 0)
        {
            break;
        }
        chr = path[num];
        if (chr != '.')
        {
            continue;
        }
        if (num == length - 1)
        {
            return String::Empty;
        }
        num++;
        return path.Substring(num, length - num);
    } while (chr != TEXT('\\') && chr != TEXT('/') && chr != TEXT(':'));

    return String::Empty;
}

void FileSystemBase::GetTempFilePath(String& tmpPath)
{
    tmpPath = Globals::TemporaryFolder / Guid::New().ToString(Guid::FormatType::N);
}

static void SplitPath(const String& path, Array<String>& splitPath)
{
    int32 start = 0;
    int32 separatorPos;

    do
    {
        separatorPos = path.FindFirstOf(TEXT("\\/"), start);
        if (separatorPos == -1)
            splitPath.Add(path.Substring(start));
        else
            splitPath.Add(path.Substring(start, separatorPos - start));
        start = separatorPos + 1;
    } while (separatorPos != -1);
}

String FileSystemBase::ConvertAbsolutePathToRelative(const String& basePath, const String& path)
{
    Array<String> toDirs;
    Array<String> fromDirs;

    SplitPath(path, toDirs);
    SplitPath(basePath, fromDirs);

    String output;

    Array<String>::Iterator toIt = toDirs.Begin(), fromIt = fromDirs.Begin();
    const Array<String>::Iterator toEnd = toDirs.End(), fromEnd = fromDirs.End();

    while (toIt != toEnd && fromIt != fromEnd && *toIt == *fromIt)
    {
        ++toIt;
        ++fromIt;
    }

    while (fromIt != fromEnd)
    {
        output += TEXT("../");
        ++fromIt;
    }

    while (toIt != toEnd)
    {
        output += *toIt;
        ++toIt;

        if (toIt != toEnd)
            output += '/';
    }

    return output;
}

bool FileSystemBase::CopyFile(const StringView& dst, const StringView& src)
{
    // Open and create files
    const auto srcFile = File::Open(src, FileMode::OpenExisting, FileAccess::Read);
    if (!srcFile)
    {
        return true;
    }
    const auto dstFile = File::Open(dst, FileMode::CreateAlways, FileAccess::Write);
    if (!dstFile)
    {
        Delete(srcFile);
        return true;
    }

    // Skip for empty file
    uint32 size = srcFile->GetSize();
    if (size == 0)
    {
        Delete(srcFile);
        Delete(dstFile);
        return false;
    }

    // Copy data
    const uint32 bufferSize = Math::Min<uint32>(1024 * 1024, size);
    byte* buffer = (byte*)Allocator::Allocate(bufferSize);
    while (size)
    {
        const uint32 readSize = Math::Min<uint32>(bufferSize, size);
        srcFile->Read(buffer, readSize);
        dstFile->Write(buffer, readSize);
        size -= readSize;
    }
    Allocator::Free(buffer);

    // Cleanup
    Delete(srcFile);
    Delete(dstFile);

    return false;
}

bool FileSystemBase::CopyDirectory(const String& dst, const String& src, bool withSubDirectories)
{
    return !FileSystem::DirectoryExists(*src) || FileSystemBase::DirectoryCopyHelper(dst, src, withSubDirectories);
}

uint64 FileSystemBase::GetDirectorySize(const StringView& path, const Char* searchPattern, DirectorySearchOption option)
{
    uint64 result = 0;
    Array<String> files;
    FileSystem::DirectoryGetFiles(files, path, searchPattern, option);
    for (const String& file : files)
        result += FileSystem::GetFileSize(file);
    return result;
}

String FileSystemBase::ConvertRelativePathToAbsolute(const String& path)
{
    return ConvertRelativePathToAbsolute(Globals::StartupFolder, path);
}

String FileSystemBase::ConvertRelativePathToAbsolute(const String& basePath, const String& path)
{
    String fullyPathed;
    if (IsRelative(path))
    {
        fullyPathed = basePath;
    }

    fullyPathed /= path;

    NormalizePath(fullyPathed);
    return fullyPathed;
}

String FileSystemBase::ConvertAbsolutePathToRelative(const String& path)
{
    return ConvertAbsolutePathToRelative(Globals::StartupFolder, path);
}

bool FileSystemBase::DirectoryCopyHelper(const String& dst, const String& src, bool withSubDirectories)
{
    // Create dst directory
    if (!FileSystem::DirectoryExists(dst))
    {
        if (FileSystem::CreateDirectory(dst))
            return true;
    }

    // Copy all files
    Array<String> cache(32);
    if (FileSystem::DirectoryGetFiles(cache, *src, TEXT("*"), DirectorySearchOption::TopDirectoryOnly))
        return true;
    for (int32 i = 0; i < cache.Count(); i++)
    {
        String dstFile = dst / StringUtils::GetFileName(cache[i]);
        if (FileSystem::CopyFile(*dstFile, *cache[i]))
            return true;
    }

    // Copy all subdirectories (if need to)
    if (withSubDirectories)
    {
        cache.Clear();
        if (FileSystem::GetChildDirectories(cache, src))
            return true;
        for (int32 i = 0; i < cache.Count(); i++)
        {
            String dstDir = dst / StringUtils::GetFileName(cache[i]);
            if (DirectoryCopyHelper(dstDir, cache[i], true))
                return true;
        }
    }

    return false;
}

bool FileSystemBase::PathFilterHelper(const char* path, const char* searchPattern)
{
    // Validate with filter
    const int32 pathLength = StringUtils::Length(path);
    const int32 searchPatternLength = StringUtils::Length(searchPattern);
    if (searchPatternLength == 0 ||
        StringUtils::Compare(searchPattern, "*") == 0 ||
        StringUtils::Compare(searchPattern, "*.*") == 0)
    {
        // All files
        return true;
    }
    else if (searchPattern[0] == '*' && StringUtils::Find(searchPattern + 1, "*") == nullptr)
    {
        // Path ending
        return searchPatternLength < pathLength && StringUtils::Compare(path + pathLength - searchPatternLength + 1, searchPattern + 1, searchPatternLength - 1) == 0;
    }
    else if (searchPattern[0] == '*' && searchPatternLength > 2 && searchPattern[searchPatternLength - 1] == '*')
    {
        // Contains pattern
        bool match = false;
        for (int32 i = 0; i < pathLength - searchPatternLength - 1; i++)
        {
            int32 len = Math::Min(searchPatternLength - 2, pathLength - i);
            if (StringUtils::Compare(&path[i], &searchPattern[1], len) == 0)
                return true;
        }
    }
    else
    {
        // TODO: implement all cases in a generic way
        LOG(Warning, "DirectoryGetFiles: Wildcard filter is not implemented ({})", String(searchPattern));
    }
    return false;
}
