/*
 * CPLD sysfs driver for Celestica Pebble BMC E1052
 *
 * Copyright (C) 2015, 2017 Cumulus Networks, Inc.
 * Author: Dave Olson <olson@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; version 2.
 *
 *  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.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.
 */

#include <stddef.h>

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/mutex.h>
#include <linux/of_platform.h>
#include <linux/io.h>

#include "cel_pebble_b_cpld.h"
#include "platform_defs.h"

static const char driver_name[] = "cel_pebble_b_cpld";
#define DRIVER_VERSION "1.0"

#define CPLDNAMSZ 20

static uint8_t *cel_pebble_b_cpld_regs;

/*
 *
 * CPLD I/O
 *
 */
static uint8_t cpld_reg_read(uint32_t reg)
{
	uint8_t data;

	data = ioread8(cel_pebble_b_cpld_regs + reg - CPLD_IO_BASE);
	return data;
}

static void cpld_reg_write(uint32_t reg, uint8_t data)
{
	iowrite8(data, cel_pebble_b_cpld_regs + reg - CPLD_IO_BASE);
}

#define cel_pebble_b_CPLD_STRING_NAME_SIZE 30

/*
 * mmc write protect
 */
static ssize_t flash_write_protect_show(struct device *dev,
					struct device_attribute *dattr,
					char *buf)
{
	uint8_t data;

	data = cpld_reg_read(CPLD_MMC_FLASH_WP_REG);
	return sprintf(buf, "0x%02X\n", data);
}

static ssize_t flash_write_protect_store(struct device *dev,
					 struct device_attribute *dattr,
					 const char *buf, size_t count)
{
	int tmp;

	if (kstrtouint(buf, 0, &tmp) != 0)
		return -EINVAL;
	cpld_reg_write(CPLD_MMC_FLASH_WP_REG, tmp);

	return count;
}
static SYSFS_ATTR_RW(flash_write_protect, flash_write_protect_show,
		     flash_write_protect_store);

/*
 * MMC (Module Management) cpld version
 */
static ssize_t cpld_mmc_version_show(struct device *dev,
				     struct device_attribute *dattr,
				     char *buf)
{
	uint8_t tmp;
	uint8_t type;
	uint8_t rev;

	tmp = cpld_reg_read(CPLD_MMC_VERSION_REG);
	type = (tmp & CPLD_MMC_VERSION_H_MASK) >> CPLD_MMC_VERSION_H_SHIFT;
	rev  = (tmp & CPLD_MMC_VERSION_L_MASK) >> CPLD_MMC_VERSION_L_SHIFT;

	return sprintf(buf, "%d.%d\n", type, rev);
}

static SYSFS_ATTR_RO(cpld_mmc_version, cpld_mmc_version_show);

/*------------------------------------------------------------------------------
 *
 * LED, PSU definitions
 *
 */
#define PLATFORM_DEV_STATE_SIZE 20
struct dev_state_info {
	char name[PLATFORM_DEV_STATE_SIZE];
	uint8_t value;
};

struct dev_state {
	char name[cel_pebble_b_CPLD_STRING_NAME_SIZE];
	uint32_t reg;
	uint8_t mask;
	uint8_t shift;
	int n_state;
	struct dev_state_info state[8];
};

/* not supporting CPLD_MMC_SYS_PWRLED_AUTO being changed, leave at 0 for auto */
static struct dev_state cpld_devs[] = {
	{
	.name = "led_system_status",
	.reg  = CPLD_MMC_SYS_LED_REG,
	.mask = CPLD_MMC_SYS_LED_GREEN_MASK,
	.shift = CPLD_MMC_SYS_LED_GREEN_SHIFT,
	.n_state = 3,
	.state = {
	    { PLATFORM_LED_GREEN, CPLD_MMC_SYS_LED_GREEN_ON},
	    { PLATFORM_LED_GREEN_SLOW_BLINKING,
	      CPLD_MMC_SYS_LED_GREEN_SLOW_BLINK},
	    { PLATFORM_LED_OFF, CPLD_MMC_SYS_LED_GREEN_OFF},
	},
	},
};

static int n_devs = ARRAY_SIZE(cpld_devs);

/*
 * LEDs
 */
static ssize_t dev_show(struct device *dev,
			struct device_attribute *dattr,
			char *buf)
{
	uint8_t tmp;
	int i;
	struct dev_state *target = NULL;

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

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

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

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

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

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

	tmp = cpld_reg_read(target->reg);
	tmp &= ~target->mask;
	tmp |= (target->state[i].value << target->shift) & target->mask;
	cpld_reg_write(target->reg, tmp);

	return count;
}

