/*
 * delta_ag9032v2_platform.c - delta-ag9032v2 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/notifier.h>
#include <linux/reboot.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/platform_data/pca953x.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 "delta_ag9032v2.h"

#define DRIVER_NAME	   "delta_ag9032v2_platform"
#define DRIVER_VERSION	   "1.0"

/*
 * This platform has two i2c busses:
 *  SMBus_0: SMBus iSMT adapter at dff5a000
 *  SMBus_1: SMBus I801 adapter at e000
 *
 * The iSMT bus is connected to the following devices:
 *   system cpld (0x31)
 *   board eeprom (0x53)
 *   master cpld (0x6a)
 *
 * The i801 bus is connected to the following devices:
 *   dimm jc42 (0x18)
 *   dimm jc42 (0x1a)
 *   dimm eeprom (0x50)
 *   dimm eeprom (0x52)
 */

enum {
	I2C_I801_BUS = 0,
	I2C_ISMT_BUS,

	CPLD_MUX_BUS1 = 11,
	CPLD_MUX_BUS2,
	CPLD_MUX_BUS3,
	CPLD_MUX_BUS4,
	CPLD_MUX_BUS5,
	CPLD_MUX_BUS6,
	CPLD_MUX_BUS7,
	CPLD_MUX_BUS8,
	CPLD_MUX_BUS9,
	CPLD_MUX_BUS10,
	CPLD_MUX_BUS11,
	CPLD_MUX_BUS12,
	CPLD_MUX_BUS13,
	CPLD_MUX_BUS14,
	CPLD_MUX_BUS15,
	CPLD_MUX_BUS16,
	CPLD_MUX_BUS17,
	CPLD_MUX_BUS18,
	CPLD_MUX_BUS19,
	CPLD_MUX_BUS20,
	CPLD_MUX_BUS21,
	CPLD_MUX_BUS22,
	CPLD_MUX_BUS23,
	CPLD_MUX_BUS24,
	CPLD_MUX_BUS25,
	CPLD_MUX_BUS26,
	CPLD_MUX_BUS27,
	CPLD_MUX_BUS28,
	CPLD_MUX_BUS29,
	CPLD_MUX_BUS30,
	CPLD_MUX_BUS31,
	CPLD_MUX_BUS32,
};

mk_eeprom(spd1,	 50, 256,
	  AT24_FLAG_READONLY | AT24_FLAG_IRUGO | AT24_FLAG_DISABLE_I2CBLOCK);
mk_eeprom(spd2,	 52, 256,
	  AT24_FLAG_READONLY | AT24_FLAG_IRUGO | AT24_FLAG_DISABLE_I2CBLOCK);

mk_eeprom(board, 53, 256, AT24_FLAG_IRUGO);

/* all of the devices that exist on one of the CPLD muxes */
mk_qsfp_port_eeprom(port1,  50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port2,  50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port3,  50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port4,  50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port5,  50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port6,  50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port7,  50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port8,  50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port9,  50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port10, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port11, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port12, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port13, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port14, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port15, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port16, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port17, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port18, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port19, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port20, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port21, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port22, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port23, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port24, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port25, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port26, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port27, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port28, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port29, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port30, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port31, 50, 256,  SFF_8436_FLAG_IRUGO);
mk_qsfp_port_eeprom(port32, 50, 256,  SFF_8436_FLAG_IRUGO);

/*
 * 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 the i801 bus */
	mk_i2cdev(I2C_I801_BUS, "spd",	 0x50, &spd1_50_at24),
	mk_i2cdev(I2C_I801_BUS, "spd",	 0x52, &spd2_52_at24),

	/* devices on the iSMT bus */
	mk_i2cdev(I2C_ISMT_BUS, "cpld",	 0x31, NULL),
	mk_i2cdev(I2C_ISMT_BUS, "24c02", 0x53, &board_53_at24),
	mk_i2cdev(I2C_ISMT_BUS, "cpld",	 0x6a, NULL),
};

static struct i2c_client *cpld_clients[NUM_CPLDS];

