/*
 * Copyright © 2012 Linaro Limited
 * Copyright © 2013 Canonical Ltd
 *
 * This file is part of the glmark2 OpenGL (ES) 2.0 benchmark.
 *
 * glmark2 is free software: you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 *
 * glmark2 is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * glmark2.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *  Simon Que 
 *  Jesse Barker
 *  Alexandros Frantzis
 */
#include "native-state-drm.h"
#include "log.h"

#include <fcntl.h>
#include <libudev.h>
#include <cstring>
#include <string>

/******************
 * Public methods *
 ******************/

bool
NativeStateDRM::init_display()
{
    if (!dev_)
        init();

    return (dev_ != 0);
}

void*
NativeStateDRM::display()
{
    return static_cast<void*>(dev_);
}

bool
NativeStateDRM::create_window(WindowProperties const& /*properties*/)
{
    if (!dev_) {
        Log::error("Error: DRM device has not been initialized!\n");
        return false;
    }

    return true;
}

void*
NativeStateDRM::window(WindowProperties& properties)
{
    properties = WindowProperties(mode_->hdisplay,
                                  mode_->vdisplay,
                                  true, 0);
    return static_cast<void*>(surface_);
}

void
NativeStateDRM::visible(bool /*visible*/)
{
}

bool
NativeStateDRM::should_quit()
{
    return should_quit_;
}

void
NativeStateDRM::flip()
{
    if (!crtc_set_ && drmSetMaster(fd_) < 0) {
        Log::error("Failed to become DRM master "
                   "(hint: glmark2-drm needs to be run in a VT)\n");
        should_quit_ = true;
        return;
    }

    gbm_bo* next = gbm_surface_lock_front_buffer(surface_);
    fb_ = fb_get_from_bo(next);
    unsigned int waiting(1);

    if (!next || !fb_) {
        Log::error("Failed to get gbm front buffer\n");
        return;
    }

    if (!crtc_set_) {
        int status = drmModeSetCrtc(fd_, encoder_->crtc_id, fb_->fb_id, 0, 0,
                                    &connector_->connector_id, 1, mode_);
        if (status >= 0) {
            crtc_set_ = true;
            bo_ = next;
        }
        else {
            Log::error("Failed to set crtc: %d\n", status);
        }
        return;
    }

    int status = drmModePageFlip(fd_, encoder_->crtc_id, fb_->fb_id,
                                 DRM_MODE_PAGE_FLIP_EVENT, &waiting);
    if (status < 0) {
        Log::error("Failed to enqueue page flip: %d\n", status);
        return;
    }

    fd_set fds;
    FD_ZERO(&fds);
    FD_SET(fd_, &fds);
    drmEventContext evCtx;
    memset(&evCtx, 0, sizeof(evCtx));
    evCtx.version = 2;
    evCtx.page_flip_handler = page_flip_handler;

    while (waiting) {
        status = select(fd_ + 1, &fds, 0, 0, 0);
        if (status < 0) {
            // Most of the time, select() will return an error because the
            // user pressed Ctrl-C.  So, only print out a message in debug
            // mode, and just check for the likely condition and release
            // the current buffer object before getting out.
            Log::debug("Error in select\n");
            if (should_quit()) {
                gbm_surface_release_buffer(surface_, bo_);
                bo_ = next;
            }
            return;
        }
        drmHandleEvent(fd_, &evCtx);
    }

    gbm_surface_release_buffer(surface_, bo_);
    bo_ = next;
}

/*******************
 * Private methods *
 *******************/

/* Simple helpers */

inline static bool valid_fd(int fd)
{
    return fd >= 0;
}

inline static bool valid_drm_node_path(std::string const& provided_node_path)
{
    return !provided_node_path.empty();
}

inline static bool invalid_drm_node_path(std::string const& provided_node_path)
{
    return !(valid_drm_node_path(provided_node_path));
}

/* Udev methods */
// Udev detection functions
#define UDEV_TEST_FUNC_SIGNATURE(udev_identifier, device_identifier, syspath_identifier) \
    struct udev * __restrict const udev_identifier, \
    struct udev_device * __restrict const device_identifier, \
    char const * __restrict syspath_identifier

/* Omitting the parameter names is kind of ugly but is the only way
 * to force G++ to forget about the unused parameters.
 * Having big warnings during the compilation isn't very nice.
 *
 * These functions will be used as function pointers and should have
 * the same signature to avoid weird stack related issues.
 *
 * Another way to deal with that issue will be to mark unused parameters
 * with __attribute__((unused))
 */
