/* Copyright 2021 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#define DT_DRV_COMPAT cros_sn5s330_emul

#include <device.h>
#include <drivers/i2c.h>
#include <drivers/i2c_emul.h>
#include <emul.h>
#include <errno.h>
#include <sys/__assert.h>
#include <devicetree/gpio.h>
#include <drivers/gpio/gpio_emul.h>

#include "driver/ppc/sn5s330.h"
#include "driver/ppc/sn5s330_public.h"
#include "emul/emul_common_i2c.h"
#include "emul/emul_sn5s330.h"
#include "i2c.h"

#include <logging/log.h>
LOG_MODULE_REGISTER(sn5s330_emul, CONFIG_SN5S330_EMUL_LOG_LEVEL);

#define SN5S330_DATA_FROM_I2C_EMUL(_emul)                                    \
	CONTAINER_OF(CONTAINER_OF(_emul, struct i2c_common_emul_data, emul), \
		     struct sn5s330_emul_data, common)

struct sn5s330_emul_data {
	/** Common I2C data */
	struct i2c_common_emul_data common;
	/** Emulated int_gpio port */
	const struct device *gpio_int_port;
	/** Emulated int_gpio pin */
	gpio_pin_t gpio_int_pin;
	/** Emulated FUNC_SET1 register */
	uint8_t func_set1_reg;
	/** Emulated FUNC_SET2 register */
	uint8_t func_set2_reg;
	/** Emulated FUNC_SET3 register */
	uint8_t func_set3_reg;
	/** Emulated FUNC_SET4 register */
	uint8_t func_set4_reg;
	/** Emulated FUNC_SET5 register */
	uint8_t func_set5_reg;
	/** Emulated FUNC_SET6 register */
	uint8_t func_set6_reg;
	/** Emulated FUNC_SET7 register */
	uint8_t func_set7_reg;
	/** Emulated FUNC_SET8 register */
	uint8_t func_set8_reg;
	/** Emulated FUNC_SET9 register */
	uint8_t func_set9_reg;
	/** Emulated FUNC_SET10 register */
	uint8_t func_set10_reg;
	/** Emulated FUNC_SET11 register */
	uint8_t func_set11_reg;
	/** Emulated FUNC_SET12 register */
	uint8_t func_set12_reg;
	/** Emulated INT_STATUS_REG1 register */
	uint8_t int_status_reg1;
	/** Emulated INT_STATUS_REG2 register */
	uint8_t int_status_reg2;
	/** Emulated INT_STATUS_REG3 register */
	uint8_t int_status_reg3;
	/** Emulated INT_STATUS_REG4 register */
	/*
	 * TODO(b/205754232): Register name discrepancy
	 */
	uint8_t int_status_reg4;
	/*
	 * TODO(b/203364783): For all falling edge registers, implement
	 * interrupt and bit change to correspond to change in interrupt status
	 * registers.
	 */
	/** Emulated INT_MASK_RISE_REG1 register */
	uint8_t int_mask_rise_reg1;
	/** Emulated INT_MASK_RISE_REG2 register */
	uint8_t int_mask_rise_reg2;
	/** Emulated INT_MASK_RISE_REG3 register */
	uint8_t int_mask_rise_reg3;
	/** Emulated INT_MASK_FALL_REG1 register */
	uint8_t int_mask_fall_reg1;
	/** Emulated INT_MASK_FALL_REG2 register */
	uint8_t int_mask_fall_reg2;
	/** Emulated INT_MASK_FALL_REG3 register */
	uint8_t int_mask_fall_reg3;
	/** Emulated INT_TRIP_RISE_REG1 register */
	uint8_t int_trip_rise_reg1;
	/** Emulated INT_TRIP_RISE_REG2 register */
	uint8_t int_trip_rise_reg2;
	/** Emulated INT_TRIP_RISE_REG3 register */
	uint8_t int_trip_rise_reg3;
	/** Emulated INT_TRIP_FALL_REG1 register */
	uint8_t int_trip_fall_reg1;
	/** Emulated INT_TRIP_FALL_REG2 register */
	uint8_t int_trip_fall_reg2;
	/** Emulated INT_TRIP_FALL_REG3 register */
	uint8_t int_trip_fall_reg3;
};

struct sn5s330_emul_cfg {
	/** Common I2C config */
	const struct i2c_common_emul_cfg common;
};

test_mockable_static void sn5s330_emul_interrupt_set_stub(void)
{
	/* Stub to be used by fff fakes during test */
}

struct i2c_emul *sn5s330_emul_to_i2c_emul(const struct emul *emul)
{
	struct sn5s330_emul_data *data = emul->data;

	return &(data->common.emul);
}

