/*
 * Copyright (c) 2022 Intel Corporation
 *
 * Intel I/O Controller Hub (ICH) later renamed to Intel Platform Controller
 * Hub (PCH) SMBus driver.
 *
 * PCH provides SMBus 2.0 - compliant Host Controller.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <zephyr/types.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>

#include <zephyr/drivers/smbus.h>
#include <zephyr/drivers/pcie/pcie.h>

#define DT_DRV_COMPAT intel_pch_smbus

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(intel_pch, CONFIG_SMBUS_LOG_LEVEL);

#include "smbus_utils.h"
#include "intel_pch_smbus.h"

/**
 * @note Following notions are used:
 *  * periph_addr - Peripheral address (Slave address mentioned in the Specs)
 *  * command - First byte to send in the SMBus protocol operations except for
 *              Quick and Byte Read. Also known as register.
 */

/**
 * Intel PCH configuration acquired from DTS during device initialization
 */
struct pch_config {
	/* IRQ configuration function */
	void (*config_func)(const struct device *dev);
	struct pcie_dev *pcie;
};

/**
 * Intel PCH internal driver data
 */
struct pch_data {
	DEVICE_MMIO_RAM;
	io_port_t sba;
	uint32_t config;
	uint8_t status;

	struct k_mutex mutex;
	struct k_sem completion_sync;
	const struct device *dev;

#if defined(CONFIG_SMBUS_INTEL_PCH_SMBALERT)
	/* smbalert callback list */
	sys_slist_t smbalert_cbs;
	/* smbalert work */
	struct k_work smb_alert_work;
#endif /* CONFIG_SMBUS_INTEL_PCH_SMBALERT */

#if defined(CONFIG_SMBUS_INTEL_PCH_HOST_NOTIFY)
	/* Host Notify callback list */
	sys_slist_t host_notify_cbs;
	/* Host Notify work */
	struct k_work host_notify_work;
	/* Host Notify peripheral device address */
	uint8_t notify_addr;
	/* Host Notify data received */
	uint16_t notify_data;
#endif /* CONFIG_SMBUS_INTEL_PCH_HOST_NOTIFY */
};

/**
 * Helpers for accessing Intel PCH SMBus registers. Depending on
 * configuration option MMIO or IO method will be used.
 */
#if defined(CONFIG_SMBUS_INTEL_PCH_ACCESS_MMIO)
static uint8_t pch_reg_read(const struct device *dev, uint8_t reg)
{
	return sys_read8(DEVICE_MMIO_GET(dev) + reg);
}

static void pch_reg_write(const struct device *dev, uint8_t reg, uint8_t val)
{
	sys_write8(val, DEVICE_MMIO_GET(dev) + reg);
}
#elif defined(CONFIG_SMBUS_INTEL_PCH_ACCESS_IO)
static uint8_t pch_reg_read(const struct device *dev, uint8_t reg)
{
	struct pch_data *data = dev->data;

	return sys_in8(data->sba + reg);
}

static void pch_reg_write(const struct device *dev, uint8_t reg, uint8_t val)
{
	struct pch_data *data = dev->data;

	sys_out8(val, data->sba + reg);
}
#else
#error Wrong PCH Register Access Mode
#endif

#if defined(CONFIG_SMBUS_INTEL_PCH_HOST_NOTIFY)
static void host_notify_work(struct k_work *work)
{
	struct pch_data *data = CONTAINER_OF(work, struct pch_data,
					     host_notify_work);
	const struct device *dev = data->dev;
	uint8_t addr = data->notify_addr;

	smbus_fire_callbacks(&data->host_notify_cbs, dev, addr);
}

static int pch_smbus_host_notify_set_cb(const struct device *dev,
					struct smbus_callback *cb)
{
	struct pch_data *data = dev->data;

	LOG_DBG("dev %p cb %p", dev, cb);

	return smbus_callback_set(&data->host_notify_cbs, cb);
}

static int pch_smbus_host_notify_remove_cb(const struct device *dev,
					   struct smbus_callback *cb)
{
	struct pch_data *data = dev->data;

	LOG_DBG("dev %p cb %p", dev, cb);

	return smbus_callback_remove(&data->host_notify_cbs, cb);
}
#endif /* CONFIG_SMBUS_INTEL_PCH_HOST_NOTIFY */

