/*
 * Copyright (c) Hisilicon Technologies Co., Ltd. 2006-2020. All rights reserved.
 * Description: proc drv
 * Author: Hisilicon
 * Create: 2006-8-2
 */

#include "drv_proc_ext.h"

#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>
#include <linux/ctype.h>
#include <linux/file.h>
#include <uapi/linux/major.h>
#include <linux/version.h>

#include "soc_log.h"
#include "drv_module_ext.h"
#include "drv_ioctl_userproc.h"
#include "drv_mem_ext.h"
#include "linux/huanglong/securec.h"

#define PROC_MAX_ENTRIES 256
#define PROC_DEFAULT_ECHO_DEVICE_HANDLE 0

struct proc_dir_entry *g_proc_cmpi = TD_NULL;
struct proc_dir_entry *g_proc_soc = TD_NULL;
static ext_proc_item g_proc_items[PROC_MAX_ENTRIES] = {{{0}}};

static DEFINE_MUTEX(g_proc_mutex_lock);

/* for some ttyXXX device major number */
#define PROC_SERIAL_MAJOR_NUM 204

static ext_proc_module_fn *g_proc_intf_para = TD_NULL;

struct proc_dir_entry *drv_proc_get_cmpi(td_void)
{
    if (g_proc_cmpi == TD_NULL) {
        soc_print("g_proc_cmpi is null!\n");
    }
    return g_proc_cmpi;
}

struct proc_dir_entry *drv_proc_get_soc(td_void)
{
    if (g_proc_soc == TD_NULL) {
        soc_print("g_proc_soc is null!\n");
    }
    return g_proc_soc;
}

td_s32 ext_drv_proc_register_para(ext_proc_module_fn *para)
{
    if (para == TD_NULL) {
        return TD_FAILURE;
    }
    if ((para->add_module_fn == TD_NULL) || (para->remove_module_fn == TD_NULL)) {
        return TD_FAILURE;
    }
    g_proc_intf_para = para;
    return TD_SUCCESS;
}

td_void ext_drv_proc_unregister_para(td_void)
{
    g_proc_intf_para = TD_NULL;
    return;
}

ext_proc_item *ext_drv_proc_add_module(const td_char *entry_name, ext_proc_fn_set *fn_set, td_void *data)
{
    if (g_proc_intf_para != TD_NULL) {
        if (g_proc_intf_para->add_module_fn) {
            return g_proc_intf_para->add_module_fn(entry_name, fn_set, data);
        }
    }
    return TD_NULL;
}

td_void ext_drv_proc_remove_module(const td_char *entry_name)
{
    if (g_proc_intf_para != TD_NULL) {
        if (g_proc_intf_para->remove_module_fn) {
            g_proc_intf_para->remove_module_fn(entry_name);
        }
    }
    return;
}

td_void drv_proc_echo_helper(const td_char *buf)
{
    struct kstat stat = {0};
    td_s32 ret;

    if (buf == TD_NULL) {
        soc_print("Invalid argument buf or size!\n");
        return;
    }

    ret = vfs_fstat(PROC_DEFAULT_ECHO_DEVICE_HANDLE, &stat);
    if (ret) {
        soc_print("Default echo device handle(%u) invalid!\n", PROC_DEFAULT_ECHO_DEVICE_HANDLE);
        return;
    }

    /*
     * echo device must be chrdev and major number must be PROC_SERIAL_MAJOR_NUM or
     * TTYAUX_MAJOR or UNIX98_PTY_SLAVE_MAJOR
     */
    if (S_ISCHR(stat.mode) && (MAJOR(stat.rdev) == PROC_SERIAL_MAJOR_NUM || MAJOR(stat.rdev) == TTYAUX_MAJOR ||
                               MAJOR(stat.rdev) == UNIX98_PTY_SLAVE_MAJOR)) {
        struct file *file = fget(PROC_DEFAULT_ECHO_DEVICE_HANDLE);
        if (file != TD_NULL) {
#if (LINUX_VERSION_CODE < KERNEL_VERSION(5, 15, 0))
            mm_segment_t old_fs = get_fs();
#endif
            loff_t pos = 0; /* file pos is invalid for chrdev */
#if (LINUX_VERSION_CODE < KERNEL_VERSION(5, 15, 0))
            set_fs(KERNEL_DS);
#endif
            ret = vfs_write(file, buf, strlen(buf), &pos);
            if (ret < 0) {
                soc_print("write to echo device failed(%d)!\n", ret);
            }
#if (LINUX_VERSION_CODE < KERNEL_VERSION(5, 15, 0))
            set_fs(old_fs);
#endif
            fput(file);
        }
    } else {
        soc_print("Default echo device is invalid!\n");
    }
}

