/*
 * SPDX-FileCopyrightText: Copyright (c) 2012-2013 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
 * SPDX-License-Identifier: MIT
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

#include "os-interface.h"
#include "nv-linux.h"
#include "nv-reg.h"
#include "nv-frontend.h"

MODULE_LICENSE("Dual MIT/GPL");

MODULE_INFO(supported, "external");
MODULE_VERSION(NV_VERSION_STRING);
MODULE_ALIAS_CHARDEV_MAJOR(NV_MAJOR_DEVICE_NUMBER);

/*
 * MODULE_IMPORT_NS() is added by commit id 8651ec01daeda
 * ("module: add support for symbol namespaces") in 5.4
 */
#if defined(MODULE_IMPORT_NS)

/*
 * DMA_BUF namespace is added by commit id 16b0314aa746
 * ("dma-buf: move dma-buf symbols into the DMA_BUF module namespace") in 5.16
 */
MODULE_IMPORT_NS(DMA_BUF);

#endif

static NvU32 nv_num_instances;

// lock required to protect table.
struct semaphore nv_module_table_lock;

// minor number table
nvidia_module_t *nv_minor_num_table[NV_FRONTEND_CONTROL_DEVICE_MINOR_MAX + 1];

int nvidia_init_module(void);
void nvidia_exit_module(void);

/* EXPORTS to Linux Kernel */

int          nvidia_frontend_open(struct inode *, struct file *);
int          nvidia_frontend_close(struct inode *, struct file *);
unsigned int nvidia_frontend_poll(struct file *, poll_table *);
int          nvidia_frontend_ioctl(struct inode *, struct file *, unsigned int, unsigned long);
long         nvidia_frontend_unlocked_ioctl(struct file *, unsigned int, unsigned long);
long         nvidia_frontend_compat_ioctl(struct file *, unsigned int, unsigned long);
int          nvidia_frontend_mmap(struct file *, struct vm_area_struct *);

/* character driver entry points */
static struct file_operations nv_frontend_fops = {
    .owner     = THIS_MODULE,
    .poll      = nvidia_frontend_poll,
    .unlocked_ioctl = nvidia_frontend_unlocked_ioctl,
#if NVCPU_IS_X86_64 || NVCPU_IS_AARCH64
    .compat_ioctl = nvidia_frontend_compat_ioctl,
#endif
    .mmap      = nvidia_frontend_mmap,
    .open      = nvidia_frontend_open,
    .release   = nvidia_frontend_close,
};

/* Helper functions */

static int add_device(nvidia_module_t *module, nv_linux_state_t *device, NvBool all)
{
    NvU32 i;
    int rc = -1;

    // look for free a minor number and assign unique minor number to this device
    for (i = 0; i <= NV_FRONTEND_CONTROL_DEVICE_MINOR_MIN; i++)
    {
        if (nv_minor_num_table[i] == NULL)
        {
            nv_minor_num_table[i] = module;
            device->minor_num = i;
            if (all == NV_TRUE)
            {
                device = device->next;
                if (device == NULL)
                {
                    rc = 0;
                    break;
                }
            }
            else
            {
                rc = 0;
                break;
            }
        }
    }
    return rc;
}

static int remove_device(nvidia_module_t *module, nv_linux_state_t *device)
{
    int rc = -1;

    // remove this device from minor_number table
    if ((device != NULL) && (nv_minor_num_table[device->minor_num] != NULL))
    {
        nv_minor_num_table[device->minor_num] = NULL;
        device->minor_num = 0;
        rc = 0;
    }
    return rc;
}

/* Export functions */

int nvidia_register_module(nvidia_module_t *module)
{
    int rc = 0;
    NvU32 ctrl_minor_num;

    down(&nv_module_table_lock);
    if (module->instance >= NV_MAX_MODULE_INSTANCES)
    {
        printk("NVRM: NVIDIA module instance %d registration failed.\n",
                module->instance);
        rc = -EINVAL;
        goto done;
    }

    ctrl_minor_num = NV_FRONTEND_CONTROL_DEVICE_MINOR_MAX - module->instance;
    nv_minor_num_table[ctrl_minor_num] = module;
    nv_num_instances++;
done:
    up(&nv_module_table_lock);

    return rc;
}
EXPORT_SYMBOL(nvidia_register_module);

int nvidia_unregister_module(nvidia_module_t *module)
{
    int rc = 0;
    NvU32 ctrl_minor_num;

    down(&nv_module_table_lock);

    ctrl_minor_num = NV_FRONTEND_CONTROL_DEVICE_MINOR_MAX - module->instance;
    if (nv_minor_num_table[ctrl_minor_num] == NULL)
    {
        printk("NVRM: NVIDIA module for %d instance does not exist\n",
                module->instance);
        rc = -1;
    }
    else
    {
        nv_minor_num_table[ctrl_minor_num] = NULL;
        nv_num_instances--;
    }

    up(&nv_module_table_lock);

    return rc;
}
EXPORT_SYMBOL(nvidia_unregister_module);

int nvidia_frontend_add_device(nvidia_module_t *module, nv_linux_state_t * device)
{
    int rc = -1;
    NvU32 ctrl_minor_num;

    down(&nv_module_table_lock);
    ctrl_minor_num = NV_FRONTEND_CONTROL_DEVICE_MINOR_MAX - module->instance;
    if (nv_minor_num_table[ctrl_minor_num] == NULL)
    {
        printk("NVRM: NVIDIA module for %d instance does not exist\n",
                module->instance);
        rc = -1;
    }
    else
    {
        rc = add_device(module, device, NV_FALSE);
    }
    up(&nv_module_table_lock);

    return rc;
}
EXPORT_SYMBOL(nvidia_frontend_add_device);