#if defined(CONFIG_SMBUS_INTEL_PCH_SMBALERT)
static void smbalert_work(struct k_work *work)
{
	struct pch_data *data = CONTAINER_OF(work, struct pch_data,
					     smb_alert_work);
	const struct device *dev = data->dev;

	/**
	 * There might be several peripheral devices and the he highest
	 * priority (lowest address) device wins arbitration, we need to
	 * read them all.
	 *
	 * The format of the transaction is:
	 *
	 *  0                   1                   2
	 *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
	 *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
	 *  |S|  Alert Addr |R|A|   Address   |X|N|P|
	 *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
	 */
	do {
		uint8_t addr;
		int ret;

		ret = smbus_byte_read(dev, SMBUS_ADDRESS_ARA, &addr);
		if (ret < 0) {
			LOG_DBG("Cannot read peripheral address (anymore)");
			return;
		}

		LOG_DBG("Read addr 0x%02x, ret %d", addr, ret);

		smbus_fire_callbacks(&data->smbalert_cbs, dev, addr);
	} while (true);
}

static int pch_smbus_smbalert_set_sb(const struct device *dev,
				     struct smbus_callback *cb)
{
	struct pch_data *data = dev->data;

	LOG_DBG("dev %p cb %p", dev, cb);

	return smbus_callback_set(&data->smbalert_cbs, cb);
}

static int pch_smbus_smbalert_remove_sb(const struct device *dev,
					struct smbus_callback *cb)
{
	struct pch_data *data = dev->data;

	LOG_DBG("dev %p cb %p", dev, cb);

	return smbus_callback_remove(&data->smbalert_cbs, cb);
}
#endif /* CONFIG_SMBUS_INTEL_PCH_SMBALERT */

static int pch_configure(const struct device *dev, uint32_t config)
{
	struct pch_data *data = dev->data;

	LOG_DBG("dev %p config 0x%x", dev, config);

	if (config & SMBUS_MODE_HOST_NOTIFY) {
		uint8_t status;

		if (!IS_ENABLED(CONFIG_SMBUS_INTEL_PCH_HOST_NOTIFY)) {
			LOG_ERR("Error configuring Host Notify");
			return -EINVAL;
		}

		/* Enable Host Notify interrupts */
		status = pch_reg_read(dev, PCH_SMBUS_SCMD);
		status |= PCH_SMBUS_SCMD_HNI_EN;
		pch_reg_write(dev, PCH_SMBUS_SCMD, status);
	}

	if (config & SMBUS_MODE_SMBALERT) {
		uint8_t status;

		if (!IS_ENABLED(CONFIG_SMBUS_INTEL_PCH_SMBALERT)) {
			LOG_ERR("Error configuring SMBALERT");
			return -EINVAL;
		}

		/* Disable SMBALERT_DIS */
		status = pch_reg_read(dev, PCH_SMBUS_SCMD);
		status &= ~PCH_SMBUS_SCMD_SMBALERT_DIS;
		pch_reg_write(dev, PCH_SMBUS_SCMD, status);
	}

	/* Keep config for a moment */
	data->config = config;

	return 0;
}

static int pch_get_config(const struct device *dev, uint32_t *config)
{
	struct pch_data *data = dev->data;

	*config = data->config;

	return 0;
}

/* Device initialization function */
static int pch_smbus_init(const struct device *dev)
{
	const struct pch_config * const config = dev->config;
	struct pch_data *data = dev->data;
	struct pcie_bar mbar;
	uint32_t val;

	if (config->pcie->bdf == PCIE_BDF_NONE) {
		LOG_ERR("Cannot probe PCI device");
		return -ENODEV;
	}

	val = pcie_conf_read(config->pcie->bdf, PCIE_CONF_CMDSTAT);
	if (val & PCIE_CONF_CMDSTAT_INTERRUPT) {
		LOG_WRN("Pending interrupt, continuing");
	}

	if (IS_ENABLED(CONFIG_SMBUS_INTEL_PCH_ACCESS_MMIO)) {
		pcie_probe_mbar(config->pcie->bdf, 0, &mbar);
		pcie_set_cmd(config->pcie->bdf, PCIE_CONF_CMDSTAT_MEM, true);

		device_map(DEVICE_MMIO_RAM_PTR(dev), mbar.phys_addr, mbar.size,
			   K_MEM_CACHE_NONE);

		LOG_DBG("Mapped 0x%lx size 0x%lx to 0x%lx",
			mbar.phys_addr, mbar.size, DEVICE_MMIO_GET(dev));
	} else {
		pcie_set_cmd(config->pcie->bdf, PCIE_CONF_CMDSTAT_IO, true);
		val = pcie_conf_read(config->pcie->bdf, PCIE_CONF_BAR4);
		if (!PCIE_CONF_BAR_IO(val)) {
			LOG_ERR("Cannot read IO BAR");
			return -EINVAL;
		}

		data->sba = PCIE_CONF_BAR_ADDR(val);

		LOG_DBG("Using I/O address 0x%x", data->sba);
	}

	val = pcie_conf_read(config->pcie->bdf, PCH_SMBUS_HCFG);
	if ((val & PCH_SMBUS_HCFG_HST_EN) == 0) {
		LOG_ERR("SMBus Host Controller is disabled");
		return -EINVAL;
	}

	/* Initialize mutex and semaphore */
	k_mutex_init(&data->mutex);
	k_sem_init(&data->completion_sync, 0, 1);

	data->dev = dev;

	/* Initialize work structures */

#if defined(CONFIG_SMBUS_INTEL_PCH_SMBALERT)
	k_work_init(&data->smb_alert_work, smbalert_work);
#endif /* CONFIG_SMBUS_INTEL_PCH_SMBALERT */

#if defined(CONFIG_SMBUS_INTEL_PCH_HOST_NOTIFY)
	k_work_init(&data->host_notify_work, host_notify_work);
#endif /* CONFIG_SMBUS_INTEL_PCH_HOST_NOTIFY */

	config->config_func(dev);

	if (pch_configure(dev, SMBUS_MODE_CONTROLLER)) {
		LOG_ERR("SMBus: Cannot set default configuration");
		return -EIO;
	}

	return 0;
}

