/*	$NetBSD: umcpmio_iic.c,v 1.2 2025/12/12 17:49:35 andvar Exp $	*/

/*
 * Copyright (c) 2024, 2025 Brad Spencer <brad@anduin.eldar.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: umcpmio_iic.c,v 1.2 2025/12/12 17:49:35 andvar Exp $");

#ifdef _KERNEL_OPT
#include "opt_usb.h"
#endif

#include <sys/param.h>
#include <sys/types.h>

#include <dev/i2c/i2cvar.h>

#include <dev/usb/umcpmio.h>
#include <dev/usb/umcpmio_hid_reports.h>
#include <dev/usb/umcpmio_transport.h>
#include <dev/usb/umcpmio_iic.h>
#include <dev/usb/umcpmio_subr.h>

#define UMCPMIO_DEBUG 1
#ifdef UMCPMIO_DEBUG
#define DPRINTF(x)	do { if (umcpmiodebug) printf x; } while (0)
#define DPRINTFN(n, x)	do { if (umcpmiodebug > (n)) printf x; } while (0)
extern int umcpmiodebug;
#else
#define DPRINTF(x)	__nothing
#define DPRINTFN(n, x)	__nothing
#endif

/* Stuff required for the I2C part of the MCP2221 / MCP2221A */

/* Clear status of the I2C engine */

static void
mcp2221_set_i2c_speed(struct mcp2221_status_req *req, int flags)
{
	int i2cbaud = MCP2221_DEFAULT_I2C_SPEED;

	if (flags & I2C_F_SPEED)
		i2cbaud = 400000;

	req->set_i2c_speed = MCP2221_I2C_SET_SPEED;
	if (i2cbaud <= 0)
		i2cbaud = MCP2221_DEFAULT_I2C_SPEED;

	/* Everyone and their brother seems to store the I2C divider like this,
	 * so do likewise */
	req->i2c_clock_divider = (MCP2221_INTERNAL_CLOCK / i2cbaud) - 3;
}

static int
mcp2221_set_i2c_speed_one(struct umcpmio_softc *sc, int flags)
{
	int err = 0;
	struct mcp2221_status_req req;
	struct mcp2221_status_res res;

	KASSERT(mutex_owned(&sc->sc_action_mutex));

	memset(&req, 0, MCP2221_REQ_BUFFER_SIZE);
	mcp2221_set_i2c_speed(&req, flags);
	err = mcp2221_put_status(sc, &req, &res);
	if (err)
		goto out;
	if (res.set_i2c_speed == MCP2221_I2C_SPEED_BUSY)
		err = EBUSY;
out:
	return err;
}

static int
mcp2221_i2c_clear(struct umcpmio_softc *sc)
{
	int err = 0;
	struct mcp2221_status_req status_req;
	struct mcp2221_status_res status_res;

	KASSERT(mutex_owned(&sc->sc_action_mutex));

	memset(&status_req, 0, MCP2221_REQ_BUFFER_SIZE);
	status_req.cmd = MCP2221_CMD_STATUS;
	status_req.cancel_transfer = MCP2221_I2C_DO_CANCEL;

	err = umcpmio_send_report(sc,
	    (uint8_t *) & status_req, MCP2221_REQ_BUFFER_SIZE,
	    (uint8_t *) & status_res, sc->sc_cv_wait);

	if (err) {
		err = EIO;
		goto out;
	}
	umcpmio_dump_buffer(sc->sc_dumpbuffer,
	    (uint8_t *) & status_res, MCP2221_RES_BUFFER_SIZE,
	    "mcp2221_i2c_clear buffer copy");

	if (status_res.cmd != MCP2221_CMD_STATUS &&
	    status_res.completion != MCP2221_CMD_COMPLETE_OK) {
		err = EIO;
		goto out;
	}
	umcpmio_dump_buffer(true,
	    (uint8_t *) & status_res, MCP2221_RES_BUFFER_SIZE,
	    "mcp2221_i2c_clear res buffer");

out:
	return err;
}

/*
 * There isn't much required to acquire or release the I2C bus, but the man
 * pages says these are needed
 */

static int
mcp2221_acquire_bus(void *v, int flags)
{
	return 0;
}

static void
mcp2221_release_bus(void *v, int flags)
{
	return;
}

