/*-
 * Copyright (c) 2018 Stormshield.
 * Copyright (c) 2018 Semihalf.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/cdefs.h>
#include "tpm20.h"

/*
 * CRB register space as defined in
 * TCG_PC_Client_Platform_TPM_Profile_PTP_2.0_r1.03_v22
 */
#define	TPM_LOC_STATE			0x0
#define	TPM_LOC_CTRL			0x8
#define	TPM_LOC_STS			0xC
#define	TPM_CRB_INTF_ID			0x30
#define	TPM_CRB_CTRL_EXT		0x38
#define	TPM_CRB_CTRL_REQ		0x40
#define	TPM_CRB_CTRL_STS		0x44
#define	TPM_CRB_CTRL_CANCEL		0x48
#define	TPM_CRB_CTRL_START		0x4C
#define	TPM_CRB_INT_ENABLE		0x50
#define	TPM_CRB_INT_STS			0x54
#define	TPM_CRB_CTRL_CMD_SIZE		0x58
#define	TPM_CRB_CTRL_CMD_LADDR		0x5C
#define	TPM_CRB_CTRL_CMD_HADDR		0x60
#define	TPM_CRB_CTRL_RSP_SIZE		0x64
#define	TPM_CRB_CTRL_RSP_ADDR		0x68
#define	TPM_CRB_CTRL_RSP_HADDR		0x6c
#define	TPM_CRB_DATA_BUFFER		0x80

#define	TPM_LOC_STATE_ESTB		BIT(0)
#define	TPM_LOC_STATE_ASSIGNED		BIT(1)
#define	TPM_LOC_STATE_ACTIVE_MASK	0x9C
#define	TPM_LOC_STATE_VALID		BIT(7)

#define	TPM_CRB_INTF_ID_TYPE_CRB	0x1
#define	TPM_CRB_INTF_ID_TYPE		0x7

#define	TPM_LOC_CTRL_REQUEST		BIT(0)
#define	TPM_LOC_CTRL_RELINQUISH		BIT(1)

#define	TPM_CRB_CTRL_REQ_GO_READY	BIT(0)
#define	TPM_CRB_CTRL_REQ_GO_IDLE	BIT(1)

#define	TPM_CRB_CTRL_STS_ERR_BIT	BIT(0)
#define	TPM_CRB_CTRL_STS_IDLE_BIT	BIT(1)

#define	TPM_CRB_CTRL_CANCEL_CMD		0x1
#define	TPM_CRB_CTRL_CANCEL_CLEAR	0x0

#define	TPM_CRB_CTRL_START_CMD		BIT(0)

#define	TPM_CRB_INT_ENABLE_BIT		BIT(31)

struct tpmcrb_sc;
/* Attach */
typedef bool (sm_attach_t)(struct tpmcrb_sc *, void *, size_t);
/* State change notification (timeout == 0 for 'no timeout') */
typedef bool (sm_statechange_t)(struct tpmcrb_sc *, int);

struct tpmcrb_sm_cfg {
	sm_attach_t		*sm_attach;
	sm_statechange_t	*sm_statechange;
	sm_statechange_t	*sm_cmdready;
};

static sm_attach_t		pluton_attach;
static sm_statechange_t		pluton_doorbell;

static const struct tpmcrb_sm_cfg_map {
	int				acpi_sm;
	const char			*desc;
	const struct tpmcrb_sm_cfg	sm_cfg;
} tpmcrb_sm_cfg_map[] = {
	{
		.acpi_sm = TPM2_START_METHOD_CRB,
		.desc = "Trusted Platform Module 2.0, CRB mode",
		.sm_cfg = { NULL },	/* No notifications required */
	},
	{
		.acpi_sm = ACPI_TPM2_COMMAND_BUFFER_WITH_PLUTON,
		.desc = "Trusted Platform Module 2.0, CRB mode (Pluton)",
		.sm_cfg = {
			.sm_attach = &pluton_attach,
			.sm_statechange = &pluton_doorbell,
			.sm_cmdready = &pluton_doorbell,
		},
	},
};