static bool udev_drm_test_virtual(
    UDEV_TEST_FUNC_SIGNATURE(,,tested_node_syspath))
{
    return strstr(tested_node_syspath, "virtual") != NULL;
}

static bool udev_drm_test_not_virtual(
    UDEV_TEST_FUNC_SIGNATURE(udev, current_device, tested_node_syspath))
{
    return !udev_drm_test_virtual(udev,
                                  current_device,
                                  tested_node_syspath);
}

static bool
udev_drm_test_primary_gpu(UDEV_TEST_FUNC_SIGNATURE(, current_device,))
{
    bool is_main_gpu = false;

    auto const drm_node_parent = udev_device_get_parent(current_device);

    /* While tempting, using udev_device_unref will generate issues
     * when unreferencing the child in udev_get_node_that_pass_in_enum
     *
     * udev_device_unref WILL unreference the parent, so avoid doing
     * that here.
     *
     * ( See udev sources : src/libudev/libudev-device.c )
     */
    if (drm_node_parent != NULL) {
        is_main_gpu =
            (udev_device_get_sysattr_value(drm_node_parent, "boot_vga")
             != NULL);
    }

    return is_main_gpu;
}

/* Test if the drm-device is actually modeset capable.
 * Render-only devices cannot drive an actual display,
 * so the GETRESOURCES ioctl will fail in that case.
 */
static bool udev_drm_test_modeset(std::string const& dev_path)
{
    struct drm_mode_card_res res {};
    int fd, ret;

    fd = open(dev_path.c_str(), O_RDWR);
    if (!valid_fd(fd))
        return false;

    ret = drmIoctl(fd, DRM_IOCTL_MODE_GETRESOURCES, &res);
    drmClose(fd);

    return !ret;
}

static std::string
udev_get_node_that_pass_in_enum(
    struct udev * __restrict const udev,
    struct udev_enumerate * __restrict const dev_enum,
    bool (* check_function)(UDEV_TEST_FUNC_SIGNATURE(,,)))
{
    std::string result;

    auto current_element = udev_enumerate_get_list_entry(dev_enum);

    while (current_element && result.empty()) {
        char const * __restrict current_element_sys_path =
            udev_list_entry_get_name(current_element);

        if (current_element_sys_path) {
            struct udev_device * current_device =
                udev_device_new_from_syspath(udev,
                                             current_element_sys_path);
            auto check_passed = check_function(
                udev, current_device, current_element_sys_path);

            if (check_passed) {
                const char * device_node_path =
                    udev_device_get_devnode(current_device);

                if (device_node_path &&
                    udev_drm_test_modeset(device_node_path)) {
                        result = device_node_path;
                }

            }

            udev_device_unref(current_device);
        }

        current_element = udev_list_entry_get_next(current_element);
    }

    return result;
}

/* Inspired by KWin detection mechanism */
/* And yet KWin got it wrong too, it seems.
 * 1 - Looking for the primary GPU by checking the flag 'boot_vga'
 *     won't get you far with some embedded chipsets, like Rockchip.
 * 2 - Looking for a GPU plugged in PCI will fail on various embedded
 *     devices !
 * 3 - Looking for a render node is not guaranteed to work on some
 *     poorly maintained DRM drivers, which plague some embedded
 *     devices too...
 *
 * So, we won't play too smart here.
 * - We first check for a primary GPU plugged in PCI with the 'boot_vga'
 *   attribute, to take care of Desktop users using multiple GPU.
 * - Then, we just check for a DRM node that is not virtual
 * - At least, we use the first virtual node we get, if we didn't find
 *   anything yet.
 * This should take care of almost every potential use case.
 *
 * The remaining ones will be dealt with an additional option to
 * specify the DRM dev node manually.
 */
static std::string udev_main_gpu_drm_node_path()
{
    Log::debug("Using Udev to detect the right DRM node to use\n");
    auto udev = udev_new();
    auto dev_enumeration = udev_enumerate_new(udev);

    udev_enumerate_add_match_subsystem(dev_enumeration, "drm");
    udev_enumerate_add_match_sysname(dev_enumeration, "card[0-9]*");
    udev_enumerate_scan_devices(dev_enumeration);

    Log::debug("Looking for the main GPU DRM node...\n");
    std::string node_path = udev_get_node_that_pass_in_enum(
        udev, dev_enumeration, udev_drm_test_primary_gpu);

    if (invalid_drm_node_path(node_path)) {
        Log::debug("Not found!\n");
        Log::debug("Looking for a concrete GPU DRM node...\n");
        node_path = udev_get_node_that_pass_in_enum(
            udev, dev_enumeration, udev_drm_test_not_virtual);
    }
    if (invalid_drm_node_path(node_path)) {
        Log::debug("Not found!?\n");
        Log::debug("Looking for a virtual GPU DRM node...\n");
        node_path = udev_get_node_that_pass_in_enum(
            udev, dev_enumeration, udev_drm_test_virtual);
    }
    if (invalid_drm_node_path(node_path)) {
        Log::debug("Not found.\n");
        Log::debug("Cannot find a single DRM node using UDEV...\n");
    }

    if (valid_drm_node_path(node_path)) {
        Log::debug("Success!\n");
    }

    udev_enumerate_unref(dev_enumeration);
    udev_unref(udev);

    return node_path;
}

