/*
    i2c Support for Apple SMU Controller

    Copyright (c) 2005 Benjamin Herrenschmidt, IBM Corp.
                       <benh@kernel.crashing.org>

    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.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

*/

#include <linux/config.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/i2c.h>
#include <linux/init.h>
#include <linux/completion.h>
#include <linux/device.h>
#include <asm/prom.h>
#include <asm/of_device.h>
#include <asm/smu.h>

static int probe;

MODULE_AUTHOR("Benjamin Herrenschmidt <benh@kernel.crashing.org>");
MODULE_DESCRIPTION("I2C driver for Apple's SMU");
MODULE_LICENSE("GPL");
module_param(probe, bool, 0);


/* Physical interface */
struct smu_iface
{
	struct i2c_adapter	adapter;
	struct completion	complete;
	u32			busid;
};

static void smu_i2c_done(struct smu_i2c_cmd *cmd, void *misc)
{
	struct smu_iface	*iface = misc;
	complete(&iface->complete);
}

/*
 * SMBUS-type transfer entrypoint
 */
static s32 smu_smbus_xfer(	struct i2c_adapter*	adap,
				u16			addr,
				unsigned short		flags,
				char			read_write,
				u8			command,
				int			size,
				union i2c_smbus_data*	data)
{
	struct smu_iface	*iface = i2c_get_adapdata(adap);
	struct smu_i2c_cmd	cmd;
	int			rc = 0;
	int			read = (read_write == I2C_SMBUS_READ);

	cmd.info.bus = iface->busid;
	cmd.info.devaddr = (addr << 1) | (read ? 0x01 : 0x00);

	/* Prepare datas & select mode */
	switch (size) {
        case I2C_SMBUS_QUICK:
		cmd.info.type = SMU_I2C_TRANSFER_SIMPLE;
		cmd.info.datalen = 0;
	    	break;
        case I2C_SMBUS_BYTE:
		cmd.info.type = SMU_I2C_TRANSFER_SIMPLE;
		cmd.info.datalen = 1;
		if (!read)
			cmd.info.data[0] = data->byte;
	    	break;
        case I2C_SMBUS_BYTE_DATA:
		cmd.info.type = SMU_I2C_TRANSFER_STDSUB;
		cmd.info.datalen = 1;
		cmd.info.sublen = 1;
		cmd.info.subaddr[0] = command;
		cmd.info.subaddr[1] = 0;
		cmd.info.subaddr[2] = 0;
		if (!read)
			cmd.info.data[0] = data->byte;
	    	break;
        case I2C_SMBUS_WORD_DATA:
		cmd.info.type = SMU_I2C_TRANSFER_STDSUB;
		cmd.info.datalen = 2;
		cmd.info.sublen = 1;
		cmd.info.subaddr[0] = command;
		cmd.info.subaddr[1] = 0;
		cmd.info.subaddr[2] = 0;
		if (!read) {
			cmd.info.data[0] = data->byte & 0xff;
			cmd.info.data[1] = (data->byte >> 8) & 0xff;
		}
		break;
	/* Note that these are broken vs. the expected smbus API where
	 * on reads, the lenght is actually returned from the function,
	 * but I think the current API makes no sense and I don't want
	 * any driver that I haven't verified for correctness to go
	 * anywhere near a pmac i2c bus anyway ...
	 */
        case I2C_SMBUS_BLOCK_DATA:
		cmd.info.type = SMU_I2C_TRANSFER_STDSUB;
		cmd.info.datalen = data->block[0] + 1;
		if (cmd.info.datalen > 6)
			return -EINVAL;
		if (!read)
			memcpy(cmd.info.data, data->block, cmd.info.datalen);
		cmd.info.sublen = 1;
		cmd.info.subaddr[0] = command;
		cmd.info.subaddr[1] = 0;
		cmd.info.subaddr[2] = 0;
		break;
	case I2C_SMBUS_I2C_BLOCK_DATA:
		cmd.info.type = SMU_I2C_TRANSFER_STDSUB;
		cmd.info.datalen = data->block[0];
		if (cmd.info.datalen > 7)
			return -EINVAL;
		if (!read)
			memcpy(cmd.info.data, &data->block[1],
			       cmd.info.datalen);
		cmd.info.sublen = 1;
		cmd.info.subaddr[0] = command;
		cmd.info.subaddr[1] = 0;
		cmd.info.subaddr[2] = 0;
		break;

        default:
	    	return -EINVAL;
	}

	/* Turn a standardsub read into a combined mode access */
 	if (read_write == I2C_SMBUS_READ &&
	    cmd.info.type == SMU_I2C_TRANSFER_STDSUB)
		cmd.info.type = SMU_I2C_TRANSFER_COMBINED;

	/* Finish filling command and submit it */
	cmd.done = smu_i2c_done;
	cmd.misc = iface;
	rc = smu_queue_i2c(&cmd);
	if (rc < 0)
		return rc;
	wait_for_completion(&iface->complete);
	rc = cmd.status;