struct tpmcrb_sc {
	struct tpm_sc	base;
	const struct tpmcrb_sm_cfg	*sm_cfg;
	union {
		/* StartMethod data */
		struct {
			uint64_t	 start_reg;
			uint64_t	 reply_reg;
		} pluton;
	};
	bus_size_t	cmd_off;
	bus_size_t	rsp_off;
	size_t		cmd_buf_size;
	size_t		rsp_buf_size;
};

int tpmcrb_transmit(device_t dev, size_t size);

static int tpmcrb_acpi_probe(device_t dev);
static int tpmcrb_attach(device_t dev);
static int tpmcrb_detach(device_t dev);

static ACPI_STATUS tpmcrb_fix_buff_offsets(ACPI_RESOURCE *res, void *arg);

static bool tpm_wait_for_u32(struct tpm_sc *sc, bus_size_t off,
    uint32_t mask, uint32_t val, int32_t timeout);
static bool tpmcrb_request_locality(struct tpm_sc *sc, int locality);
static void tpmcrb_relinquish_locality(struct tpm_sc *sc);
static bool tpmcrb_cancel_cmd(struct tpm_sc *sc);

char *tpmcrb_ids[] = {"MSFT0101", NULL};

static const struct tpmcrb_sm_cfg_map *
tpmcrb_acpi_startmethod_cfg(int method)
{
	const struct tpmcrb_sm_cfg_map *entry;

	for (size_t i = 0; i < nitems(tpmcrb_sm_cfg_map); i++) {
		entry = &tpmcrb_sm_cfg_map[i];

		if (method == entry->acpi_sm)
			return (entry);
	}

	return (NULL);
}

static int
tpmcrb_acpi_probe(device_t dev)
{
	int err, smethod;
	const struct tpmcrb_sm_cfg_map *sm_cfg_map;
	ACPI_TABLE_TPM23 *tbl;
	ACPI_STATUS status;

	err = ACPI_ID_PROBE(device_get_parent(dev), dev, tpmcrb_ids, NULL);
	if (err > 0)
		return (err);
	/*Find TPM2 Header*/
	status = AcpiGetTable(ACPI_SIG_TPM2, 1, (ACPI_TABLE_HEADER **) &tbl);
	if (ACPI_FAILURE(status))
		return (ENXIO);

	smethod = tbl->StartMethod;
	AcpiPutTable((ACPI_TABLE_HEADER *)tbl);

	sm_cfg_map = tpmcrb_acpi_startmethod_cfg(smethod);
	if (sm_cfg_map == NULL)
		return (ENXIO);

	device_set_desc(dev, sm_cfg_map->desc);
	return (0);
}

static ACPI_STATUS
tpmcrb_fix_buff_offsets(ACPI_RESOURCE *res, void *arg)
{
	struct tpmcrb_sc *crb_sc;
	size_t length;
	uint32_t base_addr;

	crb_sc = (struct tpmcrb_sc *)arg;

	if (res->Type != ACPI_RESOURCE_TYPE_FIXED_MEMORY32)
		return (AE_OK);

	base_addr = res->Data.FixedMemory32.Address;
	length = res->Data.FixedMemory32.AddressLength;

	if (crb_sc->cmd_off > base_addr && crb_sc->cmd_off < base_addr + length)
		crb_sc->cmd_off -= base_addr;
	if (crb_sc->rsp_off > base_addr && crb_sc->rsp_off < base_addr + length)
		crb_sc->rsp_off -= base_addr;

	return (AE_OK);
}

