/*
 * qsmedia.c — synthetic MII MAC controller (DF-5053 PoC trigger)
 *
 * Reproduces the hardware precondition for DF-5053 without hardware:
 * a MII bus carrying exactly one PHY whose ID registers answer
 * "Quality Semiconductor QS6612" (OUI 0x006051, model 0x0000 —
 * miidevs:66,241; qsphys[] qsphy.c:109-112) but whose BMSR loses its
 * media capability bits across the software reset the driver itself
 * issues at attach (qsphy_reset -> mii_phy_reset writes BMCR_RESET,
 * qsphy.c:162 -> mii_physubr.c:191).
 *
 * The resulting attach sequence on a stock kernel:
 *
 *   miibus_probe   (mii.c:105-116)  BMSR pre-reset has media bits -> PHY exists
 *   qsphy_probe    (qsphy.c:129)    PHYIDR1/2 match QUALSEMI/QS6612 -> qsphy
 *   qsphy_attach   (qsphy.c:143)    qsphy_reset -> BMCR_RESET write [latch]
 *                                  PHY_READ(MII_BMSR) == 0 (media gone)
 *                                  "no media present" (qsphy.c:171)
 *                                  mii_phy_add_media SKIPPED (qsphy.c:168-171)
 *   MIIBUS_MEDIAINIT -> miibus_mediainit (mii.c:227-245)
 *                                  parent adds nothing; ifm_list EMPTY;
 *                                  ifmedia_set(&mii->mii_media, 0) (mii.c:244)
 *   ifmedia_set    (if_media.c)     no match for 0x0 -> panic("ifmedia_set")
 *
 * i.e. the DF-4930/DF-4957/DF-4981/DF-5003 empty-media family panic (hub:
 * DF-5001) with a REAL qsphy driver in the backtrace — the 5th leaf.
 *
 * The register image:
 *   - pre-reset BMSR: BMSR_ANEG|100TXFDX|100TXHDX|10TFDX|10THDX (enough for
 *     miibus_no_phy, mii.c:114-115, and a plausible QS6612);
 *   - any BMCR write with BMCR_RESET latches "hosed": all later BMSR reads
 *     return 0 — the flaky/faulty-PHY story the family documents;
 *   - PHYIDR1=0x0181, PHYIDR2=0x4401  =>  MII_OUI()=0x6051 (QUALSEMI),
 *     MII_MODEL()=0 (QS6612), MII_REV()=1  (mii.h:109-111);
 *   - BMCR reads back with BMCR_RESET clear so mii_phy_reset's poll
 *     (mii_physubr.c:204-209) completes at once.
 *
 * fixcheck mode (hw.qsmedia_fixcheck=1, run after the panic on a reset
 * guest): replays the FIXED attach shape — the fix.diff branch adds
 *     ifmedia_add(&mii->mii_media, IFM_MAKEWORD(IFM_ETHER, IFM_NONE, 0,
 *         sc->mii_inst), MII_MEDIA_NONE, NULL);
 * to the no-media branch (mirroring DF-5003's leaf fix) — against a real
 * struct ifmedia, then runs the hub walk verbatim (mii.c:232-244) and
 * verifies ifm_cur is non-NULL afterwards (which is what keeps every
 * mii *_service()'s `ife->ifm_cur->ifm_media' dereference safe).
 *
 * Usage in the guest:
 *   make SYSDIR=/usr/src/sys          # builds qsmedia.ko
 *   kldload ./qsmedia.ko              # baseline: REAL qsphy attach -> panic
 *   (after vm.sh reset:)
 *   sysctl hw.qsmedia_fixcheck=1      # fixed attach shape survives
 */
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/bus.h>
#include <sys/malloc.h>
#include <sys/socket.h>
#include <sys/sysctl.h>

#include <net/if.h>
#include <net/if_media.h>
#include <net/if_var.h>

#include <dev/netif/mii_layer/mii.h>
#include <dev/netif/mii_layer/miivar.h>
#include "miibus_if.h"

/* QS6612: OUI 0x006051 -> PHYIDR1 = OUI>>6 = 0x0181,
 *                    PHYIDR2 = (OUI&0x3f)<<10 | model<<4 | rev = 0x4401 */
#define	QS6612_ID1	0x0181
#define	QS6612_ID2	0x4401

