#include "../include/FileUtil.hpp"
#include <cstdio>

#if OS_FAMILY == OS_FAMILY_POSIX
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <fcntl.h>
#else
#include <UserEnv.h>
// Copied from linux libc sys/stat.h:
#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
#pragma comment(lib, "Userenv.lib")
#endif

using namespace std;

namespace sibs
{
#if OS_FAMILY == OS_FAMILY_POSIX
#define toUtf8(input) input
    FileString toFileString(const std::string &utf8Str)
    {
        return utf8Str;
    }
    
    FileString toFileString(const StringView &utf8Str)
    {
        return FileString(utf8Str.data, utf8Str.size);
    }
#else
    std::string toUtf8(const sibs::FileString &input)
    {
        std::string result;
        utf8::utf16to8(input.data(), input.data() + input.size(), std::back_inserter(result));
        return result;
    }

    std::string toUtf8(const TCHAR *input)
    {
        size_t inputSize = wcslen(input);
        std::string result;
        utf8::utf16to8(input, input + inputSize, std::back_inserter(result));
        return result;
    }

    FileString utf8To16(const StringView &utf8Str)
    {
        FileString result;
        utf8::utf8to16(utf8Str.data, utf8Str.data + utf8Str.size, std::back_inserter(result));
        return result;
    }

    FileString utf8To16(const std::string &utf8Str)
    {
        FileString result;
        utf8::utf8to16(utf8Str.data(), utf8Str.data() + utf8Str.size(), std::back_inserter(result));
        return result;
    }

    FileString toFileString(const std::string &utf8Str)
    {
        return utf8To16(utf8Str);
    }
    
    FileString toFileString(const StringView &utf8Str)
    {
        FileString result;
        utf8::utf8to16(utf8Str.data, utf8Str.data + utf8Str.size, std::back_inserter(result));
        return result;
    }

    FileString getLastErrorAsString()
    {
        DWORD errorMessageId = GetLastError();
        if (errorMessageId == 0) return TINYDIR_STRING("");
        LPWSTR messageBuffer = nullptr;
        size_t size = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, errorMessageId, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&messageBuffer, 0, nullptr);
        FileString message(messageBuffer, size);
        LocalFree(messageBuffer);
        return message;
    }

    void replaceChar(FileString &input, wchar_t charToReplace, wchar_t charToReplaceWith)
    {
        for (int i = 0; i < input.size(); ++i)
        {
            wchar_t c = input[i];
            if (c == charToReplace)
                input[i] = charToReplaceWith;
        }
    }
#endif

#if OS_FAMILY == OS_FAMILY_POSIX
    FileType getFileType(const _tinydir_char_t *path)
    {
        struct stat64 fileStat;
        if (stat64(path, &fileStat) == 0)
            return S_ISREG(fileStat.st_mode) ? FileType::REGULAR : FileType::DIRECTORY;
        else
            return FileType::FILE_NOT_FOUND;
    }
    
    Result<u64> getFileLastModifiedTime(const _tinydir_char_t *path)
    {
        struct stat64 fileStat;
        if (stat64(path, &fileStat) == 0)
            return Result<u64>::Ok(fileStat.st_mtim.tv_sec);
        else
        {
            string errMsg = "File not found: ";
            errMsg += toUtf8(path);
            return Result<u64>::Err(errMsg);
        }
    }
#else
    FileType getFileType(const _tinydir_char_t *path)
    {
        struct _stat64i32 fileStat;
        if (_wstat(path, &fileStat) == 0)
            return S_ISREG(fileStat.st_mode) ? FileType::REGULAR : FileType::DIRECTORY;
        else
            return FileType::FILE_NOT_FOUND;
    }
    
    Result<u64> getFileLastModifiedTime(const _tinydir_char_t *path)
    {
        struct _stat64i32 fileStat;
        if (_wstat(path, &fileStat) == 0)
            return Result<u64>::Ok(fileStat.st_mtime);
        else
        {
            string errMsg = "File not found: ";
            errMsg += toUtf8(path);
            return Result<u64>::Err(errMsg);
        }
    }
