// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (C) 2019 CERN
 * Author: Federico Vaga <federico.vaga@cern.ch>
 */
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/memory.h>
#include <linux/fmc.h>
#include <linux/ipmi/fru.h>
#include "fmc-internal.h"
#include "fmc-compat.h"

#define FRU_EEPROM_NAME "fru_eeprom"

/**
 * Default EEPROM type according to the standard.
 */
#define FMC_EEPROM_TYPE_DEFAULT "24c02"

/**
 * Setup function for the AT24C02 EEPROM. What we need to do here is to
 * quickly validate the EEPROM content. The EEPROM should contain a valid
 * FRU.
 */
static void fmc_slot_eeprom_setup(struct memory_accessor *macc, void *context)
{
	struct fmc_slot *slot = context;

	slot->macc = macc;
}

/**
 * Default configuration for AT24C02 EEPROM type
 */
static const struct at24_platform_data at24_24c02 = {
	.byte_len = 256,
	.page_size = 8,
	.flags = 0,
	.setup = fmc_slot_eeprom_setup,
};

/**
 * Initialize I2C EEPROM info with standad values
 */
static void fmc_slot_eeprom_init(struct fmc_slot *slot,
				 struct i2c_board_info *info,
				 const char *name)
{
	strncpy(info->type, name, I2C_NAME_SIZE);
	info->addr = FMC_EEPROM_ADDR_SPACE;
	info->platform_data = &slot->at24_data;
}

static void fmc_slot_eeprom_init_default(struct fmc_slot *slot,
				  struct i2c_board_info *info)
{
	memset(info, 0, sizeof(*info));
	fmc_slot_eeprom_init(slot, info, FMC_EEPROM_TYPE_DEFAULT);
	memcpy(&slot->at24_data, &at24_24c02, sizeof(slot->at24_data));
	slot->at24_data.context = slot;
}

/**
 * Read from EEPROM
 */
ssize_t fmc_slot_eeprom_read(struct fmc_slot *slot,
			     void *buf, off_t offset, size_t count)
{
	/*
	 * TODO if we export this function, do we have to lock it when we
	 * use it? Think about it
	 */
	if (!slot->macc || !slot->macc->read)
		return -ENODEV;
	return slot->macc->read(slot->macc, buf, offset, count);
}
EXPORT_SYMBOL(fmc_slot_eeprom_read);

/**
 * Add EEPROM which will be associated to the given FMC slot
 * @slot: FMC slot instance
 * @info: I2C EEPROM information
 *
 * This creates also a symlink to the EEPROM device in the FMC slot sysfs
 * directory. If this fails it will be reported on dmesg but this does not
 * prevent the system from running; for this reason the function will
 * succeed anyway.
 *
 * This should be used only by the FMC framework itself. But, this is also
 * exported to other users in order to be able to support corner cases
 * where the EEPROM type is not supported by the AT24 driver (in which case
 * the sysfs attribute eeprom_type can be easily used).
 *
 * Return: 0 on success, otherwise a negative error number
 */
static int __fmc_slot_eeprom_add(struct fmc_slot *slot,
				 struct i2c_board_info *info)
{
	struct i2c_board_info info_l = *info;
	int err;

	if (!fmc_slot_present(slot))
		return -ENODEV;

	if (!slot->adapter) {
		dev_err(&slot->dev, "missing I2C adapter\n");
		return -ENODEV;
	}

	info_l.addr = fmc_slot_i2c_address(info_l.addr, slot->ga);
	slot->eeprom = i2c_new_device(slot->adapter, &info_l);
	if (!slot->eeprom)
		return -ENODEV;

	err = sysfs_create_link(&slot->dev.kobj, &slot->eeprom->dev.kobj,
				FRU_EEPROM_NAME);
	if (err)
		dev_err(&slot->dev, "Failed to create eeprom symlink to %s\n",
			dev_name(&slot->eeprom->dev));

	return 0;
}

/**
 * Add EEPROM to a given FMC slot
 * @slot: FMC slot instance
 *
 * Return: 0 on success, otherwise a negative error number
 */
int fmc_slot_eeprom_add(struct fmc_slot *slot)
{
	struct i2c_board_info i2c_info;

	fmc_slot_eeprom_init_default(slot, &i2c_info);
	return __fmc_slot_eeprom_add(slot, &i2c_info);
}

/**
 * Remove EEPROM associated to the given FMC slot
 * @slot: FMC slot instance
 *
 * This should be used only by the FMC framework itself. But, this is also
 * exported to other users in order to be able to support corner cases
 * where the EEPROM type is not supported by the AT24 driver (in which case
 * the sysfs attribute eeprom_type can be easily used).
 */
void fmc_slot_eeprom_del(struct fmc_slot *slot)
{
	if (!slot || !slot->eeprom)
		return;

	sysfs_remove_link(&slot->dev.kobj, FRU_EEPROM_NAME);
	i2c_unregister_device(slot->eeprom);
	slot->eeprom = NULL;
	slot->macc = NULL;
}