/*
 * The I2C write and I2C read functions mostly use an algorithm that Adafruit
 * came up with in their Python based driver.  A lot of other people have used
 * this same algorithm to good effect.  If changes are made to the I2C read and
 * write functions, it is HIGHLY advisable that a MCP2221 or MCP2221A be on
 * hand to test them.
 */

/* This is what is considered a fatal return from the engine. */

static bool
mcp2221_i2c_fatal(uint8_t state)
{
	int r = false;

	if (state == MCP2221_ENGINE_ADDRNACK ||
	    state == MCP2221_ENGINE_STARTTIMEOUT ||
	    state == MCP2221_ENGINE_REPSTARTTIMEOUT ||
	    state == MCP2221_ENGINE_STOPTIMEOUT ||
	    state == MCP2221_ENGINE_READTIMEOUT ||
	    state == MCP2221_ENGINE_WRITETIMEOUT ||
	    state == MCP2221_ENGINE_ADDRTIMEOUT)
		r = true;
	return r;
}

static int
mcp2221_i2c_write(struct umcpmio_softc *sc, i2c_op_t op, i2c_addr_t addr,
    const void *cmdbuf, size_t cmdlen, void *databuf, size_t datalen,
    int flags)
{
	struct mcp2221_i2c_req i2c_req;
	struct mcp2221_i2c_res i2c_res;
	struct mcp2221_status_res status_res;
	int remaining;
	int err = 0;
	uint8_t cmd;
	size_t totallen = 0;
	int wretry = sc->sc_retry_busy_write;
	int wsretry = sc->sc_retry_busy_write;

	mutex_enter(&sc->sc_action_mutex);
	err = mcp2221_get_status(sc, &status_res);
	mutex_exit(&sc->sc_action_mutex);
	if (err)
		goto out;
	if (status_res.internal_i2c_state != 0) {
		DPRINTF(("mcp2221_i2c_write: internal state not zero,"
		    " clearing. internal_i2c_state=%02x\n",
		    status_res.internal_i2c_state));
		mutex_enter(&sc->sc_action_mutex);
		err = mcp2221_i2c_clear(sc);
		mutex_exit(&sc->sc_action_mutex);
	}
	if (err)
		goto out;

	if (cmdbuf != NULL)
		totallen += cmdlen;
	if (databuf != NULL)
		totallen += datalen;

again:
	memset(&i2c_req, 0, MCP2221_REQ_BUFFER_SIZE);
	cmd = MCP2221_I2C_WRITE_DATA_NS;
	if (I2C_OP_STOP_P(op))
		cmd = MCP2221_I2C_WRITE_DATA;
	i2c_req.cmd = cmd;
	i2c_req.lsblen = totallen;
	i2c_req.msblen = 0;
	i2c_req.slaveaddr = addr << 1;

	remaining = 0;
	if (cmdbuf != NULL) {
		memcpy(&i2c_req.data[0], cmdbuf, cmdlen);
		remaining = cmdlen;
	}
	if (databuf != NULL)
		memcpy(&i2c_req.data[remaining], databuf, datalen);

	DPRINTF(("mcp2221_i2c_write: I2C WRITE: cmd: %02x\n", cmd));
	umcpmio_dump_buffer(sc->sc_dumpbuffer,
	    (uint8_t *) & i2c_req, MCP2221_REQ_BUFFER_SIZE,
	    "mcp2221_i2c_write: write req buffer copy");

	mutex_enter(&sc->sc_action_mutex);
	err = umcpmio_send_report(sc,
	    (uint8_t *) & i2c_req, MCP2221_REQ_BUFFER_SIZE,
	    (uint8_t *) & i2c_res, sc->sc_cv_wait);
	mutex_exit(&sc->sc_action_mutex);
	if (err) {
		err = EIO;
		goto out;
	}
	umcpmio_dump_buffer(sc->sc_dumpbuffer,
	    (uint8_t *) & i2c_res, MCP2221_RES_BUFFER_SIZE,
	    "mcp2221_i2c_write: write res buffer copy");
	if (i2c_res.cmd == cmd &&
	    i2c_res.completion == MCP2221_CMD_COMPLETE_OK) {
		/* Adafruit does a read back of the status at this point.  We
		 * choose not to do that.  That is done later anyway, and it
		 * seemed to be redundant. */
	} else if (i2c_res.cmd == cmd &&
	    i2c_res.completion == MCP2221_I2C_ENGINE_BUSY) {
		DPRINTF(("mcp2221_i2c_write:"
		    " I2C engine busy\n"));

		if (mcp2221_i2c_fatal(i2c_res.internal_i2c_state)) {
			err = EIO;
			goto out;
		}
		wretry--;
		if (wretry > 0) {
			WAITMS(sc->sc_busy_delay);
			goto again;
		} else {
			err = EBUSY;
			goto out;
		}
	} else {
		err = EIO;
		goto out;
	}

	while (wsretry > 0) {
		wsretry--;

		DPRINTF(("mcp2221_i2c_write: checking status loop:"
		    " wcretry=%d\n", wsretry));

		mutex_enter(&sc->sc_action_mutex);
		err = mcp2221_get_status(sc, &status_res);
		mutex_exit(&sc->sc_action_mutex);
		if (err) {
			err = EIO;
			break;
		}
		umcpmio_dump_buffer(sc->sc_dumpbuffer,
		    (uint8_t *) & status_res,
		    MCP2221_RES_BUFFER_SIZE,
		    "mcp2221_i2c_write post check status");
		/* Since there isn't any documentation on what some of the
		 * internal state means, it isn't clear that this is any
		 * different than than MCP2221_ENGINE_ADDRNACK in the other
		 * state register. */

		if (status_res.internal_i2c_state20 &
		    MCP2221_ENGINE_T1_MASK_NACK) {
			DPRINTF(("mcp2221_i2c_write post check:"
			    " engine internal state T1 says NACK\n"));
			err = EIO;
			break;
		}
		if (status_res.internal_i2c_state == 0) {
			DPRINTF(("mcp2221_i2c_write post check:"
			    " engine internal state is ZERO\n"));
			err = 0;
			break;
		}
		if (status_res.internal_i2c_state ==
		    MCP2221_ENGINE_WRITINGNOSTOP &&
		    cmd == MCP2221_I2C_WRITE_DATA_NS) {
			DPRINTF(("mcp2221_i2c_write post check:"
			    " engine internal state is WRITINGNOSTOP\n"));
			err = 0;
			break;
		}
		if (mcp2221_i2c_fatal(status_res.internal_i2c_state)) {
			DPRINTF(("mcp2221_i2c_write post check:"
			    " engine internal state is fatal: %02x\n",
			    status_res.internal_i2c_state));
			err = EIO;
			break;
		}
		WAITMS(sc->sc_busy_delay);
	}

out:
	return err;
}