static struct platform_i2c_device_info cpld_mux_devices[] = {
	mk_i2cdev(CPLD_MUX_BUS1,  "sff8436", 0x50, &port1_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS2,  "sff8436", 0x50, &port2_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS3,  "sff8436", 0x50, &port3_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS4,  "sff8436", 0x50, &port4_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS5,  "sff8436", 0x50, &port5_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS6,  "sff8436", 0x50, &port6_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS7,  "sff8436", 0x50, &port7_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS8,  "sff8436", 0x50, &port8_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS9,  "sff8436", 0x50, &port9_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS10, "sff8436", 0x50, &port10_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS11, "sff8436", 0x50, &port11_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS12, "sff8436", 0x50, &port12_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS13, "sff8436", 0x50, &port13_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS14, "sff8436", 0x50, &port14_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS15, "sff8436", 0x50, &port15_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS16, "sff8436", 0x50, &port16_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS17, "sff8436", 0x50, &port17_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS18, "sff8436", 0x50, &port18_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS19, "sff8436", 0x50, &port19_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS20, "sff8436", 0x50, &port20_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS21, "sff8436", 0x50, &port21_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS22, "sff8436", 0x50, &port22_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS23, "sff8436", 0x50, &port23_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS24, "sff8436", 0x50, &port24_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS25, "sff8436", 0x50, &port25_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS26, "sff8436", 0x50, &port26_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS27, "sff8436", 0x50, &port27_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS28, "sff8436", 0x50, &port28_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS29, "sff8436", 0x50, &port29_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS30, "sff8436", 0x50, &port30_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS31, "sff8436", 0x50, &port31_50_sff8436),
	mk_i2cdev(CPLD_MUX_BUS32, "sff8436", 0x50, &port32_50_sff8436),
};

/*
 * Utility functions for I2C
 */

static void i2c_exit(void)
{
	int i;

	for (i = ARRAY_SIZE(i2c_devices); --i >= 0;) {
		if (i2c_devices[i].client) {
			i2c_unregister_device(i2c_devices[i].client);
			i2c_devices[i].client = NULL;
		}
	}
}

static struct i2c_adapter *get_adapter(int bus)
{
	int bail = 10;
	struct i2c_adapter *adapter;

	for (; bail; bail--) {
		adapter = i2c_get_adapter(bus);
		if (adapter)
			return adapter;

		msleep(100);
	}
	return NULL;
}

static int i2c_init(void)
{
	int ismt_bus;
	int i801_bus;
	int i;
	int cpld_count = 0;
	int ret;
	int bus;
	struct i2c_client *client;

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

	for (i = 0; i < ARRAY_SIZE(i2c_devices); i++) {
		bus = i2c_devices[i].bus;

		if (bus == I2C_ISMT_BUS)
			bus = ismt_bus;
		if (bus == I2C_I801_BUS)
			bus = i801_bus;
		client = cumulus_i2c_add_client(bus,
						&i2c_devices[i].board_info);
		if (IS_ERR(client))
			return PTR_ERR(client);

		i2c_devices[i].client = client;
		if (strcmp(i2c_devices[i].board_info.type, "cpld") == 0)
			cpld_clients[cpld_count++] = client;
	}
	return 0;
}

/*
 * CPLD driver
 */

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

static int cpld_mux_init(void);
static void cpld_mux_exit(void);

static int add_cpld_mux_devices(void);

static struct platform_device *cpld_device;

#define CPLD_NAME "delta_ag9032v2_cpld"

static uint8_t cpld_reg_read(uint32_t reg)
{
	int cpld_idx = GET_CPLD_IDX(reg);
	uint8_t val;
	struct i2c_client *client;

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

	reg = STRIP_CPLD_IDX(reg);
	client = cpld_clients[cpld_idx];

	val = i2c_smbus_read_byte_data(client, reg);
	if (val < 0)
		pr_err("CPLD read error - addr: 0x%02X, reg: 0x%02X\n",
		       client->addr, reg);
	return val;
}

static s32 cpld_reg_write(uint32_t reg, uint8_t write_val)
{
	int cpld_idx = GET_CPLD_IDX(reg);
	int res;
	struct i2c_client *client;

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

	reg = STRIP_CPLD_IDX(reg);
	client = cpld_clients[cpld_idx];
	res = i2c_smbus_write_byte_data(client, reg, write_val);
	if (res < 0)
		pr_err("CPLD write error - addr: 0x%02X, reg: 0x%02X, val: 0x%02X\n",
		       client->addr, reg, write_val);
	return res;
}

/* System CPLD Registers */

static ssize_t system_cpld_rev_show(struct device *dev,
				    struct device_attribute *dattr,
				    char *buf)
{
	return sprintf(buf, "0x%02X\n",
		cpld_reg_read(SYSTEM_CPLD_SYS_CPLD_REV_REG));
}

static ssize_t cpu_board_ver_show(struct device *dev,
				  struct device_attribute *dattr,
				  char *buf)
{
	return sprintf(buf, "0x%02X\n",
		cpld_reg_read(SYSTEM_CPLD_CPU_BRD_REV_REG));
}