/*
 * echo string to current terminal display(serial console or tty).
 * this implement implicit that current task file handle '0' must be terminal device file.
 * otherwise do nothing.
 */
td_void ext_drv_proc_echo_helper_vargs(td_char *buf, td_u32 size, const td_char *fmt, va_list args)
{
    td_s32 ret;

    if ((buf == TD_NULL) || (size == 0) || (fmt == TD_NULL)) {
        soc_print("Invalid argument buf or size or fmt!\n");
        return;
    }

    ret = vsnprintf_s(buf, size, size - 1, fmt, args);
    if (ret < 0) {
        soc_print("vsnprintf_s error!\n");
        return;
    }

    drv_proc_echo_helper(buf);
}
EXPORT_SYMBOL(ext_drv_proc_echo_helper_vargs);

/* general echo helper function */
td_void ext_drv_proc_echo_helper(const td_char *fmt, ...)
{
    va_list args;
    td_char *buf = TD_NULL;

    if (fmt == TD_NULL) {
        soc_print("Invalid argument fmt!\n");
        return;
    }
    buf = SOC_KZALLOC(SOC_ID_PROC, EXT_USER_PROC_BUF_SIZE, GFP_KERNEL);
    if (buf == TD_NULL) {
        soc_print("Memory allocate failed for proc\n");
        return;
    }

    va_start(args, fmt);
    ext_drv_proc_echo_helper_vargs(buf, EXT_USER_PROC_BUF_SIZE, fmt, args);
    va_end(args);

    SOC_KFREE(SOC_ID_PROC, buf);
    buf = TD_NULL;
}
EXPORT_SYMBOL(ext_drv_proc_echo_helper);

static td_s32 drv_proc_cmpi_open(struct inode *inode, struct file *file)
{
    ext_proc_item *item = PDE_DATA(inode);

    if (item != TD_NULL && item->read) {
        return single_open(file, item->read, item);
    }

    return -ENOSYS;
}

static ssize_t drv_proc_cmpi_write(struct file *file, const td_char __user *buf, size_t count, loff_t *pos)
{
    struct seq_file *s = file->private_data;
    ext_proc_item *item = s->private;

    if (item->write) {
        return item->write(file, buf, count, pos);
    }
    return -ENOSYS;
}

static td_slong drv_proc_cmpi_unlocked_ioctl(struct file *file, td_u32 cmd, td_ulong arg)
{
    struct seq_file *s = file->private_data;
    ext_proc_item *item = s->private;

    if (item->ioctl) {
        return item->ioctl(s, cmd, arg);
    }

    return 0;
}

#ifdef CONFIG_COMPAT
static td_slong drv_proc_cmpi_compat_ioctl(struct file *file, td_u32 cmd, td_ulong arg)
{
    return -ENOSYS;
}
#endif

#if (LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0))
static struct proc_ops g_proc_cmpi_ops = {
    .proc_open         = drv_proc_cmpi_open,
    .proc_read         = seq_read,
    .proc_write        = drv_proc_cmpi_write,
    .proc_lseek        = seq_lseek,
    .proc_release      = single_release,
    .proc_ioctl        = drv_proc_cmpi_unlocked_ioctl,
#ifdef CONFIG_COMPAT
    .proc_compat_ioctl = drv_proc_cmpi_compat_ioctl,
#endif
};
#else
static struct file_operations g_proc_cmpi_ops __attribute__((unused)) = {
    .owner          = THIS_MODULE,
    .open           = drv_proc_cmpi_open,
    .read           = seq_read,
    .write          = drv_proc_cmpi_write,
    .llseek         = seq_lseek,
    .release        = single_release,
    .unlocked_ioctl = drv_proc_cmpi_unlocked_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl   = drv_proc_cmpi_compat_ioctl,
#endif
};
#endif

