/*
 * This file is part of mpv.
 *
 * mpv is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * mpv 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with mpv.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "context.h"
#include "options/m_config.h"
#include "utils.h"

#if HAVE_DRM
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

#include "libmpv/render_gl.h"
#include "video/out/drm_common.h"
#endif

struct vulkan_display_opts {
    int display;
    int mode;
    int plane;
};

struct mode_selector {
    // Indexes of selected display/mode/plane.
    int display_idx;
    int mode_idx;
    int plane_idx;

    // Must be freed with talloc_free
    VkDisplayModePropertiesKHR *out_mode_props;
};

/**
 * If a selector is passed, verify that it is valid and return the matching
 * mode properties. If null is passed, walk all modes and print them out.
 */
static bool walk_display_properties(struct mp_log *log,
                                    int msgl_err,
                                    VkPhysicalDevice device,
                                    struct mode_selector *selector) {
    bool ret = false;
    VkResult res;

    int msgl_info = selector ? MSGL_TRACE : MSGL_INFO;

    // Use a dummy as parent for all other allocations.
    void *tmp = talloc_new(NULL);

    VkPhysicalDeviceProperties prop;
    vkGetPhysicalDeviceProperties(device, &prop);
    mp_msg(log, msgl_info, "  '%s' (GPU ID %x:%x)\n", prop.deviceName,
           (unsigned)prop.vendorID, (unsigned)prop.deviceID);

    // Count displays. This must be done before enumerating planes with the
    // Intel driver, or it will not enumerate any planes. WTF.
    int num_displays = 0;
    vkGetPhysicalDeviceDisplayPropertiesKHR(device, &num_displays, NULL);
    if (!num_displays) {
        mp_msg(log, msgl_info, "    No available displays for device.\n");
        goto done;
    }
    if (selector && selector->display_idx + 1 > num_displays) {
        mp_msg(log, msgl_err, "Selected display (%d) not present.\n",
               selector->display_idx);
        goto done;
    }

    // Enumerate Planes
    int num_planes = 0;
    vkGetPhysicalDeviceDisplayPlanePropertiesKHR(device, &num_planes, NULL);
    if (!num_planes) {
        mp_msg(log, msgl_info, "    No available planes for device.\n");
        goto done;
    }
    if (selector && selector->plane_idx + 1 > num_planes) {
        mp_msg(log, msgl_err, "Selected plane (%d) not present.\n",
               selector->plane_idx);
        goto done;
    }

    VkDisplayPlanePropertiesKHR *planes =
        talloc_array(tmp, VkDisplayPlanePropertiesKHR, num_planes);
    res = vkGetPhysicalDeviceDisplayPlanePropertiesKHR(device, &num_planes,
                                                       planes);
    if (res != VK_SUCCESS) {
        mp_msg(log, msgl_err, "    Failed enumerating planes\n");
        goto done;
    }

    // Allocate zeroed arrays so that planes with no displays have a null entry.
    VkDisplayKHR **planes_to_displays =
        talloc_zero_array(tmp, VkDisplayKHR *, num_planes);
    for (int j = 0; j < num_planes; j++) {
        int num_displays_for_plane = 0;
        vkGetDisplayPlaneSupportedDisplaysKHR(device, j,
                                              &num_displays_for_plane, NULL);
        if (!num_displays_for_plane)
            continue;

        // Null terminated array
        VkDisplayKHR *displays =
            talloc_zero_array(planes_to_displays, VkDisplayKHR,
                              num_displays_for_plane + 1);
        res = vkGetDisplayPlaneSupportedDisplaysKHR(device, j,
                                                    &num_displays_for_plane,
                                                    displays);
        if (res != VK_SUCCESS) {
            mp_msg(log, msgl_err, "      Failed enumerating plane displays\n");
            continue;
        }
        planes_to_displays[j] = displays;
    }

    // Enumerate Displays and Modes
    VkDisplayPropertiesKHR *props =
        talloc_array(tmp, VkDisplayPropertiesKHR, num_displays);
    res = vkGetPhysicalDeviceDisplayPropertiesKHR(device, &num_displays, props);
    if (res != VK_SUCCESS) {
        mp_msg(log, msgl_err, "    Failed enumerating display properties\n");
        goto done;
    }

    for (int j = 0; j < num_displays; j++) {
        if (selector && selector->display_idx != j)
            continue;

        mp_msg(log, msgl_info, "    Display %d: '%s' (%dx%d)\n",
               j,
               props[j].displayName,
               props[j].physicalResolution.width,
               props[j].physicalResolution.height);

        VkDisplayKHR display = props[j].display;

        mp_msg(log, msgl_info, "    Modes:\n");

        int num_modes = 0;
        vkGetDisplayModePropertiesKHR(device, display, &num_modes, NULL);
        if (!num_modes) {
            mp_msg(log, msgl_info, "      No available modes for display.\n");
            continue;
        }
        if (selector && selector->mode_idx + 1 > num_modes) {
            mp_msg(log, msgl_err, "Selected mode (%d) not present.\n",
                   selector->mode_idx);
            goto done;
        }

        VkDisplayModePropertiesKHR *modes =
            talloc_array(tmp, VkDisplayModePropertiesKHR, num_modes);
        res = vkGetDisplayModePropertiesKHR(device, display, &num_modes, modes);
        if (res != VK_SUCCESS) {
            mp_msg(log, msgl_err, "      Failed enumerating display modes\n");
            continue;
        }

        for (int k = 0; k < num_modes; k++) {
            if (selector && selector->mode_idx != k)
                continue;

            mp_msg(log, msgl_info, "      Mode %02d: %dx%d (%02d.%03d Hz)\n", k,
                   modes[k].parameters.visibleRegion.width,
                   modes[k].parameters.visibleRegion.height,
                   modes[k].parameters.refreshRate / 1000,
                   modes[k].parameters.refreshRate % 1000);

            if (selector)
                selector->out_mode_props = talloc_dup(NULL, &modes[k]);
        }

        int found_plane = -1;
        mp_msg(log, msgl_info, "    Planes:\n");
        for (int k = 0; k < num_planes; k++) {
            VkDisplayKHR *displays = planes_to_displays[k];
            if (!displays) {
                // This plane is not connected to any displays.
                continue;
            }
            for (int d = 0; displays[d]; d++) {
                if (displays[d] == display) {
                    if (selector && selector->plane_idx != k)
                        continue;

                    mp_msg(log, msgl_info, "      Plane: %d\n", k);
                    found_plane = k;
                }
            }
        }
        if (selector && selector->plane_idx != found_plane) {
            mp_msg(log, msgl_err,
                   "Selected plane (%d) not available on selected display.\n",
                   selector->plane_idx);
            goto done;
        }
    }
    ret = true;
done:
    talloc_free(tmp);
    return ret;
}

