#include <linux/version.h>
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 19, 0)
#include <linux/uaccess.h>
#endif

#include <asm/uaccess.h>
#include <linux/kernel.h> /* Wd're doing kernel work */
#include <linux/module.h> /* specifically, a module */
#include <linux/types.h>
#include <linux/init.h>   /* Need for the macros */
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/ioport.h>
#include <linux/io.h>
#include <linux/pci.h>
#include <linux/sched.h>
#include <net/sock.h>
#include <net/genetlink.h>
#include <linux/netlink.h>
#include <linux/miscdevice.h>
#include "lpc_dbg.h"

typedef struct rg_lpc_device_s {
    u16 base;
    u16 size;
    u8  type;
    u8  id;
    u8  lpc_pci_addr;
} rg_lpc_device_t;

typedef enum rg_lpc_dev_type_s {
    LPC_DEVICE_CPLD         = 1,
    LPC_DEVICE_FPGA         = 2,
} rg_lpc_dev_type_t;

#define MAX_LPC_DEV_NUM                 (4)
#define LPC_PCI_CFG_BASE(__lgir)        ((0x84) + ((__lgir) * 4))
#define MAX_CPLD_REG_SIZE               (0x100)
#define MAX_FPGA_REG_SIZE               (0x100) //#  fix compile actual value  0x10000
#define LPC_GET_CPLD_ID(addr)           ((addr >> 16) & 0xff)
#define LPC_GET_CPLD_OFFSET(addr)       ((addr) & 0xff)

int lpc_dbg_verbose = 0;
int lpc_dbg_error = 0;
int lpc_dbg_info = 0;
module_param(lpc_dbg_verbose, int, S_IRUGO | S_IWUSR);
module_param(lpc_dbg_error, int, S_IRUGO | S_IWUSR);
module_param(lpc_dbg_info, int, S_IRUGO | S_IWUSR);


#define LPC_DBG_VERBOSE(fmt, args...) do {                                        \
    if (lpc_dbg_verbose) { \
        printk(KERN_ERR "[LPC_DBG][VERBOSE][func:%s line:%d]\r\n"fmt, __func__, __LINE__, ## args); \
    } \
} while (0)

#define LPC_DBG_ERROR(fmt, args...) do {                                        \
    if (lpc_dbg_error) { \
        printk(KERN_ERR "[LPC_DBG][ERROR][func:%s line:%d]\r\n"fmt, __func__, __LINE__, ## args); \
    } \
} while (0)

#define LPC_DBG_INFO(fmt, args...) do {                                        \
    if (lpc_dbg_info) { \
        printk(KERN_ERR ""fmt, ## args); \
    } \
} while (0)

static rg_lpc_device_t g_rg_lpc_dev_default[] = {
    {.base = 0x700, .size = MAX_CPLD_REG_SIZE, .type = LPC_DEVICE_CPLD, .id = 0, .lpc_pci_addr = 0x84},
    {.base = 0x900, .size = MAX_CPLD_REG_SIZE, .type = LPC_DEVICE_CPLD, .id = 1, .lpc_pci_addr = 0x88},
    {.base = 0xb00, .size = MAX_CPLD_REG_SIZE, .type = LPC_DEVICE_CPLD, .id = 2, .lpc_pci_addr = 0x90},
};

static rg_lpc_device_t *g_rg_lpc_dev = g_rg_lpc_dev_default;

static rg_lpc_device_t* lpc_get_device_info(int type, int id)
{
    int i;
    int size;

    size = ARRAY_SIZE(g_rg_lpc_dev_default);
    for (i = 0; i < size; i++) {
        if ((g_rg_lpc_dev[i].type == type) && (g_rg_lpc_dev[i].id == id)) {
            return &g_rg_lpc_dev[i];
        }
    }

    return NULL;
}


int lpc_cpld_read(int address, u8 *val)
{
    int cpld_id;
    rg_lpc_device_t *info;

    cpld_id = LPC_GET_CPLD_ID(address);
    info = lpc_get_device_info(LPC_DEVICE_CPLD, cpld_id);
    if (info == NULL) {
        LPC_DBG_ERROR("lpc_get_device_info addr 0x%x id %d failed.\r\n", address, cpld_id);
        return -1;
    }

    *val = inb(info->base + LPC_GET_CPLD_OFFSET(address));
    LPC_DBG_VERBOSE("Leave info->base 0x%x, addr 0x%x, cpld_id %d, val 0x%x.\r\n", info->base, address, cpld_id, *val);
    return 0;
}

