/*
 *  dellemc_s4128t_platform.c - Dell EMC S4128T Platform Support.
 *
 *  Copyright (C) 2018 Cumulus Networks, Inc.  All Rights Reserved
 *  Author: David Yen (dhyen@cumulusnetworks.com)
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful, but
 *  WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 *  General Public License for more details.
 *
 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/gpio.h>
#include <linux/mutex.h>
#include <linux/i2c-ismt.h>
#include <linux/i2c-mux.h>
#include <linux/i2c-mux-gpio.h>
#include <linux/platform_device.h>
#include <linux/platform_data/at24.h>
#include <linux/i2c/sff-8436.h>
#include <linux/i2c/pca954x.h>
#include <linux/hwmon-sysfs.h>

#include <linux/cumulus-platform.h>
#include "platform_defs.h"
#include "dellemc_s41xx_cpld.h"

#define DRIVER_NAME	"dellemc_s4128t"
#define DRIVER_VERSION	"1.0"

/*
 * This platform has two i2c busses:
 *   SMBus_0: SMBus i801 adapter at PCIe address 0000:00:1f.3
 *   SMBus_1: SMBus iSMT adapter at PCIe address 0000:00:13.0
 *
 * SMBus_0 is connected to the following on the CPU Board:
 *   9vrs4420d clock gen (0x69)
 *   isl90727 ddr3 vref digital potentiometer (0x2e)
 *   ddr3 temperature sensor (0x1a)
 *   ddr3 spd eeprom (0x52)
 *   ddr3 wp? (0x32)
 *
 * SMBus_1 is connected to the following on the Main Board:
 *   pca9548 (0x70)
 *	0 system cpld (0x31)
 *	  master cpld (0x32)
 *	  tmp75 temperature sensor (0x4d)
 *	  id eeprom (0x53)
 *	1 lm25066a psu #1 (0x40)
 *	  lm25066a psu #2 (0x42)
 *	  fan tray #1 (0x51)
 *	  fan tray #2 (0x52)
 *	  fan tray #3 (0x53)
 *	  fan tray #4 (0x54)
 *	  fan controller #1 (0x4d)
 *	  PM6764TR AVS power (0x6f)
 *	2 sn74cbtlv3251 i2c mux (0x50)
 *	3 tmp75 - close to qsfp28 cage (0x49)
 *	  tmp75 - close to right PHY (0x4a)
 *	  tmp75 - close to MAC (0x4b)
 *	  tmp75 - close to left PHY (0x4c)
 *	  tmp75 - close to left front vents (0x4e)
 *	  tmp75 - close to fan tray (0x4f)
 *	  1588 module (0x40, 0x44)
 *	4 psu #1 eeprom (0x50)
 *	  psu #1 fan controller (0x58)
 *	  psu #2 eeprom (0x51)
 *	  psu #2 fan controller (0x59)
 *
 */

enum {
	I2C_I801_BUS = -10,
	I2C_ISMT_BUS,

	/* top-level main board mux (actually resides on CPU module) */
	I2C_MUX1_BUS0 = 10,
	I2C_MUX1_BUS1,
	I2C_MUX1_BUS2,
	I2C_MUX1_BUS3,
	I2C_MUX1_BUS4,

	/* muxes for the SFP+ and QSFP28 ports */
	I2C_QSFP_PORT_MUX = 45,
};

static struct i2c_client *cpld_devices[NO_S4128T_CPLD_I2C_CLIENTS];
static int num_cpld_devices;

/*
 * The list of i2c devices and their bus connections for this platform.
 *
 * First we construct the necessary data struction for each device,
 * using the method specific to the device type.  Then we put them
 * all together in a big table (see i2c_devices below).
 *
 * For muxes, we specify the starting bus number for the block of ports,
 * using the magic mk_pca954*() macros.
 *
 * For eeproms, including ones in the qsfp+ transceivers,
 * we specify the label, i2c address, size, and some flags,
 * all done in mk*_eeprom() macros.  The label is the string
 * that ends up in /sys/class/eeprom_dev/eepromN/label,
 * which we use to identify them at user level.
 *
 * See the comment below for gpio.
 */

mk_pca9548(mux1, I2C_MUX1_BUS0, 1)

mk_eeprom(spd, 52, 256, AT24_FLAG_READONLY | AT24_FLAG_IRUGO |
	  AT24_FLAG_DISABLE_I2CBLOCK);
mk_eeprom(board, 53, 256, AT24_FLAG_IRUGO);
mk_eeprom(fan1,	 51, 256, AT24_FLAG_IRUGO);
mk_eeprom(fan2,	 52, 256, AT24_FLAG_IRUGO);
mk_eeprom(fan3,	 53, 256, AT24_FLAG_IRUGO);
mk_eeprom(fan4,	 54, 256, AT24_FLAG_IRUGO);
mk_eeprom(psu1,	 50, 256, AT24_FLAG_IRUGO);
mk_eeprom(psu2,	 51, 256, AT24_FLAG_IRUGO);

struct i2c_device_info {
	int bus;
	struct i2c_board_info board_info;
};

/*
 * Main i2c device table
 *
 * We use the mk_i2cdev() macro to construct the entries.
 * Each entry is a bus number and a i2c_board_info.
 * The i2c_board_info structure specifies the device type, address,
 * and platform data specific to the device type.
 */

