/*
 * df5642_rig.c -- DF-5642/5643 rig: instantiate the REAL in-kernel
 * smbus driver (sys/bus/smbus/smbus.c, unmodified production code)
 * under a fake SMBus-controller parent and drive the newbus
 * attach/detach lifecycle from userland.
 *
 * Targets (sys/bus/smbus/smbus.c):
 *   DF-5642  smbus_attach() (smbus.c:94-97) creates the generic child
 *            and the whole address-probe child set UNCONDITIONALLY on
 *            every attach, while smbus_detach() (smbus.c:105-115) and
 *            the module teardown path (devclass_delete_driver,
 *            subr_bus.c:888-897) only DETACH children, never DELETE
 *            them.  Every detach+re-attach cycle of the smbus device
 *            leaks the full child set and mints duplicate remapped
 *            /dev/smbN[-XX] nodes (devclass_alloc_unit remap,
 *            subr_bus.c:1072-1081), breaking the 0x0400 unit-encoding
 *            contract (smb.c:134-141/229-231, cyapa.c:466-472).
 *   DF-5643  smbus_detach() never checks struct smbus_softc.owner
 *            (smbus.h:34): a bus owned by a client detaches "cleanly",
 *            device_detach() then FREES the softc (device_set_driver,
 *            subr_bus.c:1941-1946, called from subr_bus.c:2150) under
 *            (a) the owner -- whose next smbus_release_bus()
 *            (the smbioctl exit path, smb.c:356) derefs
 *            device_get_softc(bus)==NULL -> Fatal trap 12, and
 *            (b) parked SMB_WAIT requesters tsleep'ing ON the softc
 *            (smbconf.c:89/93) which are never woken and keep looping
 *            on freed memory.
 *
 * Rig structure (family standard, mirrors the DF-5559/5636 fakebb rigs):
 *
 *    nexus -> fakesmbb0 (this module: SMBus controller emulator
 *                        + /dev/df5642 ioctl surface)
 *                -> smbus0  (REAL in-kernel smbus driver, registered
 *                            on "fakesmbb" exactly like intpm.c:903
 *                            registers it on "intsmb"; created via
 *                            device_add_child+device_probe_and_attach
 *                            exactly like intpm.c:331-337)
 *                     -> smb children (REAL smb.ko, kldload'ed after
 *                        this rig; mints /dev/smbN[-XX] nodes)
 *
 * /dev/df5642 ioctls:
 *   STAT     list smbus0's children (count + nameunit each)
 *   CYCLE n  n x (device_detach(smbus0) + device_probe_and_attach(smbus0))
 *            -- the exact production APIs the module handlers invoke
 *   GRAB     smbus_request_bus(smbus0, fakesmbb0, SMB_DONTWAIT) -> owner
 *   VETO     device_detach(smbus0) WHILE OWNED -- must return EBUSY
 *   RELFREE  smbus_release_bus(smbus0, owner) after VETO freed the
 *            softc -- reproduces the smbioctl exit path (smb.c:356)
 *   PARK     kthread: smbus_request_bus(smbus0, nexus0, SMB_WAIT|NOINTR)
 *   WAKUAF   wakeup(freed softc) -- parked waiter resumes on freed mem
 */

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/bus.h>
#include <sys/malloc.h>
#include <sys/types.h>
#include <sys/conf.h>
#include <sys/device.h>
#include <sys/sysctl.h>
#include <sys/kthread.h>
#include <sys/ioccom.h>

#include <bus/smbus/smbconf.h>

/* struct smbus_softc (copied from sys/bus/smbus/smbus.h, family
 * standard: the header is tiny and private to the audit rig). */
struct smbus_softc {
	device_t owner;		/* smbus owner device structure */
};

#include "smbus_if.h"

/* df5642ctl.c <-> rig ioctl protocol */
#define DF5642_STAT	_IOWR('d', 1, int)
#define DF5642_CYCLE	_IOWR('d', 2, int)	/* arg = cycle count */
#define DF5642_GRAB	_IOWR('d', 3, int)
#define DF5642_VETO	_IOWR('d', 4, int)
#define DF5642_RELFREE	_IOWR('d', 5, int)
#define DF5642_PARK	_IOWR('d', 6, int)
#define DF5642_WAKUAF	_IOWR('d', 7, int)

static int df5642_open(struct dev_open_args *ap);
static int df5642_close(struct dev_close_args *ap);
static int df5642_ioctl(struct dev_ioctl_args *ap);

