/**
 * @file utf8wrap.cpp
 * @author Christian Schenk
 * @brief UTF-8 wrapper functions
 *
 * @copyright Copyright © 2011-2022 Christian Schenk
 *
 * This file is part of the MiKTeX UTF8Wrap Library.
 *
 * The MiKTeX UTF8Wrap Library is licensed under GNU General Public
 * License version 2 or any later version.
 */

#include <Windows.h>

#include <fcntl.h>

#include <cstdarg>
#include <memory>
#include <stdexcept>
#include <unordered_map>
#include <vector>

#include <miktex/Util/PathNameUtil>
#include <miktex/Util/StringUtil>

#include "internal.h"

using namespace std;

using namespace MiKTeX::Util;

class utf8wraperror :
    public std::exception
{
public:

    utf8wraperror(const char* function, const char* utf8)
    {
        sprintf_s(errorMessage, "UTF-8 conversion failed; func=%s, utf8=\"%s\"", function, utf8);
    }

    utf8wraperror(const char* function, const wchar_t* wch)
    {
        sprintf_s(errorMessage, "UTF-8 conversion failed; func=%s, LPCWCH=\"%S\"", function, wch);
    }

    const char* what() const override
    {
        return errorMessage;
    }

private:

    char errorMessage[2048];
};

MIKTEXSTATICFUNC(unique_ptr<wchar_t[]>) UTF8ToLengthExtendedPath(const char* utf8String, const char* function)
{
    try
    {
        wstring wch = PathNameUtil::ToLengthExtendedPathName(utf8String);
        unique_ptr<wchar_t[]> buf(new wchar_t[wch.length() + 1]);
        StringUtil::CopyString(buf.get(), wch.length() + 1, wch.c_str());
        return buf;
    }
    catch (const exception&)
    {
        throw utf8wraperror(function, utf8String);
    }
}


MIKTEXSTATICFUNC(unique_ptr<wchar_t[]>) UTF8ToWideChar(const char* utf8String, const char* function)
{
    try
    {
        wstring wch = StringUtil::UTF8ToWideChar(utf8String);
        unique_ptr<wchar_t[]> buf(new wchar_t[wch.length() + 1]);
        StringUtil::CopyString(buf.get(), wch.length() + 1, wch.c_str());
        return buf;
    }
    catch (const exception&)
    {
        throw utf8wraperror(function, utf8String);
    }
}

MIKTEXSTATICFUNC(unique_ptr<char[]>) WideCharToUTF8(const wchar_t* wideCharString, const char* function)
{
    try
    {
        string utf8 = StringUtil::WideCharToUTF8(wideCharString);
        unique_ptr<char[]> buf(new char[utf8.length() + 1]);
        StringUtil::CopyString(buf.get(), utf8.length() + 1, utf8.c_str());
        return buf;
    }
    catch (const exception&)
    {
        throw utf8wraperror(function, wideCharString);
    }
};

#define EXPATH_(x) UTF8ToLengthExtendedPath(x, __func__).get()
#define UW_(x) UTF8ToWideChar(x, __func__).get()
#define WU_(x) WideCharToUTF8(x, __func__).get()

MIKTEXUTF8WRAPCEEAPI(FILE*) miktex_utf8_fopen(const char* path, const char* mode)
{
    return _wfopen(EXPATH_(path), UW_(mode));
}

MIKTEXUTF8WRAPCEEAPI(int) miktex_utf8__open(const char* path, int flags, ...)
{
    int pmode = 0;
    if ((flags & _O_CREAT) != 0)
    {
        va_list ap;
        va_start(ap, flags);
        pmode = va_arg(ap, int);
        va_end(ap);
    }
    return _wopen(EXPATH_(path), flags, pmode);
}

MIKTEXUTF8WRAPCEEAPI(int) miktex_utf8__stat64i32(const char* path, struct _stat64i32* statBuf)
{
    return _wstat(EXPATH_(path), statBuf);
}

MIKTEXUTF8WRAPCEEAPI(int) miktex_utf8__stat64(const char* path, struct _stat64* statBuf)
{
    return _wstat64(EXPATH_(path), statBuf);
}

