/*
 * Copyright (c) 2026 Abdelkader Boudih <freebsd@seuros.com>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

/*
 * MMIO backend for Apple SMC (T2 and later Macs).
 *
 * T2 Macs expose the SMC via memory-mapped registers instead of I/O ports.
 * Protocol: clear status, write key/cmd, poll for ready, read result.
 */

#include <sys/param.h>
#include <sys/bus.h>
#include <sys/endian.h>
#include <sys/kernel.h>
#include <sys/lock.h>
#include <sys/mutex.h>
#include <sys/module.h>
#include <sys/rman.h>
#include <sys/sysctl.h>
#include <sys/systm.h>
#include <sys/taskqueue.h>

#include <machine/bus.h>

#include <dev/asmc/asmcvar.h>
#include <dev/asmc/asmcmmio.h>

/*
 * Wait for MMIO status register bit 5 (ready) with exponential backoff.
 * Caller must hold sc_mtx.
 */
static int
asmc_mmio_wait(device_t dev)
{
	struct asmc_softc *sc = device_get_softc(dev);
	int i;
	uint8_t status;
	int delay_us = 10;

	for (i = 0; i < ASMC_MMIO_MAX_WAIT; i++) {
		status = bus_read_1(sc->sc_iomem, ASMC_MMIO_STATUS);
		if (status & ASMC_MMIO_STATUS_READY)
			return (0);
		DELAY(delay_us);
		if (delay_us < 3200)
			delay_us *= 2;
	}

	return (ETIMEDOUT);
}

int
asmc_mmio_key_read(device_t dev, const char *key, uint8_t *buf, uint8_t len)
{
	struct asmc_softc *sc = device_get_softc(dev);
	uint32_t key_int;
	int error, i;
	uint8_t cmd_result, rlen;

	if (len > ASMC_MAXVAL)
		return (EINVAL);

	mtx_lock_spin(&sc->sc_mtx);

	/* Clear status if non-zero */
	if (bus_read_1(sc->sc_iomem, ASMC_MMIO_STATUS))
		bus_write_1(sc->sc_iomem, ASMC_MMIO_STATUS, 0);

	/* Write key name as raw 4 bytes */
	memcpy(&key_int, key, 4);
	bus_write_4(sc->sc_iomem, ASMC_MMIO_KEY_NAME, key_int);

	/* Write SMC ID (always 0) and command */
	bus_write_1(sc->sc_iomem, ASMC_MMIO_SMC_ID, 0);
	bus_write_1(sc->sc_iomem, ASMC_MMIO_CMD, ASMC_CMDREAD);

	/* Wait for ready */
	error = asmc_mmio_wait(dev);
	if (error != 0) {
		uint8_t st = bus_read_1(sc->sc_iomem, ASMC_MMIO_STATUS);
		uint8_t cm = bus_read_1(sc->sc_iomem, ASMC_MMIO_CMD);
		mtx_unlock_spin(&sc->sc_mtx);
		device_printf(dev,
		    "%s: timeout key %.4s status=0x%02x cmd=0x%02x\n",
		    __func__, key, st, cm);
		return (error);
	}

	/* Check command result (0 = success, 0x84 = key not found) */
	cmd_result = bus_read_1(sc->sc_iomem, ASMC_MMIO_CMD);
	if (cmd_result != 0) {
		mtx_unlock_spin(&sc->sc_mtx);
		device_printf(dev,
		    "%s: key %.4s cmd error 0x%02x\n",
		    __func__, key, cmd_result);
		return (EIO);
	}

	/* Read data length and data bytes; zero-fill remainder */
	rlen = bus_read_1(sc->sc_iomem, ASMC_MMIO_DATA_LEN);
	rlen = MIN(rlen, len);
	for (i = rlen; i < len; i++)
		buf[i] = 0;
	for (i = 0; i < rlen; i++)
		buf[i] = bus_read_1(sc->sc_iomem, ASMC_MMIO_DATA + i);

	mtx_unlock_spin(&sc->sc_mtx);
	return (0);
}