int lpc_cpld_write(int address, u8 reg_val)
{
    int cpld_id;
    rg_lpc_device_t *info;

    cpld_id = LPC_GET_CPLD_ID(address);
    info = lpc_get_device_info(LPC_DEVICE_CPLD, cpld_id);
    if (info == NULL) {
        LPC_DBG_ERROR("lpc_get_device_info addr 0x%x id %d failed.\r\n", address, cpld_id);
        return -1;
    }

    outb(reg_val, info->base + LPC_GET_CPLD_OFFSET(address));
    LPC_DBG_VERBOSE("Leave info->base 0x%x, addr 0x%x, cpld_id %d, val 0x%x.\r\n", info->base, address, cpld_id, reg_val);
    return 0;
}

int lpc_fpga_read(int address, u8 *val)
{
    return -1;
}

int lpc_fpga_write(int address, u8 reg_val)
{
    return -1;
}

static ssize_t lpc_misc_cpld_dev_read (struct file *file, char __user *buf, size_t count,
        loff_t *offset)
{
    int ret;
    u8 value8[MAX_CPLD_REG_SIZE];
    int i;

    if ((count > MAX_CPLD_REG_SIZE)
            || ((LPC_GET_CPLD_OFFSET(file->f_pos) + count) > MAX_CPLD_REG_SIZE)) {
        return -EFAULT;
    }

    for (i = 0; i < count; i++) {
        ret = lpc_cpld_read((int)(file->f_pos + i), &value8[i]);
        if (ret) {
            LPC_DBG_ERROR("lpc_cpld_read i %d addr 0x%x failed ret %d.\n",
                    i, ((unsigned int)file->f_pos + i), ret);
            return i;
        }
    }

    if (copy_to_user(buf, value8, count)) {
        return -EFAULT;
    }

    return count;
}


static ssize_t lpc_misc_cpld_dev_write (struct file *file, const char __user *buf, size_t count,
        loff_t *offset)
{
    u8 value8[MAX_CPLD_REG_SIZE];
    int i;
    int ret;

    if ((count > MAX_CPLD_REG_SIZE)
            || ((LPC_GET_CPLD_OFFSET(file->f_pos) + count) > MAX_CPLD_REG_SIZE)) {
        return -EFAULT;
    }

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

    for (i = 0; i < count; i++) {
        ret = lpc_cpld_write((int)(file->f_pos + i), value8[i]);
        if (ret) {
            LPC_DBG_ERROR("lpc_cpld_write i %d addr 0x%x value 0x%x failed ret %d.\n",
                    i, (unsigned int)file->f_pos + i, value8[i], ret);
            return i;
        }
    }

    return count;
}


static loff_t lpc_misc_cpld_dev_llseek(struct file *file, loff_t offset, int origin)
{
    loff_t ret;

#if LINUX_VERSION_CODE < KERNEL_VERSION(4,0,36)
    mutex_lock(&file->f_path.dentry->d_inode->i_mutex);
#else
    /*  do noting add tjm */
    inode_lock(file_inode(file));
#endif

    switch (origin) {
        case 0:
            file->f_pos = offset;
            ret = file->f_pos;
            break;
        case 1:
            file->f_pos += offset;
            ret = file->f_pos;
            break;
        default:
            ret = -EINVAL;
    }

#if LINUX_VERSION_CODE < KERNEL_VERSION(4,0,36)
    mutex_unlock(&file->f_path.dentry->d_inode->i_mutex);
#else
    /*  do noting add tjm */
    inode_unlock(file_inode(file));
#endif


    return ret;
}


static long lpc_misc_cpld_dev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    return -1;
}

static int lpc_misc_cpld_dev_open(struct inode *inode, struct file *file)
{
    file->private_data = NULL;
    file->f_pos = 0;
    return 0;

}

static int lpc_misc_cpld_dev_release(struct inode *inode, struct file *file)
{
    file->private_data = NULL;
    file->f_pos = 0;
    return 0;
}

static const struct file_operations lpc_misc_cpld_dev_fops = {
    .owner      = THIS_MODULE,
    .llseek     = lpc_misc_cpld_dev_llseek,
    .read       = lpc_misc_cpld_dev_read,
    .write      = lpc_misc_cpld_dev_write,
    .unlocked_ioctl = lpc_misc_cpld_dev_ioctl,
    .open       = lpc_misc_cpld_dev_open,
    .release    = lpc_misc_cpld_dev_release,
};

