/*
 * df5632_rig.c -- DF-5632/5633/5634/5635 rig: instantiate the REAL
 * in-tree iicbus stack (iicbus.ko + iicbb.ko [+ iic.ko]) under a fake
 * GPIO parent and drive the iicbus.c lifecycle primitives.
 *
 * Targets (sys/bus/iicbus/iicbus.c):
 *   DF-5632  iicbus_add_child() is registered (DEVMETHOD at :66) with a
 *            FreeBSD-era 4-arg/int prototype while DragonFly's
 *            bus_add_child interface is 5-arg returning device_t
 *            (sys/kern/bus_if.m:128-134).  On dispatch the callee reads
 *            order <- child (device_t), name <- order (int 0 -> NULL),
 *            unit <- name (rodata address truncated), and the caller
 *            receives 0-as-NULL instead of the new device.  The rig's
 *            df5632k driver uses the exact identify pattern of
 *            iic.c:104-110 / iicsmb.c:137-142, so the skew fires on
 *            stock iicbus.ko the moment the rig module loads.
 *   DF-5633  iicbus_attach() (:144-146) unconditionally re-creates the
 *            ic/iic/iicsmb trio on every attach; children survive
 *            module/driver teardown (DS_NOTPRESENT), so every
 *            detach+re-attach cycle leaks 3 struct bsd_device and mints
 *            duplicate devclass units (iic0, iic1, ... -> duplicate
 *            /dev/iicN on ONE physical bus).
 *   DF-5634  children are created with no struct iicbus_ivar ever
 *            allocated (root cause of DF-5559): stock iicbus has no
 *            bus_read_ivar/write_ivar at all; with DF-5559's fix the
 *            write side returns success while discarding the value
 *            (ivar == NULL).  Complete fix allocates ivars in
 *            bus_add_child and frees them in bus_child_detached.
 *   DF-5635  iicbus_detach() (:156-162) swallows bus_generic_detach()'s
 *            error: a child that fails to detach leaves the bus
 *            marked detached + driverless with its softc freed
 *            (device_set_driver(NULL) kfree's dev->softc,
 *            subr_bus.c:1943-1945) while the child is still attached;
 *            the child's next iicbus_request_bus() derefs
 *            device_get_softc(bus) == NULL (iiconf.c:88 -> :101).
 *
 * Rig structure (mirrors the DF-5539/5558/5563 fakebb rig):
 *
 *    nexus -> fakebb0 (this module: GPIO emulator + /dev/df5632)
 *               -> iicbb0   (real iicbb.ko)
 *                  -> iicbus0 (real iicbus.ko)  <-- code under test
 *                     -> ic/iic/iicsmb trio (iicbus.c:144-146)
 *                     -> df5632k0 (this module's identify-based client
 *                                  driver -- the DF-5632 skew victim)
 *
 * /dev/df5632 ioctls (dfctl) drive production newbus APIs on the
 * production iicbus device: device_detach(), device_probe_and_attach(),
 * BUS_ADD_CHILD(), BUS_READ_IVAR()/BUS_WRITE_IVAR().
 */

#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 <bus/iicbus/iiconf.h>
#include <bus/iicbus/iicbus.h>

#include "bus_if.h"
#include "iicbb_if.h"

#define FAKEBB_NACK_ADDR	0x6e	/* slave refuses this address */
#define FAKEBB_TX_BASE		0xc0	/* slave TX pattern base */

/* dfctl <-> rig ioctl protocol */
#define DF5632_IOCTL_MAGIC	'D'
struct df5632_req {
	int32_t		arg;	/* in: cycle count etc. */
	int32_t		rc;	/* out: kernel return code */
	int64_t		v0, v1, v2, v3;	/* out: witness values */
};
#define DF5632_STAT		_IOWR(DF5632_IOCTL_MAGIC, 1, struct df5632_req)
#define DF5632_CYCLE		_IOWR(DF5632_IOCTL_MAGIC, 2, struct df5632_req)
#define DF5632_ADDCHILD		_IOWR(DF5632_IOCTL_MAGIC, 3, struct df5632_req)
#define DF5632_IVAR_RT		_IOWR(DF5632_IOCTL_MAGIC, 4, struct df5632_req)
#define DF5632_FAILCHILD	_IOWR(DF5632_IOCTL_MAGIC, 5, struct df5632_req)
#define DF5632_DETACH_BUS	_IOWR(DF5632_IOCTL_MAGIC, 6, struct df5632_req)
#define DF5632_REQBUS		_IOWR(DF5632_IOCTL_MAGIC, 7, struct df5632_req)

static struct dev_ops df5632_ops;