/**
 * Replace current EEPROM instance with a given one
 * @slot: FMC slot instance
 * @info: I2C EEPROM information
 *
 * Return: 0 on success, otherwise a negative error number
 */
static int fmc_slot_eeprom_replace(struct fmc_slot *slot,
				   struct i2c_board_info *info)
{
	fmc_slot_eeprom_del(slot);
	return __fmc_slot_eeprom_add(slot, info);
}


int fmc_slot_eeprom_type_set(struct fmc_slot *slot, const char *type)
{
#define FMC_EEPROM_SIZE_OFF 3
	struct i2c_board_info i2c_info;
	unsigned int len;
	int ret;

	if (strncmp(type, "24c", 3)) {
		if (strncmp(type, "at24c", 5)) {
			dev_err(&slot->dev,
				"Invalid EEPROM type. Expected 'at24cXX' or '24cXX' (got %s)\n",
				type);
			return -EINVAL;
		}
		type += 2; /* skip 'at' */
	}

	ret = kstrtouint(type + FMC_EEPROM_SIZE_OFF, 10, &len);
	if (ret < 0) {
		dev_err(&slot->dev,
			"Failed to get EEPROM size %d ('%s' %d)\n",
			ret, type + FMC_EEPROM_SIZE_OFF, len);
		return -EINVAL;
	}


	memset(&i2c_info, 0, sizeof(i2c_info));
	memset(&slot->at24_data, 0, sizeof(slot->at24_data));

	len = (len * 1024) / 8;
	/*
	 * For sizes between 1K and 16K the EEPROM uses part of the device
	 * address as internal memory address
	 */
	if (len > 4096) /* 32K 4KiB */
		slot->at24_data.flags = AT24_FLAG_ADDR16;
	else if (len > 131072) /* 1024K 128KiB */
		return -EINVAL;

	fmc_slot_eeprom_init(slot, &i2c_info, type);
	slot->at24_data.byte_len = len;
	slot->at24_data.page_size = 1; /* 1Byte page to play safe */
	slot->at24_data.setup = fmc_slot_eeprom_setup;
	slot->at24_data.context = slot;

	dev_dbg(&slot->dev, "%s 0x%x %d %d 0x%x\n",
		i2c_info.type, i2c_info.addr,
		slot->at24_data.byte_len, slot->at24_data.page_size,
		slot->at24_data.flags);

	return fmc_slot_eeprom_replace(slot, &i2c_info);
}
EXPORT_SYMBOL(fmc_slot_eeprom_type_set);

/**
 * Check if an FMC mezzanine in the FMC slot has a valid FRU
 * @slot: FMC slot to verify
 *
 * Return: 1 if the slot is present, otherwise 0
 */
int fmc_slot_fru_valid(struct fmc_slot *slot)
{
	struct fru_common_header fru_h;
	ssize_t ret;

	ret = fmc_slot_eeprom_read(slot, (void *)&fru_h, 0x0,
				   sizeof(struct fru_common_header));
	if (ret != sizeof(struct fru_common_header)) {
		dev_err(&slot->dev,
			"Failed while reading mezzanine's EEPROM (ret: %zd)\n",
			ret);
		return 0;
	}

	if (!fru_header_cksum_ok(&fru_h)) {
		dev_warn(&slot->dev, "Invalid FRU: checksum failure\n");
		return 0;
	}

	return 1;
}
EXPORT_SYMBOL(fmc_slot_fru_valid);

static ssize_t fru_valid_show(struct device *dev,
			      struct device_attribute *attr,
			      char *buf)
{
	struct fmc_slot *slot = to_fmc_slot(dev);

	return sprintf(buf, "%d\n", fmc_slot_fru_valid(slot));
}
static DEVICE_ATTR_RO(fru_valid);

const char *fmc_slot_eeprom_type_get(struct fmc_slot *slot)
{
	return slot && slot->eeprom ? slot->eeprom->name : "none";
}
EXPORT_SYMBOL(fmc_slot_eeprom_type_get);

static ssize_t eeprom_type_show(struct device *dev,
				struct device_attribute *attr,
				char *buf)
{
	return sprintf(buf, "%s\n",
		       fmc_slot_eeprom_type_get(to_fmc_slot(dev)));
}

static ssize_t eeprom_type_store(struct device *dev,
				 struct device_attribute *attr,
				 const char *buf, size_t count)
{
	struct fmc_slot *slot = to_fmc_slot(dev);
	int ret;

	if (!fmc_slot_present(slot))
		return -ENODEV;

	ret = fmc_slot_eeprom_type_set(slot, buf);

	return ret ? ret : count;
}
static DEVICE_ATTR_RW(eeprom_type);


static struct attribute *fmc_slot_eeprom_attrs[] = {
	&dev_attr_fru_valid.attr,
	&dev_attr_eeprom_type.attr,
	NULL,
};

const struct attribute_group fmc_slot_eeprom_group = {
	.attrs = fmc_slot_eeprom_attrs,
};