static ssize_t lpc_misc_fpga_dev_read (struct file *file, char __user *buf, size_t count,
        loff_t *offset)
{
    int ret;
    u8 value8[MAX_FPGA_REG_SIZE];
    int i;

    if ((count > MAX_FPGA_REG_SIZE) || ((file->f_pos + count) > MAX_FPGA_REG_SIZE)) {
        return -EFAULT;
    }

    for (i = 0; i < count; i++) {
        ret = lpc_fpga_read((int)(file->f_pos + i), &value8[i]);
        if (ret) {
            LPC_DBG_ERROR("lpc_fpga_read i %d addr 0x%x failed ret %d.\n",
                    i, ((unsigned int)file->f_pos + i), ret);
            return i;
        }

    }

    if (copy_to_user(buf, value8, count)) {
        return -EFAULT;
    }

    return count;
}


static ssize_t lpc_misc_fpga_dev_write (struct file *file, const char __user *buf, size_t count,
        loff_t *offset)
{
    int ret;
    u8 value8[MAX_FPGA_REG_SIZE];
    int i;

    if ((count > MAX_FPGA_REG_SIZE) || ((file->f_pos + count) > MAX_FPGA_REG_SIZE)) {
        return -EFAULT;
    }

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

    for (i = 0; i < count; i++) {
        ret = lpc_fpga_write((int)(file->f_pos + i), value8[i]);
        if (ret) {
            LPC_DBG_ERROR("lpc_fpga_write i %d addr 0x%x value 0x%x failed ret %d.\n",
                    i, (int)(file->f_pos + i), value8[i], ret);
            return i;
        }
    }

    return count;
}


static loff_t lpc_misc_fpga_dev_llseek(struct file *file, loff_t offset, int origin)
{
    loff_t ret;

#if LINUX_VERSION_CODE < KERNEL_VERSION(4,0,36)
    mutex_lock(&file->f_path.dentry->d_inode->i_mutex);
#else
    /*  do noting add tjm */
    inode_lock(file_inode(file));
#endif

    switch (origin) {
        case 0:
            file->f_pos = offset;
            ret = file->f_pos;
            break;
        case 1:
            file->f_pos += offset;
            ret = file->f_pos;
            break;
        default:
            ret = -EINVAL;
    }

#if LINUX_VERSION_CODE < KERNEL_VERSION(4,0,36)
    mutex_unlock(&file->f_path.dentry->d_inode->i_mutex);
#else
    /*  do noting add tjm */
    inode_unlock(file_inode(file));
#endif


    return ret;
}


static long lpc_misc_fpga_dev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    return -1;
}

static int lpc_misc_fpga_dev_open(struct inode *inode, struct file *file)
{
    file->private_data = NULL;
    file->f_pos = 0;
    return 0;

}

static int lpc_misc_fpga_dev_release(struct inode *inode, struct file *file)
{
    file->private_data = NULL;
    file->f_pos = 0;
    return 0;
}

static const struct file_operations lpc_misc_fpga_dev_fops = {
    .owner      = THIS_MODULE,
    .llseek     = lpc_misc_fpga_dev_llseek,
    .read       = lpc_misc_fpga_dev_read,
    .write      = lpc_misc_fpga_dev_write,
    .unlocked_ioctl = lpc_misc_fpga_dev_ioctl,
    .open       = lpc_misc_fpga_dev_open,
    .release    = lpc_misc_fpga_dev_release,
};

static struct miscdevice lpc_misc_cpld_dev = {
    .minor  = MISC_DYNAMIC_MINOR,
    .name   = "lpc_cpld",
    .fops   = &lpc_misc_cpld_dev_fops,
};

static struct miscdevice lpc_misc_fpga_dev = {
    .minor  = MISC_DYNAMIC_MINOR,
    .name   = "lpc_fpga",
    .fops   = &lpc_misc_fpga_dev_fops,
};

static int lpc_misc_drv_init(void)
{
    if (misc_register(&lpc_misc_cpld_dev) != 0) {
        LPC_DBG_ERROR("Register %s failed.\r\n", lpc_misc_cpld_dev.name);
        return -ENXIO;
    }

    if (misc_register(&lpc_misc_fpga_dev) != 0) {
        LPC_DBG_ERROR("Register %s failed.\r\n", lpc_misc_fpga_dev.name);
        return -ENXIO;
    }
    return 0;
}

