/*
 * 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.
 */

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/jiffies.h>
#include <linux/i2c.h>
#include <linux/hwmon.h>
#include <linux/hwmon-sysfs.h>
#include <linux/err.h>
#include <linux/mutex.h>

//#include "I2CHostCommunication.h"

#define USE_SMBUS    1

/* definition */
#define CPLD_INFO_OFFSET   0x00
#define CPLD_PSU_OFFSET    0x08
#define CPLD_LED_OFFSET    0x0E
#define CPLD_LED_STATU_OFFSET    0x0D
#define CPLD_CTL_OFFSET    0x0C
#define CPLD_BIOSCS_OFFSET	0x04


/* Each client has this additional data */
struct cpld_data {
	struct device		*hwmon_dev;
	struct mutex		update_lock;
};

/*-----------------------------------------------------------------------*/

static ssize_t cpld_i2c_read(struct i2c_client *client, u8 *buf, u8 offset, size_t count)
{
#if USE_SMBUS    
	int i;
	
    for(i=0; i<count; i++) {
        buf[i] = i2c_smbus_read_byte_data(client, offset+i);
    }	
    return count;
#else
	struct i2c_msg msg[2];
	char msgbuf[2];
	int status;

	memset(msg, 0, sizeof(msg));
	
	msgbuf[0] = offset;
	
	msg[0].addr = client->addr;
	msg[0].buf = msgbuf;
	msg[0].len = 1;

	msg[1].addr = client->addr;
	msg[1].flags = I2C_M_RD;
	msg[1].buf = buf;
	msg[1].len = count;
	
	status = i2c_transfer(client->adapter, msg, 2);
	
	if(status == 2)
	    status = count;
	    
	return status;    
#endif	
}

static ssize_t cpld_i2c_write(struct i2c_client *client, char *buf, unsigned offset, size_t count)
{
#if USE_SMBUS    
	int i;
	
    for(i=0; i<count; i++) {
        i2c_smbus_write_byte_data(client, offset+i, buf[i]);
    }	
    return count;
#else
	struct i2c_msg msg;
	int status;
    u8 writebuf[64];
	
	int i = 0;

	msg.addr = client->addr;
	msg.flags = 0;

	/* msg.buf is u8 and casts will mask the values */
	msg.buf = writebuf;
	
	msg.buf[i++] = offset;
	memcpy(&msg.buf[i], buf, count);
	msg.len = i + count;
	
	status = i2c_transfer(client->adapter, &msg, 1);
	if (status == 1)
		status = count;
	
    return status;	
#endif    
}

/*-----------------------------------------------------------------------*/

/* sysfs attributes for hwmon */

static ssize_t show_info(struct device *dev, struct device_attribute *da,
			 char *buf)
{
	u32 status;
	//struct sensor_device_attribute *attr = to_sensor_dev_attr(da);
	struct i2c_client *client = to_i2c_client(dev);
	struct cpld_data *data = i2c_get_clientdata(client);
	u8 b[4];
    
    memset(b, 0, 4);
    
	mutex_lock(&data->update_lock);
    status = cpld_i2c_read(client, b, CPLD_INFO_OFFSET, 4);
	mutex_unlock(&data->update_lock);
	
	if(status != 4) return sprintf(buf, "read cpld info fail\n");
	
	status = sprintf (buf,   "The CPLD release date is %02d/%02d/%d.\n", b[2] & 0xf, (b[3] & 0x1f), 2014+(b[2] >> 4));	/* mm/dd/yyyy*/
	status = sprintf (buf, "%sThe PCB  version is %X%X\n", buf,  b[0]>>4, b[0]&0xf);
	status = sprintf (buf, "%sThe CPLD version is %d.%d\n", buf, b[1]>>4, b[1]&0xf);
	
	return strlen(buf);
}


static ssize_t show_ctl(struct device *dev, struct device_attribute *da,
			 char *buf)
{
	u32 status;
	//struct sensor_device_attribute *attr = to_sensor_dev_attr(da);
	struct i2c_client *client = to_i2c_client(dev);
	struct cpld_data *data = i2c_get_clientdata(client);
	u8 b[1];
    
	mutex_lock(&data->update_lock);
	
    status = cpld_i2c_read(client, b, CPLD_CTL_OFFSET, 1);
	
	mutex_unlock(&data->update_lock);
	
	if(status != 1) return sprintf(buf, "read cpld ctl fail\n");
	    
	
	status = sprintf (buf, "0x%X\n", b[0]);
	    
	return strlen(buf);
}

