#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>

// Ref:
// https://linux-kernel-labs.github.io/refs/heads/master/labs/device_drivers.html
// https://olegkutkov.me/2018/03/14/simple-linux-character-device-driver/

// BEGIN_LST_DEF
#define MYPIPE_BUFFER_SIZE 4096
#define MYPIPE_DEVICE_NAME "wendy"

static dev_t dev_major = 0;

enum MyPipeMinors {
    MYPIPE_MINOR_IN = 0,
    MYPIPE_MINOR_OUT,
    MYPIPE_MINOR_COUNT,
};

struct mypipe_data {
    struct cdev cdev;
    // Whether opened by some writer
    bool is_active;
    u8 buffer[MYPIPE_BUFFER_SIZE];
    size_t write_pos;
    size_t read_pos;
    size_t data_len;
};

// This could be an array if we want multiple cdev
static struct mypipe_data pdata;

static struct class *mypipedev_class = NULL;

// Protects the buffer
static DEFINE_MUTEX(mypipe_lock);
// Protects the is_active value
static DEFINE_MUTEX(isactive_lock);
// These are two wait_queue_head_t for storing tasks that are waiting for
// writing/ reading
static DECLARE_WAIT_QUEUE_HEAD(read_queue);
static DECLARE_WAIT_QUEUE_HEAD(write_queue);
// END_LST_DEF

// BEGIN_LST_OPEN_RELASE
static int mypipe_open(struct inode *inode, struct file *file) {
    int minor = iminor(inode);
    printk(KERN_INFO "%s opened, minor = %d", MYPIPE_DEVICE_NAME, minor);

    // If its opening the writing end, we limit it to only one writer
    if (minor == MYPIPE_MINOR_IN) {
        if (mutex_lock_interruptible(&isactive_lock)) {
            return -ERESTARTSYS;
        }

        if (pdata.is_active) {
            return -EBUSY;
        }
        pdata.is_active = true;
        mutex_unlock(&isactive_lock);
    }
    return 0;
}

static int mypipe_release(struct inode *inode, struct file *file) {
    int minor = iminor(inode);
    printk(KERN_INFO "%s released, minor = %d", MYPIPE_DEVICE_NAME, minor);

    if (minor == MYPIPE_MINOR_IN) {
        mutex_lock(&isactive_lock);
        pdata.is_active = false;
        mutex_unlock(&isactive_lock);
        wake_up_interruptible(&read_queue);

    }
    return 0;
}

// END_LST_OPEN_RELEASE

// BEGIN_LST_READ_WRITE
static ssize_t mypipe_read(struct file *filep, char __user *buf, size_t size,
                           loff_t *offset) {
    ssize_t ret = 0;

    if (iminor(file_inode(filep)) != MYPIPE_MINOR_OUT) {
        return -EINVAL;
    }

    // Acquire the lock. If not acquired and interrupted by signal, let kernel
    // to try restart the syscall
    if (mutex_lock_interruptible(&mypipe_lock)) {
        return -ERESTARTSYS;
    }

    // We currently have no data, but we can wait for possible new to write in
    while (pdata.data_len == 0) {
        mutex_unlock(&mypipe_lock);
        
        // Check if the writer is still active
        if (mutex_lock_interruptible(&isactive_lock)) {
            return -ERESTARTSYS;
        }
        bool writer_is_active = pdata.is_active;
        mutex_unlock(&isactive_lock);

        if (!writer_is_active) {
            return 0; // No data left and the writer is gone -> EOF for reader
        }

        // There is still writer, wait for data
        if (filep->f_flags & O_NONBLOCK) {
            // The caller says no waiting, so we don't wait
            return -EAGAIN;
        }
        if (wait_event_interruptible(read_queue, pdata.data_len > 0)) {
            return -ERESTARTSYS;
        }
        // Woken, relock for checking the condition
        if (mutex_lock_interruptible(&mypipe_lock)) {
            return -ERESTARTSYS;
        }
    }

    // data_len > 0
    size_t to_read = min(size, pdata.data_len);
    size_t until_end = min(to_read, MYPIPE_BUFFER_SIZE - pdata.read_pos);
    if (copy_to_user(buf, pdata.buffer + pdata.read_pos, until_end)) {
        ret = -EFAULT;
        goto out;
    }

    if (to_read > until_end) {
        if (copy_to_user(buf + until_end, pdata.buffer, to_read - until_end)) {
            ret = -EFAULT;
            goto out;
        }
    }
    pdata.read_pos = (pdata.read_pos + to_read) % MYPIPE_BUFFER_SIZE;
    pdata.data_len -= to_read;
    ret = to_read;

    wake_up_interruptible(&write_queue);

out:
    mutex_unlock(&mypipe_lock);
    return ret;
}

