/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include "opengl/x11/X11DeviceInfo.hxx"
#include "opengl/x11/glxtest.hxx"

#include <config_features.h>

#include <rtl/ustring.hxx>

#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <sys/utsname.h>

#include <desktop/crashreport.hxx>

namespace glx {

static int glxtest_pipe = 0;

static pid_t glxtest_pid = 0;

}

pid_t* getGlxPid()
{
    return &glx::glxtest_pid;
}

int* getGlxPipe()
{
    return &glx::glxtest_pipe;
}

namespace {

const char*
strspnp_wrapper(const char* aDelims, const char* aStr)
{
  const char* d;
  do
  {
      for (d = aDelims; *d != '\0'; ++d)
      {
          if (*aStr == *d)
          {
              ++aStr;
              break;
          }
      }
  } while (*d);

  return aStr;
}

char* strtok_wrapper(const char* aDelims, char** aStr)
{
    if (!*aStr)
    {
        return nullptr;
    }

    char* ret = const_cast<char*>(strspnp_wrapper(aDelims, *aStr));

    if (!*ret)
    {
        *aStr = ret;
        return nullptr;
    }

    char* i = ret;
    do
    {
        for (const char* d = aDelims; *d != '\0'; ++d)
        {
            if (*i == *d) {
                *i = '\0';
                *aStr = ++i;
                return ret;
            }
        }
        ++i;
    } while (*i);

    *aStr = nullptr;
    return ret;
}

uint64_t version(uint32_t major, uint32_t minor, uint32_t revision = 0)
{
    return (uint64_t(major) << 32) + (uint64_t(minor) << 16) + uint64_t(revision);
}

}

X11OpenGLDeviceInfo::X11OpenGLDeviceInfo():
    mbIsMesa(false),
    mbIsNVIDIA(false),
    mbIsFGLRX(false),
    mbIsNouveau(false),
    mbIsIntel(false),
    mbIsOldSwrast(false),
    mbIsLlvmpipe(false),
    mbHasTextureFromPixmap(false),
    mnGLMajorVersion(0),
    mnMajorVersion(0),
    mnMinorVersion(0),
    mnRevisionVersion(0)
{
    GetData();
}

X11OpenGLDeviceInfo::~X11OpenGLDeviceInfo()
{
}