static int pch_prepare_transfer(const struct device *dev)
{
	uint8_t hsts;

	hsts = pch_reg_read(dev, PCH_SMBUS_HSTS);

	pch_dump_register_hsts(hsts);

	if (hsts & PCH_SMBUS_HSTS_HOST_BUSY) {
		LOG_ERR("Return BUSY status");
		return -EBUSY;
	}

	/* Check and clear HSTS status bits */
	hsts &= PCH_SMBUS_HSTS_ERROR | PCH_SMBUS_HSTS_BYTE_DONE |
		PCH_SMBUS_HSTS_INTERRUPT;
	if (hsts) {
		pch_reg_write(dev, PCH_SMBUS_HSTS, hsts);
	}

	/* TODO: Clear also CRC check bits */

	return 0;
}

static int pch_check_status(const struct device *dev)
{
	struct pch_data *data = dev->data;
	uint8_t status = data->status;

	/**
	 * Device Error means following:
	 * - unsupported Command Field Unclaimed Cycle
	 * - Host Device timeout
	 * - CRC Error
	 */
	if (status & PCH_SMBUS_HSTS_DEV_ERROR) {
		uint8_t auxs = pch_reg_read(dev, PCH_SMBUS_AUXS);

		LOG_WRN("Device Error (DERR) received");

		if (auxs & PCH_SMBUS_AUXS_CRC_ERROR) {
			LOG_DBG("AUXS register 0x%02x", auxs);
			/* Clear CRC error status bit */
			pch_reg_write(dev, PCH_SMBUS_AUXS,
				      PCH_SMBUS_AUXS_CRC_ERROR);
		}

		return -EIO;
	}

	/**
	 * Transaction collision, several masters are trying to access
	 * the bus and PCH detects arbitration lost.
	 */
	if (status & PCH_SMBUS_HSTS_BUS_ERROR) {
		LOG_WRN("Bus Error (BERR) received");

		return -EAGAIN;
	}

	/**
	 * Source of interrupt is failed bus transaction. This is set in
	 * response to KILL set to terminate the host transaction
	 */
	if (status & PCH_SMBUS_HSTS_FAILED) {
		LOG_WRN("Failed (FAIL) received");

		return -EIO;
	}

	return 0;
}