static struct platform_i2c_device_info i2c_devices[] = {
	/* devices on i801 bus */
	mk_i2cdev(I2C_I801_BUS,	 "spd",	    0x52, &spd_52_at24),
	mk_i2cdev(I2C_I801_BUS,	 "jc42",    0x1a, NULL),

	/* devices on iSMT bus */
	mk_i2cdev(I2C_ISMT_BUS,	 "pca9548", 0x70, &mux1_platform_data),

	/* devices on mux0 */
	mk_i2cdev(I2C_MUX1_BUS0, "dummy",    0x31, NULL), /* System CPLD */
	mk_i2cdev(I2C_MUX1_BUS0, "dummy",    0x32, NULL), /* Master CPLD */
	mk_i2cdev(I2C_MUX1_BUS0, "tmp75",    0x4d, NULL), /* U57 */
	mk_i2cdev(I2C_MUX1_BUS0, "24c02",    0x53, &board_53_at24),

	/* devices on mux1 */
	mk_i2cdev(I2C_MUX1_BUS1, "lm25066",  0x40, NULL), /* PSU 1 PMBus */
	mk_i2cdev(I2C_MUX1_BUS1, "lm25066",  0x42, NULL), /* PSU 2 PMBus */
	mk_i2cdev(I2C_MUX1_BUS1, "24c02",    0x51, &fan1_51_at24),
	mk_i2cdev(I2C_MUX1_BUS1, "24c02",    0x52, &fan2_52_at24),
	mk_i2cdev(I2C_MUX1_BUS1, "24c02",    0x53, &fan3_53_at24),
	mk_i2cdev(I2C_MUX1_BUS1, "24c02",    0x54, &fan4_54_at24),
	mk_i2cdev(I2C_MUX1_BUS1, "emc2305",  0x4d, NULL), /* fan ctrl #1 */

	/* devices on mux2 */

	/* devices on mux3 */
	mk_i2cdev(I2C_MUX1_BUS3, "tmp75",    0x49, NULL), /* U288 */
	mk_i2cdev(I2C_MUX1_BUS3, "tmp75",    0x4a, NULL), /* U244 */
	mk_i2cdev(I2C_MUX1_BUS3, "tmp75",    0x4b, NULL), /* U278 */
	mk_i2cdev(I2C_MUX1_BUS3, "tmp75",    0x4c, NULL), /* U254 */
	mk_i2cdev(I2C_MUX1_BUS3, "tmp75",    0x4e, NULL), /* U245 */
	mk_i2cdev(I2C_MUX1_BUS3, "tmp75",    0x4f, NULL), /* U81 */

	/* devices on mux4 */
	mk_i2cdev(I2C_MUX1_BUS4, "24c02",    0x50, &psu1_50_at24),
	mk_i2cdev(I2C_MUX1_BUS4, "dps460",   0x58, NULL), /* PSU 1 fan ctrl */
	mk_i2cdev(I2C_MUX1_BUS4, "24c02",    0x51, &psu2_51_at24),
	mk_i2cdev(I2C_MUX1_BUS4, "dps460",   0x59, NULL), /* PSU 2 fan ctrl */

};

static int cpld_init(void);
static void cpld_exit(void);

static int port_mux_init(void);
static void port_mux_exit(void);

/*
 * dell_s4128t_i2c_init -- Initialize the I2C subsystem.
 */

/*static struct i2c_client *i2c_devices[ARRAY_SIZE(i2c_devices)];*/

static void i2c_exit(void)
{
	int i;

	for (i = ARRAY_SIZE(i2c_devices); --i >= 0;) {
		struct i2c_client *c = i2c_devices[i].client;

		if (c) {
			i2c_devices[i].client = NULL;
			i2c_unregister_device(c);
		}
	}
}

static int i2c_init(void)
{
	int ismt_bus;
	int i801_bus;
	int ret;
	int i;

	ismt_bus = cumulus_i2c_find_adapter(ISMT_ADAPTER_NAME);
	if (ismt_bus < 0) {
		pr_err("could not find iSMT adapter bus\n");
		ret = -ENODEV;
		goto err_exit;
	}
	i801_bus = cumulus_i2c_find_adapter(I801_ADAPTER_NAME);
	if (i801_bus < 0) {
		pr_err("could not find i801 adapter bus\n");
		ret = -ENODEV;
		goto err_exit;
	}

	num_cpld_devices = 0;
	for (i = 0; i < ARRAY_SIZE(i2c_devices); i++) {
		int bus = i2c_devices[i].bus;
		struct i2c_client *client;

		if (bus == I2C_ISMT_BUS)
			bus = ismt_bus;
		else if (bus == I2C_I801_BUS)
			bus = i801_bus;

		client = cumulus_i2c_add_client(bus,
						&i2c_devices[i].board_info);
		if (IS_ERR(client)) {
			ret = PTR_ERR(client);
			goto err_exit;
		}
		i2c_devices[i].client = client;
		if (strcmp(i2c_devices[i].board_info.type, "dummy") == 0) {
			cpld_devices[num_cpld_devices] = client;
			num_cpld_devices++;
		}
	}
	return 0;

err_exit:
	i2c_exit();
	return ret;
}

/*
 * CPLD QSFP mux
 *
 *   'cpld-qsfp-mux'  for qsfp ports 25-26
 */