/* Workhorse for mapping i2c reg to internal emulator data access */
static uint8_t *sn5s330_emul_get_reg_ptr(struct sn5s330_emul_data *data,
					 int reg)
{
	switch (reg) {
	case SN5S330_FUNC_SET1:
		return &(data->func_set1_reg);
	case SN5S330_FUNC_SET2:
		return &(data->func_set2_reg);
	case SN5S330_FUNC_SET3:
		return &(data->func_set3_reg);
	case SN5S330_FUNC_SET4:
		return &(data->func_set4_reg);
	case SN5S330_FUNC_SET5:
		return &(data->func_set5_reg);
	case SN5S330_FUNC_SET6:
		return &(data->func_set6_reg);
	case SN5S330_FUNC_SET7:
		return &(data->func_set7_reg);
	case SN5S330_FUNC_SET8:
		return &(data->func_set8_reg);
	case SN5S330_FUNC_SET9:
		return &(data->func_set9_reg);
	case SN5S330_FUNC_SET10:
		return &(data->func_set10_reg);
	case SN5S330_FUNC_SET11:
		return &(data->func_set11_reg);
	case SN5S330_FUNC_SET12:
		return &(data->func_set12_reg);
	case SN5S330_INT_STATUS_REG1:
		return &(data->int_status_reg1);
	case SN5S330_INT_STATUS_REG2:
		return &(data->int_status_reg2);
	case SN5S330_INT_STATUS_REG3:
		return &(data->int_status_reg3);
	case SN5S330_INT_STATUS_REG4:
		return &(data->int_status_reg4);
	case SN5S330_INT_MASK_RISE_REG1:
		return &(data->int_mask_rise_reg1);
	case SN5S330_INT_MASK_RISE_REG2:
		return &(data->int_mask_rise_reg2);
	case SN5S330_INT_MASK_RISE_REG3:
		return &(data->int_mask_rise_reg3);
	case SN5S330_INT_MASK_FALL_REG1:
		return &(data->int_mask_fall_reg1);
	case SN5S330_INT_MASK_FALL_REG2:
		return &(data->int_mask_fall_reg2);
	case SN5S330_INT_MASK_FALL_REG3:
		return &(data->int_mask_fall_reg3);
	case SN5S330_INT_TRIP_RISE_REG1:
		return &(data->int_trip_rise_reg1);
	case SN5S330_INT_TRIP_RISE_REG2:
		return &(data->int_trip_rise_reg2);
	case SN5S330_INT_TRIP_RISE_REG3:
		return &(data->int_trip_rise_reg3);
	case SN5S330_INT_TRIP_FALL_REG1:
		return &(data->int_trip_fall_reg1);
	case SN5S330_INT_TRIP_FALL_REG2:
		return &(data->int_trip_fall_reg2);
	case SN5S330_INT_TRIP_FALL_REG3:
		return &(data->int_trip_fall_reg3);
	default:
		__ASSERT(false, "Unimplemented Register Access Error on 0x%x",
			 reg);
		/* Statement never reached, required for compiler warnings */
		return NULL;
	}
}

void sn5s330_emul_peek_reg(const struct emul *emul, uint32_t reg, uint8_t *val)
{
	struct sn5s330_emul_data *data = emul->data;
	uint8_t *data_reg = sn5s330_emul_get_reg_ptr(data, reg);

	*val = *data_reg;
}

static void sn5s330_emul_set_int_pin(struct i2c_emul *emul, bool val)
{
	struct sn5s330_emul_data *data = SN5S330_DATA_FROM_I2C_EMUL(emul);
	int res = gpio_emul_input_set(data->gpio_int_port, data->gpio_int_pin,
				      val);
	__ASSERT_NO_MSG(res == 0);
}

static void sn5s330_emul_assert_interrupt(struct i2c_emul *emul)
{
	sn5s330_emul_interrupt_set_stub();
	sn5s330_emul_set_int_pin(emul, false);
}

static void sn5s330_emul_deassert_interrupt(struct i2c_emul *emul)
{
	sn5s330_emul_set_int_pin(emul, true);
}

static int sn5s330_emul_read_byte(struct i2c_emul *emul, int reg, uint8_t *val,
				  int bytes)
{
	struct sn5s330_emul_data *data = SN5S330_DATA_FROM_I2C_EMUL(emul);
	uint8_t *reg_to_read = sn5s330_emul_get_reg_ptr(data, reg);

	__ASSERT(bytes == 0, "bytes 0x%x != 0x0 on reg 0x%x", bytes, reg);
	*val = *reg_to_read;

	return 0;
}