struct fakebb_softc {
	device_t	self;		/* fakebb0: the bus-owner token */
	device_t	iicbb;		/* our iicbb child */
	device_t	iicbus;		/* located iicbus grandchild (code under test) */
	cdev_t		cdev;		/* /dev/df5632 */

	/* emulated GPIO pin state (what the master drove) */
	int		sda, scl, last_scl;

	/* emulated I2C slave */
	int		started;	/* START seen and no STOP yet */
	int		clk;		/* SCL rising edges this byte slot (1..9) */
	u_char		shift;		/* shift-in register */
	int		first_byte;	/* next byte slot is the address */
	int		reading;	/* current transfer direction */
	u_char		addr;		/* addressed slave byte */
	int		nack_this;	/* NACK the current address */
	u_char		rx[32];		/* bytes captured from master */
	int		rxlen;
	u_char		txnext;		/* next TX pattern byte */
	u_char		txcur;		/* current TX byte being clocked out */

	int		starts, stops;
};

static int df5632_childbusy = 0;	/* df5632k detach returns EBUSY */
static int df5632_identify_add = 0;	/* fire BUS_ADD_CHILD in identify */

SYSCTL_NODE(_hw, OID_AUTO, df5632, CTLFLAG_RW, 0, "DF-5632 rig");
SYSCTL_INT(_hw_df5632, OID_AUTO, childbusy, CTLFLAG_RW, &df5632_childbusy, 0,
    "df5632k child refuses to detach (DF-5635 trigger)");
SYSCTL_INT(_hw_df5632, OID_AUTO, identify_add, CTLFLAG_RW,
    &df5632_identify_add, 0,
    "df5632k_identify calls BUS_ADD_CHILD (DF-5632 skew trigger; "
    "ON STOCK KERNELS THIS PANICS at kldload: devclass_add_device "
    "negative-index write)");
TUNABLE_INT("hw.df5632.identify_add", &df5632_identify_add);

/* ------------------------------------------------------------------ */
/* emulated wire: what a master reading SDA would see                  */
/* ------------------------------------------------------------------ */

static int
fakebb_wire_sda(struct fakebb_softc *sc)
{

	if (!sc->started)
		return (sc->sda);
	if (sc->clk == 9) {
		/* ACK slot: the slave owns the wire */
		return (sc->nack_this ? 1 : 0);
	}
	if (sc->reading && !sc->first_byte && sc->clk >= 1 && sc->clk <= 8)
		return ((sc->txcur >> (8 - sc->clk)) & 1);
	return (sc->sda);		/* slave idle: master's own level */
}

/* ------------------------------------------------------------------ */
/* iicbb_if methods (the "fake GPIO" vtable)                           */
/* ------------------------------------------------------------------ */

static void
fakebb_setsda(device_t dev, int val)
{
	struct fakebb_softc *sc = device_get_softc(dev);

	val = val ? 1 : 0;
	if (sc->scl && sc->sda && !val) {
		/* SDA falling while SCL high: START / repeated START */
		sc->started = 1;
		sc->clk = 0;
		sc->shift = 0;
		sc->first_byte = 1;
		sc->reading = 0;
		sc->nack_this = 0;
		sc->rxlen = 0;
		sc->starts++;
	} else if (sc->scl && !sc->sda && val && sc->started) {
		/* SDA rising while SCL high: STOP */
		sc->started = 0;
		sc->stops++;
	}
	sc->sda = val;
}

static void
fakebb_setscl(device_t dev, int val)
{
	struct fakebb_softc *sc = device_get_softc(dev);

	val = val ? 1 : 0;
	if (sc->started) {
		if (val && !sc->last_scl) {
			/* rising edge: data bits are sampled here */
			sc->clk++;
			if (sc->clk >= 1 && sc->clk <= 8)
				sc->shift = (u_char)((sc->shift << 1) |
				    (fakebb_wire_sda(sc) & 1));
		} else if (!val && sc->last_scl && sc->clk == 9) {
			/* falling edge of the ACK clock: byte slot done */
			if (sc->first_byte) {
				sc->addr = sc->shift;
				sc->first_byte = 0;
				sc->reading = sc->shift & 1;
				sc->nack_this =
				    (sc->addr == FAKEBB_NACK_ADDR);
				sc->txnext = FAKEBB_TX_BASE;
				if (sc->reading)
					sc->txcur = sc->txnext++;
			} else if (sc->reading) {
				sc->txcur = sc->txnext++;
			} else {
				if (sc->rxlen < (int)sizeof(sc->rx))
					sc->rx[sc->rxlen] = sc->shift;
				sc->rxlen++;
			}
			sc->clk = 0;
			sc->shift = 0;
		}
	}
	sc->last_scl = val;
	sc->scl = val;
}

