/* _NVRM_COPYRIGHT_BEGIN_
 *
 * Copyright 2013 by NVIDIA Corporation.  All rights reserved.  All
 * information contained herein is proprietary and confidential to NVIDIA
 * Corporation.  Any use, reproduction, or disclosure without the written
 * permission of NVIDIA Corporation is prohibited.
 *
 * _NVRM_COPYRIGHT_END_
 */

#define  __NO_VERSION__

#include "nv-misc.h"
#include "os-interface.h"
#include "nv-linux.h"

#if defined(NV_DRM_AVAILABLE)

#if defined(NV_DRM_DRMP_H_PRESENT)
#include <drm/drmP.h>
#endif
#if defined(NV_LINUX_FILE_H_PRESENT)
#include <linux/file.h>
#endif

#if defined(NV_DRM_DRM_DRV_H_PRESENT)
#include <drm/drm_drv.h>
#endif

#if defined(NV_DRM_DRM_PRIME_H_PRESENT)
#include <drm/drm_prime.h>
#endif

#if defined(NV_DRM_DRM_FILE_H_PRESENT)
#include <drm/drm_file.h>
#endif

#if defined(NV_DRM_DRM_IOCTL_H_PRESENT)
#include <drm/drm_ioctl.h>
#endif

#if defined(NV_DRM_DRM_PCI_H_PRESENT)
#include <drm/drm_pci.h>
#endif

#if defined(NV_DRM_DRM_GEM_H_PRESENT)
#include <drm/drm_gem.h>
#endif

#if defined(NV_DRM_LEGACY_PCI_INIT_PRESENT)
#define nv_drm_pci_init drm_legacy_pci_init
#define nv_drm_pci_exit drm_legacy_pci_exit
#elif defined(NV_DRM_PCI_INIT_PRESENT)
#define nv_drm_pci_init drm_pci_init
#define nv_drm_pci_exit drm_pci_exit
#else
#if defined(NV_DRM_GET_PCI_DEV_PRESENT)
#define nv_drm_get_pci_dev drm_get_pci_dev
#else
#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 14, 0)
#include <drm/drm_agpsupport.h>
#else
#include <drm/drm_legacy.h>
#endif
#include "linux/dma-buf.h"

struct nv_drm_agp_head {
    struct agp_kern_info agp_info;
    struct list_head memory;
    unsigned long mode;
    struct agp_bridge_data *bridge;
    int enabled;
    int acquired;
    unsigned long base;
    int agp_mtrr;
    int cant_use_aperture;
    unsigned long page_mask;
};

struct nv_drm_agp_mem {
    unsigned long handle;
    struct agp_memory *memory;
    unsigned long bound;
    int pages;
    struct list_head head;
};

struct nv_drm_extra_priv_data {
    struct pci_dev *pdev;
    struct drm_agp_head *agp;
};

/*
 * Code from drm_agp_init/nv_drm_{free,unbind}_agp
 * Extracted from commit: 5b8b9d0c6d0e0f1993c6c56deaf9646942c49d94, file: drivers/gpu/drm/drm_agpsupport.c
 */
struct drm_agp_head *nv_drm_agp_init(struct drm_device *dev)
{
    struct nv_drm_agp_head *head = NULL;
    struct nv_drm_extra_priv_data *extra = dev->dev_private;

    head = kzalloc(sizeof(*head), GFP_KERNEL);
    if (!head)
        return NULL;
    head->bridge = agp_find_bridge(extra->pdev);
    if (!head->bridge) {
        head->bridge = agp_backend_acquire(extra->pdev);
        if (!head->bridge) {
            kfree(head);
            return NULL;
        }
        agp_copy_info(head->bridge, &head->agp_info);
        agp_backend_release(head->bridge);
    } else {
        agp_copy_info(head->bridge, &head->agp_info);
    }
    if (head->agp_info.chipset == NOT_SUPPORTED) {
        kfree(head);
        return NULL;
    }
    INIT_LIST_HEAD(&head->memory);
    head->cant_use_aperture = head->agp_info.cant_use_aperture;
    head->page_mask = head->agp_info.page_mask;
    head->base = head->agp_info.aper_base;
    return (struct drm_agp_head *)head;
}

