/* 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.
 */

#include "common.h"
#include "i2c.h"
#include "ioexpander.h"
#include "system.h"
#include "tca64xxa.h"

/*
 * This chip series contain registers in the same order.
 * Difference between models is only amount of registers and
 * value of which you must multiply to access specific register.
 * For 16 bit series, registers are 2 byte away, so to access TCA64XXA_REG_CONF
 * you must multiply it by 2. For 24 bit, they are away by 4 bytes so you
 * must multiply them by 4. Flags value contains information which version
 * of chip is used.
 */
#define TCA64XXA_PORT_ID(port, reg, flags) \
	((((flags) & TCA64XXA_FLAG_VER_MASK) \
	>> TCA64XXA_FLAG_VER_OFFSET) * (reg) + (port))

static int tca64xxa_write_byte(int ioex, int port, int reg, uint8_t val)
{
	const struct ioexpander_config_t *ioex_p = &ioex_config[ioex];
	const int reg_addr = TCA64XXA_PORT_ID(port, reg, ioex_p->flags);

	return i2c_write8(ioex_p->i2c_host_port,
			  ioex_p->i2c_addr_flags,
			  reg_addr,
			  val);
}

static int tca64xxa_read_byte(int ioex, int port, int reg, int *val)
{
	const struct ioexpander_config_t *ioex_p = &ioex_config[ioex];
	const int reg_addr = TCA64XXA_PORT_ID(port, reg, ioex_p->flags);

	return i2c_read8(ioex_p->i2c_host_port,
			 ioex_p->i2c_addr_flags,
			 reg_addr,
			 val);
}

/* Restore default values in registers */
static int tca64xxa_reset(int ioex, int portsCount)
{
	int port;
	int ret;

	/*
	 * On servo_v4p1, reset pin is pulled up and it results in values
	 * not being restored to default ones after software reboot.
	 * This loop sets default values (from specification) to all registers.
	 */
	for (port = 0; port < portsCount; port++) {
		ret = tca64xxa_write_byte(ioex,
					  port,
					  TCA64XXA_REG_OUTPUT,
					  TCA64XXA_DEFAULT_OUTPUT);
		if (ret)
			return ret;

		ret = tca64xxa_write_byte(ioex,
					  port,
					  TCA64XXA_REG_POLARITY_INV,
					  TCA64XXA_DEFAULT_POLARITY_INV);
		if (ret)
			return ret;

		ret = tca64xxa_write_byte(ioex,
					  port,
					  TCA64XXA_REG_CONF,
					  TCA64XXA_DEFAULT_CONF);
		if (ret)
			return ret;
	}

	return EC_SUCCESS;
}

/* Initialize IO expander chip/driver */
static int tca64xxa_init(int ioex)
{
	const struct ioexpander_config_t *ioex_p = &ioex_config[ioex];
	int portsCount;

	if (ioex_p->flags & TCA64XXA_FLAG_VER_TCA6416A)
		portsCount = 2;
	else if (ioex_p->flags & TCA64XXA_FLAG_VER_TCA6424A)
		portsCount = 3;
	else
		return EC_ERROR_UNIMPLEMENTED;

	if (!system_jumped_late())
		return tca64xxa_reset(ioex, portsCount);

	return EC_SUCCESS;
}

/* Get the current level of the IOEX pin */
static int tca64xxa_get_level(int ioex, int port, int mask, int *val)
{
	int buf;
	int ret;

	ret = tca64xxa_read_byte(ioex, port, TCA64XXA_REG_INPUT, &buf);
	*val = !!(buf & mask);

	return ret;
}

/* Set the level of the IOEX pin */
static int tca64xxa_set_level(int ioex, int port, int mask, int val)
{
	int ret;
	int v;

	ret = tca64xxa_read_byte(ioex, port, TCA64XXA_REG_OUTPUT, &v);
	if (ret)
		return ret;

	if (val)
		v |= mask;
	else
		v &= ~mask;

	return tca64xxa_write_byte(ioex, port, TCA64XXA_REG_OUTPUT, v);
}

/* Get flags for the IOEX pin */
static int tca64xxa_get_flags_by_mask(int ioex, int port, int mask, int *flags)
{
	int ret;
	int v;

	ret = tca64xxa_read_byte(ioex, port, TCA64XXA_REG_CONF, &v);
	if (ret)
		return ret;

	*flags = 0;
	if (v & mask) {
		*flags |= GPIO_INPUT;
	} else {
		*flags |= GPIO_OUTPUT;

		ret = tca64xxa_read_byte(ioex, port, TCA64XXA_REG_OUTPUT, &v);
		if(ret)
			return ret;

		if (v & mask)
			*flags |= GPIO_HIGH;
		else
			*flags |= GPIO_LOW;
	}

	return EC_SUCCESS;
}

/* Set flags for the IOEX pin */
static int tca64xxa_set_flags_by_mask(int ioex, int port, int mask, int flags)
{
	int ret;
	int v;

	/* Output value */
	if (flags & GPIO_OUTPUT) {
		ret = tca64xxa_read_byte(ioex, port, TCA64XXA_REG_OUTPUT, &v);
		if (ret)
			return ret;

		if (flags & GPIO_LOW)
			v &= ~mask;
		else if (flags & GPIO_HIGH)
			v |= mask;
		else
			return EC_ERROR_INVAL;

		ret = tca64xxa_write_byte(ioex, port, TCA64XXA_REG_OUTPUT, v);
		if (ret)
			return ret;
	}

	/* Configuration */
	ret = tca64xxa_read_byte(ioex, port, TCA64XXA_REG_CONF, &v);
	if(ret)
		return ret;

	if (flags & GPIO_INPUT)
		v |= mask;
	else if (flags & GPIO_OUTPUT)
		v &= ~mask;
	else
		return EC_ERROR_INVAL;

	ret = tca64xxa_write_byte(ioex, port, TCA64XXA_REG_CONF, v);
	if (ret)
		return ret;

	return EC_SUCCESS;
}

#ifdef CONFIG_IO_EXPANDER_SUPPORT_GET_PORT

/* Read levels for whole IO expander port */
static int tca64xxa_get_port(int ioex, int port, int *val)
{
	return tca64xxa_read_byte(ioex, port, TCA64XXA_REG_INPUT, val);
}

#endif

/* Driver structure */
const struct ioexpander_drv tca64xxa_ioexpander_drv = {
	.init 			= tca64xxa_init,
	.get_level 		= tca64xxa_get_level,
	.set_level 		= tca64xxa_set_level,
	.get_flags_by_mask 	= tca64xxa_get_flags_by_mask,
	.set_flags_by_mask 	= tca64xxa_set_flags_by_mask,
	.enable_interrupt 	= NULL,
#ifdef CONFIG_IO_EXPANDER_SUPPORT_GET_PORT
	.get_port		= tca64xxa_get_port,
#endif
};