static int pch_smbus_block_start(const struct device *dev, uint16_t periph_addr,
				 uint8_t rw, uint8_t command, uint8_t count,
				 uint8_t *buf, uint8_t protocol)
{
	uint8_t reg;
	int ret;

	LOG_DBG("addr %x rw %d command %x", periph_addr, rw, command);

	/* Set TSA register */
	reg = PCH_SMBUS_TSA_ADDR_SET(periph_addr);
	reg |= rw & SMBUS_MSG_RW_MASK;
	pch_reg_write(dev, PCH_SMBUS_TSA, reg);

	/* Set HCMD register */
	pch_reg_write(dev, PCH_SMBUS_HCMD, command);

	/* Enable 32-byte buffer mode (E32b) to send block of data */
	reg = pch_reg_read(dev, PCH_SMBUS_AUXC);
	reg |= PCH_SMBUS_AUXC_EN_32BUF;
	pch_reg_write(dev, PCH_SMBUS_AUXC, reg);

	/* In E32B mode read and write to PCH_SMBUS_HBD translates to
	 * read and write to 32 byte storage array, index needs to be
	 * cleared by reading HCTL
	 */
	reg = pch_reg_read(dev, PCH_SMBUS_HCTL);
	ARG_UNUSED(reg); /* Avoid 'Dead assignment' warning */

	if (rw == SMBUS_MSG_WRITE) {
		/* Write count */
		pch_reg_write(dev, PCH_SMBUS_HD0, count);

		/* Write data to send */
		for (int i = 0; i < count; i++) {
			pch_reg_write(dev, PCH_SMBUS_HBD, buf[i]);
		}
	}

	ret = pch_prepare_transfer(dev);
	if (ret < 0) {
		return ret;
	}

	/* Set HCTL register */
	reg = PCH_SMBUS_HCTL_CMD_SET(protocol);
	reg |= PCH_SMBUS_HCTL_START;
	reg |= PCH_SMBUS_HCTL_INTR_EN;
	pch_reg_write(dev, PCH_SMBUS_HCTL, reg);

	return 0;
}

/* Start PCH SMBus operation */
static int pch_smbus_start(const struct device *dev, uint16_t periph_addr,
			   enum smbus_direction rw, uint8_t command,
			   uint8_t *buf, uint8_t protocol)
{
	uint8_t reg;
	int ret;

	LOG_DBG("addr 0x%02x rw %d command %x", periph_addr, rw, command);

	/* Set TSA register */
	reg = PCH_SMBUS_TSA_ADDR_SET(periph_addr);
	reg |= rw & SMBUS_MSG_RW_MASK;
	pch_reg_write(dev, PCH_SMBUS_TSA, reg);

	/* Write command for every but QUICK op */
	if (protocol != SMBUS_CMD_QUICK) {
		/* Set HCMD register */
		pch_reg_write(dev, PCH_SMBUS_HCMD, command);

		/* Set Host Data 0 (HD0) register */
		if (rw == SMBUS_MSG_WRITE && protocol != SMBUS_CMD_BYTE) {
			pch_reg_write(dev, PCH_SMBUS_HD0, buf[0]);

			/* If we need to write second byte */
			if (protocol == SMBUS_CMD_WORD_DATA ||
			    protocol == SMBUS_CMD_PROC_CALL) {
				pch_reg_write(dev, PCH_SMBUS_HD1, buf[1]);
			}
		}
	}

	ret = pch_prepare_transfer(dev);
	if (ret < 0) {
		return ret;
	}

	/* Set HCTL register */
	reg = PCH_SMBUS_HCTL_CMD_SET(protocol);
	reg |= PCH_SMBUS_HCTL_START;
	reg |= PCH_SMBUS_HCTL_INTR_EN;
	pch_reg_write(dev, PCH_SMBUS_HCTL, reg);

	return 0;
}

/* Implementation of PCH SMBus API */

/* Implementation of SMBus Quick */
static int pch_smbus_quick(const struct device *dev, uint16_t periph_addr,
			   enum smbus_direction rw)
{
	struct pch_data *data = dev->data;
	int ret;

	LOG_DBG("dev %p addr 0x%02x direction %x", dev, periph_addr, rw);

	k_mutex_lock(&data->mutex, K_FOREVER);

	ret = pch_smbus_start(dev, periph_addr, rw, 0, NULL, SMBUS_CMD_QUICK);
	if (ret < 0) {
		goto unlock;
	}

	/* Wait for completion from ISR */
	ret = k_sem_take(&data->completion_sync, K_MSEC(30));
	if (ret != 0) {
		LOG_ERR("SMBus Quick timed out");
		ret = -ETIMEDOUT;
		goto unlock;
	}

	ret = pch_check_status(dev);

unlock:
	k_mutex_unlock(&data->mutex);

	return ret;
}

/* Implementation of SMBus Byte Write */
static int pch_smbus_byte_write(const struct device *dev, uint16_t periph_addr,
				uint8_t command)
{
	struct pch_data *data = dev->data;
	int ret;

	LOG_DBG("dev %p addr 0x%02x command 0x%02x", dev, periph_addr, command);

	k_mutex_lock(&data->mutex, K_FOREVER);

	ret = pch_smbus_start(dev, periph_addr, SMBUS_MSG_WRITE, command, NULL,
			      SMBUS_CMD_BYTE);
	if (ret < 0) {
		goto unlock;
	}

	/* Wait for completion from ISR */
	ret = k_sem_take(&data->completion_sync, K_MSEC(30));
	if (ret != 0) {
		LOG_ERR("SMBus Byte Write timed out");
		ret = -ETIMEDOUT;
		goto unlock;
	}

	ret = pch_check_status(dev);

unlock:
	k_mutex_unlock(&data->mutex);

	return ret;
}

