/* SPDX-License-Identifier: (BSD-2-Clause OR GPL-2.0-or-later)
 * (C) 2019-2021 AVM GmbH <info@avm.de>
 *
 * vim:set noexpandtab shiftwidth=8 softtabstop=8 fileencoding=utf-8:
 *
 * Helper to create char device (region), add the needed device files.
 */

#define MODNAME "cchardevice"
#define pr_fmt(fmt) MODNAME ": " fmt

#include <linux/types.h>
#include <linux/module.h> // struct module, THIS_MODULE
#include <linux/fs.h>     // struct file_operations
#include <linux/cdev.h>   // cdev_*()
#include <linux/device.h> // device_
#include <linux/slab.h>   // kzmalloc

/* cprocfs */
#include <cprocfs.h>

/* self */
#include <cchardevice.h>

struct cchardevice {
	const char *name;
	unsigned int minorcount;
	struct module *module;
	const struct file_operations *fops;
	/* runtime */
	struct list_head list;
	dev_t devno;
	dev_t major;
	struct cdev *cdev;
	struct class *devclass;
	unsigned int devcount;
};

/* --------------------------------------------------------------------- */

static const struct file_operations test_fops = {
	.owner = THIS_MODULE,
};

static struct {
	struct cprocfs_dir *cprocfs_root;
	struct list_head dev_list;
	struct cchardevice test_device;
} glob = { .test_device = {
		   .name = MODNAME "_test",
		   .minorcount = 2,
		   .module = THIS_MODULE,
		   .list = LIST_HEAD_INIT(glob.test_device.list),
		   .fops = &test_fops,
	   } };

/* --------------------------------------------------------------------- */

static void cchardevice_internal_unregister(struct cchardevice *p)
{
	while (p->devcount > 0) {
		unsigned int minor = p->devcount - 1;

		pr_info("device_destroy(%s %u,%u)\n", p->name, p->major, minor);
		device_destroy(p->devclass, MKDEV(p->major, minor));
		p->devcount--;
	}
	if (p->devclass) {
		pr_info("class_destroy(%s)\n", p->name);
		class_destroy(p->devclass);
		p->devclass = 0;
	}
	if (p->cdev) {
		pr_info("cdev_del(%s)\n", p->name);
		cdev_del(p->cdev);
		kfree(p->cdev);
		p->cdev = 0;
	}
	if (p->devno) {
		pr_info("unregister_chrdev_region(%s %u,%u (%u))\n",
		        p->name,
		        MAJOR(p->devno),
		        MINOR(p->devno),
		        p->minorcount);
		unregister_chrdev_region(p->devno, p->minorcount);
		p->devno = 0;
	}
	list_del(&p->list);
}

static int cchardevice_internal_register(struct cchardevice *p)
{
	struct device *dev;
	char devname[64];
	int ret;

	if (strlen(p->name) + 3 > sizeof(devname)) {
		pr_err("name len check failed for %s\n", p->name);
		return -1;
	}
	p->devcount = 0;

	ret = alloc_chrdev_region(&p->devno, 0, p->minorcount, p->name);
	if (ret < 0) {
		pr_err("alloc_chrdev_region() for %s failed (%d)\n", p->name, ret);
		return ret;
	}
	p->major = MAJOR(p->devno);
	p->cdev = cdev_alloc();
	if (IS_ERR(p->cdev)) {
		pr_err("cdev_alloc() for %s failed (%ld)\n", p->name, PTR_ERR(p->cdev));
		p->cdev = NULL;
		cchardevice_internal_unregister(p);
		return -1;
	}
	cdev_init(p->cdev, p->fops);
	p->cdev->owner = p->module;
	ret = cdev_add(p->cdev, p->devno, p->minorcount);
	if (ret) {
		pr_err("cdev_add() for %s failed (%d)\n", p->name, ret);
		cchardevice_internal_unregister(p);
		return -1;
	}

	p->devclass = class_create(p->module, p->name);
	if (IS_ERR(p->devclass)) {
		pr_err("class_create() for %s failed (%ld)\n", p->name, PTR_ERR(p->devclass));
		p->devclass = NULL;
		cchardevice_internal_unregister(p);
		return -1;
	}

	if (p->minorcount > 1) {
		dev_t devno = p->devno;

		snprintf(devname, sizeof(devname), "%s%%d", p->name);
		while (p->devcount < p->minorcount) {
			pr_info("device_create(%s): %u,%u\n", devname, MAJOR(devno), MINOR(devno));
			dev = device_create(p->devclass, NULL, devno, NULL, devname, MINOR(devno));
			if (IS_ERR(dev)) {
				pr_err("device_create() for %s failed (%ld)\n",
				       p->name,
				       PTR_ERR(p->devclass));
				cchardevice_internal_unregister(p);
				return -1;
			}
			p->devcount++;
			devno++;
		}
	} else {
		pr_info("device_create(%s): %u,%u\n", p->name, MAJOR(p->devno), MINOR(p->devno));
		dev = device_create(p->devclass, NULL, p->devno, NULL, p->name);
		if (IS_ERR(dev)) {
			pr_err("device_create() for %s failed (%ld)\n",
			       p->name,
			       PTR_ERR(p->devclass));
			cchardevice_internal_unregister(p);
			return -1;
		}
		p->devcount++;
	}
	list_add_tail(&p->list, &glob.dev_list);
	return 0;
}