static void lpc_misc_drv_exit(void)
{
    misc_deregister(&lpc_misc_cpld_dev);
    misc_deregister(&lpc_misc_fpga_dev);
}

#define LPC_MAKE_PCI_IO_RANGE(__base)      ((0xfc0001) | ((__base) & (0xFFFC)))

static int lpc_pci_cfg_init(struct pci_dev *pdev,
        const struct pci_device_id *id)
{
    int i;
    int size;

    size = ARRAY_SIZE(g_rg_lpc_dev_default);

    for (i = 0; i < size; i++) {
        pci_write_config_dword(pdev, g_rg_lpc_dev[i].lpc_pci_addr, LPC_MAKE_PCI_IO_RANGE(g_rg_lpc_dev[i].base));
        LPC_DBG_VERBOSE("set lpc pci cfg[addr: 0x%x, value:0x%x].\n", LPC_PCI_CFG_BASE(i), LPC_MAKE_PCI_IO_RANGE(g_rg_lpc_dev[i].base));
        if (!request_region(g_rg_lpc_dev[i].base, g_rg_lpc_dev[i].size, "rg_lpc")) {
            LPC_DBG_ERROR("request_region [0x%x][0x%x] failed!\n", g_rg_lpc_dev[i].base, g_rg_lpc_dev[i].size);
            return -EBUSY;
        }
    }

    return 0;
}

static void lpc_pci_cfg_exit(void)
{
    int i;
    int size;

    size = ARRAY_SIZE(g_rg_lpc_dev_default);
    for (i = 0; i < size; i++) {
        release_region(g_rg_lpc_dev[i].base, g_rg_lpc_dev[i].size);
    }
    return;
}

static int rg_lpc_cpld_probe(struct pci_dev *pdev,
        const struct pci_device_id *id)
{
    int ret;

    LPC_DBG_VERBOSE("Enter.\n");
    ret = lpc_pci_cfg_init(pdev, id);
    if (ret) {
        LPC_DBG_ERROR("lpc_pci_cfg_init failed ret %d.\n", ret);
        return ret;
    }

    ret = lpc_misc_drv_init();
    if (ret) {
        LPC_DBG_ERROR("lpc_misc_drv_init failed ret %d.\n", ret);
        return ret;
    }
    LPC_DBG_VERBOSE("Leave success\n");

    return 0;
}

static void rg_lpc_cpld_remove(struct pci_dev *pdev)
{
    LPC_DBG_VERBOSE("Enter.\n");
    lpc_misc_drv_exit();
    lpc_pci_cfg_exit();
    LPC_DBG_VERBOSE("Leave.\n");
}


#define PCI_VENDOR_ID_D1527_LPC             (0x8c54)
#define PCI_VENDOR_ID_C3000_LPC             (0x19dc)

#if 0
static const struct pci_device_id rg_lpc_cpld_pcidev_id[] = {
    { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_VENDOR_ID_C3000_LPC) },
    { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_VENDOR_ID_D1527_LPC) },
    { 0, }
};
MODULE_DEVICE_TABLE(pci, rg_lpc_cpld_pcidev_id);

static struct pci_driver rg_lpc_driver = {
    .name = "rg_lpc",
    .id_table = rg_lpc_cpld_pcidev_id,
    .probe = rg_lpc_cpld_probe,
    .remove = rg_lpc_cpld_remove,
};

module_pci_driver(rg_lpc_driver);
#else
static int __init lpc_dbg_init(void)
{
    struct pci_dev *pdev = NULL;
    int ret;

    LPC_DBG_VERBOSE("Enter.\n");

    pdev = pci_get_device(PCI_VENDOR_ID_INTEL, PCI_VENDOR_ID_D1527_LPC, pdev);
    if (!pdev) {
        LPC_DBG_ERROR("pci_get_device(0x8086, 0x8c54) failed!\n");
        return 0;
    }

    ret = rg_lpc_cpld_probe(pdev, NULL);
    LPC_DBG_VERBOSE("Leave ret %d.\n", ret);
    return ret;
}

static void __exit lpc_dbg_exit(void)
{
    LPC_DBG_VERBOSE("Enter.\n");
    rg_lpc_cpld_remove(NULL);
    LPC_DBG_VERBOSE("Leave.\n");
}



module_init(lpc_dbg_init);
module_exit(lpc_dbg_exit);

#endif
MODULE_LICENSE("GPL");
MODULE_AUTHOR("support <support@ragile.com>");