static bool
tpmcrb_attach_startmethod(struct tpmcrb_sc *crb_sc)
{
	const struct tpmcrb_sm_cfg_map *sm_cfg_map;
	const struct tpmcrb_sm_cfg *sm_cfg;
	ACPI_TABLE_TPM23 *tbl;
	void *smdata;
	ACPI_STATUS status;
	bool ret;

	/*
	 * Grab what we need from the StartMethod.
	 */
	status = AcpiGetTable(ACPI_SIG_TPM2, 1, (ACPI_TABLE_HEADER **)(void **)&tbl);
	if (ACPI_FAILURE(status))
		return (false);

	sm_cfg_map = tpmcrb_acpi_startmethod_cfg(tbl->StartMethod);
	MPASS(sm_cfg_map != NULL);
	sm_cfg = &sm_cfg_map->sm_cfg;

	crb_sc->sm_cfg = sm_cfg;
	smdata = tbl + 1;
	if (sm_cfg->sm_attach != NULL) {
		ret = (*sm_cfg->sm_attach)(crb_sc, smdata,
		    tbl->Header.Length - sizeof(*tbl));
	} else {
		ret = true;
	}

	AcpiPutTable((ACPI_TABLE_HEADER *)tbl);
	return (ret);
}

static int
tpmcrb_attach(device_t dev)
{
	struct tpmcrb_sc *crb_sc;
	struct tpm_sc *sc;
	ACPI_HANDLE handle;
	ACPI_STATUS status;
	int result;

	crb_sc = device_get_softc(dev);
	sc = &crb_sc->base;
	handle = acpi_get_handle(dev);
	sc->dev = dev;

	sx_init(&sc->dev_lock, "TPM driver lock");
	sc->buf = malloc(TPM_BUFSIZE, M_TPM20, M_WAITOK);

	sc->mem_rid = 0;
	sc->mem_res = bus_alloc_resource_any(dev, SYS_RES_MEMORY, &sc->mem_rid,
					     RF_ACTIVE);
	if (sc->mem_res == NULL) {
		tpmcrb_detach(dev);
		return (ENXIO);
	}

	if (!tpmcrb_attach_startmethod(crb_sc)) {
		tpmcrb_detach(dev);
		return (ENXIO);
	}

	if(!tpmcrb_request_locality(sc, 0)) {
		tpmcrb_detach(dev);
		return (ENXIO);
	}

	/*
	 * Disable all interrupts for now, since I don't have a device that
	 * works in CRB mode and supports them.
	 */
	AND4(sc, TPM_CRB_INT_ENABLE, ~TPM_CRB_INT_ENABLE_BIT);
	sc->interrupts = false;

	/*
	 * Read addresses of Tx/Rx buffers and their sizes. Note that they
	 * can be implemented by a single buffer. Also for some reason CMD
	 * addr is stored in two 4 byte neighboring registers, whereas RSP is
	 * stored in a single 8 byte one.
	 */
#ifdef __amd64__
	crb_sc->rsp_off = TPM_READ_8(sc->dev, TPM_CRB_CTRL_RSP_ADDR);
#else
	crb_sc->rsp_off = TPM_READ_4(sc->dev, TPM_CRB_CTRL_RSP_ADDR);
	crb_sc->rsp_off |= ((uint64_t) TPM_READ_4(sc->dev, TPM_CRB_CTRL_RSP_HADDR) << 32);
#endif
	crb_sc->cmd_off = TPM_READ_4(sc->dev, TPM_CRB_CTRL_CMD_LADDR);
	crb_sc->cmd_off |= ((uint64_t) TPM_READ_4(sc->dev, TPM_CRB_CTRL_CMD_HADDR) << 32);
	crb_sc->cmd_buf_size = TPM_READ_4(sc->dev, TPM_CRB_CTRL_CMD_SIZE);
	crb_sc->rsp_buf_size = TPM_READ_4(sc->dev, TPM_CRB_CTRL_RSP_SIZE);

	tpmcrb_relinquish_locality(sc);

	/* Emulator returns address in acpi space instead of an offset */
	status = AcpiWalkResources(handle, "_CRS", tpmcrb_fix_buff_offsets,
		    (void *)crb_sc);
	if (ACPI_FAILURE(status)) {
		tpmcrb_detach(dev);
		return (ENXIO);
	}

	if (crb_sc->rsp_off == crb_sc->cmd_off) {
		/*
		 * If Tx/Rx buffers are implemented as one they have to be of
		 * same size
		 */
		if (crb_sc->cmd_buf_size != crb_sc->rsp_buf_size) {
			device_printf(sc->dev,
			    "Overlapping Tx/Rx buffers have different sizes\n");
			tpmcrb_detach(dev);
			return (ENXIO);
		}
	}

	result = tpm20_init(sc);
	if (result != 0)
		tpmcrb_detach(dev);

	return (result);
}