/* --------------------------------------------------------------------- */

struct cchardevice *cchardevice_register(const char *name,
                                         unsigned int minorcount,
                                         struct module *module,
                                         const struct file_operations *fops)
{
	struct cchardevice *p = kzalloc(sizeof(struct cchardevice), GFP_KERNEL);

	if (!p)
		return NULL;

	p->name = name;
	p->minorcount = minorcount ? minorcount : 1;
	p->module = module;
	p->fops = fops;
	if (cchardevice_internal_register(p) == 0)
		return p;

	kfree(p);
	return NULL;
}
EXPORT_SYMBOL(cchardevice_register);

void cchardevice_unregister(struct cchardevice *p)
{
	cchardevice_internal_unregister(p);
	kfree(p);
}
EXPORT_SYMBOL(cchardevice_unregister);

/* --------------------------------------------------------------------- */
/* -------- procfs ----------------------------------------------------- */
/* --------------------------------------------------------------------- */

static void cchardevice_show_devices(void *userdata, cprocfs_walk_cb_t cb, void *arg)
{
	struct cchardevice *p, *n;

	list_for_each_entry_safe(p, n, &glob.dev_list, list) {
		if (p->minorcount > 1) {
			cb(arg,
			   "%-16s %4d 0-%u (%s)\n",
			   p->name,
			   p->major,
			   p->minorcount,
			   p->module->name);
		} else {
			cb(arg, "%-16s %4d (%s)\n", p->name, p->major, p->module->name);
		}
	}
}

static void cchardevice_control_help(void *userdata, cprocfs_walk_cb_t cb, void *arg)
{
	cb(arg, "possible commands:\n");
	cb(arg, "  addtest\n");
	cb(arg, "  deltest\n");
}

static int word_matches(const char *haystack, const char *needle)
{
	size_t len = strlen(needle);

	return len && strncasecmp(haystack, needle, len) == 0;
}

static void cchardevice_control(void *userdata, int argc, char *argv[])
{
	if (argc == 0) {
		pr_err("%s(): empty command\n", __func__);
		return;
	}

	if (word_matches("addtest", argv[0]))
		(void) cchardevice_internal_register(&glob.test_device);
	else if (word_matches("deltest", argv[0]))
		cchardevice_internal_unregister(&glob.test_device);
	else
		pr_err("%s(): unknown command '%s'\n", __func__, argv[0]);
}

/* --------------------------------------------------------------------- */

static int cchardevice_procfs_init(void)
{
	glob.cprocfs_root = cprocfs_create_root_dir(MODNAME);
	if (glob.cprocfs_root == 0)
		return -ENOMEM;

	if (cprocfs_register_file(glob.cprocfs_root, "devices", cchardevice_show_devices, 0) < 0)
		return -ENOMEM;

	if (cprocfs_register_control(glob.cprocfs_root,
	                             "control",
	                             cchardevice_control_help,
	                             cchardevice_control,
	                             0) < 0)
		return -ENOMEM;

	return 0;
}

/* --------------------------------------------------------------------- */
/* -------- init & exit ------------------------------------------------ */
/* --------------------------------------------------------------------- */

static int __init cchardevice_init_module(void)
{
	int ret;
	INIT_LIST_HEAD(&glob.dev_list);

	ret = cchardevice_procfs_init();
	if (ret < 0)
		return ret;

	pr_info("loaded\n");
	return 0;
}

static void __exit cchardevice_exit_module(void)
{
	if (glob.cprocfs_root) {
		cprocfs_destroy(glob.cprocfs_root);
		glob.cprocfs_root = 0;
	}

	if (!list_empty(&glob.dev_list))
		pr_err("WARNING: some devices not unregistered\n");

	pr_info("unloaded\n");
}

MODULE_DESCRIPTION("AVM: easy creation of char devices");
MODULE_LICENSE("Dual BSD/GPL");

module_init(cchardevice_init_module);
module_exit(cchardevice_exit_module);