static ssize_t eeprom_wp_show(struct device *dev,
			      struct device_attribute *dattr,
			      char *buf)
{
	u8 data;

	data = cpld_reg_read(SYSTEM_CPLD_SYS_EEPROM_WP_REG);
	return sprintf(buf, "%c\n",
		(data & SYSTEM_CPLD_SYSTEM_ID_EEPROM_WP_MASK) ? '1' : '0');
}

static ssize_t eeprom_wp_store(struct device *dev,
			       struct device_attribute *dattr,
			       const char *buf, size_t count)
{
	long protect = 0;
	u8 data;

	if (kstrtol(buf, 0, &protect) < 0)
		return -EINVAL;
	data = cpld_reg_read(SYSTEM_CPLD_SYS_EEPROM_WP_REG);
	if (protect)
		data |= SYSTEM_CPLD_SYSTEM_ID_EEPROM_WP_MASK;
	else
		data &= ~SYSTEM_CPLD_SYSTEM_ID_EEPROM_WP_MASK;
	cpld_reg_write(SYSTEM_CPLD_SYS_EEPROM_WP_REG, data);

	return count;
}

static SYSFS_ATTR_RO(system_cpld_rev, system_cpld_rev_show);
static SYSFS_ATTR_RO(cpu_board_ver,   cpu_board_ver_show);
static SYSFS_ATTR_RW(eeprom_wp,	      eeprom_wp_show,	    eeprom_wp_store);

/* Master CPLD Registers */

static ssize_t platform_type_show(struct device *dev,
				  struct device_attribute *dattr,
				  char *buf)
{
	return sprintf(buf, "0x%02X\n",
			cpld_reg_read(MASTER_CPLD_HW_REVISION_REG));
}

static SYSFS_ATTR_RO(platform_type,   platform_type_show);

static ssize_t psu_present_show(struct device *dev,
				struct device_attribute *dattr,
				char *buf)
{
	struct sensor_device_attribute *attr = to_sensor_dev_attr(dattr);
	u8 data;

	data = cpld_reg_read(MASTER_CPLD_PSU_STATUS_REG) & attr->index;
	return sprintf(buf, "%c\n",
		       (data) ? '0' : '1');
}

static SENSOR_DEVICE_ATTR_RO(psu_pwr1_present, psu_present_show, 0x80);
static SENSOR_DEVICE_ATTR_RO(psu_pwr2_present, psu_present_show, 0x08);

static ssize_t psu_all_ok_show(struct device *dev,
			       struct device_attribute *dattr,
			       char *buf)
{
	struct sensor_device_attribute *attr = to_sensor_dev_attr(dattr);
	u8 data;

	data = cpld_reg_read(MASTER_CPLD_PSU_STATUS_REG) & attr->index;
	return sprintf(buf, "%c\n",
		       (data) ? '1' : '0');
}

static SENSOR_DEVICE_ATTR_RO(psu_pwr1_all_ok, psu_all_ok_show, 0x40);
static SENSOR_DEVICE_ATTR_RO(psu_pwr2_all_ok, psu_all_ok_show, 0x04);

#define NUM_QSFP28_REGS 4

struct qsfp28_reg_info {
	char name[NAME_SIZE];
	u8   regs[NUM_QSFP28_REGS];
	int  active_low;
};

static struct qsfp28_reg_info qsfp28_reg[] = {
	{
		.name = "qsfp28_modsel",
		.regs = { MASTER_CPLD_QSFP28_MODSEL_1_8_REG,
			  MASTER_CPLD_QSFP28_MODSEL_9_16_REG,
			  MASTER_CPLD_QSFP28_MODSEL_17_24_REG,
			  MASTER_CPLD_QSFP28_MODSEL_25_32_REG,
		},
		.active_low = 1,
	},
	{
		.name = "qsfp28_lpmode",
		.regs = { MASTER_CPLD_QSFP28_LPMODE_1_8_REG,
			  MASTER_CPLD_QSFP28_LPMODE_9_16_REG,
			  MASTER_CPLD_QSFP28_LPMODE_17_24_REG,
			  MASTER_CPLD_QSFP28_LPMODE_25_32_REG,
		},
	},
	{
		.name = "qsfp28_present",
		.regs = { MASTER_CPLD_QSFP28_PRESENT_1_8_REG,
			  MASTER_CPLD_QSFP28_PRESENT_9_16_REG,
			  MASTER_CPLD_QSFP28_PRESENT_17_24_REG,
			  MASTER_CPLD_QSFP28_PRESENT_25_32_REG,
		},
		.active_low = 1,
	},
	{
		.name = "qsfp28_reset",
		.regs = { MASTER_CPLD_QSFP28_RESETN_1_8_REG,
			  MASTER_CPLD_QSFP28_RESETN_9_16_REG,
			  MASTER_CPLD_QSFP28_RESETN_17_24_REG,
			  MASTER_CPLD_QSFP28_RESETN_25_32_REG,
		},
		.active_low = 1,
	},
};