MIKTEXUTF8WRAPCEEAPI(int) miktex_utf8__unlink(const char* path)
{
    return _wunlink(EXPATH_(path));
}

MIKTEXUTF8WRAPCEEAPI(int) miktex_utf8_remove(const char* path)
{
    return _wremove(EXPATH_(path));
}

MIKTEXUTF8WRAPCEEAPI(int) miktex_utf8__access(const char* path, int mode)
{
    return _waccess(EXPATH_(path), mode);
}

MIKTEXUTF8WRAPCEEAPI(int) miktex_utf8__chmod(const char* path, int mode)
{
    return _wchmod(EXPATH_(path), mode);
}

MIKTEXUTF8WRAPCEEAPI(int) miktex_utf8__mkdir(const char* path)
{
    return _wmkdir(EXPATH_(path));
}

MIKTEXUTF8WRAPCEEAPI(int) miktex_utf8__rmdir(const char* path)
{
    return _wrmdir(EXPATH_(path));
}

MIKTEXUTF8WRAPCEEAPI(int) miktex_utf8__chdir(const char* path)
{
    return _wchdir(EXPATH_(path));
}

MIKTEXUTF8WRAPCEEAPI(char*) miktex_utf8__getcwd(char* path, size_t maxSize)
{
    unique_ptr<wchar_t[]> wideChar(new wchar_t[maxSize]);
    if (_wgetcwd(wideChar.get(), static_cast<int>(maxSize)) == nullptr)
    {
        return nullptr;
    }
    unique_ptr<char[]> utf8(WideCharToUTF8(wideChar.get(), __func__));
    if (strlen(utf8.get()) >= maxSize)
    {
        throw std::runtime_error("buffer too small");
    }
    strcpy_s(path, maxSize, utf8.get());
    return path;
}

static unordered_map<string, unique_ptr<char[]>> env;

MIKTEXUTF8WRAPCEEAPI(char*) miktex_utf8_getenv(const char* name)
{
    wchar_t* wideCharString = _wgetenv(UW_(name));
    if (wideCharString == nullptr)
    {
        return nullptr;
    }
    env[name] = WideCharToUTF8(wideCharString, __func__);
    return env[name].get();
}

MIKTEXUTF8WRAPCEEAPI(int) miktex_utf8_putenv(const char* envString)
{
    return _wputenv(UW_(envString));
}

MIKTEXUTF8WRAPCEEAPI(int) miktex_utf8__utime64(const char* path, struct __utimbuf64* timeBuf)
{
    return _wutime64(EXPATH_(path), timeBuf);
}

MIKTEXUTF8WRAPCEEAPI(int) miktex_utf8_rename(const char* oldName, const char* newName)
{
    return _wrename(EXPATH_(oldName), EXPATH_(newName));
}

MIKTEXUTF8WRAPCEEAPI(intptr_t) miktex_utf8__spawnvp(int mode, const char* path, const char* const* argv)
{
    vector<wstring> wideArguments;
    int count;
    for (count = 0; argv[count] != nullptr; ++count)
    {
        wideArguments.push_back(UW_(argv[count]));
    }
    vector<const wchar_t*> wargv;
    wargv.reserve(count + 1);
    for (int idx = 0; idx < count; ++idx)
    {
        wargv.push_back(wideArguments[idx].c_str());
    }
    wargv.push_back(nullptr);
    return _wspawnvp(mode, UW_(path), &wargv[0]);
}

MIKTEXUTF8WRAPCEEAPI(int) miktex_utf8_system(const char* command)
{
    return _wsystem(command == nullptr ? nullptr : UW_(command));
}

MIKTEXSTATICFUNC(HANDLE) GetConsoleHandle(FILE* file)
{
    bool isStdout = _fileno(file) == _fileno(stdout);
    bool isStderr = _fileno(file) == _fileno(stderr);
    if (_isatty(_fileno(file)) && (isStdout || isStderr))
    {
        return isStdout ? GetStdHandle(STD_OUTPUT_HANDLE) : GetStdHandle(STD_ERROR_HANDLE);
    }
    else
    {
        return INVALID_HANDLE_VALUE;
    }
}