/*
 * This one deviates a bit from Adafruit in that is supports a straight
 * read and a write + read.  That is, write a register to read from and
 * then do the read.
 */

static int
mcp2221_i2c_read(struct umcpmio_softc *sc, i2c_op_t op, i2c_addr_t addr,
    const void *cmdbuf, size_t cmdlen, void *databuf, size_t datalen, int
    flags)
{
	struct mcp2221_i2c_req i2c_req;
	struct mcp2221_i2c_res i2c_res;
	struct mcp2221_i2c_fetch_req i2c_fetch_req;
	struct mcp2221_i2c_fetch_res i2c_fetch_res;
	struct mcp2221_status_res status_res;
	int err = 0;
	uint8_t cmd;
	int rretry = sc->sc_retry_busy_read;

	if (cmdbuf != NULL) {
		DPRINTF(("mcp2221_i2c_read: has a cmdbuf, doing write first:"
		    " addr=%02x\n", addr));
		err = mcp2221_i2c_write(sc, I2C_OP_WRITE, addr, cmdbuf, cmdlen,
		    NULL, 0, flags);
	}
	if (err)
		goto out;

	mutex_enter(&sc->sc_action_mutex);
	err = mcp2221_get_status(sc, &status_res);
	mutex_exit(&sc->sc_action_mutex);
	if (err)
		goto out;

	if (status_res.internal_i2c_state != 0 &&
	    status_res.internal_i2c_state != MCP2221_ENGINE_WRITINGNOSTOP) {
		DPRINTF(("mcp2221_i2c_read:"
		    " internal state not zero and not WRITINGNOSTOP,"
		    " clearing. internal_i2c_state=%02x\n",
		    status_res.internal_i2c_state));
		mutex_enter(&sc->sc_action_mutex);
		err = mcp2221_i2c_clear(sc);
		mutex_exit(&sc->sc_action_mutex);
	}
	if (err)
		goto out;

	memset(&i2c_req, 0, MCP2221_REQ_BUFFER_SIZE);
	if (cmdbuf == NULL &&
	    status_res.internal_i2c_state != MCP2221_ENGINE_WRITINGNOSTOP) {
		cmd = MCP2221_I2C_READ_DATA;
	} else {
		cmd = MCP2221_I2C_READ_DATA_RS;
	}

	/* The chip apparently can't do a READ without a STOP operation. Report
	 * that, and try treating it like a READ with a STOP.  This won't work
	 * for a lot of devices. */

	if (!I2C_OP_STOP_P(op) && sc->sc_reportreadnostop) {
		device_printf(sc->sc_dev,
		    "mcp2221_i2c_read: ************ called with READ"
		    " without STOP ***************\n");
	}
	i2c_req.cmd = cmd;
	i2c_req.lsblen = datalen;
	i2c_req.msblen = 0;
	i2c_req.slaveaddr = (addr << 1) | 0x01;

	DPRINTF(("mcp2221_i2c_read: I2C READ normal read:"
	    " cmd=%02x, addr=%02x\n", cmd, addr));

	umcpmio_dump_buffer(sc->sc_dumpbuffer,
	    (uint8_t *) & i2c_req, MCP2221_RES_BUFFER_SIZE,
	    "mcp2221_i2c_read normal read req buffer copy");

	mutex_enter(&sc->sc_action_mutex);
	err = umcpmio_send_report(sc,
	    (uint8_t *) & i2c_req, MCP2221_REQ_BUFFER_SIZE,
	    (uint8_t *) & i2c_res, sc->sc_cv_wait);
	mutex_exit(&sc->sc_action_mutex);

	if (err) {
		err = EIO;
		goto out;
	}
	umcpmio_dump_buffer(sc->sc_dumpbuffer,
	    (uint8_t *) & i2c_res, MCP2221_RES_BUFFER_SIZE,
	    "mcp2221_i2c_read read-request response buffer copy");

	while (rretry > 0) {
		rretry--;
		DPRINTF(("mcp2221_i2c_read: fetch loop: rretry=%d\n", rretry));
		err = 0;
		memset(&i2c_fetch_req, 0, MCP2221_REQ_BUFFER_SIZE);
		i2c_fetch_req.cmd = MCP2221_CMD_I2C_FETCH_READ_DATA;
		mutex_enter(&sc->sc_action_mutex);
		err = umcpmio_send_report(sc,
		    (uint8_t *) & i2c_fetch_req, MCP2221_REQ_BUFFER_SIZE,
		    (uint8_t *) & i2c_fetch_res, sc->sc_cv_wait);
		mutex_exit(&sc->sc_action_mutex);
		umcpmio_dump_buffer(sc->sc_dumpbuffer,
		    (uint8_t *) & i2c_fetch_req, MCP2221_RES_BUFFER_SIZE,
		    "mcp2221_i2c_read fetch res buffer copy");

		if (i2c_fetch_res.cmd != MCP2221_CMD_I2C_FETCH_READ_DATA) {
			err = EIO;
			break;
		}
		if (i2c_fetch_res.completion ==
		    MCP2221_FETCH_READ_PARTIALDATA ||
		    i2c_fetch_res.fetchlen == MCP2221_FETCH_READERROR) {
			DPRINTF(("mcp2221_i2c_read: fetch loop:"
			    " partial data or read error:"
			    " completion=%02x, fetchlen=%02x\n",
			    i2c_fetch_res.completion,
			    i2c_fetch_res.fetchlen));
			WAITMS(sc->sc_busy_delay);
			err = EAGAIN;
			continue;
		}
		if (i2c_fetch_res.internal_i2c_state ==
		    MCP2221_ENGINE_ADDRNACK) {
			DPRINTF(("mcp2221_i2c_read:"
			    " fetch loop: engine NACK\n"));
			err = EIO;
			break;
		}
		if (i2c_fetch_res.internal_i2c_state == 0 &&
		    i2c_fetch_res.fetchlen == 0) {
			DPRINTF(("mcp2221_i2c_read: fetch loop:"
			    " internal state and fetch len are ZERO\n"));
			err = 0;
			break;
		}
		if (i2c_fetch_res.internal_i2c_state ==
		    MCP2221_ENGINE_READPARTIAL ||
		    i2c_fetch_res.internal_i2c_state ==
		    MCP2221_ENGINE_READCOMPLETE) {
			DPRINTF(("mcp2221_i2c_read:"
			    " fetch loop: read partial or"
			    " read complete: internal_i2c_state=%02x\n",
			    i2c_fetch_res.internal_i2c_state));
			err = 0;
			break;
		}
	}
	if (err == EAGAIN)
		err = ETIMEDOUT;
	if (err)
		goto out;

	if (databuf == NULL ||
	    i2c_fetch_res.fetchlen == MCP2221_FETCH_READERROR) {
		DPRINTF(("mcp2221_i2c_read: copy data:"
		    " databuf is NULL\n"));
		goto out;
	}
	const int size = uimin(i2c_fetch_res.fetchlen, datalen);
	DPRINTF(("mcp2221_i2c_read: copy data: size=%d, fetchlen=%d\n",
	    size, i2c_fetch_res.fetchlen));
	if (size > 0) {
		memcpy(databuf, &i2c_fetch_res.data[0], size);
	}
out:
	return err;
}