void nv_drm_free_agp(struct agp_memory *handle, int pages)
{
    agp_free_memory(handle);
}

int nv_drm_unbind_agp(struct agp_memory *handle)
{
    return agp_unbind_memory(handle);
}

/*
 * Code from drm_pci_agp_{clear,destroy,init}/drm_get_pci_dev
 * Extracted from commit: 5b8b9d0c6d0e0f1993c6c56deaf9646942c49d94, file: drivers/gpu/drm/drm_pci.c
 */
static void nv_drm_pci_agp_init(struct drm_device *dev)
{
    if (drm_core_check_feature(dev, DRIVER_USE_AGP)) {
        struct nv_drm_extra_priv_data *extra = dev->dev_private;

        if (pci_find_capability(extra->pdev, PCI_CAP_ID_AGP))
            extra->agp = nv_drm_agp_init(dev);
        if (extra->agp) {
            extra->agp->agp_mtrr = arch_phys_wc_add(
                extra->agp->agp_info.aper_base,
                extra->agp->agp_info.aper_size *
                1024 * 1024);
        }
    }
}

#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 14, 0)

#else
/* copied from v5.14.5 */
int nv_drm_legacy_agp_release(struct drm_device *dev)
{
    struct nv_drm_extra_priv_data *extra = dev->dev_private;

    if (!extra->agp || !extra->agp->acquired)
        return -EINVAL;
    agp_backend_release(extra->agp->bridge);
    extra->agp->acquired = 0;
    return 0;
}
#endif

void nv_drm_legacy_agp_clear(struct drm_device *dev)
{
    struct nv_drm_agp_mem *entry, *tempe;
    struct nv_drm_extra_priv_data *extra = dev->dev_private;

    if (!extra->agp)
        return;
    if (!drm_core_check_feature(dev, DRIVER_LEGACY))
        return;

    list_for_each_entry_safe(entry, tempe, &extra->agp->memory, head) {
        if (entry->bound)
            nv_drm_unbind_agp(entry->memory);
        nv_drm_free_agp(entry->memory, entry->pages);
        kfree(entry);
    }
    INIT_LIST_HEAD(&extra->agp->memory);

    if (extra->agp->acquired)
#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 14, 0)
        drm_agp_release(dev);
#else
        nv_drm_legacy_agp_release(dev);
#endif
    extra->agp->acquired = 0;
    extra->agp->enabled = 0;
}

void nv_drm_pci_agp_destroy(struct drm_device *dev)
{
    struct nv_drm_extra_priv_data *extra = dev->dev_private;
    if (extra->agp) {
        arch_phys_wc_del(extra->agp->agp_mtrr);
        nv_drm_legacy_agp_clear(dev);
        kfree(extra->agp);
        extra->agp = NULL;
    }
}

static int nv_drm_get_pci_dev(struct pci_dev *pdev,
               const struct pci_device_id *ent,
               struct drm_driver *driver)
{
    struct drm_device *dev;
    struct nv_drm_extra_priv_data *extra;
    int ret;

    DRM_DEBUG("\n");

    dev = drm_dev_alloc(driver, &pdev->dev);
    if (IS_ERR(dev))
        return PTR_ERR(dev);

    extra = kzalloc(sizeof(*extra), GFP_KERNEL);
    if (IS_ERR(extra))
        goto err_free;

    extra->pdev = pdev;

    ret = pci_enable_device(pdev);
    if (ret)
        goto err_free2;

    /* use the not used (i hope) dev_private to store deprecated/legacy pointers */
    dev->dev_private = extra;
#ifdef __alpha__
    dev->hose = pdev->sysdata;
#endif

    if (drm_core_check_feature(dev, DRIVER_MODESET))
        pci_set_drvdata(pdev, dev);