struct port_mux_info_item {
	int num_items;
	struct platform_device *mux_dev;
	struct i2c_adapter **mux_adapters;
	struct i2c_client **mux_clients;
	struct i2c_board_info **mux_board_infos;
};

struct port_mux_info_struct {
	struct port_mux_info_item mux_item[SFF_MUXES];
	int num_port_muxes;
};

static struct port_mux_info_struct *port_mux_info;

/*
 * Invalid QSFP mux number
 */

#define QSFP_MUX_INVALID_CHANNEL (0xDEADBEEF)

static void free_port_mux_board_info(struct i2c_board_info *board_info)
{
	struct sff_8436_platform_data *sff8436_data =
		board_info->platform_data;
	struct eeprom_platform_data *eeprom_data = sff8436_data->eeprom_data;

	kfree(eeprom_data->label);
	kfree(eeprom_data);
	kfree(sff8436_data);
	kfree(board_info);
}

static void port_mux_info_free_data(void)
{
	int i, j;

	if (port_mux_info) {
		for (i = 0; i < port_mux_info->num_port_muxes; i++) {
			for (j = 0; j < port_mux_info->mux_item[i].num_items;
			     j++) {
				if (port_mux_info->mux_item[i].mux_clients[j])
					i2c_unregister_device(
					    port_mux_info->
					    mux_item[i].mux_clients[j]);
				if (port_mux_info->mux_item[i].mux_adapters[j])
					i2c_del_mux_adapter(
					    port_mux_info->
					    mux_item[i].mux_adapters[j]);
				if (port_mux_info->
				    mux_item[i].mux_board_infos[j])
					free_port_mux_board_info(
					    port_mux_info->
					    mux_item[i].mux_board_infos[j]);
			}
			kfree(port_mux_info->mux_item[i].mux_clients);
			kfree(port_mux_info->mux_item[i].mux_adapters);
			kfree(port_mux_info->mux_item[i].mux_board_infos);
			platform_device_unregister(port_mux_info->
						   mux_item[i].mux_dev);
		}
		kfree(port_mux_info);
		port_mux_info = NULL;
	}
}

static bool is_qsfp_port(int port)
{
	if ((port >= FIRST_QSFP_PORT_NUM) &
	    (port < FIRST_QSFP_PORT_NUM + NUM_4128_QSFP_PORTS))
		return true;
	else
		return false;
}

#define PORT_LABEL_SIZE	 8
static struct i2c_board_info *alloc_port_mux_board_info(int port)
{
	char *label = NULL;
	struct eeprom_platform_data *eeprom_data = NULL;
	struct i2c_board_info *board_info = NULL;

	label = kzalloc(PORT_LABEL_SIZE, GFP_KERNEL);
	if (!label)
		goto err_kzalloc;

	eeprom_data = kzalloc(sizeof(*eeprom_data), GFP_KERNEL);
	if (!eeprom_data)
		goto err_kzalloc;

	board_info = kzalloc(sizeof(*board_info), GFP_KERNEL);
	if (!board_info)
		goto err_kzalloc;

	snprintf(label, PORT_LABEL_SIZE, "port%u", port);
	eeprom_data->label = label;

	if (is_qsfp_port(port)) {
		struct sff_8436_platform_data *sff8436_data;

		sff8436_data = kzalloc(sizeof(*sff8436_data), GFP_KERNEL);
		if (!sff8436_data)
			goto err_kzalloc;

		sff8436_data->byte_len = 256;
		sff8436_data->flags = SFF_8436_FLAG_IRUGO;
		sff8436_data->page_size = 1;
		sff8436_data->eeprom_data = eeprom_data;
		board_info->platform_data = sff8436_data;
		strcpy(board_info->type, "sff8436");
	} else {
		struct at24_platform_data *at24_data;

		at24_data = kzalloc(sizeof(*at24_data), GFP_KERNEL);
		if (!at24_data)
			goto err_kzalloc;

		at24_data->byte_len = 512;
		at24_data->flags = AT24_FLAG_IRUGO;
		at24_data->page_size = 1;
		at24_data->eeprom_data = eeprom_data;
		board_info->platform_data = at24_data;
		strcpy(board_info->type, "24c04");
	}

	board_info->addr = 0x50;

	return board_info;

 err_kzalloc:
	kfree(board_info);
	kfree(eeprom_data);
	kfree(label);
	return NULL;
}

static int qsfp_mux_i2c_reg_write(uint8_t reg, u8 val)
{
	int rv;
	struct i2c_client *client;
	struct i2c_adapter *adap;

	adap = i2c_get_adapter(I2C_MUX1_BUS0);
	client = cpld_devices[GET_CPLD_IDX(reg)];
	reg = STRIP_CPLD_IDX(reg);

	if (adap->algo->master_xfer) {
		struct i2c_msg msg = { 0 };
		char buf[2];

		msg.addr = client->addr;
		msg.flags = 0;
		msg.len = 2;
		buf[0] = reg;
		buf[1] = val;
		msg.buf = buf;
		rv = adap->algo->master_xfer(adap, &msg, 1);
	} else {
		union i2c_smbus_data data;

		data.byte = val;
		rv = adap->algo->smbus_xfer(adap, client->addr,
					    client->flags,
					    I2C_SMBUS_WRITE,
					    reg, I2C_SMBUS_BYTE_DATA, &data);
	}
	i2c_put_adapter(adap);
	return rv;
}

