/*******************************************************************************
    Copyright (c) 2015-2019 NVidia Corporation

    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 "conftest.h"

#include "nvlink_os.h"
#include "nvlink_linux.h"
#include "nvlink_errors.h"
#include "nvlink_export.h"
#include "nv-linux.h"
#include "nv-procfs.h"
#include "nv-time.h"
#include "nvlink_caps.h"

#include <linux/module.h>
#include <linux/major.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/string.h>
#include <linux/mutex.h>

#define MAX_ERROR_STRING           512

typedef struct nvlink_file_private
{
    struct
    {
        /* A duped file descriptor for fabric_mgmt capability */
        int fabric_mgmt;
    } capability_fds;
} nvlink_file_private_t;

#define NVLINK_SET_FILE_PRIVATE(filp, data) ((filp)->private_data = (data))
#define NVLINK_GET_FILE_PRIVATE(filp) ((nvlink_file_private_t *)(filp)->private_data)

typedef struct
{
    struct mutex    lock;
    NvBool          initialized;
    struct cdev     cdev;
    dev_t           devno;
    int             opened;
    int             major_devnum;
} _nvlink_drvctx;


// nvlink driver local state
static _nvlink_drvctx nvlink_drvctx;

#if defined(CONFIG_PROC_FS)
#define NV_DEFINE_SINGLE_NVLINK_PROCFS_FILE(name) \
    NV_DEFINE_SINGLE_PROCFS_FILE_READ_ONLY_NEW(name, nv_system_pm_lock)
#endif

#define NVLINK_PROCFS_DIR "driver/nvidia-nvlink"

static struct proc_dir_entry *nvlink_procfs_dir = NULL;

#if defined(CONFIG_PROC_FS)
    static int nvlink_is_procfs_available = 1;
#else
    static int nvlink_is_procfs_available = 0;
#endif

static struct proc_dir_entry *nvlink_permissions = NULL;

static int nv_procfs_read_permissions(struct seq_file *s, void *v)
{
    // Restrict device node permissions - 0666.
    seq_printf(s, "%s: %u\n", "DeviceFileMode", 438);

    return 0;
}

NV_DEFINE_SINGLE_NVLINK_PROCFS_FILE(permissions);

static void nvlink_permissions_exit(void)
{
    if (!nvlink_permissions)
    {
        return;
    }

    NV_REMOVE_PROC_ENTRY(nvlink_permissions);
    nvlink_permissions = NULL;
}

static int nvlink_permissions_init(void)
{
    if (!nvlink_procfs_dir)
    {
        return -EACCES;
    }

    nvlink_permissions = NV_CREATE_PROC_FILE("permissions",
                                             nvlink_procfs_dir,
                                             permissions,
                                             NULL);
    if (!nvlink_permissions)
    {
        return -EACCES;
    }

    return 0;
}

static void nvlink_procfs_exit(void)
{
    nvlink_permissions_exit();

    if (!nvlink_procfs_dir)
    {
        return;
    }

    NV_REMOVE_PROC_ENTRY(nvlink_procfs_dir);
    nvlink_procfs_dir = NULL;
}

static int nvlink_procfs_init(void)
{
    int rc = 0;

    if (!nvlink_is_procfs_available)
    {
        return -EACCES;
    }

    nvlink_procfs_dir = NV_CREATE_PROC_DIR(NVLINK_PROCFS_DIR, NULL);
    if (!nvlink_procfs_dir)
    {
        return -EACCES;
    }

    rc = nvlink_permissions_init();
    if (rc < 0)
    {
        goto cleanup;
    }

    return 0;

cleanup:

    nvlink_procfs_exit();

    return rc;
}

static int nvlink_fops_open(struct inode *inode, struct file *filp)
{
    int rc = 0;
    nvlink_file_private_t *private = NULL;

    nvlink_print(NVLINK_DBG_INFO, "nvlink driver open\n");

    mutex_lock(&nvlink_drvctx.lock);

    // nvlink lib driver is currently exclusive open.
    if (nvlink_drvctx.opened)
    {
        rc = -EBUSY;
        goto open_error;
    }

    private = (nvlink_file_private_t *)nvlink_malloc(sizeof(*private));
    if (private == NULL)
    {
        rc = -ENOMEM;
        goto open_error;
    }

    private->capability_fds.fabric_mgmt = -1;
    NVLINK_SET_FILE_PRIVATE(filp, private);

    // mark our state as opened
    nvlink_drvctx.opened = NV_TRUE;

open_error:
    mutex_unlock(&nvlink_drvctx.lock);
    return rc;
}