#define	QSM_BMSR_GOOD	(BMSR_ANEG | BMSR_100TXFDX | BMSR_100TXHDX | \
			 BMSR_10TFDX | BMSR_10THDX)

struct qsmedia_softc {
	struct ifnet		ifnet;		/* mii_ifp; zeroed => !IFF_UP */
	device_t		miibus;
	uint16_t		phy[8];		/* canned register image */
	int			hosed;		/* set by a BMCR_RESET write */
};

static void	qsmedia_identify(driver_t *, device_t);
static int	qsmedia_probe(device_t);
static int	qsmedia_attach(device_t);
static int	qsmedia_detach(device_t);
static int	qsmedia_readreg(device_t, int, int);
static int	qsmedia_writereg(device_t, int, int, int);
static void	qsmedia_statchg(device_t);
static void	qsmedia_mediainit(device_t);
static int	qsmedia_ifmedia_upd(struct ifnet *);
static void	qsmedia_ifmedia_sts(struct ifnet *, struct ifmediareq *);

static device_method_t qsmedia_methods[] = {
	DEVMETHOD(device_identify,	qsmedia_identify),
	DEVMETHOD(device_probe,		qsmedia_probe),
	DEVMETHOD(device_attach,	qsmedia_attach),
	DEVMETHOD(device_detach,	qsmedia_detach),
	DEVMETHOD(miibus_readreg,	qsmedia_readreg),
	DEVMETHOD(miibus_writereg,	qsmedia_writereg),
	DEVMETHOD(miibus_statchg,	qsmedia_statchg),
	DEVMETHOD(miibus_mediainit,	qsmedia_mediainit),
	DEVMETHOD_END
};

static devclass_t qsmedia_devclass;

static driver_t qsmedia_driver = {
	"qsmedia",
	qsmedia_methods,
	sizeof(struct qsmedia_softc)
};

DRIVER_MODULE(qsmedia, nexus, qsmedia_driver, qsmedia_devclass, NULL, NULL);
DRIVER_MODULE(miibus, qsmedia, miibus_driver, miibus_devclass, NULL, NULL);
MODULE_DEPEND(qsmedia, miibus, 1, 1, 1);

static void
qsmedia_identify(driver_t *driver, device_t parent)
{
	if (device_find_child(parent, "qsmedia", -1) == NULL)
		BUS_ADD_CHILD(parent, parent, 0, "qsmedia", -1);
}

static int
qsmedia_probe(device_t dev)
{
	device_set_desc(dev, "synthetic single-PHY MII controller (DF-5053)");
	return (0);
}

static int
qsmedia_attach(device_t dev)
{
	struct qsmedia_softc *sc = device_get_softc(dev);
	int error;

	bzero(sc, sizeof(*sc));

	sc->phy[MII_BMCR] = 0;
	sc->phy[MII_BMSR] = QSM_BMSR_GOOD;
	sc->phy[MII_PHYIDR1] = QS6612_ID1;
	sc->phy[MII_PHYIDR2] = QS6612_ID2;
	sc->phy[MII_ANAR] = ANAR_CSMA;

	/* fixmode (kenv hw.qsmedia.fixmode=1 before kldload) suppresses the
	 * probe so the fixcheck sysctl can run on a live guest */
	if (kgetenv("hw.qsmedia.fixmode")) {
		kprintf("qsmedia: fixmode -- miibus probe suppressed\n");
		return (0);
	}

	kprintf("qsmedia: probing MII bus (QS6612 id1=0x%04x id2=0x%04x, "
	    "pre-reset BMSR=0x%04x)\n", QS6612_ID1, QS6612_ID2, QSM_BMSR_GOOD);

	error = mii_phy_probe(dev, &sc->miibus,
	    qsmedia_ifmedia_upd, qsmedia_ifmedia_sts);
	if (error) {
		kprintf("qsmedia: mii_phy_probe failed: %d\n", error);
		return (error);
	}

	/* NOT REACHED on a stock kernel: qsphy_attach panics inside the
	 * MIIBUS_MEDIAINIT below (empty media list -> ifmedia_set panic) */
	kprintf("qsmedia: NOT REACHED (expected panic in qsphy_attach)\n");
	return (0);
}

static int
qsmedia_detach(device_t dev)
{
	return (0);
}