static struct dev_ops df5642_ops = {
	{ "df5642", 0, 0 },
	.d_open =	df5642_open,
	.d_close =	df5642_close,
	.d_ioctl =	df5642_ioctl,
};

struct fakesmbb_softc {
	device_t	self;		/* fakesmbb0: owner token */
	device_t	smbus;		/* smbus0: the bus under test */
	cdev_t		cdev;		/* /dev/df5642 */
	u_int		respond;	/* address mask the slave answers */
	/* parked-waiter state (DF-5643 run C) */
	volatile int	park_started;
	volatile int	park_done;
	int		park_error;
	void		*park_sc;	/* softc the waiter sleeps on */
};

/* address masks the emulated slave answers (addr 0..63 / 64..127) */
static uint64_t df5642_respond = ((uint64_t)1 << 0x10);	/* 0x10 */
static uint64_t df5642_respond_hi = (((uint64_t)1 << (0x50 - 64)) |
				      ((uint64_t)1 << (0x67 - 64)));

SYSCTL_NODE(_hw, OID_AUTO, df5642, CTLFLAG_RW, 0, "DF-5642 rig");
SYSCTL_ULONG(_hw_df5642, OID_AUTO, respond, CTLFLAG_RW,
    &df5642_respond, 0, "SMBus address mask lo (addr 0-63)");
SYSCTL_ULONG(_hw_df5642, OID_AUTO, respond_hi, CTLFLAG_RW,
    &df5642_respond_hi, 0, "SMBus address mask hi (addr 64-127)");

static int
fakesmbb_answers(int slave)
{

	if (slave < 0 || slave > 127)
		return (0);
	if (slave < 64)
		return ((df5642_respond >> slave) & 1);
	return ((df5642_respond_hi >> (slave - 64)) & 1);
}

/* ------------------------------------------------------------------ */
/* smbus_if methods: the emulated controller                           */
/* ------------------------------------------------------------------ */

static int
fakesmbb_callback(device_t dev, int index, void *data)
{

	(void)dev;
	(void)data;
	if (index == SMB_REQUEST_BUS || index == SMB_RELEASE_BUS)
		return (0);
	return (SMB_EINVAL);
}

static int
fakesmbb_trans(device_t dev, int slave, char cmd, int op,
    char *wbuf, int wcount, char *rbuf, int rcount, int *actualp)
{
	struct fakesmbb_softc *sc = (struct fakesmbb_softc *)device_get_softc(dev);

	(void)cmd;
	(void)op;
	(void)wbuf;
	(void)wcount;
	(void)sc;

	if (fakesmbb_answers(slave)) {
		if (rcount > 0 && rbuf != NULL)
			rbuf[0] = (char)0x5a;
		if (actualp != NULL)
			*actualp = (rcount > 0) ? 1 : 0;
		return (0);
	}
	return (SMB_ENOACK);
}

static void
fakesmbb_intr(device_t dev, u_char devaddr, char low, char high, int error)
{

	(void)dev; (void)devaddr; (void)low; (void)high; (void)error;
}

/* ------------------------------------------------------------------ */
/* helpers                                                             */
/* ------------------------------------------------------------------ */

static void
stat_children(device_t bus, const char *tag)
{
	device_t *list;
	int n = 0, i, error;

	error = device_get_children(bus, &list, &n);
	if (error) {
		kprintf("[df5642] %s: device_get_children error=%d\n", tag, error);
		return;
	}
	kprintf("[df5642] %s: nchildren=%d\n", tag, n);
	for (i = 0; i < n; i++) {
		const char *nu = device_get_nameunit(list[i]);

		kprintf("[df5642] %s: child[%d] = %s (unit=%d state=%d)\n",
		    tag, i, nu ? nu : "(unnamed)",
		    device_get_unit(list[i]), device_get_state(list[i]));
	}
	kfree(list, M_TEMP);
}

/* DF-5643 run C: kthread parking in smbus_request_bus(SMB_WAIT) */
static void
park_thread(void *arg)
{
	struct fakesmbb_softc *sc = arg;
	device_t bus = sc->smbus;
	device_t token = device_get_parent(sc->self);	/* nexus0: non-owner */

	sc->park_sc = device_get_softc(bus);
	kprintf("[df5642] PARK-T: calling request(nexus0, WAIT|NOINTR) "
	    "softc=%p\n", sc->park_sc);
	sc->park_started = 1;
	wakeup(&sc->park_started);

	sc->park_error = smbus_request_bus(bus, token, SMB_WAIT | SMB_NOINTR);
	kprintf("[df5642] PARK-T: request RETURNED %d (post-free resume)\n",
	    sc->park_error);
	sc->park_done = 1;
	wakeup(&sc->park_done);
	kthread_exit();
}