static td_bool drv_proc_is_hex(const td_char *p)
{
    td_s32 i, len;

    len = (td_s32)strlen(p);
    if (len <= 0x2) {
        return TD_FALSE;
    }

    p += 0x2;
    for (i = 0; i < len - 0x2; i++) {
        if ((p[i] < '0') || p[i] > 'f') {
            return TD_FALSE;
        }

        if ((p[i] > '9') && p[i] < 'a') {
            return TD_FALSE;
        }
    }
    return TD_TRUE;
}

static td_s32 drv_proc_str_to_digit(const unsigned char *str, td_u32 *digit)
{
    td_s32 i, len;
    td_u32 val = 0;

    len = (td_s32)strlen(str);
    if (len <= 0x2) {
        return TD_FAILURE;
    }

    str += 0x2;
    for (i = 0; i < len - 0x2; i++) {
        val <<= 0x4;
        if (str[i] <= '9') {
            val += str[i] - '0';
        } else {
            val += str[i] - 'a' + 0x0a;
        }
    }
    *digit = val;

    return TD_SUCCESS;
}

static td_s32 drv_proc_cmpi_parse_cmd_data_deal(char *ptr, td_u32 size)
{
    td_s32 i;

    while (*ptr == ' ' && *ptr++ != '\0') {
    }

    /* covert into lowercase string */
    for (i = 0; i < strlen(ptr); i++) {
        if (i >= size) {
            return -1;
        }
        ptr[i] = (char)tolower(ptr[i]);
    }

    for (i = (td_s32)strlen(ptr); i > 0; i--) {
        if ((*(ptr + i - 1) < '0') || (*(ptr + i - 1) > 'f')) {
            *(ptr + i - 1) = '\0';
        } else if ((*(ptr + i - 1) > '9') && (*(ptr + i - 1) < 'a')) {
            *(ptr + i - 1) = '\0';
        } else {
            break;
        }
    }

    return TD_SUCCESS;
}

static td_s32 drv_proc_get_args(const td_char *ptr, td_char (*the_args)[0x40])
{
    td_s32 i;

    for (i = 0; i < 0x2; i++) {
        td_s32 j = 0;
        while (*ptr == ' ' && *ptr++ != '\0');
        while ((*ptr != ' ') && (*ptr != '\0') && (j < 64)) { /* max row 64 */
            the_args[i][j++] = *ptr++;
        }

        if (j < 64) { /* max row 64 */
            the_args[i][j] = '\0';
        } else {
            the_args[i][64 - 1] = '\0'; /* max row 64 */
        }

        if (*ptr == '\0') {
            i++;
            break;
        }
    }

    return i;
}

td_s32 drv_proc_cmpi_parse_cmd(char *ptr, td_u32 size, td_u32 *para1, td_u32 *para2)
{
    td_s32 i;
    td_char the_args[2][64]; /* row 64, col 2 */

    if (ptr == TD_NULL) {
        soc_log_err("ptr is null!\n");
        return -1;
    }
    if (drv_proc_cmpi_parse_cmd_data_deal(ptr, size) != TD_SUCCESS) {
        soc_log_err("drv_proc_cmpi_parse_cmd_data_deal failed!\n");
        return -1;
    }

    i = drv_proc_get_args(ptr, the_args);

    if ((the_args[0][0] != '0') || (the_args[0][1] != 'x') ||
        (the_args[1][0] != '0') || (the_args[1][1] != 'x')) {
        return -1;
    }

    if ((!drv_proc_is_hex(the_args[0])) || (!drv_proc_is_hex(the_args[1]))) {
        return -1;
    }

    if (drv_proc_str_to_digit(the_args[0], para1) != TD_SUCCESS) {
        return -1;
    }

    if (drv_proc_str_to_digit(the_args[1], para2) != TD_SUCCESS) {
        return -1;
    }

    return i;
}

static td_void drv_proc_write_show_help(td_void)
{
    ext_drv_proc_echo_helper("\nPLS type \"echo 0xxxxxxxxx 0xxxxxxxxx > /proc/msp/demux\"\n");
    ext_drv_proc_echo_helper("E.g.: \"echo 0x40002 0x4010 > /proc/msp/demux\"\n");
}