int
asmc_mmio_key_write(device_t dev, const char *key, uint8_t *buf, uint8_t len)
{
	struct asmc_softc *sc = device_get_softc(dev);
	uint32_t key_int;
	int error, i;
	uint8_t cmd_result;

	if (len > ASMC_MAXVAL)
		return (EINVAL);

	mtx_lock_spin(&sc->sc_mtx);

	/* Clear status */
	bus_write_1(sc->sc_iomem, ASMC_MMIO_STATUS, 0);

	/* Write data bytes first */
	for (i = 0; i < len; i++)
		bus_write_1(sc->sc_iomem, ASMC_MMIO_DATA + i, buf[i]);

	/* Write key name as raw 4 bytes */
	memcpy(&key_int, key, 4);
	bus_write_4(sc->sc_iomem, ASMC_MMIO_KEY_NAME, key_int);

	/* Write length, SMC ID, command */
	bus_write_1(sc->sc_iomem, ASMC_MMIO_DATA_LEN, len);
	bus_write_1(sc->sc_iomem, ASMC_MMIO_SMC_ID, 0);
	bus_write_1(sc->sc_iomem, ASMC_MMIO_CMD, ASMC_CMDWRITE);

	/* Wait for ready */
	error = asmc_mmio_wait(dev);
	if (error != 0) {
		mtx_unlock_spin(&sc->sc_mtx);
		device_printf(dev, "%s: timeout writing key %.4s\n",
		    __func__, key);
		return (error);
	}

	cmd_result = bus_read_1(sc->sc_iomem, ASMC_MMIO_CMD);
	mtx_unlock_spin(&sc->sc_mtx);

	return (cmd_result == 0 ? 0 : EIO);
}

int
asmc_mmio_key_getinfo(device_t dev, const char *key, uint8_t *len, char *type)
{
	struct asmc_softc *sc = device_get_softc(dev);
	uint32_t key_int;
	int error, i;
	uint8_t cmd_result;

	mtx_lock_spin(&sc->sc_mtx);

	/* Clear status */
	bus_write_1(sc->sc_iomem, ASMC_MMIO_STATUS, 0);

	/* Write key name as raw 4 bytes */
	memcpy(&key_int, key, 4);
	bus_write_4(sc->sc_iomem, ASMC_MMIO_KEY_NAME, key_int);

	bus_write_1(sc->sc_iomem, ASMC_MMIO_SMC_ID, 0);
	bus_write_1(sc->sc_iomem, ASMC_MMIO_CMD, ASMC_CMDGETINFO);

	error = asmc_mmio_wait(dev);
	if (error != 0) {
		mtx_unlock_spin(&sc->sc_mtx);
		return (error);
	}

	cmd_result = bus_read_1(sc->sc_iomem, ASMC_MMIO_CMD);
	if (cmd_result != 0) {
		mtx_unlock_spin(&sc->sc_mtx);
		return (EIO);
	}

	/*
	 * GETINFO response layout (MMIO):
	 *   data[0..3] = type code (4 chars)
	 *   data[4]    = reserved
	 *   data[5]    = data length
	 *   data[6]    = flags/attributes
	 */
	if (type != NULL) {
		for (i = 0; i < ASMC_TYPELEN; i++)
			type[i] = bus_read_1(sc->sc_iomem,
			    ASMC_MMIO_DATA + i);
		type[ASMC_TYPELEN] = '\0';
	}
	if (len != NULL)
		*len = bus_read_1(sc->sc_iomem, ASMC_MMIO_DATA + 5);

	mtx_unlock_spin(&sc->sc_mtx);
	return (0);
}

int
asmc_mmio_key_getbyindex(device_t dev, int index, char *key)
{
	struct asmc_softc *sc = device_get_softc(dev);
	uint32_t idx_val;
	int error, i;
	uint8_t cmd_result;

	mtx_lock_spin(&sc->sc_mtx);

	bus_write_1(sc->sc_iomem, ASMC_MMIO_STATUS, 0);

	/* Write index as big-endian 4 bytes to key name register */
	idx_val = htobe32(index);
	bus_write_4(sc->sc_iomem, ASMC_MMIO_KEY_NAME, idx_val);

	bus_write_1(sc->sc_iomem, ASMC_MMIO_SMC_ID, 0);
	bus_write_1(sc->sc_iomem, ASMC_MMIO_CMD, ASMC_CMDGETBYINDEX);

	error = asmc_mmio_wait(dev);
	if (error != 0) {
		mtx_unlock_spin(&sc->sc_mtx);
		return (error);
	}

	cmd_result = bus_read_1(sc->sc_iomem, ASMC_MMIO_CMD);
	if (cmd_result != 0) {
		mtx_unlock_spin(&sc->sc_mtx);
		return (EIO);
	}

	/* Result: 4-byte key name in DATA */
	for (i = 0; i < ASMC_KEYLEN; i++)
		key[i] = bus_read_1(sc->sc_iomem, ASMC_MMIO_DATA + i);
	key[ASMC_KEYLEN] = '\0';

	mtx_unlock_spin(&sc->sc_mtx);
	return (0);
}

/*
 * Validate MMIO and detect T2.
 * Check that status register is accessible and LDKN firmware version >= 2.
 */