/* ------------------------------------------------------------------ */
/* /dev/df5642                                                         */
/* ------------------------------------------------------------------ */

static int
df5642_open(struct dev_open_args *ap)
{

	(void)ap;
	return (0);
}

static int
df5642_close(struct dev_close_args *ap)
{

	(void)ap;
	return (0);
}

static int
df5642_ioctl(struct dev_ioctl_args *ap)
{
	cdev_t cdev = ap->a_head.a_dev;
	struct fakesmbb_softc *sc = cdev->si_drv1;
	struct smbus_softc *ssc;
	u_long cmd = ap->a_cmd;
	int arg = *(int *)ap->a_data;
	int error, i;

	if (sc->smbus == NULL) {
		sc->smbus = devclass_get_device(devclass_find("smbus"), 0);
		if (sc->smbus == NULL)
			return (ENXIO);
	}
	ssc = (struct smbus_softc *)device_get_softc(sc->smbus);

	switch (cmd) {
	case DF5642_STAT:
		kprintf("[df5642] STAT: smbus0=%p softc=%p owner=%p\n",
		    (void *)sc->smbus, (void *)ssc,
		    ssc ? (void *)ssc->owner : NULL);
		stat_children(sc->smbus, "STAT");
		*(int *)ap->a_data = 0;
		return (0);

	case DF5642_CYCLE:
		for (i = 0; i < arg; i++) {
			error = device_detach(sc->smbus);
			kprintf("[df5642] CYCLE %d.%d: device_detach(smbus0)=%d\n",
			    i, 0, error);
			if (error)
				break;
			error = device_probe_and_attach(sc->smbus);
			kprintf("[df5642] CYCLE %d.%d: device_probe_and_attach=%d\n",
			    i, 1, error);
			if (error)
				break;
			stat_children(sc->smbus, "after-cycle");
		}
		*(int *)ap->a_data = error;
		return (0);

	case DF5642_GRAB:
		error = smbus_request_bus(sc->smbus, sc->self, SMB_DONTWAIT);
		ssc = (struct smbus_softc *)device_get_softc(sc->smbus);
		kprintf("[df5642] GRAB: request(fakesmbb0, DONTWAIT)=%d "
		    "owner=%p (caller=%p)\n", error,
		    ssc ? (void *)ssc->owner : NULL, (void *)sc->self);
		*(int *)ap->a_data = error;
		return (0);

	case DF5642_VETO:
		/* detach WHILE a client verifiably owns the bus */
		ssc = (struct smbus_softc *)device_get_softc(sc->smbus);
		kprintf("[df5642] VETO: pre-detach owner=%p (must be "
		    "non-NULL)\n", ssc ? (void *)ssc->owner : NULL);
		error = device_detach(sc->smbus);
		kprintf("[df5642] VETO: device_detach(smbus0)=%d "
		    "%s\n", error,
		    (error == 0) ? "<<< BUS DETACHED UNDER ITS OWNER "
		    "(must be EBUSY=16)" : "(vetoed: GOOD)");
		kprintf("[df5642] VETO: post-detach softc=%p "
		    "(NULL = freed under the owner)\n",
		    (void *)device_get_softc(sc->smbus));
		*(int *)ap->a_data = error;
		return (0);

	case DF5642_RELFREE:
		/* the owner's release path = smbioctl exit (smb.c:356) */
		kprintf("[df5642] RELFREE: calling smbus_release_bus(smbus0, "
		    "fakesmbb0) with softc=%p ...\n",
		    (void *)device_get_softc(sc->smbus));
		error = smbus_release_bus(sc->smbus, sc->self);
		kprintf("[df5642] RELFREE: returned %d\n", error);
		*(int *)ap->a_data = error;
		return (0);

	case DF5642_PARK:
		sc->park_started = 0;
		sc->park_done = 0;
		error = kthread_create(park_thread, sc, NULL, "df5642_park");
		if (error) {
			kprintf("[df5642] PARK: kthread_create=%d\n", error);
			*(int *)ap->a_data = error;
			return (0);
		}
		for (i = 0; i < 30 && !sc->park_started; i++)
			tsleep(&sc->park_started, 0, "df42s", hz / 10);
		kprintf("[df5642] PARK: waiter started=%d done=%d "
		    "(parked on softc=%p)\n", sc->park_started,
		    sc->park_done, sc->park_sc);
		*(int *)ap->a_data = sc->park_started;
		return (0);

	case DF5642_WAKUAF:
		kprintf("[df5642] WAKUAF: done=%d; wakeup(freed sc=%p)\n",
		    sc->park_done, sc->park_sc);
		if (sc->park_sc != NULL)
			wakeup(sc->park_sc);
		for (i = 0; i < 20 && !sc->park_done; i++)
			tsleep(&sc->park_done, 0, "df42w", hz / 10);
		kprintf("[df5642] WAKUAF: after wakeup: done=%d error=%d "
		    "(done=0 -> waiter re-parked on FREED softc)\n",
		    sc->park_done, sc->park_error);
		*(int *)ap->a_data = sc->park_done;
		return (0);

	default:
		return (ENOTTY);
	}
}