	if (!read || rc < 0)
		return rc;

	switch (size) {
        case I2C_SMBUS_BYTE:
        case I2C_SMBUS_BYTE_DATA:
		data->byte = cmd.info.data[0];
	    	break;
        case I2C_SMBUS_WORD_DATA:
		data->word = ((u16)cmd.info.data[1]) << 8;
		data->word |= cmd.info.data[0];
		break;
	/* Note that these are broken vs. the expected smbus API where
	 * on reads, the lenght is actually returned from the function,
	 * but I think the current API makes no sense and I don't want
	 * any driver that I haven't verified for correctness to go
	 * anywhere near a pmac i2c bus anyway ...
	 */
        case I2C_SMBUS_BLOCK_DATA:
	case I2C_SMBUS_I2C_BLOCK_DATA:
		memcpy(&data->block[0], cmd.info.data, cmd.info.datalen);
		break;
	}

	return rc;
}

static u32
smu_smbus_func(struct i2c_adapter * adapter)
{
	return I2C_FUNC_SMBUS_QUICK | I2C_FUNC_SMBUS_BYTE |
	       I2C_FUNC_SMBUS_BYTE_DATA | I2C_FUNC_SMBUS_WORD_DATA |
	       I2C_FUNC_SMBUS_BLOCK_DATA;
}

/* For now, we only handle combined mode (smbus) */
static struct i2c_algorithm smu_algorithm = {
	.smbus_xfer	= smu_smbus_xfer,
	.functionality	= smu_smbus_func,
};

static int create_iface(struct device_node *np, struct device *dev)
{
	struct smu_iface* iface;
	u32 *reg, busid;
	int rc;

	reg = (u32 *)get_property(np, "reg", NULL);
	if (reg == NULL) {
		printk(KERN_ERR "i2c-pmac-smu: can't find bus number !\n");
		return -ENXIO;
	}
	busid = *reg;

	iface = kmalloc(sizeof(struct smu_iface), GFP_KERNEL);
	if (iface == NULL) {
		printk(KERN_ERR "i2c-pmac-smu: can't allocate inteface !\n");
		return -ENOMEM;
	}
	memset(iface, 0, sizeof(struct smu_iface));
	init_completion(&iface->complete);
	iface->busid = busid;

	dev_set_drvdata(dev, iface);

	sprintf(iface->adapter.name, "smu-i2c-%02x", busid);
	iface->adapter.algo = &smu_algorithm;
	iface->adapter.algo_data = NULL;
	iface->adapter.client_register = NULL;
	iface->adapter.client_unregister = NULL;
	i2c_set_adapdata(&iface->adapter, iface);
	iface->adapter.dev.parent = dev;

	rc = i2c_add_adapter(&iface->adapter);
	if (rc) {
		printk(KERN_ERR "i2c-pamc-smu.c: Adapter %s registration "
		       "failed\n", iface->adapter.name);
		i2c_set_adapdata(&iface->adapter, NULL);
	}

	if (probe) {
		unsigned char addr;
		printk("Probe: ");
		for (addr = 0x00; addr <= 0x7f; addr++) {
			if (i2c_smbus_xfer(&iface->adapter,addr,
					   0,0,0,I2C_SMBUS_QUICK,NULL) >= 0)
				printk("%02x ", addr);
		}
		printk("\n");
	}

	printk(KERN_INFO "SMU i2c bus %x registered\n", busid);

	return 0;
}

static int dispose_iface(struct device *dev)
{
	struct smu_iface *iface = dev_get_drvdata(dev);
	int rc;

	rc = i2c_del_adapter(&iface->adapter);
	i2c_set_adapdata(&iface->adapter, NULL);
	/* We aren't that prepared to deal with this... */
	if (rc)
		printk("i2c-pmac-smu.c: Failed to remove bus %s !\n",
		       iface->adapter.name);
	dev_set_drvdata(dev, NULL);
	kfree(iface);

	return 0;
}


static int create_iface_of_platform(struct of_device* dev,
				    const struct of_device_id *match)
{
	return create_iface(dev->node, &dev->dev);
}


static int dispose_iface_of_platform(struct of_device* dev)
{
	return dispose_iface(&dev->dev);
}


static struct of_device_id i2c_smu_match[] =
{
	{
		.compatible	= "smu-i2c",
	},
	{},
};
static struct of_platform_driver i2c_smu_of_platform_driver =
{
	.name 		= "i2c-smu",
	.match_table	= i2c_smu_match,
	.probe		= create_iface_of_platform,
	.remove		= dispose_iface_of_platform
};


static int __init i2c_pmac_smu_init(void)
{
	of_register_driver(&i2c_smu_of_platform_driver);
	return 0;
}


static void __exit i2c_pmac_smu_cleanup(void)
{
	of_unregister_driver(&i2c_smu_of_platform_driver);
}

module_init(i2c_pmac_smu_init);
module_exit(i2c_pmac_smu_cleanup);