static int print_display_info(struct mp_log *log, const struct m_option *opt,
                              struct bstr name) {
    VkResult res;
    VkPhysicalDevice *devices = NULL;

    // Create a dummy instance to list the resources
    VkInstanceCreateInfo info = {
        .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
        .enabledExtensionCount = 1,
        .ppEnabledExtensionNames = (const char*[]) {
            VK_KHR_DISPLAY_EXTENSION_NAME
        },
    };

    VkInstance inst = NULL;
    res = vkCreateInstance(&info, NULL, &inst);
    if (res != VK_SUCCESS) {
        mp_warn(log, "Unable to create Vulkan instance.\n");
        goto done;
    }

    uint32_t num_devices = 0;
    vkEnumeratePhysicalDevices(inst, &num_devices, NULL);
    if (!num_devices) {
        mp_info(log, "No Vulkan devices detected.\n");
        goto done;
    }

    devices = talloc_array(NULL, VkPhysicalDevice, num_devices);
    vkEnumeratePhysicalDevices(inst, &num_devices, devices);
    if (res != VK_SUCCESS) {
        mp_warn(log, "Failed enumerating physical devices.\n");
        goto done;
    }

    mp_info(log, "Vulkan Devices:\n");
    for (int i = 0; i < num_devices; i++) {
        walk_display_properties(log, MSGL_WARN, devices[i], NULL);
    }

done:
    talloc_free(devices);
    vkDestroyInstance(inst, NULL);
    return M_OPT_EXIT;
}