static int
fakebb_getsda(device_t dev)
{
	struct fakebb_softc *sc = device_get_softc(dev);

	return (fakebb_wire_sda(sc));
}

static int
fakebb_getscl(device_t dev)
{
	struct fakebb_softc *sc = device_get_softc(dev);

	return (sc->scl);
}

static int
fakebb_reset(device_t dev, u_char speed, u_char addr, u_char *oldaddr)
{

	(void)dev;
	(void)speed;
	(void)addr;
	if (oldaddr != NULL)
		*oldaddr = 0;
	return (0);
}

static int
fakebb_callback(device_t dev, int index, caddr_t data)
{

	(void)dev;
	(void)data;
	return (0);		/* bridge always grants */
}

/* ------------------------------------------------------------------ */
/* newbus topology witness: dump the iicbus0 child list                */
/* ------------------------------------------------------------------ */

static void
df5632_dump_children(device_t bus, const char *tag)
{
	device_t *kids;
	int nkid, i, error;

	error = device_get_children(bus, &kids, &nkid);
	if (error) {
		kprintf("[df5632] %s: device_get_children error=%d\n",
		    tag, error);
		return;
	}
	kprintf("[df5632] %s: nchildren=%d\n", tag, nkid);
	for (i = 0; i < nkid; i++) {
		devclass_t dc = device_get_devclass(kids[i]);
		driver_t *dr = device_get_driver(kids[i]);
		device_state_t st = device_get_state(kids[i]);

		kprintf("[df5632] %s: child[%d] dev=%p devclass=%s unit=%d "
		    "driver=%s state=%d\n",
		    tag, i, kids[i],
		    dc ? devclass_get_name(dc) : "(NULL)",
		    device_get_unit(kids[i]),
		    dr ? dr->name : "(none)",
		    (int)st);
	}
	kfree(kids, M_TEMP);
}

static void
df5632_dump_bus(device_t bus, const char *tag)
{
	devclass_t dc;
	driver_t *dr;

	if (bus == NULL) {
		kprintf("[df5632] %s: iicbus == NULL\n", tag);
		return;
	}
	dc = device_get_devclass(bus);
	dr = device_get_driver(bus);
	kprintf("[df5632] %s: iicbus=%p devclass=%s driver=%s state=%d "
	    "softc=%p\n", tag, bus,
	    dc ? devclass_get_name(dc) : "(NULL)",
	    dr ? dr->name : "(NONE-DRIVERLESS)",
	    (int)device_get_state(bus),
	    device_get_softc(bus));
}

/* ------------------------------------------------------------------ */
/* df5632k: an identify-based iicbus client driver, the EXACT pattern  */
/* of iic.c:104-110 / iicsmb.c:137-142 -- the DF-5632 skew victim      */
/* ------------------------------------------------------------------ */

static devclass_t df5632k_devclass;

static void
df5632k_identify(driver_t *driver, device_t parent)
{
	device_t rc;

	if (!df5632_identify_add) {
		kprintf("[df5632] df5632k_identify: SKIPPED "
		    "(hw.df5632.identify_add=0)\n");
		return;
	}
	if (device_find_child(parent, "df5632k", -1) != NULL)
		return;
	/* iic.c:109 verbatim (name substituted) */
	rc = BUS_ADD_CHILD(parent, parent, 0, "df5632k", -1);
	kprintf("[df5632] df5632k_identify: BUS_ADD_CHILD returned %p "
	    "(wanted: a real device_t)\n", rc);
	df5632_dump_children(parent, "identify-post-BUS_ADD_CHILD");
}

static int
df5632k_probe(device_t dev)
{

	device_set_desc(dev, "DF-5632 identify-based iicbus client");
	return (0);
}

static int
df5632k_attach(device_t dev)
{

	kprintf("[df5632] df5632k%d attached (unit=%d softc=%p)\n",
	    device_get_unit(dev), device_get_unit(dev),
	    device_get_softc(dev));
	return (0);
}

static int
df5632k_detach(device_t dev)
{

	if (df5632_childbusy) {
		kprintf("[df5632] df5632k%d detach: REFUSING (EBUSY)\n",
		    device_get_unit(dev));
		return (EBUSY);
	}
	return (0);
}

static device_method_t df5632k_methods[] = {
	DEVMETHOD(device_identify,	df5632k_identify),
	DEVMETHOD(device_probe,		df5632k_probe),
	DEVMETHOD(device_attach,	df5632k_attach),
	DEVMETHOD(device_detach,	df5632k_detach),

	DEVMETHOD_END
};