static int open_using_udev_scan()
{
    auto dev_path = udev_main_gpu_drm_node_path();

    int fd = -1;
    if (valid_drm_node_path(dev_path)) {
        Log::debug("Trying to use the DRM node %s\n", dev_path.c_str());
        fd = open(dev_path.c_str(), O_RDWR);
    }
    else {
        Log::error("Can't determine the main graphic card "
                   "DRM device node\n");
    }

    if (!valid_fd(fd)) {
        // %m is GLIBC specific... Maybe use strerror here...
        Log::error("Tried to use '%s' but failed.\nReason : %m\n",
                   dev_path.c_str());
    }
    else
        Log::debug("Success!\n");

    return fd;
}

/* -- End of Udev helpers -- */

/*
 * This method is there to take care of cases that would not be handled
 * by open_using_udev_scan. This should not happen.
 *
 * If your driver defines a /dev/dri/cardX node and open_using_udev_scan
 * were not able to detect it, you should probably file an issue.
 *
 */
static int open_using_module_checking()
{
    static const char* drm_modules[] = {
        "i915",
        "imx-drm",
        "nouveau",
        "radeon",
        "vmgfx",
        "omapdrm",
        "exynos",
        "pl111",
        "vc4",
        "msm",
        "meson",
        "rockchip",
        "sun4i-drm",
        "stm",
    };

    int fd = -1;
    unsigned int num_modules(sizeof(drm_modules)/sizeof(drm_modules[0]));
    for (unsigned int m = 0; m < num_modules; m++) {
        fd = drmOpen(drm_modules[m], 0);
        if (fd < 0) {
            Log::debug("Failed to open DRM module '%s'\n", drm_modules[m]);
            continue;
        }
        Log::debug("Opened DRM module '%s'\n", drm_modules[m]);
        break;
    }

    return fd;
}

void
NativeStateDRM::fb_destroy_callback(gbm_bo* bo, void* data)
{
    DRMFBState* fb = reinterpret_cast<DRMFBState*>(data);
    if (fb && fb->fb_id) {
        drmModeRmFB(fb->fd, fb->fb_id);
    }
    delete fb;
    gbm_device* dev = gbm_bo_get_device(bo);
    Log::debug("Got GBM device handle %p from buffer object\n", dev);
}

NativeStateDRM::DRMFBState*
NativeStateDRM::fb_get_from_bo(gbm_bo* bo)
{
    if (!bo) {
        return NULL;
    }

    DRMFBState* fb = reinterpret_cast<DRMFBState*>(gbm_bo_get_user_data(bo));
    if (fb) {
        return fb;
    }

    unsigned int width = gbm_bo_get_width(bo);
    unsigned int height = gbm_bo_get_height(bo);
    unsigned int stride = gbm_bo_get_stride(bo);
    unsigned int handle = gbm_bo_get_handle(bo).u32;
    unsigned int fb_id(0);
    int status = drmModeAddFB(fd_, width, height, 24, 32, stride, handle, &fb_id);
    if (status < 0) {
        Log::error("Failed to create FB: %d\n", status);
        return 0;
    }

    fb = new DRMFBState();
    fb->fd = fd_;
    fb->bo = bo;
    fb->fb_id = fb_id;

    gbm_bo_set_user_data(bo, fb, fb_destroy_callback);
    return fb;
}

bool
NativeStateDRM::init_gbm()
{
    dev_ = gbm_create_device(fd_);
    if (!dev_) {
        Log::error("Failed to create GBM device\n");
        return false;
    }

    surface_ = gbm_surface_create(dev_, mode_->hdisplay, mode_->vdisplay,
                                  GBM_FORMAT_XRGB8888,
                                  GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING);
    if (!surface_) {
        Log::error("Failed to create GBM surface\n");
        return false;
    }

    return true;
}