/* Implementation of SMBus Byte Read */
static int pch_smbus_byte_read(const struct device *dev, uint16_t periph_addr,
			       uint8_t *byte)
{
	struct pch_data *data = dev->data;
	int ret;

	LOG_DBG("dev %p addr 0x%02x", dev, periph_addr);

	k_mutex_lock(&data->mutex, K_FOREVER);

	ret = pch_smbus_start(dev, periph_addr, SMBUS_MSG_READ, 0, NULL,
			      SMBUS_CMD_BYTE);
	if (ret < 0) {
		goto unlock;
	}

	/* Wait for completion from ISR */
	ret = k_sem_take(&data->completion_sync, K_MSEC(30));
	if (ret != 0) {
		LOG_ERR("SMBus Byte Read timed out");
		ret = -ETIMEDOUT;
		goto unlock;
	}

	ret = pch_check_status(dev);
	if (ret < 0) {
		goto unlock;
	}

	*byte = pch_reg_read(dev, PCH_SMBUS_HD0);

unlock:
	k_mutex_unlock(&data->mutex);

	return ret;
}

/* Implementation of SMBus Byte Data Write */
static int pch_smbus_byte_data_write(const struct device *dev,
				     uint16_t periph_addr,
				     uint8_t command, uint8_t byte)
{
	struct pch_data *data = dev->data;
	int ret;

	LOG_DBG("dev %p addr 0x%02x command 0x%02x", dev, periph_addr, command);

	k_mutex_lock(&data->mutex, K_FOREVER);

	ret = pch_smbus_start(dev, periph_addr, SMBUS_MSG_WRITE, command,
			      &byte, SMBUS_CMD_BYTE_DATA);
	if (ret < 0) {
		goto unlock;
	}

	/* Wait for completion from ISR */
	ret = k_sem_take(&data->completion_sync, K_MSEC(30));
	if (ret != 0) {
		LOG_ERR("SMBus Byte Data Write timed out");
		ret = -ETIMEDOUT;
		goto unlock;
	}

	ret = pch_check_status(dev);

unlock:
	k_mutex_unlock(&data->mutex);

	return ret;
}

/* Implementation of SMBus Byte Data Read */
static int pch_smbus_byte_data_read(const struct device *dev,
				    uint16_t periph_addr,
				    uint8_t command, uint8_t *byte)
{
	struct pch_data *data = dev->data;
	int ret;

	LOG_DBG("dev %p addr 0x%02x command 0x%02x", dev, periph_addr, command);

	k_mutex_lock(&data->mutex, K_FOREVER);

	ret = pch_smbus_start(dev, periph_addr, SMBUS_MSG_READ, command,
			      NULL, SMBUS_CMD_BYTE_DATA);
	if (ret < 0) {
		goto unlock;
	}

	/* Wait for completion from ISR */
	ret = k_sem_take(&data->completion_sync, K_MSEC(30));
	if (ret != 0) {
		LOG_ERR("SMBus Byte Data Read timed out");
		ret = -ETIMEDOUT;
		goto unlock;
	}

	ret = pch_check_status(dev);
	if (ret < 0) {
		goto unlock;
	}

	*byte = pch_reg_read(dev, PCH_SMBUS_HD0);

unlock:
	k_mutex_unlock(&data->mutex);

	return ret;
}

/* Implementation of SMBus Word Data Write */
static int pch_smbus_word_data_write(const struct device *dev,
				     uint16_t periph_addr,
				     uint8_t command, uint16_t word)
{
	struct pch_data *data = dev->data;
	int ret;

	LOG_DBG("dev %p addr 0x%02x command 0x%02x", dev, periph_addr, command);

	k_mutex_lock(&data->mutex, K_FOREVER);

	ret = pch_smbus_start(dev, periph_addr, SMBUS_MSG_WRITE, command,
			      (uint8_t *)&word, SMBUS_CMD_WORD_DATA);
	if (ret < 0) {
		goto unlock;
	}

	/* Wait for completion from ISR */
	ret = k_sem_take(&data->completion_sync, K_MSEC(30));
	if (ret != 0) {
		LOG_ERR("SMBus Word Data Write timed out");
		ret = -ETIMEDOUT;
		goto unlock;
	}

	ret = pch_check_status(dev);

unlock:
	k_mutex_unlock(&data->mutex);

	return ret;
}