static ssize_t set_ctl(struct device *dev,
			   struct device_attribute *devattr,
			   const char *buf, size_t count)
{
	//struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr);
	struct i2c_client *client = to_i2c_client(dev);
	struct cpld_data *data = i2c_get_clientdata(client);
	u8 byte;

	u8 temp = simple_strtol(buf, NULL, 10);
    
	mutex_lock(&data->update_lock);
        cpld_i2c_read(client, &byte, CPLD_CTL_OFFSET, 1);
	if(temp) byte |= (1<<0);
	else     byte &= ~(1<<0);
	cpld_i2c_write(client, &byte, CPLD_CTL_OFFSET, 1);
	mutex_unlock(&data->update_lock);

	return count;
}

static ssize_t show_bios_cs(struct device *dev, struct device_attribute *da,
                         char *buf)
{
        u32 status;
        //struct sensor_device_attribute *attr = to_sensor_dev_attr(da);
        struct i2c_client *client = to_i2c_client(dev);
        struct cpld_data *data = i2c_get_clientdata(client);
        u8 b[1];

        mutex_lock(&data->update_lock);

    status = cpld_i2c_read(client, b, CPLD_BIOSCS_OFFSET, 1);

        mutex_unlock(&data->update_lock);

        if(status != 1) return sprintf(buf, "read cpld BIOS_CS fail\n");


        status = sprintf (buf, "0x%X\n", b[0] & 0x01);

        return strlen(buf);
}

static ssize_t set_bios_cs(struct device *dev,
                           struct device_attribute *devattr,
                           const char *buf, size_t count)
{
        //struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr);
        struct i2c_client *client = to_i2c_client(dev);
        struct cpld_data *data = i2c_get_clientdata(client);
        u8 byte;

        u8 temp = simple_strtol(buf, NULL, 10);

        mutex_lock(&data->update_lock);
        cpld_i2c_read(client, &byte, CPLD_BIOSCS_OFFSET, 1);
        if(temp) byte |= 0x01;
        else     byte &= ~(0x01);
        cpld_i2c_write(client, &byte, CPLD_BIOSCS_OFFSET, 1);
        mutex_unlock(&data->update_lock);

        return count;
}


static char* led_str[] = {
    "OFF",     //000
    "0.5 Hz",  //001
    "1 Hz",    //010
    "2 Hz",    //011
    "NA",      //100
    "NA",      //101
    "NA",      //110
    "ON",      //111
};

static ssize_t show_led(struct device *dev, struct device_attribute *da,
			 char *buf)
{
	u32 status;
	struct sensor_device_attribute *attr = to_sensor_dev_attr(da);
	struct i2c_client *client = to_i2c_client(dev);
	struct cpld_data *data = i2c_get_clientdata(client);
	u8 byte;
	int shift = (attr->index == 0)?3:0;
    
	mutex_lock(&data->update_lock);
    status = cpld_i2c_read(client, &byte, CPLD_LED_OFFSET, 1);
	mutex_unlock(&data->update_lock);
	
	if(status != 1) return sprintf(buf, "read cpld offset 0x%x\n", CPLD_LED_OFFSET);
	
    byte = (byte >> shift) & 0x7;
	
	/*
	    0: off
	    1: 0.5hz
	    2: 1 hz
	    3: 2 hz
	    4~6: not define
	    7: on
	*/
	
	status = sprintf (buf, "%d: %s\n", byte, led_str[byte]);
	    
	return strlen(buf);
}

static ssize_t set_led(struct device *dev,
			   struct device_attribute *devattr,
			   const char *buf, size_t count)
{
	struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr);
	struct i2c_client *client = to_i2c_client(dev);
	struct cpld_data *data = i2c_get_clientdata(client);

	u8 temp = simple_strtol(buf, NULL, 16);
	u8 byte;
	int shift = (attr->index == 0)?3:0;
    
    temp &= 0x7;    
    //validate temp value: 0,1,2,3,7, TBD
    
	mutex_lock(&data->update_lock);
    cpld_i2c_read(client, &byte, CPLD_LED_OFFSET, 1);
    byte &= ~(0x7<<shift);
    byte |= (temp<<shift);
	cpld_i2c_write(client, &byte, CPLD_LED_OFFSET, 1);
	mutex_unlock(&data->update_lock);

	return count;
}

/*
CPLD report the PSU0 status
000 = PSU normal operation
100 = PSU fault
010 = PSU unpowered
111 = PSU not installed

7 6 | 5 4 3 |  2 1 0
----------------------
    | psu0  |  psu1
*/
static char* psu_str[] = {
    "normal",           //000
    "NA",               //001
    "unpowered",        //010
    "NA",               //011
    "fault",            //100
    "NA",               //101
    "NA",               //110
    "not installed",    //111
};