bool
NativeStateDRM::init()
{
    // TODO: The user should be able to define *exactly* which device
    //       node to open and the program should try to open only
    //       this node, in order to take care of unknown use cases.
    int fd = open_using_udev_scan();

    if (!valid_fd(fd)) {
        fd = open_using_module_checking();
    }

    if (!valid_fd(fd)) {
        Log::error("Failed to find a suitable DRM device\n");
        return false;
    }

    fd_ = fd;

    resources_ = drmModeGetResources(fd);
    if (!resources_) {
        Log::error("drmModeGetResources failed\n");
        return false;
    }

    // Find a connected connector
    for (int c = 0; c < resources_->count_connectors; c++) {
        connector_ = drmModeGetConnector(fd, resources_->connectors[c]);
        if (DRM_MODE_CONNECTED == connector_->connection) {
            break;
        }
        drmModeFreeConnector(connector_);
        connector_ = 0;
    }

    if (!connector_) {
        Log::error("Failed to find a suitable connector\n");
        return false;
    }

    // Find the best resolution (we will always operate full-screen).
    unsigned int bestArea(0);
    for (int m = 0; m < connector_->count_modes; m++) {
        drmModeModeInfo* curMode = &connector_->modes[m];
        unsigned int curArea = curMode->hdisplay * curMode->vdisplay;
        if (curArea > bestArea) {
            mode_ = curMode;
            bestArea = curArea;
        }
    }

    if (!mode_) {
        Log::error("Failed to find a suitable mode\n");
        return false;
    }

    // Find a suitable encoder
    for (int e = 0; e < resources_->count_encoders; e++) {
        bool found = false;
        encoder_ = drmModeGetEncoder(fd_, resources_->encoders[e]);
        for (int ce = 0; ce < connector_->count_encoders; ce++) {
            if (encoder_ && encoder_->encoder_id == connector_->encoders[ce]) {
                found = true;
                break;
            }
        }
        if (found)
            break;
        drmModeFreeEncoder(encoder_);
        encoder_ = 0;
    }

    // If encoder is not connected to the connector try to find
    // a suitable one
    if (!encoder_) {
        for (int e = 0; e < connector_->count_encoders; e++) {
            encoder_ = drmModeGetEncoder(fd, connector_->encoders[e]);
            for (int c = 0; c < resources_->count_crtcs; c++) {
                if (encoder_->possible_crtcs & (1 << c)) {
                    encoder_->crtc_id = resources_->crtcs[c];
                    break;
                }
            }
            if (encoder_->crtc_id) {
                break;
            }

            drmModeFreeEncoder(encoder_);
            encoder_ = 0;
        }
    }

    if (!encoder_) {
        Log::error("Failed to find a suitable encoder\n");
        return false;
    }

    if (!init_gbm()) {
        return false;
    }

    crtc_ = drmModeGetCrtc(fd_, encoder_->crtc_id);
    if (!crtc_) {
        // if there is no current CRTC, make sure to attach a suitable one
        for (int c = 0; c < resources_->count_crtcs; c++) {
            if (encoder_->possible_crtcs & (1 << c)) {
                encoder_->crtc_id = resources_->crtcs[c];
                break;
            }
        }
    }

    signal(SIGINT, &NativeStateDRM::quit_handler);

    return true;
}

volatile std::sig_atomic_t NativeStateDRM::should_quit_(false);

void
NativeStateDRM::quit_handler(int /*signo*/)
{
    should_quit_ = true;
}

void
NativeStateDRM::page_flip_handler(int/*  fd */, unsigned int /* frame */, unsigned int /* sec */, unsigned int /* usec */, void* data)
{
    unsigned int* waiting = reinterpret_cast<unsigned int*>(data);
    *waiting = 0;
}

void
NativeStateDRM::cleanup()
{
    // Restore CRTC state if necessary
    if (crtc_) {
        int status = drmModeSetCrtc(fd_, crtc_->crtc_id, crtc_->buffer_id,
                                    crtc_->x, crtc_->y, &connector_->connector_id,
                                    1, &crtc_->mode);
        if (status < 0) {
            Log::error("Failed to restore original CRTC: %d\n", status);
        }
        drmModeFreeCrtc(crtc_);
        crtc_ = 0;
    }
    if (surface_) {
        gbm_surface_destroy(surface_);
        surface_ = 0;
    }
    if (dev_) {
        gbm_device_destroy(dev_);
        dev_ = 0;
    }
    if (connector_) {
        drmModeFreeConnector(connector_);
        connector_ = 0;
    }
    if (encoder_) {
        drmModeFreeEncoder(encoder_);
        encoder_ = 0;
    }
    if (resources_) {
        drmModeFreeResources(resources_);
        resources_ = 0;
    }
    if (fd_ > 0) {
        drmClose(fd_);
    }
    fd_ = 0;
    mode_ = 0;
}