/* Implementation of SMBus Word Data Read */
static int pch_smbus_word_data_read(const struct device *dev,
				    uint16_t periph_addr,
				    uint8_t command, uint16_t *word)
{
	struct pch_data *data = dev->data;
	uint8_t *p = (uint8_t *)word;
	int ret;

	LOG_DBG("dev %p addr 0x%02x command 0x%02x", dev, periph_addr, command);

	k_mutex_lock(&data->mutex, K_FOREVER);

	ret = pch_smbus_start(dev, periph_addr, SMBUS_MSG_READ, command,
			      NULL, SMBUS_CMD_WORD_DATA);
	if (ret < 0) {
		goto unlock;
	}

	/* Wait for completion from ISR */
	ret = k_sem_take(&data->completion_sync, K_MSEC(30));
	if (ret != 0) {
		LOG_ERR("SMBus Word Data Read timed out");
		ret = -ETIMEDOUT;
		goto unlock;
	}

	ret = pch_check_status(dev);
	if (ret < 0) {
		goto unlock;
	}

	p[0] = pch_reg_read(dev, PCH_SMBUS_HD0);
	p[1] = pch_reg_read(dev, PCH_SMBUS_HD1);

unlock:
	k_mutex_unlock(&data->mutex);

	return ret;
}

/* Implementation of SMBus Process Call */
static int pch_smbus_pcall(const struct device *dev,
			   uint16_t periph_addr, uint8_t command,
			   uint16_t send_word, uint16_t *recv_word)
{
	struct pch_data *data = dev->data;
	uint8_t *p = (uint8_t *)recv_word;
	int ret;

	LOG_DBG("dev %p addr 0x%02x command 0x%02x", dev, periph_addr, command);

	k_mutex_lock(&data->mutex, K_FOREVER);

	ret = pch_smbus_start(dev, periph_addr, SMBUS_MSG_WRITE, command,
			      (uint8_t *)&send_word, SMBUS_CMD_PROC_CALL);
	if (ret < 0) {
		goto unlock;
	}

	/* Wait for completion from ISR */
	ret = k_sem_take(&data->completion_sync, K_MSEC(30));
	if (ret != 0) {
		LOG_ERR("SMBus Proc Call timed out");
		ret = -ETIMEDOUT;
		goto unlock;
	}

	ret = pch_check_status(dev);
	if (ret < 0) {
		goto unlock;
	}

	p[0] = pch_reg_read(dev, PCH_SMBUS_HD0);
	p[1] = pch_reg_read(dev, PCH_SMBUS_HD1);

unlock:
	k_mutex_unlock(&data->mutex);

	return ret;
}

/* Implementation of SMBus Block Write */
static int pch_smbus_block_write(const struct device *dev, uint16_t periph_addr,
				 uint8_t command, uint8_t count, uint8_t *buf)
{
	struct pch_data *data = dev->data;
	int ret;

	LOG_DBG("dev %p addr 0x%02x command 0x%02x count %u",
		dev, periph_addr, command, count);

	k_mutex_lock(&data->mutex, K_FOREVER);

	ret = pch_smbus_block_start(dev, periph_addr, SMBUS_MSG_WRITE, command,
				    count, buf, SMBUS_CMD_BLOCK);
	if (ret < 0) {
		goto unlock;
	}

	/* Wait for completion from ISR */
	ret = k_sem_take(&data->completion_sync, K_MSEC(30));
	if (ret != 0) {
		LOG_ERR("SMBus Block Write timed out");
		ret = -ETIMEDOUT;
		goto unlock;
	}

	ret = pch_check_status(dev);

unlock:
	k_mutex_unlock(&data->mutex);

	return ret;
}

/* Implementation of SMBus Block Read */
static int pch_smbus_block_read(const struct device *dev, uint16_t periph_addr,
				uint8_t command, uint8_t *count, uint8_t *buf)
{
	struct pch_data *data = dev->data;
	int ret;

	LOG_DBG("dev %p addr 0x%02x command 0x%02x",
		dev, periph_addr, command);

	k_mutex_lock(&data->mutex, K_FOREVER);

	ret = pch_smbus_block_start(dev, periph_addr, SMBUS_MSG_READ, command,
				    0, buf, SMBUS_CMD_BLOCK);
	if (ret < 0) {
		goto unlock;
	}

	/* Wait for completion from ISR */
	ret = k_sem_take(&data->completion_sync, K_MSEC(30));
	if (ret != 0) {
		LOG_ERR("SMBus Block Read timed out");
		ret = -ETIMEDOUT;
		goto unlock;
	}

	ret = pch_check_status(dev);
	if (ret < 0) {
		goto unlock;
	}

	*count = pch_reg_read(dev, PCH_SMBUS_HD0);
	if (*count == 0 || *count > SMBUS_BLOCK_BYTES_MAX) {
		ret = -ENODATA;
		goto unlock;
	}

	for (int i = 0; i < *count; i++) {
		buf[i] = pch_reg_read(dev, PCH_SMBUS_HBD);
	}

unlock:
	k_mutex_unlock(&data->mutex);

	return ret;
}