static ssize_t mypipe_write(struct file *filep, const char __user *buf,
                            size_t size, loff_t *offset) {
    // This is very similar to read(), just write instead of read

    ssize_t ret = 0;

    if (iminor(file_inode(filep)) != MYPIPE_MINOR_IN) {
        return -EINVAL;
    }

    if (mutex_lock_interruptible(&mypipe_lock)) {
        return -ERESTARTSYS;
    }

    while (pdata.data_len == MYPIPE_BUFFER_SIZE) {
        mutex_unlock(&mypipe_lock);
        if (filep->f_flags & O_NONBLOCK) {
            return -EAGAIN;
        }
        if (wait_event_interruptible(write_queue,
                                     pdata.data_len < MYPIPE_BUFFER_SIZE)) {
            return -ERESTARTSYS;
        }
        if (mutex_lock_interruptible(&mypipe_lock)) {
            return -ERESTARTSYS;
        }
    }

    size_t space_left = MYPIPE_BUFFER_SIZE - pdata.data_len;
    size_t to_write = min(size, space_left);
    size_t until_end = min(to_write, MYPIPE_BUFFER_SIZE - pdata.write_pos);

    if (copy_from_user(pdata.buffer + pdata.write_pos, buf, until_end)) {
        ret = -EFAULT;
        goto out;
    }

    if (to_write > until_end) {
        if (copy_from_user(pdata.buffer, buf + until_end,
                           to_write - until_end)) {
            ret = -EFAULT;
            goto out;
        }
    }

    pdata.write_pos = (pdata.write_pos + to_write) % MYPIPE_BUFFER_SIZE;
    pdata.data_len += to_write;
    ret = to_write;

    wake_up_interruptible(&read_queue);

out:
    mutex_unlock(&mypipe_lock);
    return ret;
}

// END_LST_READ_WRITE

// BEGIN_LST_FOP
const struct file_operations mypipe_fops = {.owner = THIS_MODULE,
                                            .open = mypipe_open,
                                            .read = mypipe_read,
                                            .write = mypipe_write,
                                            .release = mypipe_release};
// END_LST_FOP

// Used to set the permission to allow any user to r/w
static int mypipe_uevent(const struct device *dev,
                         struct kobj_uevent_env *env) {
    add_uevent_var(env, "DEVMODE=%#o", 0666);
    return 0;
}

// BEGIN_LST_INIT
static int __init mypipe_init(void) {
    int ret;
    dev_t dev;

    // Acquire a region of major, minor for us to use. The name appears in
    // /proc/devices (and potentially /sys ?).
    ret = alloc_chrdev_region(&dev, 0, MYPIPE_MINOR_COUNT, MYPIPE_DEVICE_NAME);
    if (ret) {
        return ret;
    }

    dev_major = MAJOR(dev);

    // Create the class in sysfs. The name appears in /sys/class/<name>
    mypipedev_class = class_create(MYPIPE_DEVICE_NAME);
    if (IS_ERR(mypipedev_class)) {
        ret = PTR_ERR(mypipedev_class);
        goto del_cdev;
    }
    mypipedev_class->dev_uevent = mypipe_uevent;

    // Create a cdev in our pdata
    cdev_init(&pdata.cdev, &mypipe_fops);
    pdata.cdev.owner = THIS_MODULE;

    //  Register this newly created cdev to kernel. We only have one dev, so we
    // can MKDEV(dev_major, 0).

    // Also, we want to devices that one handles write and one read, so the last
    // arg is not 1, but 2 (In this case, MYPIPE_MINOR_COUNT)
    ret = cdev_add(&pdata.cdev, MKDEV(dev_major, 0), MYPIPE_MINOR_COUNT);
    if (ret)
        goto unregister;

    // Create device node /dev/mypipe_in and /dev/mypipe_out
    device_create(mypipedev_class, NULL, MKDEV(dev_major, MYPIPE_MINOR_IN),
                  NULL, "%s_in", MYPIPE_DEVICE_NAME);
    device_create(mypipedev_class, NULL, MKDEV(dev_major, MYPIPE_MINOR_OUT),
                  NULL, "%s_out", MYPIPE_DEVICE_NAME);

    pdata.is_active = false;

    printk(KERN_INFO "%s: module loaded\n", MYPIPE_DEVICE_NAME);
    return 0;

del_cdev:
    cdev_del(&pdata.cdev);
unregister:
    unregister_chrdev_region(MKDEV(dev_major, 0), MYPIPE_MINOR_COUNT);
    return ret;
}

static void __exit mypipe_destroy(void) {
    // This is basically the reverse operation of mypipe_init
    device_destroy(mypipedev_class, MKDEV(dev_major, MYPIPE_MINOR_IN));
    device_destroy(mypipedev_class, MKDEV(dev_major, MYPIPE_MINOR_OUT));
    class_unregister(mypipedev_class);
    class_destroy(mypipedev_class);
    cdev_del(&pdata.cdev);
    unregister_chrdev_region(MKDEV(dev_major, 0), MYPIPE_MINOR_COUNT);
    printk(KERN_INFO "%s: module unloaded", MYPIPE_DEVICE_NAME);
}

module_init(mypipe_init);
module_exit(mypipe_destroy);
// END_LST_INIT

MODULE_LICENSE("GPL");