int
asmc_mmio_probe(device_t dev)
{
	struct asmc_softc *sc = device_get_softc(dev);
	rman_res_t size;
	uint8_t status, ldkn;
	int error;

	size = rman_get_size(sc->sc_iomem);
	if (size < ASMC_MMIO_MIN_SIZE) {
		device_printf(dev, "MMIO region too small (%jd < %d)\n",
		    (intmax_t)size, ASMC_MMIO_MIN_SIZE);
		return (ENXIO);
	}

	/* Check status register isn't stuck at 0xFF */
	status = bus_read_1(sc->sc_iomem, ASMC_MMIO_STATUS);
	if (status == 0xFF) {
		device_printf(dev, "MMIO status register reads 0xFF\n");
		return (ENXIO);
	}

	/*
	 * We need the mutex initialized before calling mmio_key_read,
	 * but attach hasn't done it yet. Initialize early.
	 */
	if (!mtx_initialized(&sc->sc_mtx))
		mtx_init(&sc->sc_mtx, "asmc", NULL, MTX_SPIN);

	/* Read LDKN (firmware version) -- must be >= 2 for MMIO */
	error = asmc_mmio_key_read(dev, ASMC_KEY_LDKN, &ldkn, 1);
	if (error != 0) {
		device_printf(dev, "MMIO: failed to read LDKN key\n");
		return (ENXIO);
	}

	if (ldkn < 2) {
		device_printf(dev, "MMIO: LDKN=%d (need >= 2)\n", ldkn);
		return (ENXIO);
	}

	device_printf(dev, "MMIO: LDKN=%d, T2 SMC detected\n", ldkn);
	sc->sc_is_t2 = 1;

	return (0);
}

void
asmc_mmio_detach(device_t dev, struct asmc_softc *sc)
{

	if (sc->sc_iomem != NULL) {
		bus_release_resource(dev, SYS_RES_MEMORY, sc->sc_rid_mem,
		    sc->sc_iomem);
		sc->sc_iomem = NULL;
	}
	sc->sc_is_mmio = 0;
	sc->sc_is_t2 = 0;
}

/*
 * Convert IEEE 754 float (as u32) to unsigned integer.
 * Kernel soft-float: extract integer part only.
 * Used for T2 fan RPM values (always positive, reasonable range).
 */
uint32_t
asmc_float_to_u32(uint32_t d)
{
	int32_t exp;
	uint32_t fr;

	/* Negative or zero */
	if (d == 0 || (d >> 31) != 0)
		return (0);

	exp = (int32_t)((d >> 23) & 0xff) - 0x7f;
	fr = d & 0x7fffff;	/* 23-bit mantissa */

	if (exp < 0)
		return (0);
	if (exp > 23) {
		if (exp > 30)
			return (0xffffffffu);
		return ((1u << exp) | (fr << (exp - 23)));
	}
	/* Normal case: 0 <= exp <= 23 */
	return ((1u << exp) + (fr >> (23 - exp)));
}

/*
 * Convert unsigned integer to IEEE 754 float (as u32).
 * Only handles values in fan RPM range (0-65535).
 */
uint32_t
asmc_u32_to_float(uint32_t d)
{
	uint32_t dc, bc, exp;

	if (d == 0)
		return (0);

	/* Find highest set bit position */
	dc = d;
	bc = 0;
	while (dc >>= 1)
		++bc;

	bc = MIN(bc, 30);

	exp = 0x7f + bc;

	/*
	 * Mantissa: strip the implicit leading 1-bit and place
	 * remaining bits into the 23-bit mantissa field.
	 */
	if (bc >= 23)
		return ((exp << 23) | ((d >> (bc - 23)) & 0x7fffff));
	else
		return ((exp << 23) | ((d << (23 - bc)) & 0x7fffff));
}

/*
 * Battery charge limit sysctl (T2 Macs).
 * BCLM key: 1 byte, 0-100 (percentage).
 */
int
asmc_bclm_sysctl(SYSCTL_HANDLER_ARGS)
{
	device_t dev = (device_t)arg1;
	uint8_t bclm;
	int val, error;

	error = asmc_mmio_key_read(dev, ASMC_KEY_BCLM, &bclm, 1);
	if (error != 0)
		return (EIO);

	val = (int)bclm;
	error = sysctl_handle_int(oidp, &val, 0, req);
	if (error != 0 || req->newptr == NULL)
		return (error);

	if (val < 0 || val > 100)
		return (EINVAL);

	bclm = (uint8_t)val;
	error = asmc_mmio_key_write(dev, ASMC_KEY_BCLM, &bclm, 1);

	return (error != 0 ? EIO : 0);
}