static driver_t df5632k_driver = {
	"df5632k",
	df5632k_methods,
	1,		/* minimal softc */
};

DRIVER_MODULE(df5632k, iicbus, df5632k_driver, df5632k_devclass, NULL, NULL);
MODULE_DEPEND(df5632k, iicbus, 1, 1, 1);

/* ------------------------------------------------------------------ */
/* /dev/df5632: userland entry into the newbus lifecycle APIs          */
/* ------------------------------------------------------------------ */

static int
df5632_open(struct dev_open_args *ap)
{

	return (0);
}

static int
df5632_close(struct dev_close_args *ap)
{

	return (0);
}

static int
df5632_ioctl(struct dev_ioctl_args *ap)
{
	cdev_t cdev = ap->a_head.a_dev;
	struct fakebb_softc *sc = cdev->si_drv1;
	struct df5632_req *r = (struct df5632_req *)ap->a_data;
	u_long cmd = ap->a_cmd;
	device_t bus, child, rc;
	uintptr_t val = 0x42424242;
	int error = 0;
	int i;

	if (sc->iicbus == NULL) {
		sc->iicbus = devclass_get_device(devclass_find("iicbus"), 0);
		if (sc->iicbus == NULL)
			return (ENXIO);
	}
	bus = sc->iicbus;

	switch (cmd) {
	case DF5632_STAT:
		df5632_dump_bus(bus, "STAT");
		df5632_dump_children(bus, "STAT");
		r->rc = 0;
		break;

	case DF5632_CYCLE:
		/* production detach + re-attach of iicbus0: exactly what
		 * devclass_delete_driver()/devclass_add_driver() do on
		 * kldunload/kldload of iicbus.ko with a resident bridge */
		for (i = 0; i < r->arg; i++) {
			error = device_detach(bus);
			if (error) {
				kprintf("[df5632] CYCLE %d: device_detach "
				    "error=%d\n", i, error);
				break;
			}
			error = device_probe_and_attach(bus);
			if (error) {
				kprintf("[df5632] CYCLE %d: re-attach "
				    "error=%d\n", i, error);
				break;
			}
			kprintf("[df5632] CYCLE %d: detach+reattach ok\n", i);
		}
		df5632_dump_children(bus, "CYCLE-done");
		r->rc = error;
		break;

	case DF5632_ADDCHILD:
		/* manual BUS_ADD_CHILD through the iicbus driver's own
		 * (skewed) method -- return value is the witness */
		rc = BUS_ADD_CHILD(bus, bus, 0, "df5632c", -1);
		kprintf("[df5632] ADDCHILD: BUS_ADD_CHILD(\"df5632c\",-1) "
		    "returned %p (iicbus_add_child \"returns 0\")\n", rc);
		df5632_dump_children(bus, "ADDCHILD-post");
		r->rc = 0;
		r->v0 = (int64_t)(uintptr_t)rc;
		break;

	case DF5632_IVAR_RT:
		child = device_find_child(bus, "df5632k", -1);
		if (child == NULL)
			child = device_find_child(bus, "iic", -1);
		if (child == NULL) {
			kprintf("[df5632] IVAR_RT: no child\n");
			r->rc = ENOENT;
			break;
		}
		error = BUS_WRITE_IVAR(bus, child, IICBUS_IVAR_ADDR, 0x42);
		kprintf("[df5632] IVAR_RT: WRITE_IVAR(0x42) error=%d\n",
		    error);
		error = BUS_READ_IVAR(bus, child, IICBUS_IVAR_ADDR, &val);
		kprintf("[df5632] IVAR_RT: READ_IVAR error=%d val=0x%lx "
		    "iicbus_get_addr=0x%x\n",
		    error, (unsigned long)val, iicbus_get_addr(child));
		r->rc = error;
		r->v0 = error;
		r->v1 = (int64_t)val;
		r->v2 = (int64_t)iicbus_get_addr(child);
		break;

	case DF5632_FAILCHILD:
		/* create the df5632k child the DIRECT way (no dispatch):
		 * the asymmetry vs BUS_ADD_CHILD is itself DF-5632
		 * evidence -- the rendezvous contract only works when
		 * callers bypass the iicbus bus_add_child method */
		child = device_add_child(bus, "df5632k", -1);
		kprintf("[df5632] FAILCHILD: device_add_child -> %p\n", child);
		if (child == NULL) {
			r->rc = ENXIO;
			break;
		}
		error = device_probe_and_attach(child);
		kprintf("[df5632] FAILCHILD: probe_and_attach error=%d\n",
		    error);
		df5632_dump_children(bus, "FAILCHILD-post");
		r->rc = error;
		break;

	case DF5632_DETACH_BUS:
		error = device_detach(bus);
		kprintf("[df5632] DETACH_BUS: device_detach(iicbus0) "
		    "returned %d  (child df5632k0 busy=%d)\n",
		    error, df5632_childbusy);
		df5632_dump_bus(bus, "DETACH_BUS-post");
		df5632_dump_children(bus, "DETACH_BUS-post");
		r->rc = error;
		break;

	case DF5632_REQBUS:
		kprintf("[df5632] REQBUS: calling iicbus_request_bus"
		    "(iicbus0, self, WAIT|INTR) -- NULL softc => trap\n");
		error = iicbus_request_bus(bus, sc->self, IIC_WAIT | IIC_INTR);
		kprintf("[df5632] REQBUS: returned %d\n", error);
		if (error == 0)
			iicbus_release_bus(bus, sc->self);
		r->rc = error;
		break;

	default:
		return (ENOTTY);
	}

	return (0);
}