static int cpld_qsfp_mux_select_chan(struct i2c_adapter *adapter,
				     void *client, u32 chan)
{
	static u32 prev_chan = QSFP_MUX_INVALID_CHANNEL;
	uint8_t val;
	int ret;

	if (likely(chan == prev_chan))
		return 0;

	prev_chan = chan;

	val = chan - I2C_QSFP_PORT_MUX + FIRST_QSFP_PORT_NUM;
	ret = qsfp_mux_i2c_reg_write(CPLD_PORT_I2C_MUX_REG, val);
	if (ret < 0)
		return ret;

	return 0;
}

static int create_port_mux(int bus, int num_mux_clients, int first_port_num,
			   int first_mux_num,
			   const char *mux_name,
			   int (select_function(struct i2c_adapter *, void *,
						u32)))
{
	struct i2c_adapter *adapter;
	struct port_mux_info_item *mux_item;
	int ret;
	int i;

	adapter = i2c_get_adapter(bus);
	if (!adapter) {
		pr_err("Could not find i2c adapter for %s bus %d.\n",
		       mux_name, bus);
		return -ENODEV;
	}

	mux_item = &port_mux_info->mux_item[port_mux_info->num_port_muxes];
	mux_item->mux_adapters = kzalloc(num_mux_clients *
					 sizeof(struct i2c_adapter *),
					 GFP_KERNEL);
	if (!mux_item->mux_adapters)
		goto err_exit;

	mux_item->mux_clients = kzalloc(num_mux_clients *
					sizeof(struct i2c_client *),
					GFP_KERNEL);
	if (!mux_item->mux_clients)
		goto err_exit_adapters;

	mux_item->mux_board_infos = kzalloc(num_mux_clients *
					    sizeof(struct qsfp_board_info *),
					    GFP_KERNEL);
	if (!mux_item->mux_board_infos)
		goto err_exit_clients;

	mux_item->num_items = num_mux_clients;

	mux_item->mux_dev = platform_device_alloc(mux_name, 0);
	if (!mux_item->mux_dev) {
		pr_err("platform_device_alloc() failed for %s.\n", mux_name);
		goto err_exit_infos;
	}

	ret = platform_device_add(mux_item->mux_dev);
	if (ret) {
		pr_err("platform_device_add() failed for %s.\n", mux_name);
		goto err_exit_device;
	}
	port_mux_info->num_port_muxes++;

	for (i = 0; i < num_mux_clients; i++) {
		struct i2c_adapter *virt_adap;
		struct i2c_board_info *eeprom_info;
		struct i2c_client *client;
		int mux_num;

		mux_num = first_mux_num + i;
		virt_adap = i2c_add_mux_adapter(adapter,
						&mux_item->mux_dev->dev,
						NULL, mux_num, mux_num,
						0, select_function, NULL);
		if (!virt_adap) {
			pr_err("%s failed to add mux for channel %u.\n",
			       mux_name, mux_num);
			goto err_exit;
		}

		mux_item->mux_adapters[i] = virt_adap;
		eeprom_info = alloc_port_mux_board_info(first_port_num + i);
		if (!eeprom_info)
			goto err_exit;

		mux_item->mux_board_infos[i] = eeprom_info;
		client = i2c_new_device(virt_adap, eeprom_info);
		if (!client) {
			pr_err("i2c_new_device failed for %s device.\n",
			       mux_name);
			goto err_exit;
		}
		mux_item->mux_clients[i] = client;
	}

	i2c_put_adapter(adapter);
	return 0;

 err_exit_device:
	platform_device_put(mux_item->mux_dev);

 err_exit_infos:
	kfree(mux_item->mux_board_infos);

 err_exit_clients:
	kfree(mux_item->mux_clients);

 err_exit_adapters:
	kfree(mux_item->mux_adapters);

 err_exit:
	port_mux_info_free_data();
	return -ENOMEM;
}

static int __init port_mux_init(void)
{
	int ret;

	port_mux_info = kzalloc(sizeof(*port_mux_info),
				GFP_KERNEL);
	if (!port_mux_info)
		return -ENOMEM;

	ret = create_port_mux(I2C_MUX1_BUS2, NUM_4128_QSFP_PORTS,
			      FIRST_QSFP_PORT_NUM, I2C_QSFP_PORT_MUX,
			      "cpld-qsfp-mux",
			      cpld_qsfp_mux_select_chan);
	if (ret)
		goto exit;

 exit:
	return ret;
}

static void __exit port_mux_exit(void)
{
	port_mux_info_free_data();
}

/*
 * CPLD
 */

static uint8_t cpld_reg_read(uint32_t reg)
{
	int cpld_idx = GET_CPLD_IDX(reg);
	uint8_t val;

	if (cpld_idx < 0 || cpld_idx >= NO_S4128T_CPLD_I2C_CLIENTS) {
		pr_err("attempt to read invalid CPLD register [0x%02X]", reg);
		return -EINVAL;
	}

	reg = STRIP_CPLD_IDX(reg);
	val = i2c_smbus_read_byte_data(cpld_devices[cpld_idx], reg);
	if (val < 0)
		pr_err("I2C read err: addr: 0x%02X, offset: 0x%02X: err: %d\n",
		       cpld_devices[cpld_idx]->addr,
		       STRIP_CPLD_IDX(reg), val);
	return val;
}