static int nvlink_fops_release(struct inode *inode, struct file *filp)
{
    nvlink_file_private_t *private = NVLINK_GET_FILE_PRIVATE(filp);

    nvlink_print(NVLINK_DBG_INFO, "nvlink driver close\n");

    WARN_ON(private == NULL);

    mutex_lock(&nvlink_drvctx.lock);

    if (private->capability_fds.fabric_mgmt > 0)
    {
        nvlink_cap_release(private->capability_fds.fabric_mgmt);
        private->capability_fds.fabric_mgmt = -1;
    }

    nvlink_free(filp->private_data);
    NVLINK_SET_FILE_PRIVATE(filp, NULL);

    // mark the device as not opened
    nvlink_drvctx.opened = NV_FALSE;

    mutex_unlock(&nvlink_drvctx.lock);

    return 0;
}

static int nvlink_fops_ioctl(struct inode *inode,
                             struct file *filp,
                             unsigned int cmd,
                             unsigned long arg)
{
    nvlink_ioctrl_params ctrl_params = {0};
    int param_size = _IOC_SIZE(cmd);
    void *param_buf = NULL;
    NvlStatus ret_val = 0;
    int rc = 0;

    // no buffer for simple _IO types
    if (param_size)
    {
        // allocate a buffer to hold user input
        param_buf = kzalloc(param_size, GFP_KERNEL);
        if (NULL == param_buf) 
        {
            rc = -ENOMEM;
            goto nvlink_ioctl_fail;
        }

        // copy user input to kernel buffers. Simple _IOR() ioctls can skip this step.
        if (_IOC_DIR(cmd) & _IOC_WRITE)
        {
            // copy user input to local buffer
            if (copy_from_user(param_buf, (const void *)arg, param_size))
            {
                rc = -EFAULT;
                goto nvlink_ioctl_fail;
            }
        }
    }

    ctrl_params.osPrivate = filp->private_data;
    ctrl_params.cmd = _IOC_NR(cmd);
    ctrl_params.buf = param_buf;
    ctrl_params.size = param_size;

    ret_val = nvlink_lib_ioctl_ctrl(&ctrl_params);
    if (NVL_SUCCESS != ret_val)
    {
        rc = -EINVAL;
        goto nvlink_ioctl_fail;
    }

    //  no copy for write-only ioctl
    if ((param_size) && (_IOC_DIR(cmd) & _IOC_READ))
    {
        if (copy_to_user((void *)arg, ctrl_params.buf, ctrl_params.size))
        {
            rc = -EFAULT;
            goto nvlink_ioctl_fail;
        }
    }

nvlink_ioctl_fail:
    if (param_buf) 
    {
        kfree(param_buf);
    }
    return rc;
}

#if defined(NV_FILE_HAS_INODE)
#define NV_FILE_INODE(file) (file)->f_inode
#else
#define NV_FILE_INODE(file) (file)->f_dentry->d_inode
#endif

static long nvlink_fops_unlocked_ioctl(struct file *file,
                                       unsigned int cmd,
                                       unsigned long arg)
{
    return nvlink_fops_ioctl(NV_FILE_INODE(file), file, cmd, arg);
}


static const struct file_operations nvlink_fops = {
    .owner           = THIS_MODULE,
    .open            = nvlink_fops_open,
    .release         = nvlink_fops_release,
#if defined(NV_FILE_OPERATIONS_HAS_IOCTL)
    .ioctl           = nvlink_fops_ioctl,   
#endif    
    .unlocked_ioctl  = nvlink_fops_unlocked_ioctl,
};