int nvidia_frontend_remove_device(nvidia_module_t *module, nv_linux_state_t * device)
{
    int rc = 0;
    NvU32 ctrl_minor_num;

    down(&nv_module_table_lock);
    ctrl_minor_num = NV_FRONTEND_CONTROL_DEVICE_MINOR_MAX - module->instance;
    if (nv_minor_num_table[ctrl_minor_num] == NULL)
    {
        printk("NVRM: NVIDIA module for %d instance does not exist\n",
                module->instance);
        rc = -1;
    }
    else
    {
        rc = remove_device(module, device);
    }
    up(&nv_module_table_lock);

    return rc;
}
EXPORT_SYMBOL(nvidia_frontend_remove_device);

int nvidia_frontend_open(
    struct inode *inode,
    struct file *file
)
{
    int rc = -ENODEV;
    nvidia_module_t *module = NULL;

    NvU32 minor_num = NV_FRONTEND_MINOR_NUMBER(inode);

    down(&nv_module_table_lock);
    module = nv_minor_num_table[minor_num];

    if ((module != NULL) && (module->open != NULL))
    {
        // Increment the reference count of module to ensure that module does
        // not get unloaded if its corresponding device file is open, for
        // example nvidiaN.ko should not get unloaded if /dev/nvidiaN is open.
        if (!try_module_get(module->owner))
        {
            up(&nv_module_table_lock);
            return -ENODEV;
        }
        rc = module->open(inode, file);
        if (rc < 0)
        {
            module_put(module->owner);
        }
    }

    up(&nv_module_table_lock);
    return rc;
}

int nvidia_frontend_close(
    struct inode *inode,
    struct file *file
)
{
    int rc = -ENODEV;
    nvidia_module_t *module = NULL;

    NvU32 minor_num = NV_FRONTEND_MINOR_NUMBER(inode);

    module = nv_minor_num_table[minor_num];

    if ((module != NULL) && (module->close != NULL))
    {
        rc = module->close(inode, file);

        // Decrement the reference count of module.
        module_put(module->owner);
    }

    return rc;
}

unsigned int nvidia_frontend_poll(
    struct file *file,
    poll_table *wait
)
{
    unsigned int mask = 0;
    struct inode *inode = NV_FILE_INODE(file);
    NvU32 minor_num = NV_FRONTEND_MINOR_NUMBER(inode);
    nvidia_module_t *module = nv_minor_num_table[minor_num];

    if ((module != NULL) && (module->poll != NULL))
        mask = module->poll(file, wait);

    return mask;
}

int nvidia_frontend_ioctl(
    struct inode *inode,
    struct file *file,
    unsigned int cmd,
    unsigned long i_arg)
{
    int rc = -ENODEV;
    nvidia_module_t *module = NULL;

    NvU32 minor_num = NV_FRONTEND_MINOR_NUMBER(inode);
    module = nv_minor_num_table[minor_num];

    if ((module != NULL) && (module->ioctl != NULL))
        rc = module->ioctl(inode, file, cmd, i_arg);

    return rc;
}

long nvidia_frontend_unlocked_ioctl(
    struct file *file,
    unsigned int cmd,
    unsigned long i_arg
)
{
    return nvidia_frontend_ioctl(NV_FILE_INODE(file), file, cmd, i_arg);
}

long nvidia_frontend_compat_ioctl(
    struct file *file,
    unsigned int cmd,
    unsigned long i_arg
)
{
    return nvidia_frontend_ioctl(NV_FILE_INODE(file), file, cmd, i_arg);
}

int nvidia_frontend_mmap(
    struct file *file,
    struct vm_area_struct *vma
)
{
    int rc = -ENODEV;
    struct inode *inode = NV_FILE_INODE(file);
    NvU32 minor_num = NV_FRONTEND_MINOR_NUMBER(inode);
    nvidia_module_t *module = nv_minor_num_table[minor_num];

    if ((module != NULL) && (module->mmap != NULL))
        rc = module->mmap(file, vma);

    return rc;
}

static int __init nvidia_frontend_init_module(void)
{
    int status = 0;

    // initialise nvidia module table;
    nv_num_instances = 0;
    memset(nv_minor_num_table, 0, sizeof(nv_minor_num_table));
    NV_INIT_MUTEX(&nv_module_table_lock);

    status = nvidia_init_module();
    if (status < 0)
    {
        return status;
    }

    // register char device
    status = register_chrdev(NV_MAJOR_DEVICE_NUMBER, "nvidia-frontend", &nv_frontend_fops);
    if (status < 0)
    {
        printk("NVRM: register_chrdev() failed!\n");
        nvidia_exit_module();
    }

    return status;
}

static void __exit nvidia_frontend_exit_module(void)
{
    /*
     * If this is the last nvidia_module to be unregistered, cleanup and
     * unregister char dev
     */
    if (nv_num_instances == 1)
    {
        unregister_chrdev(NV_MAJOR_DEVICE_NUMBER, "nvidia-frontend");
    }

    nvidia_exit_module();
}

module_init(nvidia_frontend_init_module);
module_exit(nvidia_frontend_exit_module);