    nv_drm_pci_agp_init(dev);

    ret = drm_dev_register(dev, ent->driver_data);
    if (ret)
        goto err_agp;

    /* No locking needed since shadow-attach is single-threaded since it may
     * only be called from the per-driver module init hook. */
#if LINUX_VERSION_CODE <= KERNEL_VERSION(5, 10, 0)
    if (drm_core_check_feature(dev, DRIVER_LEGACY))
        list_add_tail(&dev->legacy_dev_list, &driver->legacy_dev_list);
#endif

    return 0;

err_agp:
    nv_drm_pci_agp_destroy(dev);
    pci_disable_device(pdev);
err_free2:
    kfree(extra);
err_free:
    drm_dev_put(dev);
    return ret;
}
#endif

/*
 * Code from drm_legacy_pci_{init,exit}
 * Extracted from tag: v5.6.3, file: drivers/gpu/drm/drm_pci.c
 */
int nv_drm_pci_init(struct drm_driver *driver, struct pci_driver *pdriver)
{
    struct pci_dev *pdev = NULL;
    const struct pci_device_id *pid;
    int i;

    DRM_DEBUG("\n");

    if (WARN_ON(!(driver->driver_features & DRIVER_LEGACY)))
        return -EINVAL;

#if LINUX_VERSION_CODE <= KERNEL_VERSION(5, 10, 0)
    /* If not using KMS, fall back to stealth mode manual scanning. */
    INIT_LIST_HEAD(&driver->legacy_dev_list);
#endif
    for (i = 0; pdriver->id_table[i].vendor != 0; i++) {
        pid = &pdriver->id_table[i];

        /* Loop around setting up a DRM device for each PCI device
         * matching our ID and device class.  If we had the internal
         * function that pci_get_subsys and pci_get_class used, we'd
         * be able to just pass pid in instead of doing a two-stage
         * thing.
         */
        pdev = NULL;
        while ((pdev =
            pci_get_subsys(pid->vendor, pid->device, pid->subvendor,
                       pid->subdevice, pdev)) != NULL) {
            if ((pdev->class & pid->class_mask) != pid->class)
                continue;

            /* stealth mode requires a manual probe */
            pci_dev_get(pdev);
            nv_drm_get_pci_dev(pdev, pid, driver);
        }
    }
    return 0;
}

void nv_drm_pci_exit(struct drm_driver *driver, struct pci_driver *pdriver)
{
    struct drm_device *dev, *tmp;
    DRM_DEBUG("\n");

    if (!(driver->driver_features & DRIVER_LEGACY)) {
        WARN_ON(1);
    } else {
#if LINUX_VERSION_CODE <= KERNEL_VERSION(5, 10, 0)
        list_for_each_entry_safe(dev, tmp, &driver->legacy_dev_list,
                     legacy_dev_list) {
            list_del(&dev->legacy_dev_list);
            drm_put_dev(dev);
        }
#endif
    }
    DRM_INFO("Module unloaded\n");
}
#endif

extern nv_linux_state_t *nv_linux_devices;

struct nv_gem_object {
    struct drm_gem_object base;
    struct page **pages;
};

static int nv_drm_load(
    struct drm_device *dev,
    unsigned long flags
)
{
    nv_linux_state_t *nvl;
    struct nv_drm_extra_priv_data *extra = dev->dev_private;

    for (nvl = nv_linux_devices; nvl != NULL; nvl = nvl->next)
    {
        if (nvl->dev == extra->pdev)
        {
            nvl->drm = dev;
            return 0;
        }
    }

    return -ENODEV;
}

/**
 * The return type of unload hook was changed from int
 * to void by the following kernel commit:- 
 *       
 * 2017-01-06  11b3c20bdd15d17382068be569740de1dccb173d
 */