static int sn5s330_emul_write_byte(struct i2c_emul *emul, int reg, uint8_t val,
				   int bytes)
{
	struct sn5s330_emul_data *data = SN5S330_DATA_FROM_I2C_EMUL(emul);
	uint8_t *reg_to_write;
	bool deassert_int = false;

	__ASSERT(bytes == 1, "bytes 0x%x != 0x1 on reg 0x%x", bytes, reg);

	/* Specially check for read-only reg */
	switch (reg) {
	case SN5S330_INT_TRIP_RISE_REG1:
		/* fallthrough */
	case SN5S330_INT_TRIP_RISE_REG2:
		/* fallthrough */
	case SN5S330_INT_TRIP_RISE_REG3:
		/* fallthrough */
	case SN5S330_INT_TRIP_FALL_REG1:
		/* fallthrough */
	case SN5S330_INT_TRIP_FALL_REG2:
		/* fallthrough */
	case SN5S330_INT_TRIP_FALL_REG3:
		reg_to_write = sn5s330_emul_get_reg_ptr(data, reg);
		/* Clearing any bit deasserts /INT interrupt signal */
		deassert_int = (*reg_to_write & val) != 0;
		/* Write 0 is noop and 1 clears the bit. */
		val = (~val & *reg_to_write);
		*reg_to_write = val;
		break;
	case SN5S330_INT_STATUS_REG1:
		/* fallthrough */
	case SN5S330_INT_STATUS_REG2:
		/* fallthrough */
	case SN5S330_INT_STATUS_REG3:
		__ASSERT(false,
			 "Write to an unverified-as-safe read-only register on "
			 "0x%x",
			 reg);
		/* fallthrough for checkpath */
	default:
		reg_to_write = sn5s330_emul_get_reg_ptr(data, reg);
		*reg_to_write = val;
	}

	if (deassert_int)
		sn5s330_emul_deassert_interrupt(emul);

	return 0;
}

void sn5s330_emul_make_vbus_overcurrent(const struct emul *emul)
{
	struct sn5s330_emul_data *data = emul->data;
	struct i2c_emul *i2c_emul = &data->common.emul;

	data->int_status_reg1 |= SN5S330_ILIM_PP1_MASK;
	data->int_trip_rise_reg1 |= SN5S330_ILIM_PP1_MASK;

	/* driver disabled this interrupt trigger */
	if (data->int_mask_rise_reg1 & SN5S330_ILIM_PP1_MASK)
		return;

	sn5s330_emul_assert_interrupt(i2c_emul);
}

void sn5s330_emul_lower_vbus_below_minv(const struct emul *emul)
{
	struct sn5s330_emul_data *data = emul->data;
	struct i2c_emul *i2c_emul = &data->common.emul;

	data->int_status_reg4 |= SN5S330_VSAFE0V_STAT;

	/* driver disabled this interrupt trigger */
	if (data->int_status_reg4 & SN5S330_VSAFE0V_MASK)
		return;

	sn5s330_emul_assert_interrupt(i2c_emul);
}

void sn5s330_emul_reset(const struct emul *emul)
{
	struct sn5s330_emul_data *data = emul->data;
	struct i2c_common_emul_data common = data->common;
	const struct device *gpio_int_port = data->gpio_int_port;
	gpio_pin_t gpio_int_pin = data->gpio_int_pin;

	sn5s330_emul_deassert_interrupt(&data->common.emul);

	/*
	 * TODO(b/203364783): Some registers reset with set bits; this should be
	 * reflected in the emul_reset function.
	 */

	/* Only Reset the sn5s330 Register Data */
	memset(data, 0, sizeof(struct sn5s330_emul_data));
	data->common = common;
	data->gpio_int_port = gpio_int_port;
	data->gpio_int_pin = gpio_int_pin;
}

static int emul_sn5s330_init(const struct emul *emul,
			     const struct device *parent)
{
	const struct sn5s330_emul_cfg *cfg = emul->cfg;
	struct sn5s330_emul_data *data = emul->data;

	sn5s330_emul_deassert_interrupt(&data->common.emul);

	data->common.emul.api = &i2c_common_emul_api;
	data->common.emul.addr = cfg->common.addr;
	data->common.emul.parent = emul;
	data->common.i2c = parent;
	data->common.cfg = &cfg->common;
	i2c_common_emul_init(&data->common);

	return i2c_emul_register(parent, emul->dev_label, &data->common.emul);
}

#define SN5S330_GET_GPIO_INT_PORT(n) \
	DEVICE_DT_GET(DT_GPIO_CTLR(DT_INST_PROP(n, int_gpio), gpios))

#define SN5S330_GET_GPIO_INT_PIN(n) \
	DT_GPIO_PIN(DT_INST_PROP(n, int_gpio), gpios)

#define INIT_SN5S330(n)                                                        \
	static struct sn5s330_emul_data sn5s330_emul_data_##n = {              \
		.common = {                                                    \
			.write_byte = sn5s330_emul_write_byte,                 \
			.read_byte = sn5s330_emul_read_byte,                   \
		},                                                             \
		.gpio_int_port = SN5S330_GET_GPIO_INT_PORT(n),		       \
		.gpio_int_pin = SN5S330_GET_GPIO_INT_PIN(n),		       \
	};                                                                     \
	static struct sn5s330_emul_cfg sn5s330_emul_cfg_##n = {                \
		.common = {                                                    \
			.i2c_label = DT_INST_BUS_LABEL(n),                     \
			.dev_label = DT_INST_LABEL(n),                         \
			.addr = DT_INST_REG_ADDR(n),                           \
		},                                                             \
	};                                                                     \
	EMUL_DEFINE(emul_sn5s330_init, DT_DRV_INST(n), &sn5s330_emul_cfg_##n,  \
		    &sn5s330_emul_data_##n)

DT_INST_FOREACH_STATUS_OKAY(INIT_SN5S330)