/* ------------------------------------------------------------------ */
/* fakesmbb bus device plumbing                                        */
/* ------------------------------------------------------------------ */

static void
fakesmbb_identify(driver_t *driver, device_t parent)
{

	if (device_find_child(parent, "fakesmbb", 0) == NULL)
		BUS_ADD_CHILD(parent, parent, 0, "fakesmbb", 0);
}

static int
fakesmbb_probe(device_t dev)
{

	device_set_desc(dev, "DF-5642 fake SMBus controller");
	return (0);
}

static int
fakesmbb_attach(device_t dev)
{
	struct fakesmbb_softc *sc = (struct fakesmbb_softc *)device_get_softc(dev);

	sc->self = dev;

	/* mirror intpm.c:331-337 exactly */
	sc->smbus = device_add_child(dev, "smbus", -1);
	if (sc->smbus == NULL) {
		kprintf("[df5642] failed to add smbus child\n");
		return (ENXIO);
	}
	if (device_probe_and_attach(sc->smbus) != 0)
		kprintf("[df5642] smbus child failed to attach\n");

	sc->cdev = make_dev(&df5642_ops, 0, UID_ROOT, GID_WHEEL, 0600,
	    "df5642");
	if (sc->cdev == NULL)
		kprintf("[df5642] make_dev failed\n");
	else
		sc->cdev->si_drv1 = sc;

	kprintf("[df5642] attach done: smbus0=%p cdev=%p "
	    "(now: kldload smb)\n", (void *)sc->smbus, (void *)sc->cdev);
	stat_children(sc->smbus, "attach");
	return (0);
}

static int
fakesmbb_detach(device_t dev)
{
	struct fakesmbb_softc *sc = (struct fakesmbb_softc *)device_get_softc(dev);
	device_t child = sc->smbus;

	if (sc->cdev) {
		dev_ops_remove_minor(&df5642_ops, 0);
		sc->cdev = NULL;
	}
	bus_generic_detach(dev);
	if (child)
		device_delete_child(dev, child);
	return (0);
}

static device_method_t fakesmbb_methods[] = {
	/* device interface */
	DEVMETHOD(device_identify,	fakesmbb_identify),
	DEVMETHOD(device_probe,		fakesmbb_probe),
	DEVMETHOD(device_attach,	fakesmbb_attach),
	DEVMETHOD(device_detach,	fakesmbb_detach),

	/* smbus interface (the emulated controller vtable) */
	DEVMETHOD(smbus_callback,	fakesmbb_callback),
	DEVMETHOD(smbus_trans,		fakesmbb_trans),
	DEVMETHOD(smbus_intr,		fakesmbb_intr),

	DEVMETHOD_END
};

static driver_t fakesmbb_driver = {
	"fakesmbb",
	fakesmbb_methods,
	sizeof(struct fakesmbb_softc),
};

static devclass_t fakesmbb_devclass;

DRIVER_MODULE(fakesmbb, nexus, fakesmbb_driver, fakesmbb_devclass, NULL, NULL);
/* register the REAL production smbus driver on the fake bus
 * (exactly what intpm.c:903 does for the PIIX4) */
DRIVER_MODULE(smbus, fakesmbb, smbus_driver, smbus_devclass, NULL, NULL);
MODULE_DEPEND(fakesmbb, smbus, SMBUS_MINVER, SMBUS_PREFVER, SMBUS_MAXVER);
MODULE_VERSION(fakesmbb, 1);