static int
mcp2221_i2c_exec(void *v, i2c_op_t op, i2c_addr_t addr, const void *cmdbuf,
    size_t cmdlen, void *databuf, size_t datalen, int flags)
{
	struct umcpmio_softc *sc = v;
	size_t totallen = 0;
	int err = 0;

	if (sc->sc_dying)
		return EIO;

	if (addr > 0x7f)
		return ENOTSUP;

	if (cmdbuf != NULL)
		totallen += cmdlen;
	if (databuf != NULL)
		totallen += datalen;

	/* There is a way to do a transfer that is larger than 60 bytes, but it
	 * requires that your break the transfer up into pieces and send them
	 * in 60 byte chunks.  We just won't support that right now. It would
	 * be somewhat unusual for there to be a transfer that big, unless you
	 * are trying to do block transfers and that isn't natively supported
	 * by the chip anyway...  so those have to be broken up and sent as
	 * bytes. */

	if (totallen > 60)
		return ENOTSUP;

	if (I2C_OP_WRITE_P(op)) {
		err = mcp2221_i2c_write(sc, op, addr, cmdbuf, cmdlen,
		    databuf, datalen, flags);

		DPRINTF(("umcpmio_exec: I2C WRITE: err=%d\n", err));
	} else {
		err = mcp2221_i2c_read(sc, op, addr, cmdbuf, cmdlen,
		    databuf, datalen, flags);

		DPRINTF(("umcpmio_exec: I2C READ: err=%d\n", err));
	}

	return err;
}