int num_qsfp28_info = ARRAY_SIZE(qsfp28_reg);

static ssize_t qsfp28_show(struct device *dev,
			   struct device_attribute *dattr,
			   char *buf)
{
	uint8_t tmp;
	uint32_t data;
	int i;
	struct qsfp28_reg_info *target = NULL;

	for (i = 0; i < num_qsfp28_info; i++) {
		if (strcmp(dattr->attr.name,
			   qsfp28_reg[i].name) == 0) {
			target = &qsfp28_reg[i];
			break;
		}
	}
	if (!target)
		return sprintf(buf, "undefined\n");

	data = 0;
	for (i = 0; i < NUM_QSFP28_REGS; i++) {
		tmp = cpld_reg_read(target->regs[i]);
		if (target->active_low)
			tmp = ~tmp;
		/* the registers have the highest numbered port in
		 * the first bit, we have to reverse the order.
		 */
		tmp = (tmp & 0xf0) >> 4 | (tmp & 0x0f) << 4;
		tmp = (tmp & 0xcc) >> 2 | (tmp & 0x33) << 2;
		tmp = (tmp & 0xaa) >> 1 | (tmp & 0x55) << 1;
		data |= ((uint32_t)tmp << (8 * i));
	}
	return sprintf(buf, "0x%08X\n", data);
}

static ssize_t qsfp28_store(struct device *dev,
			    struct device_attribute *dattr,
			    const char *buf, size_t count)
{
	uint8_t tmp;
	uint32_t val;
	int i;
	int ret;
	struct qsfp28_reg_info *target = NULL;

	for (i = 0; i < num_qsfp28_info; i++) {
		if (strcmp(dattr->attr.name,
			   qsfp28_reg[i].name) == 0) {
			target = &qsfp28_reg[i];
			break;
		}
	}
	if (!target)
		return -EINVAL;

	ret = kstrtou32(buf, 0, &val);
	if (ret < 0)
		return ret;
	for (i = 0; i < NUM_QSFP28_REGS; i++) {
		tmp = val >> (i * 8) & 0xff;
		if (target->active_low)
			tmp = ~tmp;
		/* the registers have the highest numbered port in
		 * the first bit, we have to reverse the order.
		 */
		tmp = (tmp & 0xf0) >> 4 | (tmp & 0x0f) << 4;
		tmp = (tmp & 0xcc) >> 2 | (tmp & 0x33) << 2;
		tmp = (tmp & 0xaa) >> 1 | (tmp & 0x55) << 1;
		cpld_reg_write(target->regs[i], tmp);
	}
	return count;
}

SYSFS_ATTR_RW(qsfp28_modsel,  qsfp28_show, qsfp28_store);
SYSFS_ATTR_RW(qsfp28_lpmode,  qsfp28_show, qsfp28_store);
SYSFS_ATTR_RO(qsfp28_present, qsfp28_show);
SYSFS_ATTR_RW(qsfp28_reset,   qsfp28_show, qsfp28_store);

/* LEDs */

struct led_info {
	char name[NAME_SIZE];
	unsigned int offset;
	uint8_t mask;
	int n_colors;
	struct led_color colors[8];
};