#define OPT_BASE_STRUCT struct vulkan_display_opts
const struct m_sub_options vulkan_display_conf = {
    .opts = (const struct m_option[]) {
        {"vulkan-display-display", OPT_INT(display),
            .help = print_display_info,
        },
        {"vulkan-display-mode", OPT_INT(mode),
            .help = print_display_info,
        },
        {"vulkan-display-plane", OPT_INT(plane),
            .help = print_display_info,
        },
        {0}
    },
    .size = sizeof(struct vulkan_display_opts),
    .defaults = &(struct vulkan_display_opts) {
        .display = 0,
        .mode = 0,
        .plane = 0,
    },
};

struct priv {
    struct mpvk_ctx vk;
    struct vulkan_display_opts *opts;
    uint32_t width;
    uint32_t height;

#if HAVE_DRM
    struct mpv_opengl_drm_params_v2 drm_params;
#endif
};

#if HAVE_DRM
static void open_render_fd(struct ra_ctx *ctx, const char *render_path)
{
    struct priv *p = ctx->priv;
    p->drm_params.fd = -1;
    p->drm_params.render_fd = open(render_path, O_RDWR | O_CLOEXEC);
    if (p->drm_params.render_fd == -1) {
        MP_WARN(ctx, "Failed to open render node: %s\n",
                strerror(errno));
    }
}

static bool drm_setup(struct ra_ctx *ctx, int display_idx,
                      VkPhysicalDevicePCIBusInfoPropertiesEXT *pci_props)
{
    drmDevice *devs[32] = {};
    int count = drmGetDevices2(0, devs, MP_ARRAY_SIZE(devs));
    for (int i = 0; i < count; i++) {
        drmDevice *dev = devs[i];

        if (dev->bustype != DRM_BUS_PCI ||
            dev->businfo.pci->domain != pci_props->pciDomain ||
            dev->businfo.pci->bus != pci_props->pciBus ||
            dev->businfo.pci->dev != pci_props->pciDevice ||
            dev->businfo.pci->func != pci_props->pciFunction)
        {
            continue;
        }

        // Found our matching device.
        MP_DBG(ctx, "DRM device found for Vulkan device at %04X:%02X:%02X:%02X\n",
                pci_props->pciDomain, pci_props->pciBus,
                pci_props->pciDevice, pci_props->pciFunction);

        if (!(dev->available_nodes & 1 << DRM_NODE_RENDER)) {
            MP_DBG(ctx, "Card does not have a render node.\n");
            continue;
        }

        open_render_fd(ctx, dev->nodes[DRM_NODE_RENDER]);

        break;
    }
    drmFreeDevices(devs, MP_ARRAY_SIZE(devs));

    struct priv *p = ctx->priv;
    if (p->drm_params.render_fd == -1) {
        MP_WARN(ctx, "Couldn't open DRM render node for Vulkan device "
                     "at: %04X:%02X:%02X:%02X\n",
                     pci_props->pciDomain, pci_props->pciBus,
                     pci_props->pciDevice, pci_props->pciFunction);
        return false;
    }

    return true;
}
#endif

static void display_uninit(struct ra_ctx *ctx)
{
    struct priv *p = ctx->priv;

    ra_vk_ctx_uninit(ctx);
    mpvk_uninit(&p->vk);

#if HAVE_DRM
    if (p->drm_params.render_fd != -1) {
        close(p->drm_params.render_fd);
        p->drm_params.render_fd = -1;
    }
#endif
}