static int
tpmcrb_detach(device_t dev)
{
	struct tpm_sc *sc;

	sc = device_get_softc(dev);
	tpm20_release(sc);

	if (sc->mem_res != NULL)
		bus_release_resource(dev, SYS_RES_MEMORY,
		    sc->mem_rid, sc->mem_res);

	return (0);
}

static bool
tpm_wait_for_u32(struct tpm_sc *sc, bus_size_t off, uint32_t mask, uint32_t val,
    int32_t timeout)
{

	/* Check for condition */
	if ((TPM_READ_4(sc->dev, off) & mask) == val)
		return (true);

	while (timeout > 0) {
		if ((TPM_READ_4(sc->dev, off) & mask) == val)
			return (true);

		pause("TPM in polling mode", 1);
		timeout -= tick;
	}
	return (false);
}

static bool
tpmcrb_request_locality(struct tpm_sc *sc, int locality)
{
	uint32_t mask;

	/* Currently we only support Locality 0 */
	if (locality != 0)
		return (false);

	mask = TPM_LOC_STATE_VALID | TPM_LOC_STATE_ASSIGNED;

	OR4(sc, TPM_LOC_CTRL, TPM_LOC_CTRL_REQUEST);
	if (!tpm_wait_for_u32(sc, TPM_LOC_STATE, mask, mask, TPM_TIMEOUT_C))
		return (false);

	return (true);
}

static void
tpmcrb_relinquish_locality(struct tpm_sc *sc)
{

	OR4(sc, TPM_LOC_CTRL, TPM_LOC_CTRL_RELINQUISH);
}

static bool
tpmcrb_cancel_cmd(struct tpm_sc *sc)
{
	uint32_t mask = ~0;

	TPM_WRITE_4(sc->dev, TPM_CRB_CTRL_CANCEL, TPM_CRB_CTRL_CANCEL_CMD);
	if (!tpm_wait_for_u32(sc, TPM_CRB_CTRL_START,
		    mask, ~mask, TPM_TIMEOUT_B)) {
		device_printf(sc->dev,
		    "Device failed to cancel command\n");
		return (false);
	}

	TPM_WRITE_4(sc->dev, TPM_CRB_CTRL_CANCEL, TPM_CRB_CTRL_CANCEL_CLEAR);
	return (true);
}

static bool
tpmcrb_notify_cmdready(struct tpmcrb_sc *crb_sc, int timeout)
{
	sm_statechange_t *cmdready_fn;

	cmdready_fn = crb_sc->sm_cfg->sm_cmdready;
	if (cmdready_fn == NULL)
		return (true);

	return ((*cmdready_fn)(crb_sc, timeout));
}

static bool
tpmcrb_notify_state_changing(struct tpmcrb_sc *crb_sc, int timeout)
{
	sm_statechange_t *statechange_fn;

	statechange_fn = crb_sc->sm_cfg->sm_statechange;
	if (statechange_fn == NULL)
		return (true);

	return ((*statechange_fn)(crb_sc, timeout));
}