int __init nvlink_core_init(void)
{
    NvlStatus ret_val;
    int rc;

    if (NV_TRUE == nvlink_drvctx.initialized)
    {
        nvlink_print(NVLINK_DBG_ERRORS, "nvlink core interface already initialized\n");
        return -EBUSY;
    }

    mutex_init(&nvlink_drvctx.lock);

    ret_val = nvlink_lib_initialize();
    if (NVL_SUCCESS != ret_val)
    {
        nvlink_print(NVLINK_DBG_ERRORS,  "Failed to initialize driver : %d\n", ret_val);
        rc = -ENODEV;
        goto nvlink_lib_initialize_fail;
    }

    rc = alloc_chrdev_region(&nvlink_drvctx.devno, 0, NVLINK_NUM_MINOR_DEVICES,
                             NVLINK_DEVICE_NAME);
    if (rc < 0)
    {
        nvlink_print(NVLINK_DBG_ERRORS, "alloc_chrdev_region failed: %d\n", rc);
        goto alloc_chrdev_region_fail;
    }

    nvlink_drvctx.major_devnum = MAJOR(nvlink_drvctx.devno);
    nvlink_print(NVLINK_DBG_INFO, "Nvlink Core is being initialized, major device number %d\n",
                 nvlink_drvctx.major_devnum);

    cdev_init(&nvlink_drvctx.cdev, &nvlink_fops);
    nvlink_drvctx.cdev.owner = THIS_MODULE;
    rc = cdev_add(&nvlink_drvctx.cdev, nvlink_drvctx.devno, NVLINK_NUM_MINOR_DEVICES);
    if (rc < 0)
    {
        nvlink_print(NVLINK_DBG_ERRORS, " Unable to create cdev\n");
        goto cdev_add_fail;
    }

    rc = nvlink_procfs_init();
    if (rc < 0)
    {
        goto procfs_init_fail;
    }

    rc = nvlink_cap_init(NVLINK_PROCFS_DIR);
    if (rc < 0)
    {
        nvlink_print(NVLINK_DBG_ERRORS, " Unable to create capability\n");
        goto cap_init_fail;
    }

    nvlink_drvctx.initialized = NV_TRUE;

    return 0;

cap_init_fail:
    nvlink_procfs_exit();

procfs_init_fail:
    cdev_del(&nvlink_drvctx.cdev);

cdev_add_fail:
    unregister_chrdev_region(nvlink_drvctx.devno, NVLINK_NUM_MINOR_DEVICES);

alloc_chrdev_region_fail:
    nvlink_lib_unload();

nvlink_lib_initialize_fail:
    nv_mutex_destroy(&nvlink_drvctx.lock);
    return rc;
}

void nvlink_core_exit(void)
{
    if (NV_FALSE == nvlink_drvctx.initialized)
    {
        return;
    }

    nvlink_cap_exit();

    nvlink_procfs_exit();

    cdev_del(&nvlink_drvctx.cdev);

    unregister_chrdev_region(nvlink_drvctx.devno, NVLINK_NUM_MINOR_DEVICES);

    nvlink_lib_unload();

    nv_mutex_destroy(&nvlink_drvctx.lock);

    nvlink_print(NVLINK_DBG_INFO, "Unregistered the Nvlink Core, major device number %d\n",
                 nvlink_drvctx.major_devnum);
}

void
nvlink_print
(
    const char *file,
    int         line,
    const char *function,
    int         log_level,
    const char *fmt,
    ...
)
{
    va_list arglist;
    char    nv_string[MAX_ERROR_STRING];
    char   *sys_log_level;

    switch (log_level) {
    case NVLINK_DBG_LEVEL_INFO:
        sys_log_level = KERN_INFO;
        break;
    case NVLINK_DBG_LEVEL_SETUP:
        sys_log_level = KERN_DEBUG;
        break;
    case NVLINK_DBG_LEVEL_USERERRORS:
        sys_log_level = KERN_NOTICE;
        break;
    case NVLINK_DBG_LEVEL_WARNINGS:
        sys_log_level = KERN_WARNING;
        break;
    case NVLINK_DBG_LEVEL_ERRORS:
        sys_log_level = KERN_ERR;
        break;
    default:
        sys_log_level = KERN_INFO;
        break;
    }

    va_start(arglist, fmt);
    vsnprintf(nv_string, sizeof(nv_string), fmt, arglist);
    va_end(arglist);

    nv_string[sizeof(nv_string) - 1] = '\0';
    printk("%snvidia-nvlink: %s", sys_log_level, nv_string);
}

void * nvlink_malloc(NvLength size)
{
   return kmalloc(size, GFP_KERNEL);
}