static int
qsmedia_readreg(device_t dev, int phy, int reg)
{
	struct qsmedia_softc *sc = device_get_softc(dev);
	uint16_t v;

	if (phy != 0 || reg < 0 || reg > 7)
		return (0xffff);		/* absent */
	if (reg == MII_BMSR && sc->hosed)
		return (0);			/* media bits gone after reset */
	v = sc->phy[reg];
	if (reg == MII_BMCR)
		v &= ~BMCR_RESET;		/* reset completes instantly */
	return (v);
}

static int
qsmedia_writereg(device_t dev, int phy, int reg, int data)
{
	struct qsmedia_softc *sc = device_get_softc(dev);

	if (phy != 0 || reg < 0 || reg > 7)
		return (0);
	if (reg == MII_BMCR && (data & BMCR_RESET))
		sc->hosed = 1;			/* PHY comes out of reset broken */
	sc->phy[reg] = (uint16_t)data;
	return (0);
}

static void
qsmedia_statchg(device_t dev)
{
}

static void
qsmedia_mediainit(device_t dev)
{
	/* parent NIC adds no media of its own (the family precondition) */
}

static int
qsmedia_ifmedia_upd(struct ifnet *ifp)
{
	return (0);
}

static void
qsmedia_ifmedia_sts(struct ifnet *ifp, struct ifmediareq *ifmr)
{
	ifmr->ifm_active = IFM_ETHER | IFM_AUTO;
	ifmr->ifm_status = IFM_AVALID;
}

/*
 * Fix-mechanism replay (DF-5001's run_fix.sh pattern): the FIXED
 * qsphy_attach no-media branch (fix.diff, mirroring DF-5003's leaf fix)
 * adds an IFM_NONE fallback entry before MIIBUS_MEDIAINIT, so the hub
 * walk (mii.c:232-244, replayed verbatim) finds it and ifmedia_set()
 * succeeds.  Must survive with ifm_cur non-NULL — the exact invariant
 * that keeps the *_service() entry dereferences safe.
 */
static struct ifmedia qsmedia_ifm;

static void
qsmedia_fixed_attach_shape(struct ifmedia *ifmp)
{
	/* qsphy_attach no-media branch WITH fix.diff applied (qsphy.c:168ff):
	 * kprintf("no media present"); + the fix's ifmedia_add fallback */
	ifmedia_add(ifmp,
	    IFM_MAKEWORD(IFM_ETHER, IFM_NONE, 0, 0), MII_MEDIA_NONE, NULL);
	/* MIIBUS_MEDIAINIT -> miibus_mediainit hub walk, mii.c:232-244 */
	{
		struct ifmedia_entry *m;
		int media = 0;

		for (m = LIST_FIRST(&ifmp->ifm_list); m != NULL;
		     m = LIST_NEXT(m, ifm_list)) {
			media = m->ifm_media;
			if (media == (IFM_ETHER | IFM_AUTO))
				break;
		}
		ifmedia_set(ifmp, media);
	}
}

static int
qsmedia_fixcheck_sysctl(SYSCTL_HANDLER_ARGS)
{
	int error, t = 0;

	error = sysctl_handle_int(oidp, &t, 0, req);
	if (error || req->newptr == NULL)
		return (error);
	if (t == 0)
		return (0);

	kprintf("qsmedia-fix: ifmedia_init (EMPTY list; qsphy took the "
	    "'no media present' branch)\n");
	ifmedia_init(&qsmedia_ifm, IFM_IMASK, NULL, NULL);
	qsmedia_fixed_attach_shape(&qsmedia_ifm);
	kprintf("qsmedia-fix: survived; ifm_cur=%p media=0x%x\n",
	    qsmedia_ifm.ifm_cur,
	    qsmedia_ifm.ifm_cur ? qsmedia_ifm.ifm_cur->ifm_media : -1);
	kprintf("qsmedia-fix: %s (service deref %s)\n",
	    qsmedia_ifm.ifm_cur ? "ifm_cur non-NULL" : "ifm_cur NULL",
	    qsmedia_ifm.ifm_cur ? "safe" : "UNSAFE");
	return (0);
}
SYSCTL_PROC(_hw, OID_AUTO, qsmedia_fixcheck, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, qsmedia_fixcheck_sysctl, "I", "replay DF-5053 fixed attach shape");