static int cpld_reg_write(uint32_t reg, uint8_t write_val)
{
	int cpld_idx = GET_CPLD_IDX(reg);
	int res = 0;

	if (cpld_idx < 0 || cpld_idx >= NO_S4128T_CPLD_I2C_CLIENTS) {
		pr_err("attempt to write invalid CPLD register [0x%02X]", reg);
		return -EINVAL;
	}

	res = i2c_smbus_write_byte_data(
		cpld_devices[cpld_idx],
		STRIP_CPLD_IDX(reg), write_val);
	if (res)
		pr_err("Write err - addr: 0x%02X, reg: 0x%02X, val: 0x%02X, err: %d\n",
		       cpld_devices[cpld_idx]->addr,
		       STRIP_CPLD_IDX(reg), write_val, res);
	return res;
}

static ssize_t bulk_power_show(struct device *dev,
			       struct device_attribute *dattr,
			       char *buf)
{
	uint8_t read_val;
	uint8_t present_l;
	uint8_t pwr_ok;
	uint8_t error;

	read_val = cpld_reg_read(CPLD_PSU_EN_STATUS_REG);
	if (strcmp(dattr->attr.name, xstr(PLATFORM_PS_NAME_0)) == 0) {
		present_l = CPLD_PSU1_PRESENT_L;
		pwr_ok	  = CPLD_PSU1_GOOD;
		error	  = CPLD_PSU1_ERROR;
	} else {
		present_l = CPLD_PSU2_PRESENT_L;
		pwr_ok	  = CPLD_PSU2_GOOD;
		error	  = CPLD_PSU2_ERROR;
	}

	if ((~read_val & present_l)) {
		sprintf(buf, PLATFORM_INSTALLED);
		if (!(read_val & pwr_ok) || (read_val & error))
			strcat(buf, ", " PLATFORM_PS_POWER_BAD);
		else
			strcat(buf, ", " PLATFORM_OK);
	} else { /* Not Present */
		sprintf(buf, PLATFORM_NOT_INSTALLED);
	}
	strcat(buf, "\n");

	return strlen(buf);
}

static ssize_t cpld_revision_show(struct device *dev,
				  struct device_attribute *dattr,
				  char *buf)
{
	uint8_t read_sys_val;
	uint8_t read_mstr_val;

	read_sys_val = cpld_reg_read(CPLD_SYS_CPLD_REV_REG);
	read_mstr_val = cpld_reg_read(CPLD_MSTR_CPLD_REV_REG);

	return sprintf(buf, "System: %d.%d, Master: %d.%d\n",
		       read_sys_val & CPLD_SYS_MJR_REV_MASK,
		       read_sys_val & CPLD_SYS_MNR_REV_MASK,
		       read_mstr_val & CPLD_SYS_MJR_REV_MASK,
		       read_mstr_val & CPLD_SYS_MNR_REV_MASK);
}

static ssize_t hw_revision_show(struct device *dev,
				struct device_attribute *dattr,
				char *buf)
{
	return sprintf(buf, "CPU: 0x%02X, Mainboard: 0x%02X\n",
		       cpld_reg_read(CPLD_CPU_BRD_REV_TYPE_REG),
		       cpld_reg_read(CPLD_MB_BRD_REV_TYPE_REG));
}

static ssize_t phy_reset_show(struct device *dev,
			      struct device_attribute *dattr,
			      char *buf)
{
	uint8_t read_phy_8_1_rst_val;

	read_phy_8_1_rst_val  = cpld_reg_read(CPLD_PHY_8_1_RST_REG);

	return sprintf(buf, "0x%02x\n",
		       (~read_phy_8_1_rst_val & 0x7F));
}

static ssize_t phy_reset_store(struct device *dev,
			       struct device_attribute *dattr,
			       const char *buf, size_t count)
{
	uint64_t  data;

	if (kstrtoull(buf, 0, &data) != 0)
		return -EINVAL;

	/* Only the lower 7 bits are valid */
	if (data > 0x7f)
		return -EINVAL;

	cpld_reg_write(CPLD_PHY_8_1_RST_REG, ~data & 0x7F);

	return count;
}

static ssize_t i2c_mux_show(struct device *dev,
			    struct device_attribute *dattr,
			    char *buf)
{
	uint8_t port_i2c_mux;

	port_i2c_mux = cpld_reg_read(CPLD_PORT_I2C_MUX_REG);

	return sprintf(buf, "0x%02x\n",
		       (port_i2c_mux & 0x3F));
}

static ssize_t i2c_mux_store(struct device *dev,
			     struct device_attribute *dattr,
			     const char *buf, size_t count)
{
	uint64_t  data;

	if (kstrtoull(buf, 0, &data) != 0)
		return -EINVAL;

	/* Only the lower 6 bits are valid */
	if (data > 0x3f)
		return -EINVAL;

	cpld_reg_write(CPLD_PORT_I2C_MUX_REG, data);

	return count;
}

static ssize_t fan_show(struct device *dev,
			struct device_attribute *dattr,
			char *buf)
{
	struct sensor_device_attribute *attr = to_sensor_dev_attr(dattr);
	int idx = attr->index;
	int val = cpld_reg_read(CPLD_FAN_TRAY_STATUS_REG);

	if (val < 0)
		return val;
	if (val & (1 << idx))
		return sprintf(buf, PLATFORM_NOT_INSTALLED "\n");
	return sprintf(buf, PLATFORM_OK "\n");
}