void X11OpenGLDeviceInfo::GetData()
{
    if (!glx::glxtest_pipe)
       return;

    // to understand this function, see bug moz#639842. We retrieve the OpenGL driver information in a
    // separate process to protect against bad drivers.
    enum { buf_size = 1024 };
    char buf[buf_size];
    ssize_t bytesread = read(glx::glxtest_pipe,
            &buf,
            buf_size-1); // -1 because we'll append a zero
    close(glx::glxtest_pipe);
    glx::glxtest_pipe = 0;

    // bytesread < 0 would mean that the above read() call failed.
    // This should never happen. If it did, the outcome would be to blacklist anyway.
    if (bytesread < 0)
        bytesread = 0;

    // let buf be a zero-terminated string
    buf[bytesread] = 0;

    // Wait for the glxtest process to finish. This serves 2 purposes:
    // * avoid having a zombie glxtest process laying around
    // * get the glxtest process status info.
    int glxtest_status = 0;
    bool wait_for_glxtest_process = true;
    bool waiting_for_glxtest_process_failed = false;
    int waitpid_errno = 0;
    while(wait_for_glxtest_process)
    {
        wait_for_glxtest_process = false;
        if (waitpid(glx::glxtest_pid, &glxtest_status, 0) == -1)
        {
            waitpid_errno = errno;
            if (waitpid_errno == EINTR)
            {
                wait_for_glxtest_process = true;
            }
            else
            {
                // Bug moz#718629
                // ECHILD happens when the glxtest process got reaped got reaped after a PR_CreateProcess
                // as per bug moz#227246. This shouldn't matter, as we still seem to get the data
                // from the pipe, and if we didn't, the outcome would be to blacklist anyway.
                waiting_for_glxtest_process_failed = (waitpid_errno != ECHILD);
            }
        }
    }

    bool exited_with_error_code = !waiting_for_glxtest_process_failed &&
        WIFEXITED(glxtest_status) &&
        WEXITSTATUS(glxtest_status) != EXIT_SUCCESS;
    bool received_signal = !waiting_for_glxtest_process_failed &&
        WIFSIGNALED(glxtest_status);

    bool error = waiting_for_glxtest_process_failed || exited_with_error_code || received_signal;

    OString textureFromPixmap;
    OString *stringToFill = nullptr;
    char *bufptr = buf;
    if (!error)
    {
        while(true)
        {
            char *line = strtok_wrapper("\n", &bufptr);
            if (!line)
                break;
            if (stringToFill) {
                *stringToFill = OString(line);
                stringToFill = nullptr;
            }
            else if(!strcmp(line, "VENDOR"))
                stringToFill = &maVendor;
            else if(!strcmp(line, "RENDERER"))
                stringToFill = &maRenderer;
            else if(!strcmp(line, "VERSION"))
                stringToFill = &maVersion;
            else if(!strcmp(line, "TFP"))
                stringToFill = &textureFromPixmap;
        }
    }

    if (!strcmp(textureFromPixmap.getStr(), "TRUE"))
        mbHasTextureFromPixmap = true;

    // only useful for Linux kernel version check for FGLRX driver.
    // assumes X client == X server, which is sad.
    struct utsname unameobj;
    if (!uname(&unameobj))
    {
        maOS = OString(unameobj.sysname);
        maOSRelease = OString(unameobj.release);
    }

    // determine the major OpenGL version. That's the first integer in the version string.
    mnGLMajorVersion = strtol(maVersion.getStr(), nullptr, 10);

    // determine driver type (vendor) and where in the version string
    // the actual driver version numbers should be expected to be found (whereToReadVersionNumbers)
    const char *whereToReadVersionNumbers = nullptr;
    const char *Mesa_in_version_string = strstr(maVersion.getStr(), "Mesa");
    if (Mesa_in_version_string)
    {
        mbIsMesa = true;
        // with Mesa, the version string contains "Mesa major.minor" and that's all the version information we get:
        // there is no actual driver version info.
        whereToReadVersionNumbers = Mesa_in_version_string + strlen("Mesa");
        if (strcasestr(maVendor.getStr(), "nouveau"))
            mbIsNouveau = true;
        if (strcasestr(maRenderer.getStr(), "intel")) // yes, intel is in the renderer string
            mbIsIntel = true;
        if (strcasestr(maRenderer.getStr(), "llvmpipe"))
            mbIsLlvmpipe = true;
        if (strcasestr(maRenderer.getStr(), "software rasterizer"))
            mbIsOldSwrast = true;
    }
    else if (strstr(maVendor.getStr(), "NVIDIA Corporation"))
    {
        mbIsNVIDIA = true;
        // with the NVIDIA driver, the version string contains "NVIDIA major.minor"
        // note that here the vendor and version strings behave differently, that's why we don't put this above
        // alongside Mesa_in_version_string.
        const char *NVIDIA_in_version_string = strstr(maVersion.getStr(), "NVIDIA");
        if (NVIDIA_in_version_string)
            whereToReadVersionNumbers = NVIDIA_in_version_string + strlen("NVIDIA");
    }
    else if (strstr(maVendor.getStr(), "ATI Technologies Inc"))
    {
        mbIsFGLRX = true;
        // with the FGLRX driver, the version string only gives a OpenGL version :/ so let's return that.
        // that can at least give a rough idea of how old the driver is.
        whereToReadVersionNumbers = maVersion.getStr();
    }

    // read major.minor version numbers of the driver (not to be confused with the OpenGL version)
    if (whereToReadVersionNumbers)
    {
        // copy into writable buffer, for tokenization
        strncpy(buf, whereToReadVersionNumbers, buf_size-1);
        buf[buf_size-1] = 0;
        bufptr = buf;

        // now try to read major.minor version numbers. In case of failure, gracefully exit: these numbers have
        // been initialized as 0 anyways
        char *token = strtok_wrapper(".", &bufptr);
        if (token)
        {
            mnMajorVersion = strtol(token, nullptr, 10);
            token = strtok_wrapper(".", &bufptr);
            if (token)
            {
                mnMinorVersion = strtol(token, nullptr, 10);
                token = strtok_wrapper(".", &bufptr);
                if (token)
                    mnRevisionVersion = strtol(token, nullptr, 10);
            }
        }
    }
}

