#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <system_error>
#include <string>

#include <HECL/HECL.hpp>
#include <LogVisor/LogVisor.hpp>
#include "BlenderConnection.hpp"

namespace HECL
{

LogVisor::LogModule BlenderLog("BlenderConnection");
BlenderConnection* SharedBlenderConnection = nullptr;

#ifdef __APPLE__
#define DEFAULT_BLENDER_BIN "/Applications/Blender.app/Contents/MacOS/blender"
#elif _WIN32
#define DEFAULT_BLENDER_BIN _S("%ProgramFiles%\\Blender Foundation\\Blender\\blender.exe")
#else
#define DEFAULT_BLENDER_BIN "blender"
#endif

#define TEMP_SHELLSCRIPT "/home/jacko/hecl/blender/blendershell.py"

size_t BlenderConnection::_readLine(char* buf, size_t bufSz)
{
    size_t readBytes = 0;
    while (true)
    {
        if (readBytes >= bufSz)
        {
            BlenderLog.report(LogVisor::FatalError, "Pipe buffer overrun\n");
            *(buf-1) = '\0';
            return bufSz - 1;
        }
#if _WIN32
        DWORD ret = 0;
        if (!ReadFile(m_readpipe[0], buf, 1, &ret, NULL))
            goto err;
#else
        ssize_t ret = read(m_readpipe[0], buf, 1);
        if (ret < 0)
            goto err;
#endif
        else if (ret == 1)
        {
            if (*buf == '\n')
            {
                *buf = '\0';
                return readBytes;
            }
            ++readBytes;
            ++buf;
        }
        else
        {
            *buf = '\0';
            return readBytes;
        }
    }
err:
    BlenderLog.report(LogVisor::FatalError, strerror(errno));
    return 0;
}

size_t BlenderConnection::_writeLine(const char* buf)
{
#if _WIN32
    DWORD ret = 0;
    if (!WriteFile(m_writepipe[1], buf, strlen(buf), &ret, NULL))
        goto err;
    if (!WriteFile(m_writepipe[1], "\n", 1, NULL, NULL))
        goto err;
#else
    ssize_t ret, nlerr;
    ret = write(m_writepipe[1], buf, strlen(buf));
    if (ret < 0)
        goto err;
    nlerr = write(m_writepipe[1], "\n", 1);
    if (nlerr < 0)
        goto err;
#endif
    return (size_t)ret;
err:
    BlenderLog.report(LogVisor::FatalError, strerror(errno));
    return 0;
}

size_t BlenderConnection::_readBuf(char* buf, size_t len)
{
#if _WIN32
    DWORD ret = 0;
    if (!ReadFile(m_readpipe[0], buf, len, &ret, NULL))
        goto err;
#else
    ssize_t ret = read(m_readpipe[0], buf, len);
    if (ret < 0)
        goto err;
#endif
    return ret;
err:
    BlenderLog.report(LogVisor::FatalError, strerror(errno));
    return 0;
}

size_t BlenderConnection::_writeBuf(const char* buf, size_t len)
{
#if _WIN32
    DWORD ret = 0;
    if (!WriteFile(m_writepipe[1], buf, len, &ret, NULL))
        goto err;
#else
    ssize_t ret = write(m_writepipe[1], buf, len);
    if (ret < 0)
        goto err;
#endif
    return ret;
err:
    BlenderLog.report(LogVisor::FatalError, strerror(errno));
    return 0;
}

void BlenderConnection::_closePipe()
{
#if _WIN32
    CloseHandle(m_readpipe[0]);
    CloseHandle(m_writepipe[1]);
#else
    close(m_readpipe[0]);
    close(m_writepipe[1]);
#endif
}

BlenderConnection::BlenderConnection(bool silenceBlender)
{
    /* Construct communication pipes */
#if _WIN32
    SECURITY_ATTRIBUTES sattrs = {sizeof(SECURITY_ATTRIBUTES), NULL, TRUE};
    CreatePipe(&m_readpipe[0], &m_readpipe[1], &sattrs, 0);
    CreatePipe(&m_writepipe[0], &m_writepipe[1], &sattrs, 0);
#else
    pipe(m_readpipe);
    pipe(m_writepipe);
#endif

    /* User-specified blender path */
#if _WIN32
    wchar_t BLENDER_BIN_BUF[2048];
    wchar_t* blenderBin = _wgetenv(L"BLENDER_BIN");
#else
    char* blenderBin = getenv("BLENDER_BIN");
#endif

    /* Child process of blender */
#if _WIN32
    if (!blenderBin)
    {
        /* Environment not set; use registry */
        HKEY blenderKey;
        if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\BlenderFoundation", 0, KEY_READ, &blenderKey) == ERROR_SUCCESS)
        {
            DWORD bufSz = sizeof(BLENDER_BIN_BUF);
            if (RegGetValueW(blenderKey, NULL, L"Install_Dir", REG_SZ, NULL, BLENDER_BIN_BUF, &bufSz) == ERROR_SUCCESS)
            {
                wcscat_s(BLENDER_BIN_BUF, 2048, L"\\blender.exe");
                blenderBin = BLENDER_BIN_BUF;
            }
            RegCloseKey(blenderKey);
        }
    }
    if (!blenderBin)
    {
        Log.report(LogVisor::FatalError, "unable to find blender");
        return;
    }

    wchar_t cmdLine[2048];
    _snwprintf(cmdLine, 2048, L" --background -P shellscript.py -- %08X %08X", 
               (uint32_t)m_writepipe[0], (uint32_t)m_readpipe[1]);

    STARTUPINFO sinfo = {sizeof(STARTUPINFO)};
    HANDLE nulHandle = NULL;
    if (silenceBlender)
    {
        nulHandle = CreateFileW(L"nul", GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, &sattrs, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
        sinfo.hStdError = nulHandle;
        sinfo.hStdOutput = nulHandle;
        sinfo.dwFlags = STARTF_USESTDHANDLES;
    }

    PROCESS_INFORMATION pinfo;
    if (!CreateProcessW(blenderBin, cmdLine, NULL, NULL, TRUE, NORMAL_PRIORITY_CLASS, NULL, NULL, &sinfo, &pinfo))
        Log.report(LogVisor::FatalError, "unable to launch blender");

    CloseHandle(m_writepipe[1]);
    CloseHandle(m_readpipe[0]);

    if (nulHandle)
        CloseHandle(nulHandle);

#else
    pid_t pid = fork();
    if (!pid)
    {
        close(m_writepipe[1]);
        close(m_readpipe[0]);

        if (silenceBlender)
        {
            close(STDOUT_FILENO);
            close(STDERR_FILENO);
        }

        char errbuf[256];
        char readfds[32];
        snprintf(readfds, 32, "%d", m_writepipe[0]);
        char writefds[32];
        snprintf(writefds, 32, "%d", m_readpipe[1]);

        /* Try user-specified blender first */
        if (blenderBin)
        {
            execlp(blenderBin, blenderBin,
                   "--background", "-P", TEMP_SHELLSCRIPT,
                   "--", readfds, writefds, NULL);
            if (errno != ENOENT)
            {
                snprintf(errbuf, 256, "NOLAUNCH %s\n", strerror(errno));
                write(m_writepipe[1], errbuf, strlen(errbuf));
                exit(1);
            }
        }

        /* Otherwise default blender */
        execlp(DEFAULT_BLENDER_BIN, DEFAULT_BLENDER_BIN,
               "--background", "-P", TEMP_SHELLSCRIPT,
               "--", readfds, writefds, NULL);
        if (errno != ENOENT)
        {
            snprintf(errbuf, 256, "NOLAUNCH %s\n", strerror(errno));
            write(m_writepipe[1], errbuf, strlen(errbuf));
            exit(1);
        }

        /* Unable to find blender */
        write(m_writepipe[1], "NOBLENDER\n", 10);
        exit(1);

    }
    close(m_writepipe[0]);
    close(m_readpipe[1]);
    m_blenderProc = pid;
#endif

    /* Handle first response */
    char lineBuf[256];
    _readLine(lineBuf, sizeof(lineBuf));
    if (!strcmp(lineBuf, "NOLAUNCH"))
    {
        _closePipe();
        BlenderLog.report(LogVisor::FatalError, "Unable to launch blender");
    }
    else if (!strcmp(lineBuf, "NOBLENDER"))
    {
        _closePipe();
        if (blenderBin)
            BlenderLog.report(LogVisor::FatalError, _S("Unable to find blender at '%s' or '%s'"),
                       blenderBin, DEFAULT_BLENDER_BIN);
        else
            BlenderLog.report(LogVisor::FatalError, _S("Unable to find blender at '%s'"),
                       DEFAULT_BLENDER_BIN);
    }
    else if (!strcmp(lineBuf, "NOADDON"))
    {
        _closePipe();
        BlenderLog.report(LogVisor::FatalError, "HECL addon not installed within blender");
    }
    else if (strcmp(lineBuf, "READY"))
    {
        _closePipe();
        BlenderLog.report(LogVisor::FatalError, "read '%s' from blender; expected 'READY'", lineBuf);
    }
    _writeLine("ACK");

}

BlenderConnection::~BlenderConnection()
{
    _closePipe();
}

bool BlenderConnection::createBlend(const SystemString& path)
{
    _writeLine(("CREATE \"" + path + "\"").c_str());
    char lineBuf[256];
    _readLine(lineBuf, sizeof(lineBuf));
    if (!strcmp(lineBuf, "FINISHED"))
    {
        m_loadedBlend = path;
        return true;
    }
    return false;
}

bool BlenderConnection::openBlend(const SystemString& path)
{
    _writeLine(("OPEN \"" + path + "\"").c_str());
    char lineBuf[256];
    _readLine(lineBuf, sizeof(lineBuf));
    if (!strcmp(lineBuf, "FINISHED"))
    {
        m_loadedBlend = path;
        return true;
    }
    return false;
}

bool BlenderConnection::cookBlend(std::function<char*(uint32_t)> bufGetter,
                                   const std::string& expectedType,
                                   const std::string& platform,
                                   bool bigEndian)
{
    char lineBuf[256];
    char reqLine[512];
    snprintf(reqLine, 512, "COOK %s %c", platform.c_str(), bigEndian?'>':'<');
    _writeLine(reqLine);
    _readLine(lineBuf, sizeof(lineBuf));
    if (strcmp(expectedType.c_str(), lineBuf))
    {
        BlenderLog.report(LogVisor::Error, "expected '%s' to contain '%s' not '%s'",
                   m_loadedBlend.c_str(), expectedType.c_str(), lineBuf);
        return false;
    }
    _writeLine("ACK");

    for (_readLine(lineBuf, sizeof(lineBuf));
         !strcmp("BUF", lineBuf);
         _readLine(lineBuf, sizeof(lineBuf)))
    {
        uint32_t sz;
        _readBuf((char*)&sz, 4);
        char* buf = bufGetter(sz);
        _readBuf(buf, sz);
    }
    if (!strcmp("SUCCESS", lineBuf))
        return true;
    else if (!strcmp("EXCEPTION", lineBuf))
        BlenderLog.report(LogVisor::FatalError, "blender script exception");

    return false;
}

void BlenderConnection::quitBlender()
{
    _writeLine("QUIT");
    char lineBuf[256];
    _readLine(lineBuf, sizeof(lineBuf));
}

}