static SYSFS_ATTR_RO(PLATFORM_PS_NAME_0, bulk_power_show);
static SYSFS_ATTR_RO(PLATFORM_PS_NAME_1, bulk_power_show);
static SYSFS_ATTR_RO(cpld_revision,	 cpld_revision_show);
static SYSFS_ATTR_RO(hw_revision,	 hw_revision_show);
static SYSFS_ATTR_RW(phy_reset,		 phy_reset_show, phy_reset_store);
static SYSFS_ATTR_RW(port_i2c_mux,	 i2c_mux_show, i2c_mux_store);

static SENSOR_DEVICE_ATTR(fan_1, S_IRUGO, fan_show, NULL, 0);
static SENSOR_DEVICE_ATTR(fan_2, S_IRUGO, fan_show, NULL, 1);
static SENSOR_DEVICE_ATTR(fan_3, S_IRUGO, fan_show, NULL, 2);
static SENSOR_DEVICE_ATTR(fan_4, S_IRUGO, fan_show, NULL, 3);

/*
 * CPLD register bitfields with enum-like values
 *
 * For example, a 2-bit led field may have values off, green, yellow,
 * and green_blinking, corresponding to 0, 1, 2, 3.
 *
 * Maximum value string length is PLATFORM_LED_COLOR_NAME_SIZE - 1,
 * but isn't enforced anywhere.	 Also not enforced is the size
 * of the values array, which of course must match the field width.
 * So be cool.
 */

struct cpld_bf {
	const char *name;
	const char * const *values;
	u8 offset;
	u8 shift;
	u8 width;
};

#define mk_bf(_name, _offset, _shift, _width, _values) { \
	.name = #_name, \
	.offset = (_offset), \
	.shift = (_shift), \
	.width = (_width), \
	.values = (_values), \
}

#define MASK(n) ((1 << (n)) - 1)

static const char * const cpld_led_system_values[] = {
	PLATFORM_LED_OFF,
	PLATFORM_LED_GREEN,
	PLATFORM_LED_AMBER,
	PLATFORM_LED_AMBER_BLINKING,
};

static const char * const cpld_led_beacon_values[] = {
	PLATFORM_LED_OFF,
	PLATFORM_LED_BLUE_BLINKING,
};

static const char * const cpld_led_power_values[] = {
	PLATFORM_LED_OFF,
	PLATFORM_LED_AMBER,
	PLATFORM_LED_GREEN,
	PLATFORM_LED_AMBER_BLINKING,
};

static const char * const cpld_led_stack_values[] = {
	PLATFORM_LED_GREEN,
	PLATFORM_LED_OFF,
};

static const char * const cpld_led_fan_values[] = {
	PLATFORM_LED_OFF,
	PLATFORM_LED_AMBER,
	PLATFORM_LED_GREEN,
	PLATFORM_LED_AMBER_BLINKING,
};

static const char * const cpld_led_fan_tray_values[] = {
	PLATFORM_LED_OFF,
	PLATFORM_LED_GREEN,
	PLATFORM_LED_AMBER,
	PLATFORM_LED_OFF,
};

static struct cpld_bf cpld_bfs[] = {
	mk_bf(led_system,    CPLD_SYSTEM_LED_REG,   CPLD_SYSTEM_LED_SHIFT,
	      CPLD_SYSTEM_LED_WIDTH,	cpld_led_system_values),
	mk_bf(led_locator,   CPLD_SYSTEM_LED_REG,   CPLD_BEACON_LED_SHIFT,
	      CPLD_BEACON_LED_WIDTH,	cpld_led_beacon_values),
	mk_bf(led_power,     CPLD_SYSTEM_LED_REG,   CPLD_POWER_LED_SHIFT,
	      CPLD_POWER_LED_WIDTH,	cpld_led_power_values),
	mk_bf(led_master,    CPLD_SYSTEM_LED_REG,   CPLD_STACK_LED_SHIFT,
	      CPLD_STACK_LED_WIDTH,	cpld_led_stack_values),
	mk_bf(led_fan,	     CPLD_SYSTEM_LED_REG,   CPLD_FAN_LED_SHIFT,
	      CPLD_FAN_LED_WIDTH,	cpld_led_fan_values),
	mk_bf(led_fan_tray1, CPLD_FAN_TRAY_LED_REG, CPLD_FAN_TRAY1_LED_SHIFT,
	      CPLD_FAN_TRAY1_LED_WIDTH, cpld_led_fan_tray_values),
	mk_bf(led_fan_tray2, CPLD_FAN_TRAY_LED_REG, CPLD_FAN_TRAY2_LED_SHIFT,
	      CPLD_FAN_TRAY2_LED_WIDTH, cpld_led_fan_tray_values),
	mk_bf(led_fan_tray3, CPLD_FAN_TRAY_LED_REG, CPLD_FAN_TRAY3_LED_SHIFT,
	      CPLD_FAN_TRAY3_LED_WIDTH, cpld_led_fan_tray_values),
	mk_bf(led_fan_tray4, CPLD_FAN_TRAY_LED_REG, CPLD_FAN_TRAY4_LED_SHIFT,
	      CPLD_FAN_TRAY4_LED_WIDTH, cpld_led_fan_tray_values),
};