static int __nv_drm_unload(
    struct drm_device *dev
)
{
    nv_linux_state_t *nvl;
    struct nv_drm_extra_priv_data *extra = dev->dev_private;

    for (nvl = nv_linux_devices; nvl != NULL; nvl = nvl->next)
    {
        if (nvl->dev == extra->pdev)
        {
            BUG_ON(nvl->drm != dev);
            nvl->drm = NULL;
            return 0;
        }
    }

    return -ENODEV;
}

#if defined(NV_DRM_DRIVER_UNLOAD_HAS_INT_RETURN_TYPE)
static int nv_drm_unload(
    struct drm_device *dev
)
{
    return __nv_drm_unload(dev);
}

#else
static void nv_drm_unload(
    struct drm_device *dev
)
{
    __nv_drm_unload(dev);
}
#endif

static void nv_gem_free(
    struct drm_gem_object *obj
)
{
    struct nv_gem_object *nv_obj = container_of(obj, struct nv_gem_object, base);
    NV_KFREE(nv_obj, sizeof(*nv_obj));
}

static struct sg_table* nv_gem_prime_get_sg_table(
    struct drm_gem_object *obj
)
{
    struct nv_gem_object *nv_obj = container_of(obj, struct nv_gem_object, base);
    int page_count = obj->size >> PAGE_SHIFT;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0)
    return drm_prime_pages_to_sg(obj->dev, nv_obj->pages, page_count);
#else
    return drm_prime_pages_to_sg(nv_obj->pages, page_count);
#endif
}

static void* nv_gem_prime_vmap(
    struct drm_gem_object *obj
)
{
    struct nv_gem_object *nv_obj = container_of(obj, struct nv_gem_object, base);
    int page_count = obj->size >> PAGE_SHIFT;

    return vmap(nv_obj->pages, page_count, VM_USERMAP, PAGE_KERNEL);
}

static void nv_gem_prime_vunmap(
    struct drm_gem_object *obj,
    void *virtual
)
{
    vunmap(virtual);
}

static const struct file_operations nv_drm_fops = {
    .owner = THIS_MODULE,
    .open = drm_open,
    .release = drm_release,
    .unlocked_ioctl = drm_ioctl,
    .mmap = drm_gem_mmap,
    .poll = drm_poll,
    .read = drm_read,
    .llseek = noop_llseek,
};

#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0)
struct sg_table *nv_drm_gem_prime_get_sg_table(struct drm_gem_object *gem)
{
    return nv_gem_prime_get_sg_table(gem);
}

#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 18, 0)
static int nv_drm_gem_vmap(struct drm_gem_object *gem,
                           struct iosys_map *map)
{
#else
static int nv_drm_gem_vmap(struct drm_gem_object *gem,
                           struct dma_buf_map *map)
{
#endif
    map->vaddr = nv_gem_prime_vmap(gem);
    if (map->vaddr == NULL) {
        return -ENOMEM;
    }
    map->is_iomem = true;
    return 0;
}

#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 18, 0)
static void nv_drm_gem_vunmap(struct drm_gem_object *gem,
                              struct iosys_map *map)
{
#else
static void nv_drm_gem_vunmap(struct drm_gem_object *gem,
                              struct dma_buf_map *map)
{
#endif
    nv_gem_prime_vunmap(gem, map->vaddr);
    map->vaddr = NULL;
}

static struct drm_gem_object_funcs nv_drm_gem_object_funcs = {
  .free = nv_gem_free,
  .export = drm_gem_prime_export,
  .get_sg_table = nv_drm_gem_prime_get_sg_table,
  .vmap = nv_drm_gem_vmap,
  .vunmap = nv_drm_gem_vunmap,
};
#endif

static struct drm_driver nv_drm_driver = {

    .driver_features = DRIVER_GEM
#if defined(NV_DRM_DRIVER_LEGACY_FEATURE_BIT_PRESENT)
        | DRIVER_LEGACY
#endif
#if defined(NV_DRM_DRIVER_PRIME_FLAG_PRESENT)
        | DRIVER_PRIME
#endif
    ,