static struct led_info cpld_leds[] = {
	{
		.name = "led_fan_tray1",
		.offset = MASTER_CPLD_FAN_1_LED_REG,
		.mask = MASTER_CPLD_FAN1_LED_MASK,
		.n_colors = 3,
		.colors = {
			{ PLATFORM_LED_OFF,   MASTER_CPLD_FAN1_LED_OFF },
			{ PLATFORM_LED_GREEN, MASTER_CPLD_FAN1_LED_GREEN },
			{ PLATFORM_LED_RED,   MASTER_CPLD_FAN1_LED_RED },
		},
	},
	{
		.name = "led_fan_tray2",
		.offset = MASTER_CPLD_FAN_2_5_LED_REG,
		.mask = MASTER_CPLD_FAN2_LED_MASK,
		.n_colors = 3,
		.colors = {
			{ PLATFORM_LED_OFF,   MASTER_CPLD_FAN2_LED_OFF },
			{ PLATFORM_LED_GREEN, MASTER_CPLD_FAN2_LED_GREEN },
			{ PLATFORM_LED_RED,   MASTER_CPLD_FAN2_LED_RED },
		},
	},
	{
		.name = "led_fan_tray3",
		.offset = MASTER_CPLD_FAN_2_5_LED_REG,
		.mask = MASTER_CPLD_FAN3_LED_MASK,
		.n_colors = 3,
		.colors = {
			{ PLATFORM_LED_OFF,   MASTER_CPLD_FAN3_LED_OFF },
			{ PLATFORM_LED_GREEN, MASTER_CPLD_FAN3_LED_GREEN },
			{ PLATFORM_LED_RED,   MASTER_CPLD_FAN3_LED_RED },
		},
	},
	{
		.name = "led_fan_tray4",
		.offset = MASTER_CPLD_FAN_2_5_LED_REG,
		.mask = MASTER_CPLD_FAN4_LED_MASK,
		.n_colors = 3,
		.colors = {
			{ PLATFORM_LED_OFF,   MASTER_CPLD_FAN4_LED_OFF },
			{ PLATFORM_LED_GREEN, MASTER_CPLD_FAN4_LED_GREEN },
			{ PLATFORM_LED_RED,   MASTER_CPLD_FAN4_LED_RED },
		},
	},
	{
		.name = "led_fan_tray5",
		.offset = MASTER_CPLD_FAN_2_5_LED_REG,
		.mask = MASTER_CPLD_FAN5_LED_MASK,
		.n_colors = 3,
		.colors = {
			{ PLATFORM_LED_OFF,   MASTER_CPLD_FAN5_LED_OFF },
			{ PLATFORM_LED_GREEN, MASTER_CPLD_FAN5_LED_GREEN },
			{ PLATFORM_LED_RED,   MASTER_CPLD_FAN5_LED_RED },
		},
	},

	{
		.name = "led_system",
		.offset = MASTER_CPLD_SYSTEM_LED_REG,
		.mask = MASTER_CPLD_SYS_LED_MASK,
		.n_colors = 4,
		.colors = {
			{ PLATFORM_LED_OFF,   MASTER_CPLD_SYS_LED_OFF },
			{ PLATFORM_LED_GREEN, MASTER_CPLD_SYS_LED_GREEN },
			{ PLATFORM_LED_GREEN_BLINKING,
					  MASTER_CPLD_SYS_LED_GREEN_BLINKING },
			{ PLATFORM_LED_RED,   MASTER_CPLD_SYS_LED_RED },
		},
	},
	{
		.name = "led_fan",
		.offset = MASTER_CPLD_SYSTEM_LED_REG,
		.mask = MASTER_CPLD_FAN_LED_MASK,
		.n_colors = 3,
		.colors = {
			{ PLATFORM_LED_OFF,   MASTER_CPLD_FAN_LED_OFF },
			{ PLATFORM_LED_GREEN, MASTER_CPLD_FAN_LED_GREEN },
			{ PLATFORM_LED_RED,   MASTER_CPLD_FAN_LED_RED },
		},
	},
	{
		.name = "led_psu1",
		.offset = MASTER_CPLD_SYSTEM_LED_REG,
		.mask = MASTER_CPLD_PSU1_LED_MASK,
		.n_colors = 3,
		.colors = {
			{ PLATFORM_LED_OFF,   MASTER_CPLD_PSU1_LED_OFF },
			{ PLATFORM_LED_GREEN, MASTER_CPLD_PSU1_LED_GREEN },
			{ PLATFORM_LED_RED,   MASTER_CPLD_PSU1_LED_RED },
		},
	},
	{
		.name = "led_psu2",
		.offset = MASTER_CPLD_SYSTEM_LED_REG,
		.mask = MASTER_CPLD_PSU2_LED_MASK,
		.n_colors = 3,
		.colors = {
			{ PLATFORM_LED_OFF,   MASTER_CPLD_PSU2_LED_OFF },
			{ PLATFORM_LED_GREEN, MASTER_CPLD_PSU2_LED_GREEN },
			{ PLATFORM_LED_RED,   MASTER_CPLD_PSU2_LED_RED },
		},
	},
};

static int n_leds = ARRAY_SIZE(cpld_leds);