/* ------------------------------------------------------------------ */
/* fakebb bus device plumbing                                          */
/* ------------------------------------------------------------------ */

static void
fakebb_identify(driver_t *driver, device_t parent)
{
	device_t child;

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

static int
fakebb_probe(device_t dev)
{

	device_set_desc(dev, "DF-5632 fake GPIO bit-bang i2c master+slave");
	return (0);
}

static int
fakebb_attach(device_t dev)
{
	struct fakebb_softc *sc = (struct fakebb_softc *)device_get_softc(dev);
	device_t bus = NULL;
	devclass_t dc;
	int tries;

	sc->self = dev;
	sc->iicbb = device_add_child(dev, "iicbb", -1);
	if (sc->iicbb == NULL)
		return (ENXIO);

	/* cascades: iicbb0 -> iicbus0 (+ trio; df5632k via identify) */
	bus_generic_attach(dev);

	for (tries = 0; tries < 50 && bus == NULL; tries++) {
		dc = devclass_find("iicbus");
		if (dc != NULL)
			bus = devclass_get_device(dc, 0);
		if (bus == NULL)
			tsleep(sc, 0, "fkbw", hz / 20);
	}
	sc->iicbus = bus;

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

	kprintf("[df5632] attach done: iicbus=%p cdev=%p childbusy=%d\n",
	    bus, sc->cdev, df5632_childbusy);
	return (0);
}

static int
fakebb_detach(device_t dev)
{
	struct fakebb_softc *sc = (struct fakebb_softc *)device_get_softc(dev);
	device_t child;

	if (sc->cdev) {
		dev_ops_remove_minor(&df5632_ops, 0);
		sc->cdev = NULL;
	}

	child = sc->iicbb;
	bus_generic_detach(dev);
	if (child)
		device_delete_child(dev, child);

	return (0);
}

static void
fakebb_child_detached(device_t dev, device_t child)
{
	struct fakebb_softc *sc = (struct fakebb_softc *)device_get_softc(dev);

	if (child == sc->iicbb)
		sc->iicbb = NULL;
}

static struct dev_ops df5632_ops = {
	{ "df5632", 0, 0 },
	.d_open =	df5632_open,
	.d_close =	df5632_close,
	.d_ioctl =	df5632_ioctl,
};

static device_method_t fakebb_methods[] = {
	/* device interface */
	DEVMETHOD(device_identify,	fakebb_identify),
	DEVMETHOD(device_probe,		fakebb_probe),
	DEVMETHOD(device_attach,	fakebb_attach),
	DEVMETHOD(device_detach,	fakebb_detach),

	/* bus interface */
	DEVMETHOD(bus_child_detached,	fakebb_child_detached),

	/* iicbb interface (the fake GPIO vtable) */
	DEVMETHOD(iicbb_callback,	fakebb_callback),
	DEVMETHOD(iicbb_setsda,	fakebb_setsda),
	DEVMETHOD(iicbb_setscl,	fakebb_setscl),
	DEVMETHOD(iicbb_getsda,	fakebb_getsda),
	DEVMETHOD(iicbb_getscl,	fakebb_getscl),
	DEVMETHOD(iicbb_reset,		fakebb_reset),

	DEVMETHOD_END
};

static driver_t fakebb_driver = {
	"fakebb",
	fakebb_methods,
	sizeof(struct fakebb_softc),
};

static devclass_t fakebb_devclass;

DRIVER_MODULE(fakebb, nexus, fakebb_driver, fakebb_devclass, NULL, NULL);
MODULE_DEPEND(fakebb, iicbb, 1, 1, 1);
MODULE_VERSION(fakebb, 1);