static int cpld_find_bf(struct device_attribute *dattr)
{
	int i;

	for (i = 0; i < ARRAY_SIZE(cpld_bfs); i++)
		if (strcmp(cpld_bfs[i].name, dattr->attr.name) == 0)
			return i;
	return -EINVAL;
}

static int cpld_parse_bf_value(const char *buf,
			       struct cpld_bf *bf)
{
	char str[PLATFORM_LED_COLOR_NAME_SIZE];
	int nvalues = 1 << bf->width;
	int i;

#if PLATFORM_LED_COLOR_NAME_SIZE != 20
#error PLATFORM_LED_COLOR_NAME_SIZE is supposed to be 20
#endif
	if (sscanf(buf, "%19s", str) != 1)
		return -EINVAL;
	for (i = 0; i < nvalues; i++)
		if (strcmp(str, bf->values[i]) == 0)
			return i;
	return -EINVAL;
}

static ssize_t cpld_bf_show(struct device *dev,
			    struct device_attribute *dattr,
			    char *buf)
{
	int ret;
	struct cpld_bf *bf;

	ret = cpld_find_bf(dattr);
	if (ret < 0)
		return ret;
	bf = cpld_bfs + ret;

	ret = cpld_reg_read(bf->offset);

	ret >>= bf->shift;
	ret &= MASK(bf->width);

	return sprintf(buf, "%s\n", bf->values[ret]);
}

static ssize_t cpld_bf_store(struct device *dev,
			     struct device_attribute *dattr,
			     const char *buf, size_t size)
{
	struct cpld_bf *bf;
	u8 val;
	int ret;

	ret = cpld_find_bf(dattr);
	if (ret < 0)
		return ret;
	bf = cpld_bfs + ret;

	ret = cpld_parse_bf_value(buf, bf);
	if (ret < 0)
		return ret;
	val = ret << bf->shift;

	ret = cpld_reg_read(bf->offset);
	val |= ret & ~(MASK(bf->width) << bf->shift);

	ret = cpld_reg_write(bf->offset, val);
	if (ret < 0)
		return ret;

	return size;
}

static SYSFS_ATTR_RW(led_system,  cpld_bf_show, cpld_bf_store);
static SYSFS_ATTR_RW(led_locator, cpld_bf_show, cpld_bf_store);
static SYSFS_ATTR_RW(led_power,	  cpld_bf_show, cpld_bf_store);
static SYSFS_ATTR_RW(led_master,  cpld_bf_show, cpld_bf_store);
static SYSFS_ATTR_RW(led_fan,	  cpld_bf_show, cpld_bf_store);
static SYSFS_ATTR_RW(led_fan_tray1, cpld_bf_show, cpld_bf_store);
static SYSFS_ATTR_RW(led_fan_tray2, cpld_bf_show, cpld_bf_store);
static SYSFS_ATTR_RW(led_fan_tray3, cpld_bf_show, cpld_bf_store);
static SYSFS_ATTR_RW(led_fan_tray4, cpld_bf_show, cpld_bf_store);

/*
 * QSFP definitions
 */

struct qsfp_status {
	char name[CPLD_STRING_NAME_SIZE];
	uint8_t reg;
	uint8_t active_low;
};

static struct qsfp_status cpld_qsfp_status[] = {
	{
		.name = "modsel",
		.reg = CPLD_QSFP_MODSEL_REG,
		.active_low = 1,
	},
	{
		.name = "present",
		.reg = CPLD_QSFP_PRESENT_STATUS_REG,
		.active_low = 1,
	},
	{
		.name = "interrupt",
		.reg = CPLD_QSFP_IRQ_STATUS_REG,
		.active_low = 1,
	},
	{
		.name = "lp_mode",
		.reg = CPLD_QSFP_LPMODE_REG,
	},
	{
		.name = "reset",
		.reg = CPLD_QSFP_RST_REG,
		.active_low = 1,
	},
};

static int n_qsfp_status = ARRAY_SIZE(cpld_qsfp_status);

static ssize_t qsfp_show(struct device *dev,
			 struct device_attribute *dattr,
			 char *buf)
{
	int i;
	s32 val;
	uint8_t name_len = 5; /* strlen("qsfp_"); */
	struct qsfp_status *target = NULL;

	/* find the target register */
	for (i = 0; i < n_qsfp_status; i++) {
		if (strcmp(dattr->attr.name + name_len,
			   cpld_qsfp_status[i].name) == 0) {
			target = &cpld_qsfp_status[i];
			break;
		}
	}
	if (!target)
		return -EINVAL;

	val = cpld_reg_read(target->reg);
	if (target->active_low)
		val = ~val;

	/* for S4128T, only bits 0 and 1 are valid */
	return sprintf(buf, "0x%02x\n", val & 0x03);
}

static int hex_to_int64(const char *hex_str, uint64_t *val)
{
	return (kstrtoull(hex_str, 0, val) != 0);
}

static ssize_t qsfp_store(struct device *dev,
			  struct device_attribute *dattr,
			  const char *buf, size_t count)
{
	int ret, i;
	uint8_t name_len = 5; /* strlen("qsfp_"); */
	uint64_t val;
	struct qsfp_status *target = NULL;

	if (hex_to_int64(buf, &val))
		return -EINVAL;

	/* Only bits 0 and 1 are valid */
	if (val > 0x03)
		return -EINVAL;

	/* find the target register */
	for (i = 0; i < n_qsfp_status; i++) {
		if (strcmp(dattr->attr.name + name_len,
			   cpld_qsfp_status[i].name) == 0) {
			target = &cpld_qsfp_status[i];
			break;
		}
	}
	if (!target)
		return -EINVAL;

	if (target->active_low)
		val = ~val;

	ret = cpld_reg_write(target->reg, val & 0x03);
	if (ret < 0)
		return ret;

	return count;
}