#define MIKTEX_UTF8_CONSOLE_WARNING 0

#if MIKTEX_UTF8_CONSOLE_WARNING
class NonUtf8ConsoleWarning
{

public:

    NonUtf8ConsoleWarning()
        : console(console)
    {
    }

    ~NonUtf8ConsoleWarning()
    {
        const char* envvar = "MIKTEX_UTF8_SUPPRESS_CONSOLE_WARNING";
        if (console != nullptr && getenv(envvar) == nullptr)
        {
            fprintf(console, "\
\n\n\
*** Warning:\n\
***\n\
*** Some characters could not be printed because the console's active code\n\
*** page is not UTF-8.\n\
***\n\
*** See http://miktex.org/howto/utf8-console for more information.\n\
***\n\
*** You can suppress this warning by setting the environment variable\n\
*** %s\n",
"MIKTEX_UTF8_SUPPRESS_CONSOLE_WARNING");
        }
    }

    void Emit(FILE* console)
    {
        this->console = console;
    }

private:

    FILE* console;
};
#endif

MIKTEXUTF8WRAPCEEAPI(int) miktex_utf8_fputc(int ch, FILE* file)
{
    HANDLE hConsole = GetConsoleHandle(file);
    if (hConsole == INVALID_HANDLE_VALUE)
    {
        return fputc(ch, file);
    }
    if (GetConsoleOutputCP() != CP_UTF8)
    {
#if MIKTEX_UTF8_CONSOLE_WARNING
        static NonUtf8ConsoleWarning warning;
        if ((ch & 0x80) != 0)
        {
            warning.Emit(file);
            ch = '?';
        }
#endif
        return fputc(ch, file);
    }
    static int remaining = 0;
    static char buf[5];
    static int bufidx = 0;
    if (bufidx > 4)
    {
        throw std::runtime_error("invalid UTF-8 byte sequence");
    }
    buf[bufidx++] = ch;
    if ((ch & 0x80) == 0)
    {
        remaining = 0;
    }
    else if ((ch & 0xc0) == 0x80)
    {
        if (remaining == 0)
        {
            throw std::runtime_error("invalid UTF-8 byte sequence");
        }
        --remaining;
    }
    else if ((ch & 0xe0) == 0xc0)
    {
        remaining = 1;
    }
    else if ((ch & 0xf0) == 0xe0)
    {
        remaining = 2;
    }
    else if ((ch & 0xf8) == 0xf0)
    {
        remaining = 3;
    }
    if (remaining == 0)
    {
        buf[bufidx] = 0;
        bufidx = 0;
        if (fputs(buf, file) < 0)
        {
            ch = EOF;
        }
    }
    return ch;
}

MIKTEXUTF8WRAPCEEAPI(int) miktex_utf8_putc(int ch, FILE* file)
{
    return miktex_utf8_fputc(ch, file);
}

MIKTEXUTF8WRAPCEEAPI(int) miktex_utf8_putchar(int ch)
{
    return miktex_utf8_fputc(ch, stdout);
}

#if MIKTEX_UTF8_CONSOLE_WARNING
MIKTEXUTF8WRAPCEEAPI(int) miktex_utf8_fprintf(FILE* file, const char* format, ...)
{
    HANDLE hConsole = GetConsoleHandle(file);
    va_list ap;
    va_start(ap, format);
    int ret;
    if (hConsole == INVALID_HANDLE_VALUE || GetConsoleOutputCP() == CP_UTF8)
    {
        ret = vfprintf(file, format, ap);
    }
    else
    {
        int n = _vscprintf(format, ap);
        if (n >= 0)
        {
            char* buffer = new char[n + 1];
            n = vsprintf_s(buffer, n + 1, format, ap);
            if (n >= 0)
            {
                // TODO: check buffer and warn if it contains UTF-8 bytes
                n = fputs(buffer, file);
            }
            delete[] buffer;
        }
    }
    va_end(ap);
    return ret;
}
#endif