static bool
tpmcrb_state_idle(struct tpmcrb_sc *crb_sc, bool wait)
{
	struct tpm_sc *sc;
	int mask, timeout;

	timeout = wait ? TPM_TIMEOUT_C : 0;

	sc = &crb_sc->base;
	OR4(sc, TPM_CRB_CTRL_REQ, TPM_CRB_CTRL_REQ_GO_IDLE);

	if (!tpmcrb_notify_state_changing(crb_sc, timeout))
		return (false);

	if (timeout > 0) {
		mask = TPM_CRB_CTRL_STS_IDLE_BIT;
		if (!tpm_wait_for_u32(sc, TPM_CRB_CTRL_STS, mask, mask,
		    timeout))
			return (false);
	}

	return (true);
}

static bool
tpmcrb_state_ready(struct tpmcrb_sc *crb_sc, bool wait)
{
	struct tpm_sc *sc;
	int mask, timeout;

	timeout = wait ? TPM_TIMEOUT_C : 0;

	sc = &crb_sc->base;
	OR4(sc, TPM_CRB_CTRL_REQ, TPM_CRB_CTRL_REQ_GO_READY);

	if (!tpmcrb_notify_state_changing(crb_sc, timeout))
		return (false);

	if (timeout > 0) {
		mask = TPM_CRB_CTRL_REQ_GO_READY;
		if (!tpm_wait_for_u32(sc, TPM_CRB_CTRL_STS, mask, !mask,
		    timeout))
			return (false);
	}

	return (true);
}

int
tpmcrb_transmit(device_t dev, size_t length)
{
	struct tpmcrb_sc *crb_sc;
	struct tpm_sc *sc;
	uint32_t mask, curr_cmd;
	int timeout, bytes_available;

	crb_sc = device_get_softc(dev);
	sc = &crb_sc->base;

	sx_assert(&sc->dev_lock, SA_XLOCKED);

	if (length > crb_sc->cmd_buf_size) {
		device_printf(dev,
		    "Requested transfer is bigger than buffer size\n");
		return (E2BIG);
	}

	if (TPM_READ_4(dev, TPM_CRB_CTRL_STS) & TPM_CRB_CTRL_STS_ERR_BIT) {
		device_printf(dev,
		    "Device has Error bit set\n");
		return (EIO);
	}
	if (!tpmcrb_request_locality(sc, 0)) {
		device_printf(dev,
		    "Failed to obtain locality\n");
		return (EIO);
	}
	/* Clear cancellation bit */
	TPM_WRITE_4(dev, TPM_CRB_CTRL_CANCEL, TPM_CRB_CTRL_CANCEL_CLEAR);

	/* Switch device to idle state if necessary */
	if (!(TPM_READ_4(dev, TPM_CRB_CTRL_STS) & TPM_CRB_CTRL_STS_IDLE_BIT)) {
		if (!tpmcrb_state_idle(crb_sc, true)) {
			device_printf(dev,
			    "Failed to transition to idle state\n");
			return (EIO);
		}
	}

	/* Switch to ready state */
	if (!tpmcrb_state_ready(crb_sc, true)) {
		device_printf(dev,
		    "Failed to transition to ready state\n");
		return (EIO);
	}

	/*
	 * Calculate timeout for current command.
	 * Command code is passed in bytes 6-10.
	 */
	curr_cmd = be32toh(*(uint32_t *) (&sc->buf[6]));
	timeout = tpm20_get_timeout(curr_cmd);

	/* Send command and tell device to process it. */
	bus_write_region_stream_1(sc->mem_res, crb_sc->cmd_off,
	    sc->buf, length);
	TPM_WRITE_BARRIER(dev, crb_sc->cmd_off, length);

	TPM_WRITE_4(dev, TPM_CRB_CTRL_START, TPM_CRB_CTRL_START_CMD);
	TPM_WRITE_BARRIER(dev, TPM_CRB_CTRL_START, 4);

	if (!tpmcrb_notify_cmdready(crb_sc, timeout)) {
		device_printf(dev,
		    "Timeout while waiting for device to ready\n");
		if (!tpmcrb_cancel_cmd(sc))
			return (EIO);
	}

	mask = ~0;
	if (!tpm_wait_for_u32(sc, TPM_CRB_CTRL_START, mask, ~mask, timeout)) {
		device_printf(dev,
		    "Timeout while waiting for device to process cmd\n");
		if (!tpmcrb_cancel_cmd(sc))
			return (EIO);
	}

	/* Read response header. Length is passed in bytes 2 - 6. */
	bus_read_region_stream_1(sc->mem_res, crb_sc->rsp_off,
	    sc->buf, TPM_HEADER_SIZE);
	bytes_available = be32toh(*(uint32_t *) (&sc->buf[2]));

	if (bytes_available > TPM_BUFSIZE || bytes_available < TPM_HEADER_SIZE) {
		device_printf(dev,
		    "Incorrect response size: %d\n",
		    bytes_available);
		return (EIO);
	}

	bus_read_region_stream_1(sc->mem_res, crb_sc->rsp_off + TPM_HEADER_SIZE,
	      &sc->buf[TPM_HEADER_SIZE], bytes_available - TPM_HEADER_SIZE);

	/*
	 * No need to wait for the transition to idle on the way out, we can
	 * relinquish locality right away.
	 */
	if (!tpmcrb_state_idle(crb_sc, false)) {
		device_printf(dev,
		    "Failed to transition to idle state post-send\n");
		return (EIO);
	}

	tpmcrb_relinquish_locality(sc);
	sc->pending_data_length = bytes_available;
	sc->total_length = bytes_available;

	return (0);
}