static SYSFS_ATTR_RW(qsfp_modsel, qsfp_show, qsfp_store);
static SYSFS_ATTR_RW(qsfp_reset, qsfp_show, qsfp_store);
static SYSFS_ATTR_RW(qsfp_lp_mode, qsfp_show, qsfp_store);
static SYSFS_ATTR_RO(qsfp_present, qsfp_show);
static SYSFS_ATTR_RO(qsfp_interrupt, qsfp_show);

/*
 * SYSFS attributes
 */

static struct attribute *cpld_attrs[] = {
	&dev_attr_cpld_revision.attr,
	&dev_attr_hw_revision.attr,
	&dev_attr_phy_reset.attr,

	&sensor_dev_attr_fan_1.dev_attr.attr,
	&sensor_dev_attr_fan_2.dev_attr.attr,
	&sensor_dev_attr_fan_3.dev_attr.attr,
	&sensor_dev_attr_fan_4.dev_attr.attr,

	&dev_attr_led_system.attr,
	&dev_attr_led_locator.attr,
	&dev_attr_led_power.attr,
	&dev_attr_led_master.attr,
	&dev_attr_port_i2c_mux.attr,
	&dev_attr_led_fan.attr,
	&dev_attr_led_fan_tray1.attr,
	&dev_attr_led_fan_tray2.attr,
	&dev_attr_led_fan_tray3.attr,
	&dev_attr_led_fan_tray4.attr,

	&dev_attr_psu_pwr1.attr,
	&dev_attr_psu_pwr2.attr,

	&dev_attr_qsfp_reset.attr,
	&dev_attr_qsfp_modsel.attr,
	&dev_attr_qsfp_lp_mode.attr,
	&dev_attr_qsfp_present.attr,
	&dev_attr_qsfp_interrupt.attr,

	NULL,
};

static struct attribute_group cpld_attr_group = {
	.attrs = cpld_attrs,
};

static int cpld_probe(struct platform_device *dev)
{
	int ret;

	ret = sysfs_create_group(&dev->dev.kobj, &cpld_attr_group);
	if (ret)
		pr_err("sysfs_cpld_driver_group failed for cpld driver");

	return ret;
}

static int cpld_remove(struct platform_device *dev)
{
	return 0;
}

static struct platform_driver cpld_driver = {
	.driver = {
		.name = "dellemc_s4128t_cpld",
		.owner = THIS_MODULE,
	},
	.probe = cpld_probe,
	.remove = cpld_remove,
};

static struct platform_device *cpld_device;

static int cpld_init(void)
{
	int ret;

	if (num_cpld_devices != NO_S4128T_CPLD_I2C_CLIENTS) {
		pr_err("incorrect number of cpld devices (%d); expected: %d\n",
		       num_cpld_devices, NO_S4128T_CPLD_I2C_CLIENTS);
		return -ENODEV;
	}

	ret = platform_driver_register(&cpld_driver);
	if (ret) {
		pr_err("platform_driver_register() failed for cpld device\n");
		return ret;
	}

	cpld_device = platform_device_alloc("dellemc_s4128t_cpld", 0);
	if (!cpld_device) {
		pr_err("platform_device_alloc() failed for cpld device\n");
		platform_driver_unregister(&cpld_driver);
		return -ENOMEM;
	}

	ret = platform_device_add(cpld_device);
	if (ret) {
		pr_err("platform_device_add() failed for cpld device.\n");
		platform_device_put(cpld_device);
		return ret;
	}

	pr_info("CPLD driver loaded\n");
	return 0;
}

static void cpld_exit(void)
{
	platform_driver_unregister(&cpld_driver);
	platform_device_unregister(cpld_device);
	pr_info("CPLD driver unloaded\n");
}

/*
 * Module init/exit
 */

static int __init dell_s4128t_init(void)
{
	int ret = 0;

	ret = i2c_init();
	if (ret) {
		pr_err("I2C subsystem initialization failed\n");
		return ret;
	}

	ret = cpld_init();
	if (ret) {
		pr_err("CPLD initializaton failed.\n");
		i2c_exit();
		return ret;
	}

	ret = port_mux_init();
	if (ret) {
		pr_err("Initializing QSFP mux failed.\n");
		cpld_exit();
		i2c_exit();
		return ret;
	}

	pr_info(DRIVER_NAME ": version " DRIVER_VERSION
		" successfully loaded\n");
	return 0;
}

static void __exit dell_s4128t_exit(void)
{
	port_mux_exit();
	cpld_exit();
	i2c_exit();
	pr_err(DRIVER_NAME " driver unloaded\n");
}

module_init(dell_s4128t_init);
module_exit(dell_s4128t_exit);

MODULE_AUTHOR("David Yen (dhyen@cumulusnetworks.com)");
MODULE_DESCRIPTION("Dell EMC S4128T platform support");
MODULE_VERSION(DRIVER_VERSION);
MODULE_LICENSE("GPL");