int
umcpmio_i2c_attach(struct umcpmio_softc *sc)
{
	sc->sc_reportreadnostop = true;
	sc->sc_busy_delay = 1;
	sc->sc_retry_busy_read = 50;
	sc->sc_retry_busy_write = 50;

	int err = 0;

	/* The datasheet suggests that it is possible for this to fail if the
	 * I2C port is currently being used. However...  since you just plugged
	 * in the chip, the I2C port should not really be in use at that
	 * moment. In any case, try hard to set this and don't make it fatal if
	 * it did not get set. */
	int i2cspeed;
	for (i2cspeed = 0; i2cspeed < 3; i2cspeed++) {
		mutex_enter(&sc->sc_action_mutex);
		err = mcp2221_set_i2c_speed_one(sc, I2C_SPEED_SM);
		mutex_exit(&sc->sc_action_mutex);
		if (err) {
			aprint_error_dev(sc->sc_dev, "umcpmio_i2c_attach:"
			    " set I2C speed: err=%d\n",
			    err);
			delay(300);
		}
		break;
	}

	iic_tag_init(&sc->sc_i2c_tag);
	sc->sc_i2c_tag.ic_cookie = sc;
	sc->sc_i2c_tag.ic_acquire_bus = mcp2221_acquire_bus;
	sc->sc_i2c_tag.ic_release_bus = mcp2221_release_bus;
	sc->sc_i2c_tag.ic_exec = mcp2221_i2c_exec;

	sc->sc_i2c_dev = iicbus_attach(sc->sc_dev, &sc->sc_i2c_tag);

	return err;
}