static SYSFS_ATTR_RW(led_system_status, dev_show, dev_store);

/*
 * Help / README
 *
 * The string length must be less than 4K.
 *
 */

static ssize_t help_show(struct device *dev,
			 struct device_attribute *dattr,
			 char *buf)
{
	return sprintf(buf, "%s", "Description of the CPLD driver files:\n"
		       "\n"
		       "cpld_mmc_version\n"
		       "  Read-Only:\n"
		       "\n"
		       "  CPLD version register for MMC CPLD\n"
		       "  CPLD version register in the following format:\n"
		       "\n"
		       "  cpld_mmc_version\n"
		       "\n"
		       "  Example: 0.2\n"
		       "\n"
		       "led_system_status\n"
		       "  Read-Write:\n"
		       "\n"
		       "  System Status LED color.\n"
		       "  The following values are possible: green, blinking "
			       "green, and off\n"
		       "\n"
		       "help\n"
		       "README\n"
		       "  Read-Only:\n"
		       "\n"
		       "  The text you are reading now.\n");
}

static SYSFS_ATTR_RO(help, help_show);
static SYSFS_ATTR_RO(README, help_show);

/*------------------------------------------------------------------------------
 *
 * sysfs registration
 *
 */

static struct attribute *cel_pebble_b_cpld_attrs[] = {
	&dev_attr_cpld_mmc_version.attr,
	&dev_attr_led_system_status.attr,
	&dev_attr_flash_write_protect.attr,
	&dev_attr_help.attr,
	&dev_attr_README.attr,
	NULL,
};

static struct attribute_group cel_pebble_b_cpld_attr_group = {
	.attrs = cel_pebble_b_cpld_attrs,
};

/*------------------------------------------------------------------------------
 *
 * module interface
 *
 */
static struct platform_device *cel_pebble_b_cpld_device;

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

	cel_pebble_b_cpld_regs = ioport_map(CPLD_IO_BASE, CPLD_IO_SIZE);
	if (!cel_pebble_b_cpld_regs) {
		pr_err("cpld: unabled to map iomem\n");
		ret = -ENODEV;
		goto err_exit;
	}

	ret = sysfs_create_group(&dev->dev.kobj, &cel_pebble_b_cpld_attr_group);
	if (ret) {
		pr_err("cpld: sysfs_create_group failed for cpld driver");
		goto err_unmap;
	}

err_unmap:
	iounmap(cel_pebble_b_cpld_regs);

err_exit:
	return ret;
}

static int cel_pebble_b_cpld_remove(struct platform_device *dev)
{
	iounmap(cel_pebble_b_cpld_regs);
	return 0;
}

static struct platform_driver cel_pebble_b_cpld_driver = {
	.driver = {
	.name = "cel_pebble_b_cpld",
	.owner = THIS_MODULE,
	},
	.probe = cel_pebble_b_cpld_probe,
	.remove = cel_pebble_b_cpld_remove,
};

static int __init cel_pebble_b_cpld_init(void)
{
	int rv;

	rv = platform_driver_register(&cel_pebble_b_cpld_driver);
	if (rv)
		goto err_exit;

	cel_pebble_b_cpld_device = platform_device_alloc("cel_pebble_b_cpld",
							 0);
	if (!cel_pebble_b_cpld_device) {
		pr_err("platform_device_alloc() failed for cpld device\n");
		rv = -ENOMEM;
		goto err_unregister;
	}

	rv = platform_device_add(cel_pebble_b_cpld_device);
	if (rv) {
		pr_err("platform_device_add() failed for cpld device.\n");
		goto err_dealloc;
	}
	return 0;

err_dealloc:
	platform_device_unregister(cel_pebble_b_cpld_device);

err_unregister:
	platform_driver_unregister(&cel_pebble_b_cpld_driver);

err_exit:
	pr_err("%s platform_driver_register failed (%i)\n",
	       driver_name, rv);
	return rv;
}

static void __exit cel_pebble_b_cpld_exit(void)
{
	platform_driver_unregister(&cel_pebble_b_cpld_driver);
	platform_device_unregister(cel_pebble_b_cpld_device);
}

MODULE_AUTHOR("Dave Olson <olson@cumulusnetworks.com>");
MODULE_DESCRIPTION("Platform CPLD driver for Celestica Pebble BMC E1052");
MODULE_LICENSE("GPL");
MODULE_VERSION(DRIVER_VERSION);

module_init(cel_pebble_b_cpld_init);
module_exit(cel_pebble_b_cpld_exit);