ssize_t ext_drv_proc_module_write(struct file *file, const char __user *buf, size_t count,
    loff_t *pos, ext_proc_ctrl_fn ctrl_fn)
{
    char tmp_buf[64] = {0}; /* array num is 64 */
    char *tmp_buf_ptr = tmp_buf;
    td_u32 para1, para2;

    if (ctrl_fn == TD_NULL || pos == TD_NULL) {
        return -EFAULT;
    }
    if ((buf == TD_NULL) || (count >= sizeof(tmp_buf))) {
        drv_proc_write_show_help();
        goto out;
    }

    if (copy_from_user(tmp_buf_ptr, buf, count)) {
        return -EFAULT;
    }

    tmp_buf[count > 1 ? count - 1 : 0] = '\0';

    if (drv_proc_cmpi_parse_cmd(tmp_buf, sizeof(tmp_buf), &para1, &para2) > 0) {
        ctrl_fn(para1, para2);
    } else {
        drv_proc_write_show_help();
    }

out:
    *pos = count;
    return count;
}

static ext_proc_item *drv_proc_add_module(const td_char *entry_name, ext_proc_fn_set *fn_set, td_void *data)
{
    struct proc_dir_entry *entry = TD_NULL;
    td_s32 i;
    td_s32 ret;

    if ((entry_name == TD_NULL) || (strlen(entry_name) > EXT_PROC_MAX_ENTRY_NAME_LEN)) {
        return TD_NULL;
    }

    mutex_lock(&g_proc_mutex_lock);
    for (i = 0; i < PROC_MAX_ENTRIES; i++) {
        if (!g_proc_items[i].entry) {
            break;
        }
    }

    if (i == PROC_MAX_ENTRIES) {
        mutex_unlock(&g_proc_mutex_lock);
        soc_print("ERROR: add proc entry %s over LIMIT:%#x\n", entry_name, PROC_MAX_ENTRIES);
        return TD_NULL;
    }

    ret = strncpy_s(g_proc_items[i].entry_name, sizeof(g_proc_items[i].entry_name),
                    entry_name, sizeof(g_proc_items[i].entry_name) - 1);
    if (ret != EOK) {
        mutex_unlock(&g_proc_mutex_lock);
        soc_print("strncpy_s error\n");
        return TD_NULL;
    }

    if (fn_set != TD_NULL) {
        g_proc_items[i].read  = fn_set->read;
        g_proc_items[i].write = fn_set->write;
        g_proc_items[i].ioctl = fn_set->ioctl;
    } else {
        g_proc_items[i].read  = TD_NULL;
        g_proc_items[i].write = TD_NULL;
        g_proc_items[i].ioctl = TD_NULL;
    }

    g_proc_items[i].data = data;

    entry = proc_create_data(entry_name, 0, g_proc_cmpi, &g_proc_cmpi_ops, &g_proc_items[i]);
    if (entry == TD_NULL) {
        mutex_unlock(&g_proc_mutex_lock);
        return TD_NULL;
    }

    g_proc_items[i].entry = entry;

    mutex_unlock(&g_proc_mutex_lock);

    return &g_proc_items[i];
}

static td_void drv_proc_remove_module(const td_char *entry_name)
{
    td_s32 i;

    mutex_lock(&g_proc_mutex_lock);
    for (i = 0; i < PROC_MAX_ENTRIES; i++) {
        if (!strncmp(g_proc_items[i].entry_name, entry_name, sizeof(g_proc_items[i].entry_name))) {
            break;
        }
    }

    if (i == PROC_MAX_ENTRIES) {
        mutex_unlock(&g_proc_mutex_lock);
        soc_print("Not find the entry:%s\n", entry_name);
        return;
    }
    remove_proc_entry(g_proc_items[i].entry_name, g_proc_cmpi);
    g_proc_items[i].entry = TD_NULL;

    mutex_unlock(&g_proc_mutex_lock);
}

static ext_proc_module_fn g_proc_para = {
    .add_module_fn = drv_proc_add_module,
    .remove_module_fn = drv_proc_remove_module,
};

td_s32 drv_proc_init(td_void)
{
    if (ext_drv_proc_register_para(&g_proc_para) != TD_SUCCESS) {
        return TD_FAILURE;
    }

    return TD_SUCCESS;
}

td_void drv_proc_exit(td_void)
{
    ext_drv_proc_unregister_para();

    return;
}

EXPORT_SYMBOL(ext_drv_proc_module_write);
EXPORT_SYMBOL(ext_drv_proc_register_para);
EXPORT_SYMBOL(ext_drv_proc_unregister_para);
EXPORT_SYMBOL(ext_drv_proc_add_module);
EXPORT_SYMBOL(ext_drv_proc_remove_module);