void nvlink_free(void *ptr)
{
    return kfree(ptr);
}

char * nvlink_strcpy(char *dest, const char *src)
{
    return strcpy(dest, src);
}

int nvlink_strcmp(const char *dest, const char *src)
{
    return strcmp(dest, src);
}

NvLength nvlink_strlen(const char *s)
{
    return strlen(s);
}

int nvlink_snprintf(char *dest, NvLength size, const char *fmt, ...)
{
    va_list arglist;
    int chars_written;

    va_start(arglist, fmt);
    chars_written = vsnprintf(dest, size, fmt, arglist);
    va_end(arglist);

    return chars_written;
}

NvU32 nvlink_memRd32(const volatile void * address)
{
    return (*(const volatile NvU32*)(address));
}

void nvlink_memWr32(volatile void *address, NvU32 data)
{
    (*(volatile NvU32 *)(address)) = data;
}

NvU64 nvlink_memRd64(const volatile void * address)
{
    return (*(const volatile NvU64 *)(address));
}

void nvlink_memWr64(volatile void *address, NvU64 data)
{
    (*(volatile NvU64 *)(address)) = data;
}

void * nvlink_memset(void *dest, int value, NvLength size)
{
     return memset(dest, value, size);
}

void * nvlink_memcpy(void *dest, const void *src, NvLength size)
{
    return memcpy(dest, src, size);
}

int nvlink_memcmp(const void *s1, const void *s2, NvLength size)
{
    return memcmp(s1, s2, size);
}

/*
 * Sleep for specified milliseconds. Yields the CPU to scheduler.
 */
void nvlink_sleep(unsigned int ms)
{
    NV_STATUS status;

    status = nv_sleep_ms(ms);

    if (status !=  NV_OK)
    {
        if (printk_ratelimit())
        {
            nvlink_print(NVLINK_DBG_ERRORS, "NVLink: requested sleep duration"
                         " %d msec exceeded %d msec\n",
                         ms, NV_MAX_ISR_DELAY_MS);
            WARN_ON(1);
        }
    }
}

void nvlink_assert(int cond)
{
    if ((cond) == 0x0)
    {
        if (printk_ratelimit())
        {
            nvlink_print(NVLINK_DBG_ERRORS, "NVLink: Assertion failed!\n");
            WARN_ON(1);
        }

        dbg_breakpoint();
    }
}

void * nvlink_allocLock()
{
    struct semaphore *sema;

    sema = nvlink_malloc(sizeof(*sema));
    if (sema == NULL)
    {
        nvlink_print(NVLINK_DBG_ERRORS, "Failed to allocate sema!\n");
        return NULL;
    }
    sema_init(sema, 1);

    return sema;
}

void nvlink_acquireLock(void *hLock)
{
    down(hLock);
}

void nvlink_releaseLock(void *hLock)
{
    up(hLock);
}

void nvlink_freeLock(void *hLock)
{
    if (NULL == hLock)
    {
        return;
    }

    NVLINK_FREE(hLock);
}

NvBool nvlink_isLockOwner(void *hLock)
{
    return NV_TRUE;
}

NvlStatus nvlink_acquire_fabric_mgmt_cap(void *osPrivate, NvU64 capDescriptor)
{
    int dup_fd = -1;
    nvlink_file_private_t *private_data = (nvlink_file_private_t *)osPrivate;

    if (private_data == NULL)
    {
        return NVL_BAD_ARGS;
    }

    dup_fd = nvlink_cap_acquire((int)capDescriptor,
                                NVLINK_CAP_FABRIC_MANAGEMENT);
    if (dup_fd < 0)
    {
        return NVL_ERR_OPERATING_SYSTEM;
    }

    private_data->capability_fds.fabric_mgmt = dup_fd;
    return NVL_SUCCESS;
}

int nvlink_is_fabric_manager(void *osPrivate)
{
    nvlink_file_private_t *private_data = (nvlink_file_private_t *)osPrivate;

    /* Make sure that fabric mgmt capbaility fd is valid */
    if ((private_data == NULL) ||
        (private_data->capability_fds.fabric_mgmt < 0))
    {
        return 0;
    }

    return 1;
}

int nvlink_is_admin(void)
{
    return NV_IS_SUSER();
}
