// SPDX-License-Identifier: GPL-2.0

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/of_mdio.h>
#include <linux/phy.h>
#include <linux/usb.h>

#define USB_MARVELL_VID	0x1286

static const struct usb_device_id mvusb_mdio_table[] = {
	{ USB_DEVICE(USB_MARVELL_VID, 0x1fa4) },

	{}
};
MODULE_DEVICE_TABLE(usb, mvusb_mdio_table);

enum {
	MVUSB_CMD_PREAMBLE0,
	MVUSB_CMD_PREAMBLE1,
	MVUSB_CMD_ADDR,
	MVUSB_CMD_VAL,
};

struct mvusb_mdio {
	struct usb_device *udev;
	struct mii_bus *mdio;

	__le16 buf[4];
};

static int mvusb_mdio_read(struct mii_bus *mdio, int dev, int reg)
{
	struct mvusb_mdio *mvusb = mdio->priv;
	int err, alen;

	if (dev & MII_ADDR_C45)
		return -EOPNOTSUPP;

	mvusb->buf[MVUSB_CMD_ADDR] = cpu_to_le16(0xa400 | (dev << 5) | reg);

	err = usb_bulk_msg(mvusb->udev, usb_sndbulkpipe(mvusb->udev, 2),
			   mvusb->buf, 6, &alen, 100);
	if (err)
		return err;

	err = usb_bulk_msg(mvusb->udev, usb_rcvbulkpipe(mvusb->udev, 6),
			   &mvusb->buf[MVUSB_CMD_VAL], 2, &alen, 100);
	if (err)
		return err;

	return le16_to_cpu(mvusb->buf[MVUSB_CMD_VAL]);
}

static int mvusb_mdio_write(struct mii_bus *mdio, int dev, int reg, u16 val)
{
	struct mvusb_mdio *mvusb = mdio->priv;
	int alen;

	if (dev & MII_ADDR_C45)
		return -EOPNOTSUPP;

	mvusb->buf[MVUSB_CMD_ADDR] = cpu_to_le16(0x8000 | (dev << 5) | reg);
	mvusb->buf[MVUSB_CMD_VAL]  = cpu_to_le16(val);

	return usb_bulk_msg(mvusb->udev, usb_sndbulkpipe(mvusb->udev, 2),
			    mvusb->buf, 8, &alen, 100);
}

static int mvusb_mdio_probe(struct usb_interface *interface,
			    const struct usb_device_id *id)
{
	struct device *dev = &interface->dev;
	struct mvusb_mdio *mvusb;
	struct mii_bus *mdio;
	int ret;

	mdio = devm_mdiobus_alloc_size(dev, sizeof(*mvusb));
	if (!mdio)
		return -ENOMEM;

	mvusb = mdio->priv;
	mvusb->mdio = mdio;
	mvusb->udev = usb_get_dev(interface_to_usbdev(interface));

	/* Reversed from USB PCAPs, no idea what these mean. */
	mvusb->buf[MVUSB_CMD_PREAMBLE0] = cpu_to_le16(0xe800);
	mvusb->buf[MVUSB_CMD_PREAMBLE1] = cpu_to_le16(0x0001);

	snprintf(mdio->id, MII_BUS_ID_SIZE, "mvusb-%s", dev_name(dev));
	mdio->name = mdio->id;
	mdio->parent = dev;
	mdio->read = mvusb_mdio_read;
	mdio->write = mvusb_mdio_write;

	usb_set_intfdata(interface, mvusb);
	ret = of_mdiobus_register(mdio, dev->of_node);
	if (ret)
		goto put_dev;

	return 0;

put_dev:
	usb_put_dev(mvusb->udev);
	return ret;
}

static void mvusb_mdio_disconnect(struct usb_interface *interface)
{
	struct mvusb_mdio *mvusb = usb_get_intfdata(interface);
	struct usb_device *udev = mvusb->udev;

	mdiobus_unregister(mvusb->mdio);
	usb_set_intfdata(interface, NULL);
	usb_put_dev(udev);
}

static struct usb_driver mvusb_mdio_driver = {
	.name       = "mvusb_mdio",
	.id_table   = mvusb_mdio_table,
	.probe      = mvusb_mdio_probe,
	.disconnect = mvusb_mdio_disconnect,
};

module_usb_driver(mvusb_mdio_driver);

MODULE_AUTHOR("Tobias Waldekranz <tobias@waldekranz.com>");
MODULE_DESCRIPTION("Marvell USB MDIO Adapter");
MODULE_LICENSE("GPL");