static bool display_init(struct ra_ctx *ctx)
{
    struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);
    struct mpvk_ctx *vk = &p->vk;
    int msgl = ctx->opts.probing ? MSGL_V : MSGL_ERR;
    VkResult res;
    bool ret = false;

    VkDisplayModePropertiesKHR *mode = NULL;

    p->opts = mp_get_config_group(p, ctx->global, &vulkan_display_conf);
    int display_idx = p->opts->display;
    int mode_idx = p->opts->mode;
    int plane_idx = p->opts->plane;

    if (!mpvk_init(vk, ctx, VK_KHR_DISPLAY_EXTENSION_NAME))
        goto error;

    char *device_name = ra_vk_ctx_get_device_name(ctx);
    struct pl_vulkan_device_params vulkan_params = {
        .instance = vk->vkinst->instance,
        .device_name = device_name,
    };
    VkPhysicalDevice device = pl_vulkan_choose_device(vk->pllog, &vulkan_params);
    talloc_free(device_name);
    if (!device) {
        MP_MSG(ctx, msgl, "Failed to open physical device.\n");
        goto error;
    }

#if HAVE_DRM
        VkPhysicalDevicePCIBusInfoPropertiesEXT pci_props = {
            .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PCI_BUS_INFO_PROPERTIES_EXT,
        };
        VkPhysicalDeviceProperties2KHR props = {
            .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2_KHR,
            .pNext = &pci_props,
        };
        vkGetPhysicalDeviceProperties2(device, &props);

        if (!drm_setup(ctx, display_idx, &pci_props))
            MP_WARN(ctx, "Failed to set up DRM.\n");
#endif

    struct mode_selector selector = {
        .display_idx = display_idx,
        .mode_idx = mode_idx,
        .plane_idx = plane_idx,

    };
    if (!walk_display_properties(ctx->log, msgl, device, &selector))
        goto error;
    mode = selector.out_mode_props;

    VkDisplaySurfaceCreateInfoKHR xinfo = {
        .sType = VK_STRUCTURE_TYPE_DISPLAY_SURFACE_CREATE_INFO_KHR,
        .displayMode = mode->displayMode,
        .imageExtent = mode->parameters.visibleRegion,
        .planeIndex = plane_idx,
        .transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR,
        .alphaMode = VK_DISPLAY_PLANE_ALPHA_OPAQUE_BIT_KHR,
    };

    res = vkCreateDisplayPlaneSurfaceKHR(vk->vkinst->instance, &xinfo, NULL,
                                         &vk->surface);
    if (res != VK_SUCCESS) {
        MP_MSG(ctx, msgl, "Failed creating Display surface\n");
        goto error;
    }

    p->width = mode->parameters.visibleRegion.width;
    p->height = mode->parameters.visibleRegion.height;

    struct ra_vk_ctx_params params = {0};
    if (!ra_vk_ctx_init(ctx, vk, params, VK_PRESENT_MODE_FIFO_KHR))
        goto error;

#if HAVE_DRM
    if (p->drm_params.render_fd > -1) {
        ra_add_native_resource(ctx->ra, "drm_params_v2", &p->drm_params);
    } else {
        MP_WARN(ctx,
               "No DRM render fd available. VAAPI hwaccel will not be usable.\n");
    }
#endif

    ret = true;

done:
    talloc_free(mode);
    return ret;

error:
    display_uninit(ctx);
    goto done;
}

static bool display_reconfig(struct ra_ctx *ctx)
{
    struct priv *p = ctx->priv;
    return ra_vk_ctx_resize(ctx, p->width, p->height);
}

static int display_control(struct ra_ctx *ctx, int *events, int request, void *arg)
{
    return VO_NOTIMPL;
}

static void display_wakeup(struct ra_ctx *ctx)
{
    // TODO
}

static void display_wait_events(struct ra_ctx *ctx, int64_t until_time_us)
{
    // TODO
}

const struct ra_ctx_fns ra_ctx_vulkan_display = {
    .type           = "vulkan",
    .name           = "displayvk",
    .reconfig       = display_reconfig,
    .control        = display_control,
    .wakeup         = display_wakeup,
    .wait_events    = display_wait_events,
    .init           = display_init,
    .uninit         = display_uninit,
};