#endif

    // TODO: Handle failure (directory doesn't exist, no permission etc)
    void walkDir(const _tinydir_char_t *directory, FileWalkCallbackFunc callbackFunc)
    {
        tinydir_dir dir;
        tinydir_open(&dir, directory);

        while (dir.has_next)
        {
            tinydir_file file;
            tinydir_readfile(&dir, &file);
            if(_tinydir_strncmp(file.name, TINYDIR_STRING("."), 1) != 0)
                callbackFunc(&file);
            tinydir_next(&dir);
        }

        tinydir_close(&dir);
    }

    // TODO: Handle failure (directory doesn't exist, no permission etc)
    void walkDirFiles(const _tinydir_char_t *directory, FileWalkCallbackFunc callbackFunc)
    {
        tinydir_dir dir;
        tinydir_open(&dir, directory);

        while (dir.has_next)
        {
            tinydir_file file;
            tinydir_readfile(&dir, &file);
            if(file.is_reg)
                callbackFunc(&file);
            tinydir_next(&dir);
        }

        tinydir_close(&dir);
    }

    // TODO: Handle failure (directory doesn't exist, no permission etc)
    void walkDirFilesRecursive(const _tinydir_char_t *directory, FileWalkCallbackFunc callbackFunc)
    {
        tinydir_dir dir;
        tinydir_open(&dir, directory);

        while (dir.has_next)
        {
            tinydir_file file;
            tinydir_readfile(&dir, &file);
            if(file.is_reg)
                callbackFunc(&file);
            else if(_tinydir_strncmp(file.name, TINYDIR_STRING("."), 1) != 0)
                walkDirFilesRecursive(file.path, callbackFunc);
            tinydir_next(&dir);
        }

        tinydir_close(&dir);
    }

    Result<StringView> getFileContent(const _tinydir_char_t *filepath)
    {
#if OS_FAMILY == OS_FAMILY_POSIX
        FILE *file = fopen(filepath, "rb");
#else
        FILE *file = _wfopen(filepath, TINYDIR_STRING("rb"));
#endif
        if(!file)
        {
            int error = errno;
            string errMsg = "Failed to open file: ";
            errMsg += toUtf8(filepath);
            errMsg += "; reason: ";
            errMsg += strerror(error);
            return Result<StringView>::Err(errMsg);
        }

        fseek(file, 0, SEEK_END);
        size_t fileSize = ftell(file);
        fseek(file, 0, SEEK_SET);

        // TODO: Change this to string so it can be deallocated and use std::move to prevent copies
        char *result = (char*)malloc(fileSize + 1);
        if(!result)
        {
            std::string errMsg = "Failed to load file content from file: ";
            errMsg += toUtf8(filepath);
            throw std::runtime_error(errMsg);
        }
        result[fileSize] = '\0';
        fread(result, 1, fileSize, file);
        fclose(file);
        return Result<StringView>::Ok(StringView(result, fileSize));
    }
    
    Result<bool> fileWrite(const _tinydir_char_t *filepath, StringView data)
    {
        if(getFileType(filepath) != FileType::FILE_NOT_FOUND)
        {
            string errMsg = "Failed to write to file: ";
            errMsg += toUtf8(filepath);
            errMsg += "; reason: file already exists";
            return Result<bool>::Err(errMsg);
        }
        return fileOverwrite(filepath, data);
    }

    Result<bool> fileOverwrite(const _tinydir_char_t *filepath, StringView data)
    {
#if OS_FAMILY == OS_FAMILY_POSIX
        FILE *file = fopen(filepath, "wb");
#else
        FILE *file = _wfopen(filepath, TINYDIR_STRING("wb"));
#endif
        if(!file)
        {
            int error = errno;
            string errMsg = "Failed to overwrite file: ";
            errMsg += toUtf8(filepath);
            errMsg += "; reason: ";
            errMsg += strerror(error);
            return Result<bool>::Err(errMsg);
        }
        setbuf(file, NULL);
        fwrite(data.data, 1, data.size, file);
        fclose(file);
        return Result<bool>::Ok(true);
    }
#if OS_FAMILY == OS_FAMILY_POSIX
    Result<FileString> getHomeDir()
    {
        const char *homeDir = getenv("HOME");
        if(!homeDir)
        {
            passwd *pw = getpwuid(getuid());
            homeDir = pw->pw_dir;
        }
        return Result<FileString>::Ok(homeDir);
    }

    Result<FileString> getCwd()
    {
        FileString cwd;
        cwd.resize(_TINYDIR_PATH_MAX);
        if(getcwd(&cwd[0], _TINYDIR_PATH_MAX) != 0)
        {
            if(cwd.empty()) cwd = ".";
            cwd.resize(_tinydir_strlen(cwd.c_str()));
            return Result<FileString>::Ok(cwd);
        }
        return Result<FileString>::Err(strerror(errno));
    }

    Result<bool> createDirectoryRecursive(const _tinydir_char_t *path)
    {
        char pathBuffer[_TINYDIR_PATH_MAX];
        size_t pathLength = strlen(path);
        if(pathLength > sizeof(pathBuffer) - 1)
        {
            string errMsg = "Directory path too long: ";
            errMsg += string(path, pathLength);
            return Result<bool>::Err(errMsg, ENAMETOOLONG);
        }
        strcpy(pathBuffer, path);

        char *p = pathBuffer;
        for(size_t i = 0; i < pathLength; ++i)
        {
            if(i > 0 && *p == '/')
            {
                *p = '\0';
                if(mkdir(pathBuffer, S_IRWXU) != 0)
                {
                    int error = errno;
                    if(error != EEXIST)
                    {
                        string errMsg = "Failed to create directory: ";
                        errMsg += pathBuffer;
                        errMsg += "; reason: ";
                        errMsg += strerror(error);
                        return Result<bool>::Err(errMsg, error);
                    }
                }
                *p = '/';
            }
            ++p;
        }

        if(mkdir(pathBuffer, S_IRWXU) != 0)
        {
            int error = errno;
            if(error != EEXIST)
            {
                string errMsg = "Failed to create directory: ";
                errMsg += pathBuffer;
                errMsg += "; reason: ";
                errMsg += strerror(error);
                return Result<bool>::Err(errMsg, error);
            }
        }

        return Result<bool>::Ok(true);
    }

    Result<FileString> getRealPath(const _tinydir_char_t *path)
    {
        // TODO: Verify NULL can be passed as 'resolved' argument with different compilers and operating systems (clang, freebsd etc)
        char *resolved = realpath(path, nullptr);
        if(!resolved)
        {
            int error = errno;
            FileString errMsg = "Failed to get real path for \"";
            errMsg += path;
            errMsg += "\": ";
            errMsg += strerror(error);
            return Result<FileString>::Err(errMsg, error);
        }

        string result = resolved;
        free(resolved);
        return Result<FileString>::Ok(result);
    }