bool X11OpenGLDeviceInfo::isDeviceBlocked()
{
    // don't even try to use OpenGL 1.x
    if (mnGLMajorVersion == 1)
        return true;

    CrashReporter::AddKeyValue("AdapterVendorId", rtl::OStringToOUString(maVendor, RTL_TEXTENCODING_UTF8));
    CrashReporter::AddKeyValue("AdapterDeviceId", rtl::OStringToOUString(maRenderer, RTL_TEXTENCODING_UTF8));

    SAL_INFO("vcl.opengl", "Vendor: " << maVendor);
    SAL_INFO("vcl.opengl", "Renderer: " << maRenderer);
    SAL_INFO("vcl.opengl", "Version: " << maVersion);
    SAL_INFO("vcl.opengl", "OS: " << maOS);
    SAL_INFO("vcl.opengl", "OSRelease: " << maOSRelease);

    if (mbIsMesa)
    {
        if (mbIsNouveau && version(mnMajorVersion, mnMinorVersion) < version(8,0))
        {
            SAL_WARN("vcl.opengl", "blocked driver version: old nouveau driver (requires mesa 8.0+)");
            return true;
        }
        else if (version(mnMajorVersion, mnMinorVersion, mnRevisionVersion) < version(7,10,3))
        {
            SAL_WARN("vcl.opengl", "blocked driver version: requires at least mesa 7.10.3");
            return true;
        }
        else if (mbIsIntel && version(mnMajorVersion, mnMinorVersion, mnRevisionVersion) == version(9,0,2))
        {
            SAL_WARN("vcl.opengl", "blocked driver version: my broken intel driver Mesa 9.0.2");
            return true;
        }
        else if (mbIsOldSwrast)
        {
            SAL_WARN("vcl.opengl", "blocked driver version: software rasterizer");
            return true;
        }
        else if (mbIsLlvmpipe && version(mnMajorVersion, mnMinorVersion) < version(9, 1))
        {
            // bug moz#791905, Mesa bug 57733, fixed in Mesa 9.1 according to
            // https://bugs.freedesktop.org/show_bug.cgi?id=57733#c3
            SAL_WARN("vcl.opengl", "blocked driver version: fdo#57733");
            return true;
        }
    }
    else if (mbIsNVIDIA)
    {
        if (version(mnMajorVersion, mnMinorVersion, mnRevisionVersion) < version(257,21))
        {
            SAL_WARN("vcl.opengl", "blocked driver version: nvidia requires at least 257.21");
            return true;
        }
    }
    else if (mbIsFGLRX)
    {
        // FGLRX does not report a driver version number, so we have the OpenGL version instead.
        // by requiring OpenGL 3, we effectively require recent drivers.
        if (version(mnMajorVersion, mnMinorVersion, mnRevisionVersion) < version(3, 0))
        {
            SAL_WARN("vcl.opengl", "blocked driver version: require at least OpenGL 3 for fglrx");
            return true;
        }
        // Bug moz#724640: FGLRX + Linux 2.6.32 is a crashy combo
        bool unknownOS = maOS.isEmpty() || maOSRelease.isEmpty();
        bool badOS = maOS.indexOf("Linux") != -1 &&
            maOSRelease.indexOf("2.6.32") != -1;
        if (unknownOS || badOS)
        {
            SAL_WARN("vcl.opengl", "blocked OS version with fglrx");
            return true;
        }
    }
    else
    {
        // like on windows, let's block unknown vendors. Think of virtual machines.
        // Also, this case is hit whenever the GLXtest probe failed to get driver info or crashed.
        SAL_WARN("vcl.opengl", "unknown vendor => blocked");
        return true;
    }

    return false;
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