static ssize_t led_show(struct device *dev,
			struct device_attribute *dattr,
			char *buf)
{
	uint8_t tmp;
	int i;
	struct led_info *target = NULL;

	/* find the target led */
	for (i = 0; i < n_leds; i++) {
		if (strcmp(dattr->attr.name, cpld_leds[i].name) == 0) {
			target = &cpld_leds[i];
			break;
		}
	}
	if (!target)
		return sprintf(buf, "undefined\n");

	/* read the register */
	tmp = cpld_reg_read(target->offset);

	/* find the color */
	tmp &= target->mask;
	for (i = 0; i < target->n_colors; i++) {
		if (tmp == target->colors[i].value)
			break;
	}
	if (i == target->n_colors)
		return sprintf(buf, "undefined\n");
	else
		return sprintf(buf, "%s\n", target->colors[i].name);
}

static ssize_t led_store(struct device *dev,
			 struct device_attribute *dattr,
			 const char *buf, size_t count)
{
	uint8_t tmp;
	int i;
	int ret;
	struct led_info *target = NULL;
	char raw[NAME_SIZE];

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

	/* find the color */
	if (sscanf(buf, "%19s", raw) <= 0)
		return -EINVAL;
	for (i = 0; i < target->n_colors; i++) {
		if (strcmp(raw, target->colors[i].name) == 0)
			break;
	}
	if (i == target->n_colors)
		return -EINVAL;

	/* set the new value */
	tmp = cpld_reg_read(target->offset);
	tmp &= ~target->mask;
	tmp |= target->colors[i].value;

	ret = cpld_reg_write(target->offset, tmp);
	if (ret < 0)
		return ret;

	return count;
}

static SYSFS_ATTR_RW(led_fan_tray1, led_show, led_store);
static SYSFS_ATTR_RW(led_fan_tray2, led_show, led_store);
static SYSFS_ATTR_RW(led_fan_tray3, led_show, led_store);
static SYSFS_ATTR_RW(led_fan_tray4, led_show, led_store);
static SYSFS_ATTR_RW(led_fan_tray5, led_show, led_store);

static SYSFS_ATTR_RW(led_system,    led_show, led_store);
static SYSFS_ATTR_RW(led_fan,	    led_show, led_store);
static SYSFS_ATTR_RW(led_psu1,	    led_show, led_store);
static SYSFS_ATTR_RW(led_psu2,	    led_show, led_store);

static struct attribute *cpld_attrs[] = {
	&dev_attr_system_cpld_rev.attr,
	&dev_attr_cpu_board_ver.attr,
	&dev_attr_eeprom_wp.attr,

	&dev_attr_platform_type.attr,

	&sensor_dev_attr_psu_pwr1_present.dev_attr.attr,
	&sensor_dev_attr_psu_pwr1_all_ok.dev_attr.attr,
	&sensor_dev_attr_psu_pwr2_present.dev_attr.attr,
	&sensor_dev_attr_psu_pwr2_all_ok.dev_attr.attr,

	&dev_attr_qsfp28_modsel.attr,
	&dev_attr_qsfp28_lpmode.attr,
	&dev_attr_qsfp28_present.attr,
	&dev_attr_qsfp28_reset.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_led_fan_tray5.attr,

	&dev_attr_led_system.attr,
	&dev_attr_led_fan.attr,
	&dev_attr_led_psu1.attr,
	&dev_attr_led_psu2.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)
{
	struct kobject *kobj = &dev->dev.kobj;

	sysfs_remove_group(kobj, &cpld_attr_group);

	dev_info(&dev->dev, "device removed\n");
	return 0;
}

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

static int cpld_init(void)
{
	int ret;

	ret = platform_driver_register(&cpld_driver);
	if (ret) {
		pr_err("platform_driver_register() failed for CPLD device");
		goto err_drvr;
	}

	cpld_device = platform_device_alloc(CPLD_NAME, 0);
	if (!cpld_device) {
		pr_err("platform_device_alloc() failed for CPLD device");
		ret = -ENOMEM;
		goto err_dev_alloc;
	}

	ret = platform_device_add(cpld_device);
	if (ret) {
		pr_err("platform_device_add() failed for CPLD device.\n");
		goto err_dev_add;
	}

	ret = cpld_mux_init();
	if (ret) {
		pr_err("Initializing CPLD mux failed.\n");
		goto err_dev_add;
	}

	ret = add_cpld_mux_devices();
	if (ret) {
		pr_err("Initializing CPLD devices failed.\n");
		goto err_dev_add;
	};

	return 0;

err_dev_add:
	platform_device_put(cpld_device);

err_dev_alloc:
	platform_driver_unregister(&cpld_driver);

err_drvr:
	return ret;
}

static void cpld_exit(void)
{
	cpld_mux_exit();
	platform_driver_unregister(&cpld_driver);
	platform_device_unregister(cpld_device);
}

/*
 * CPLD Mux Driver
 */