static ssize_t show_psu(struct device *dev, struct device_attribute *da,
			 char *buf)
{
	u32 status;
	struct sensor_device_attribute *attr = to_sensor_dev_attr(da);
	struct i2c_client *client = to_i2c_client(dev);
	struct cpld_data *data = i2c_get_clientdata(client);
	u8 byte;
	int shift = (attr->index == 1)?0:3;
    
	mutex_lock(&data->update_lock);
    status = cpld_i2c_read(client, &byte, CPLD_PSU_OFFSET, 1);
	mutex_unlock(&data->update_lock);
	
    byte = (byte >> shift) & 0x7;
	
	status = sprintf (buf, "%d : %s\n", byte, psu_str[byte]);
	    
	return strlen(buf);
}


static SENSOR_DEVICE_ATTR(info,  S_IRUGO,			        show_info, 0, 0);
static SENSOR_DEVICE_ATTR(ctl, S_IWUSR|S_IRUGO,			show_ctl, set_ctl, 0);

static SENSOR_DEVICE_ATTR(grn_led, S_IWUSR|S_IRUGO,			show_led, set_led, 0);
static SENSOR_DEVICE_ATTR(red_led, S_IWUSR|S_IRUGO,			show_led, set_led, 1);

static SENSOR_DEVICE_ATTR(psu0,  S_IRUGO,			        show_psu, 0, 0);
static SENSOR_DEVICE_ATTR(psu1,  S_IRUGO,			        show_psu, 0, 1);

static SENSOR_DEVICE_ATTR(bios_cs,  S_IWUSR|S_IRUGO,         show_bios_cs, set_bios_cs, 0);
			
static struct attribute *cpld_attributes[] = {
    //info
	&sensor_dev_attr_info.dev_attr.attr,
	&sensor_dev_attr_ctl.dev_attr.attr,

	&sensor_dev_attr_grn_led.dev_attr.attr,
	&sensor_dev_attr_red_led.dev_attr.attr,

	&sensor_dev_attr_psu0.dev_attr.attr,
	&sensor_dev_attr_psu1.dev_attr.attr,
	
	&sensor_dev_attr_bios_cs.dev_attr.attr,

	NULL
};

static const struct attribute_group cpld_group = {
	.attrs = cpld_attributes,
};

/*-----------------------------------------------------------------------*/

/* device probe and removal */

static int
cpld_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
	struct cpld_data *data;
	int status;

    printk("+%s\n", __func__);
    
	if (!i2c_check_functionality(client->adapter,
			I2C_FUNC_SMBUS_BYTE_DATA | I2C_FUNC_SMBUS_WORD_DATA))
		return -EIO;

	data = kzalloc(sizeof(struct cpld_data), GFP_KERNEL);
	if (!data)
		return -ENOMEM;

	i2c_set_clientdata(client, data);
	mutex_init(&data->update_lock);
	
	/* Register sysfs hooks */
	status = sysfs_create_group(&client->dev.kobj, &cpld_group);
	if (status)
		goto exit_free;

	data->hwmon_dev = hwmon_device_register(&client->dev);
	if (IS_ERR(data->hwmon_dev)) {
		status = PTR_ERR(data->hwmon_dev);
		goto exit_remove;
	}

	dev_info(&client->dev, "%s: sensor '%s'\n",
		 dev_name(data->hwmon_dev), client->name);

	return 0;

exit_remove:
	sysfs_remove_group(&client->dev.kobj, &cpld_group);
exit_free:
	i2c_set_clientdata(client, NULL);
	kfree(data);
	return status;
}

static int cpld_remove(struct i2c_client *client)
{
	struct cpld_data *data = i2c_get_clientdata(client);

	hwmon_device_unregister(data->hwmon_dev);
	sysfs_remove_group(&client->dev.kobj, &cpld_group);
	i2c_set_clientdata(client, NULL);
	kfree(data);
	return 0;
}

static const struct i2c_device_id cpld_ids[] = {
	{ "inv_cpld", 0, },
	{ /* LIST END */ }
};
MODULE_DEVICE_TABLE(i2c, cpld_ids);

static struct i2c_driver cpld_driver = {
	.class		= I2C_CLASS_HWMON,
	.driver = {
		.name	= "inv_cpld",
	},
	.probe		= cpld_probe,
	.remove		= cpld_remove,
	.id_table	= cpld_ids,
};

/*-----------------------------------------------------------------------*/

/* module glue */

static int __init inv_cpld_init(void)
{
	return i2c_add_driver(&cpld_driver);
}

static void __exit inv_cpld_exit(void)
{
	i2c_del_driver(&cpld_driver);
}

MODULE_AUTHOR("eddie.lan <eddie.lan@inventec>");
MODULE_DESCRIPTION("inv cpld driver");
MODULE_LICENSE("GPL");

module_init(inv_cpld_init);
module_exit(inv_cpld_exit);