/* StartMethod Implementation Details */

/** Pluton **/
struct tpmcrb_startmethod_pluton {
	uint64_t		sm_startaddr;
	uint64_t		sm_replyaddr;
};

static bool
pluton_attach(struct tpmcrb_sc *crb_sc, void *smdataregion, size_t datasz)
{
	struct tpmcrb_startmethod_pluton *smdata;
	struct tpm_sc *sc;
	rman_res_t base_addr, end_addr;

	if (datasz < sizeof(*smdata))
		return (false);

	smdata = smdataregion;
	sc = &crb_sc->base;

	base_addr = rman_get_start(sc->mem_res);
	end_addr = rman_get_end(sc->mem_res);
	/* Sanity check */
	if (smdata->sm_startaddr < base_addr ||
	    smdata->sm_startaddr > end_addr ||
	    smdata->sm_replyaddr < base_addr ||
	    smdata->sm_replyaddr > end_addr)
		return (false);

	crb_sc->pluton.start_reg = smdata->sm_startaddr - base_addr;
	crb_sc->pluton.reply_reg = smdata->sm_replyaddr - base_addr;
	return (true);
}

static bool
pluton_doorbell(struct tpmcrb_sc *crb_sc, int timeout)
{
	struct tpm_sc *sc;
	device_t dev;

	sc = &crb_sc->base;
	dev = sc->dev;
	TPM_WRITE_4(dev, crb_sc->pluton.start_reg, 1);
	TPM_WRITE_BARRIER(dev, crb_sc->pluton.start_reg, 4);

	if (timeout > 0) {
		if (!tpm_wait_for_u32(sc, crb_sc->pluton.reply_reg, ~0U, 1,
		    timeout))
			return (false);
	}

	return (true);
}

/* ACPI Driver */
static device_method_t	tpmcrb_methods[] = {
	DEVMETHOD(device_probe,		tpmcrb_acpi_probe),
	DEVMETHOD(device_attach,	tpmcrb_attach),
	DEVMETHOD(device_detach,	tpmcrb_detach),
	DEVMETHOD(device_shutdown,	tpm20_shutdown),
	DEVMETHOD(device_suspend,	tpm20_suspend),
	DEVMETHOD(device_resume,	tpm20_resume),
	DEVMETHOD(tpm_transmit,		tpmcrb_transmit),
	{0, 0}
};

DEFINE_CLASS_1(tpmcrb, tpmcrb_driver, tpmcrb_methods, sizeof(struct tpmcrb_sc),
    tpm_bus_driver);

DRIVER_MODULE(tpmcrb, acpi, tpmcrb_driver, 0, 0);