#else

	Result<FileString> getHomeDir()
	{
        BOOL ret;
        HANDLE hToken;
        FileString homeDir;
        DWORD homeDirLen = _TINYDIR_PATH_MAX;
        homeDir.resize(homeDirLen);

        if (!OpenProcessToken(GetCurrentProcess(), TOKEN_READ, &hToken))
            return Result<FileString>::Err("Failed to open process token");

        if (!GetUserProfileDirectory(hToken, &homeDir[0], &homeDirLen))
        {
            CloseHandle(hToken);
            return Result<FileString>::Err("Failed to get home directory");
        }
        
        CloseHandle(hToken);
        homeDir.resize(_tinydir_strlen(homeDir.c_str()));
        return Result<FileString>::Ok(homeDir);
	}

	Result<FileString> getCwd()
	{
		FileString cwd;
		cwd.resize(_TINYDIR_PATH_MAX);
		if (GetCurrentDirectory(_TINYDIR_PATH_MAX, &cwd[0]) == 0)
		{
            FileString lastErrStr = getLastErrorAsString();
            return Result<FileString>::Err(toUtf8(lastErrStr));
		}
        cwd.resize(_tinydir_strlen(cwd.c_str()));
        return Result<FileString>::Ok(cwd);
	}

	Result<bool> createDirectoryRecursive(const _tinydir_char_t *path)
	{
        _tinydir_char_t pathBuffer[_TINYDIR_PATH_MAX];
		size_t pathLength = _tinydir_strlen(path);
		if (pathLength > sizeof(pathBuffer) - 1)
		{
			string errMsg = "Directory path too long: ";
			errMsg += toUtf8(FileString(path, pathLength));
			return Result<bool>::Err(errMsg, ENAMETOOLONG);
		}
		_tinydir_strcpy(pathBuffer, path);

        _tinydir_char_t *p = pathBuffer;
		for (size_t i = 0; i < pathLength; ++i)
		{
			if (i > 0 && *p == '/')
			{
				*p = '\0';
				if (_wmkdir(pathBuffer) != 0)
				{
					int error = errno;
					if (error != EEXIST)
					{
						string errMsg = "Failed to create directory: ";
						errMsg += toUtf8(pathBuffer);
						errMsg += "; reason: ";
						errMsg += strerror(error);
						return Result<bool>::Err(errMsg, error);
					}
				}
				*p = '/';
			}
			++p;
		}

		if (_wmkdir(pathBuffer) != 0)
		{
			int error = errno;
			if (error != EEXIST)
			{
				string errMsg = "Failed to create directory: ";
				errMsg += toUtf8(pathBuffer);
				errMsg += "; reason: ";
				errMsg += strerror(error);
				return Result<bool>::Err(errMsg, error);
			}
		}

		return Result<bool>::Ok(true);
	}

	Result<FileString> getRealPath(const _tinydir_char_t *path)
	{
        FileString fullPath;
        fullPath.resize(_TINYDIR_PATH_MAX);
        if (GetFullPathName(path, _TINYDIR_PATH_MAX, &fullPath[0], nullptr) == 0)
        {
            int error = GetLastError();
			string errMsg = "Failed to get real path for \"";
			errMsg += toUtf8(path);
			errMsg += "\": ";
			errMsg += toUtf8(getLastErrorAsString());
			return Result<FileString>::Err(errMsg, error);
		}
        fullPath.resize(_tinydir_strlen(fullPath.c_str()));
		return Result<FileString>::Ok(fullPath);
	}
#endif

    // TODO: Support better path equality check. For example if path contains several slashes in a row: /home/userName/.sibs//lib////libraryName
    // then it should equal: /home/userName/.sibs/lib/libraryName
    // Maybe check with OS operation if they refer to the same inode?
    bool pathEquals(const std::string &path, const std::string &otherPath)
    {
        if(path.size() != otherPath.size())
            return false;
        
        size_t size = path.size();
        for(size_t i = 0; i < size; ++i)
        {
            char c = path[i];
            char otherC = otherPath[i];
            if(c == '\\') c = '/';
            if(otherC == '\\') otherC = '/';
            if(c != otherC)
                return false;
        }
        
        return true;
    }
}