#define MUX_NAME_LEN		     20
#define CPLD_MUX_INVALID_CHANNEL     0xDEADBEEF

struct mux_info_item {
	char mux_name[MUX_NAME_LEN];
	int num_busses;
	int first_bus_num;
	int parent_bus;
	u8  mux_reg;
	u8  mux_mask;
	u8  mux_shift;
	u8  oe_mask;
	u8  oe_enabled;
	u32 prev_chan;
	u8  *channel_lookup;
};

static u8 qsfp28_channel_lookup[] = {
	0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
	0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
	0x10, 0x11, 0x12, 0x13,	0x14, 0x15, 0x16, 0x17,
	0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
};

static struct mux_info_item cpld_mux_info = {
		.mux_name = "cpld-qsfp28-mux",
		.num_busses = 32,
		.first_bus_num = CPLD_MUX_BUS1,
		.parent_bus = I2C_ISMT_BUS,
		.mux_reg = MASTER_CPLD_QSFP28_MUX_REG,
		.mux_mask = MASTER_CPLD_PORT_I2C_SEL_MASK,
		.mux_shift = MASTER_CPLD_PORT_I2C_SEL_SHIFT,
		.oe_mask = MASTER_CPLD_PORT_I2C_OE_MASK,
		.oe_enabled = MASTER_CPLD_PORT_I2C_OE_ENABLED,
		.prev_chan = CPLD_MUX_INVALID_CHANNEL,
		.channel_lookup = qsfp28_channel_lookup,
};

/* mux_info_item for the qsfp28 mux */

struct mux_data_item {
	int num_clients;
	struct platform_device *mux_dev;
	struct i2c_adapter **mux_adapters;
	struct i2c_client **mux_clients;
};

/* a container for the mux */

static struct mux_data_item cpld_mux_data;

#define NUM_CPLD_MUX_DEVICES ARRAY_SIZE(cpld_mux_devices)

static struct mutex cpld_mux_mutex;

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

	adap = get_adapter(I2C_ISMT_BUS);
	if (!adap) {
		pr_err("Unable to get mux adapter %u.\n",
		       I2C_ISMT_BUS);
		return -ENODEV;
	}

	client = cpld_clients[GET_CPLD_IDX(command)];
	command = STRIP_CPLD_IDX(command);

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

		msg.addr = client->addr;
		msg.flags = I2C_SMBUS_WRITE;
		msg.len = 2;
		buf[0] = command;
		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,
			command, I2C_SMBUS_BYTE_DATA, &data);
	}
	i2c_put_adapter(adap);
	return rv;
}

s32 cpld_mux_i2c_reg_read(uint8_t command)
{
	s32 rv;
	struct i2c_client *client;
	struct i2c_adapter *adap;
	u8 data;

	adap = get_adapter(I2C_ISMT_BUS);
	if (!adap) {
		pr_err("Unable to get mux adapter %u.\n",
		       I2C_ISMT_BUS);
		return -ENODEV;
	}

	client = cpld_clients[GET_CPLD_IDX(command)];
	command = STRIP_CPLD_IDX(command);

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

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

		rv = adap->algo->smbus_xfer(adap, client->addr,
			client->flags,
			I2C_SMBUS_READ,
			command, I2C_SMBUS_BYTE_DATA, &tdata);
		data = tdata.byte;
	}
	i2c_put_adapter(adap);
	if (!rv)
		rv = data;
	return rv;
}

static int cpld_mux_select_chan(struct i2c_adapter *adapter,
				void *client, u32 chan)
{
	struct mux_info_item *mux_info = (struct mux_info_item *)client;
	uint8_t val;
	int ret;

	if (mux_info->channel_lookup)
		chan = mux_info->channel_lookup[chan];
	if (likely(chan == mux_info->prev_chan))
		return 0;

	mutex_lock(&cpld_mux_mutex);
	mux_info->prev_chan = chan;
	val = cpld_mux_i2c_reg_read(mux_info->mux_reg);

	val &= ~mux_info->oe_mask;
	val |= mux_info->oe_enabled;

	val &= ~mux_info->mux_mask;
	val |= (chan << mux_info->mux_shift);

	ret = cpld_mux_i2c_reg_write(mux_info->mux_reg, val);
	mutex_unlock(&cpld_mux_mutex);

	if (ret < 0)
		return ret;

	return 0;
}