/* Implementation of SMBus Block Process Call */
static int pch_smbus_block_pcall(const struct device *dev,
				 uint16_t periph_addr, uint8_t command,
				 uint8_t send_count, uint8_t *send_buf,
				 uint8_t *recv_count, uint8_t *recv_buf)
{
	struct pch_data *data = dev->data;
	int ret;

	LOG_DBG("dev %p addr 0x%02x command 0x%02x",
		dev, periph_addr, command);

	k_mutex_lock(&data->mutex, K_FOREVER);

	ret = pch_smbus_block_start(dev, periph_addr, SMBUS_MSG_WRITE, command,
				    send_count, send_buf, SMBUS_CMD_BLOCK_PROC);
	if (ret < 0) {
		goto unlock;
	}

	/* Wait for completion from ISR */
	ret = k_sem_take(&data->completion_sync, K_MSEC(30));
	if (ret != 0) {
		LOG_ERR("SMBus Block Process Call timed out");
		ret = -ETIMEDOUT;
		goto unlock;
	}

	ret = pch_check_status(dev);
	if (ret < 0) {
		goto unlock;
	}

	*recv_count = pch_reg_read(dev, PCH_SMBUS_HD0);
	if (*recv_count == 0 ||
	    *recv_count + send_count > SMBUS_BLOCK_BYTES_MAX) {
		ret = -ENODATA;
		goto unlock;
	}

	for (int i = 0; i < *recv_count; i++) {
		recv_buf[i] = pch_reg_read(dev, PCH_SMBUS_HBD);
	}

unlock:
	k_mutex_unlock(&data->mutex);

	return ret;
}

static const struct smbus_driver_api funcs = {
	.configure = pch_configure,
	.get_config = pch_get_config,
	.smbus_quick = pch_smbus_quick,
	.smbus_byte_write = pch_smbus_byte_write,
	.smbus_byte_read = pch_smbus_byte_read,
	.smbus_byte_data_write = pch_smbus_byte_data_write,
	.smbus_byte_data_read = pch_smbus_byte_data_read,
	.smbus_word_data_write = pch_smbus_word_data_write,
	.smbus_word_data_read = pch_smbus_word_data_read,
	.smbus_pcall = pch_smbus_pcall,
	.smbus_block_write = pch_smbus_block_write,
	.smbus_block_read = pch_smbus_block_read,
	.smbus_block_pcall = pch_smbus_block_pcall,
#if defined(CONFIG_SMBUS_INTEL_PCH_SMBALERT)
	.smbus_smbalert_set_cb = pch_smbus_smbalert_set_sb,
	.smbus_smbalert_remove_cb = pch_smbus_smbalert_remove_sb,
#endif /* CONFIG_SMBUS_INTEL_PCH_SMBALERT */
#if defined(CONFIG_SMBUS_INTEL_PCH_HOST_NOTIFY)
	.smbus_host_notify_set_cb = pch_smbus_host_notify_set_cb,
	.smbus_host_notify_remove_cb = pch_smbus_host_notify_remove_cb,
#endif /* CONFIG_SMBUS_INTEL_PCH_HOST_NOTIFY */
};