    .load = nv_drm_load,
    .unload = nv_drm_unload,
    .fops = &nv_drm_fops,
#if defined(NV_DRM_PCI_SET_BUSID_PRESENT)
    .set_busid = drm_pci_set_busid,
#endif

#if LINUX_VERSION_CODE <= KERNEL_VERSION(5, 8, 0)
    .gem_free_object = nv_gem_free,
#elif LINUX_VERSION_CODE <= KERNEL_VERSION(5, 10, 0)
    .gem_free_object_unlocked = nv_gem_free,
#endif

    .prime_handle_to_fd = drm_gem_prime_handle_to_fd,
#if LINUX_VERSION_CODE <= KERNEL_VERSION(5, 10, 0)
    .gem_prime_export = drm_gem_prime_export,
    .gem_prime_get_sg_table = nv_gem_prime_get_sg_table,
    .gem_prime_vmap = nv_gem_prime_vmap,
    .gem_prime_vunmap = nv_gem_prime_vunmap,
#endif

    .name = "nvidia-drm",
    .desc = "NVIDIA DRM driver",
    .date = "20150116",
    .major = 0,
    .minor = 0,
    .patchlevel = 0,
};
#endif /* defined(NV_DRM_AVAILABLE) */

int __init nv_drm_init(
    struct pci_driver *pci_driver
)
{
    int ret = 0;
#if defined(NV_DRM_AVAILABLE)
    ret = nv_drm_pci_init(&nv_drm_driver, pci_driver);
#endif
    return ret;
}

void nv_drm_exit(
    struct pci_driver *pci_driver
)
{
#if defined(NV_DRM_AVAILABLE)
    nv_drm_pci_exit(&nv_drm_driver, pci_driver);
#endif
}

RM_STATUS NV_API_CALL nv_alloc_os_descriptor_handle(
    nv_state_t *nv,
    NvS32 drm_fd,
    void *private,
    NvU64 page_count,
    NvU32 *handle
)
{
    RM_STATUS status = RM_ERR_NOT_SUPPORTED;

#if defined(NV_DRM_AVAILABLE)
    nv_linux_state_t *nvl = NV_GET_NVL_FROM_NV_STATE(nv);
    nv_dma_map_t *dma_map = private;
    struct file *drmf;
    struct drm_file *file_priv;
    struct nv_gem_object *nv_obj = NULL;
    size_t size = page_count << PAGE_SHIFT;
    int ret;

    if (drm_fd < 0)
    {
        return RM_ERR_INVALID_ARGUMENT;
    }

    drmf = fget((unsigned int)drm_fd);
    if (drmf == NULL)
    {
        return RM_ERR_INVALID_ARGUMENT;
    }

    if (drmf->f_op != &nv_drm_fops)
    {
        status = RM_ERR_INVALID_ARGUMENT;
        goto done;
    }

    file_priv = drmf->private_data;

    NV_KMALLOC(nv_obj, sizeof(*nv_obj));
    if (!nv_obj)
    {
        status = RM_ERR_INSUFFICIENT_RESOURCES;
        goto done;
    }

    memset(&nv_obj->base, 0, sizeof(nv_obj->base));
    nv_obj->pages = dma_map->user_pages;

    drm_gem_private_object_init(nvl->drm, &nv_obj->base, size);

    ret = drm_gem_handle_create(file_priv, &nv_obj->base, handle);
    if (ret)
    {
        status = RM_ERR_OPERATING_SYSTEM;
        goto done;
    }

#if defined(NV_DRM_GEM_OBJECT_PUT_UNLOCKED_PRESENT)
    drm_gem_object_put_unlocked(&nv_obj->base);
#else
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 15, 0)
    drm_gem_object_put(&nv_obj->base);
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(5, 9, 0)
    drm_gem_object_put_locked(&nv_obj->base);
#else
    drm_gem_object_unreference_unlocked(&nv_obj->base);
#endif
#endif

    status = RM_OK;

done:
    if (status != RM_OK)
    {
        NV_KFREE(nv_obj, sizeof(*nv_obj));
    }

    fput(drmf);
#endif

    return status;
}