static void cpld_mux_exit(void)
{
	int i;
	struct mux_data_item *mux_data;

	mux_data = &cpld_mux_data;
	for (i = mux_data->num_clients; --i >= 0;) {
		if (mux_data->mux_adapters[i])
			i2c_del_mux_adapter(
					    mux_data->mux_adapters[i]);
		if (mux_data->mux_clients[i])
			i2c_unregister_device(
					      mux_data->mux_clients[i]);
	}
	kfree(mux_data->mux_clients);
	kfree(mux_data->mux_adapters);
	platform_device_unregister(mux_data->mux_dev);
}

static int  cpld_mux_init(void)
{
	struct i2c_adapter *adapter;
	int ret, i;
	struct mux_data_item *mux_data;
	struct mux_info_item *mux_info;

	mutex_init(&cpld_mux_mutex);
	mux_info = &cpld_mux_info;
	mux_data = &cpld_mux_data;
	adapter = i2c_get_adapter(mux_info->parent_bus);
	if (!adapter) {
		pr_err("Could not find i2c adapter for %s bus %d.\n",
		       mux_info->mux_name, mux_info->parent_bus);
		return -ENODEV;
	}

	mux_data->mux_clients = kzalloc(mux_info->num_busses *
			sizeof(struct i2c_client *), GFP_KERNEL);
	if (!mux_data->mux_clients)
		goto err_exit;

	mux_data->mux_adapters = kzalloc(mux_info->num_busses *
			 sizeof(struct i2c_adapter *), GFP_KERNEL);
	if (!mux_data->mux_adapters)
		goto err_exit_adapters;

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

	ret = platform_device_add(mux_data->mux_dev);
	if (ret) {
		pr_err("platform_device_add() failed for %s.\n",
		       mux_info->mux_name);
		goto err_exit_dev_add;
	}
	mux_data->num_clients = mux_info->num_busses;

	for (i = 0; i < mux_info->num_busses; i++) {
		mux_data->mux_adapters[i] =
			i2c_add_mux_adapter(adapter, &mux_data->mux_dev->dev,
					    mux_info,
					    mux_info->first_bus_num + i, i, 0,
					    cpld_mux_select_chan, NULL);
		if (!mux_data->mux_adapters[i]) {
			pr_err("%s failed to add mux adapter for bus %u.\n",
			       mux_info->mux_name,
			       mux_info->first_bus_num + i);
			goto err_exit;
		}
	}

	return 0;

err_exit_dev_add:
	platform_device_put(mux_data->mux_dev);

err_exit_dev:
	kfree(mux_data->mux_adapters);

err_exit_adapters:
	kfree(mux_data->mux_clients);

err_exit:
	cpld_mux_exit();
	return -ENOMEM;
}

static int add_cpld_mux_devices(void)
{
	int i;
	struct i2c_client *client;

	for (i = 0; i < NUM_CPLD_MUX_DEVICES; i++) {
		client = cumulus_i2c_add_client(cpld_mux_devices[i].bus,
						&cpld_mux_devices[i].
						board_info);
		if (IS_ERR(client))
			return PTR_ERR(client);
		cpld_mux_devices[i].client = client;
	}
	return 0;
}

static int reboot_callback(struct notifier_block *self,
			   unsigned long val,
			   void *data)
{
	u8 reset;

	if (val == SYS_RESTART) {
		reset = cpld_reg_read(MASTER_CPLD_SW_RESET_REG);
		cpld_reg_write(MASTER_CPLD_SW_RESET_REG,
			       reset & ~MASTER_CPLD_RESET_N_B56870_MASK);
		usleep_range(50, 100);
		cpld_reg_write(MASTER_CPLD_SW_RESET_REG, reset);
	}
	return NOTIFY_DONE;
}

static struct notifier_block reboot_notifier = {
	.notifier_call = reboot_callback,
};

/*
 * module init/exit
 */

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

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

	ret = register_reboot_notifier(&reboot_notifier);
	if (ret) {
		pr_err(DRIVER_NAME ": unable to register reboot notifier.\n");
		i2c_exit();
		return ret;
	}

	ret = cpld_init();
	if (ret) {
		pr_err("CPLD initialization failed.\n");
		unregister_reboot_notifier(&reboot_notifier);
		i2c_exit();
		return ret;
	}

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

static void __exit delta_ag9032v2_exit(void)
{
	cpld_exit();
	i2c_exit();
	unregister_reboot_notifier(&reboot_notifier);
	pr_info(DRIVER_NAME " driver unloaded\n");
}

module_init(delta_ag9032v2_init);
module_exit(delta_ag9032v2_exit);

MODULE_AUTHOR("David Yen (dhyen@cumulusnetworks.com)");
MODULE_DESCRIPTION("Delta AG9032v2 Platform Support");
MODULE_VERSION(DRIVER_VERSION);
MODULE_LICENSE("GPL");