static void smbus_isr(const struct device *dev)
{
	const struct pch_config * const config = dev->config;
	struct pch_data *data = dev->data;
	uint32_t sts;
	uint8_t status;

	sts = pcie_conf_read(config->pcie->bdf, PCIE_CONF_CMDSTAT);
	if (!(sts & PCIE_CONF_CMDSTAT_INTERRUPT)) {
		LOG_ERR("Not our interrupt");
		return;
	}

	/**
	 * Handle first Host Notify since for that we need to read SSTS
	 * register and for all other sources HSTS.
	 *
	 * Intel PCH implements Host Notify protocol in hardware.
	 */
#if defined(CONFIG_SMBUS_INTEL_PCH_HOST_NOTIFY)
	if (data->config & SMBUS_MODE_HOST_NOTIFY) {
		status = pch_reg_read(dev, PCH_SMBUS_SSTS);
		if (status & PCH_SMBUS_SSTS_HNS) {
			/* Notify address */
			data->notify_addr =
				pch_reg_read(dev, PCH_SMBUS_NDA) >> 1;

			/* Notify data */
			data->notify_data = pch_reg_read(dev, PCH_SMBUS_NDLB);
			data->notify_data |=
				pch_reg_read(dev, PCH_SMBUS_NDHB) << 8;

			k_work_submit(&data->host_notify_work);

			/* Clear Host Notify */
			pch_reg_write(dev, PCH_SMBUS_SSTS, PCH_SMBUS_SSTS_HNS);

			return;
		}
	}
#endif /* CONFIG_SMBUS_INTEL_PCH_HOST_NOTIFY */

	status = pch_reg_read(dev, PCH_SMBUS_HSTS);

	/* HSTS dump if logging is enabled */
	pch_dump_register_hsts(status);

	if (status & PCH_SMBUS_HSTS_BYTE_DONE) {
		LOG_WRN("BYTE_DONE interrupt is not used");
	}

	/* Handle SMBALERT# signal */
#if defined(CONFIG_SMBUS_INTEL_PCH_SMBALERT)
	if (data->config & SMBUS_MODE_SMBALERT &&
	    status & PCH_SMBUS_HSTS_SMB_ALERT) {
		k_work_submit(&data->smb_alert_work);
	}
#endif /* CONFIG_SMBUS_INTEL_PCH_SMBALERT */

	/* Clear IRQ sources */
	pch_reg_write(dev, PCH_SMBUS_HSTS, status);

	data->status = status;

	k_sem_give(&data->completion_sync);
}

/* Device macro initialization  / DTS hackery */

#define SMBUS_PCH_IRQ_FLAGS_SENSE0(n) 0
#define SMBUS_PCH_IRQ_FLAGS_SENSE1(n) DT_INST_IRQ(n, sense)
#define SMBUS_PCH_IRQ_FLAGS(n) \
	_CONCAT(SMBUS_PCH_IRQ_FLAGS_SENSE, DT_INST_IRQ_HAS_CELL(n, sense))(n)

#define SMBUS_IRQ_CONFIG(n)                                                    \
	BUILD_ASSERT(IS_ENABLED(CONFIG_DYNAMIC_INTERRUPTS),                    \
		     "SMBus PCIe requires dynamic interrupts");                \
	static void pch_config_##n(const struct device *dev)                   \
	{                                                                      \
		const struct pch_config * const config = dev->config;          \
		unsigned int irq;                                              \
		if (DT_INST_IRQN(n) == PCIE_IRQ_DETECT) {                      \
			irq = pcie_alloc_irq(config->pcie->bdf);               \
			if (irq == PCIE_CONF_INTR_IRQ_NONE) {                  \
				return;                                        \
			}                                                      \
		} else {                                                       \
			irq = DT_INST_IRQN(n);                                 \
			pcie_conf_write(config->pcie->bdf,                     \
					PCIE_CONF_INTR, irq);                  \
		}                                                              \
		pcie_connect_dynamic_irq(config->pcie->bdf, irq,               \
					 DT_INST_IRQ(n, priority),             \
					 (void (*)(const void *))smbus_isr,    \
					 DEVICE_DT_INST_GET(n),                \
					 SMBUS_PCH_IRQ_FLAGS(n));              \
		pcie_irq_enable(config->pcie->bdf, irq);                       \
		LOG_DBG("Configure irq %d", irq);                              \
	}

#define SMBUS_DEVICE_INIT(n)                                                   \
	DEVICE_PCIE_INST_DECLARE(n);                                           \
	static void pch_config_##n(const struct device *dev);                  \
	static const struct pch_config pch_config_data_##n = {                 \
		DEVICE_PCIE_INST_INIT(n, pcie),                                \
		.config_func = pch_config_##n,                                 \
	};                                                                     \
	static struct pch_data smbus_##n##_data;                               \
	SMBUS_DEVICE_DT_INST_DEFINE(n, pch_smbus_init, NULL,                   \
				    &smbus_##n##_data, &pch_config_data_##n,   \
				    POST_KERNEL, CONFIG_SMBUS_INIT_PRIORITY,   \
				    &funcs);                                   \
	SMBUS_IRQ_CONFIG(n);

DT_INST_FOREACH_STATUS_OKAY(SMBUS_DEVICE_INIT)
