/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Module for the SI4709 FM Radio Chip of Samsung YP-R0
 * - open the device (power on)
 * - close the device (power off)
 * - read registers
 * - write registers
 * - direct i2c access
 * - debug registers
 *
 * Copyright (c) 2012 Lorenzo Miori
 *
 * 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 software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/i2c.h>
#include <linux/irq.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/sysfs.h>
#include <linux/device.h>
#include <linux/interrupt.h>
#include <asm/uaccess.h>
#include <asm/mach/irq.h>

#include <linux/timer.h>
#include <linux/mutex.h>
#include <linux/kernel_stat.h>

#include <../arch/arm/mach-mx37/iomux.h>

#include "si4709.h"

/* We use both name and number of the original si470x device in order
 * to exploit the default device name present in ROM */
#define SI4709_DEV_NAME    "si470x"
#define SI4709_DEV_NUM      240

#define LOG(f, x...) \
    printk(SI4709_DEV_NAME ": " f, ## x)

#define FM_GPIO_INT_IRQ     IOMUX_TO_IRQ(MX37_PIN_GPIO1_4)

static DEFINE_MUTEX(si4709_lock);

static unsigned char reg_rw_buf[SI4702_REG_BYTE];
static bool is_si4709_init = 0;
static bool is_si4709_power = 0;

static int SI4709_open(struct inode* inode, struct file* filp);
static int SI4709_release(struct inode* inode, struct file* filp);
static int SI4709_read(struct file *filp, char *buffer, size_t count, loff_t *ppos);
static int SI4709_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
static int SI4709_detach(struct i2c_client* client);
static int SI4709_probe(struct i2c_adapter* adapter);
static int SI4709_attach(struct i2c_adapter* adapter, int address, int kind);
static int si4709_read_reg(int reg, uint16_t* buf);
static int si4709_i2c_read(int size, unsigned char* buf);
static int si4709_write_reg(int reg, uint16_t val);
static int si4709_i2c_write(int size, unsigned char* buf);
static void si4709_power_on(void);
static void si4709_power_off(void);
irqreturn_t si4709_irq_handler (int irq, void* dev_id);


static struct file_operations SI4709_fops =
{
    .owner        = THIS_MODULE,
    .open         = SI4709_open,
    .read         = SI4709_read,
    .release      = SI4709_release,
    .ioctl        = SI4709_ioctl,
};

/* this is used by i2c_add_driver */
static struct i2c_driver SI4709_driver =
{
    .driver = {
        .name = SI4709_DEV_NAME, },
    .attach_adapter = SI4709_probe,
    .detach_client = SI4709_detach,
};

static unsigned short normal_i2c[] = {SI4709_I2C_SLAVE_ADDR, I2C_CLIENT_END};    /* var. name MUST be normal_i2c */
I2C_CLIENT_INSMOD;        /* DO NOT MISS THIS after normal_i2c!!! from i2c.h */

static struct i2c_client *si4709_i2c;

static int
SI4709_read(struct file *filp, char *buffer, size_t count, loff_t *ppos)
{
    return 0;
}

/* When opening the device file, the chip will be powered up */
static int
SI4709_open(struct inode* inode, struct file* filp)
{
    si4709_power_on();
    return 0;
}

/* When closing the device file, the chip will be powered off */
static int
SI4709_release(struct inode* inode, struct file* filp)
{
    si4709_power_off();
    return 0;
}

static int
SI4709_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)
{
    int ret = -1;
    sSi4709_t sData;
    sSi4709_i2c_t i2cData;

    switch(cmd)
    {
        case IOCTL_SI4709_INIT:
                ret = 0;
                break;
        case IOCTL_SI4709_CLOSE:
                ret = 0;
                break;
        case IOCTL_SI4709_WRITE_BYTE:
            if(!copy_from_user((void*)&sData, (const void*)arg, _IOC_SIZE(cmd)))
            {
                if(si4709_write_reg(sData.addr, sData.value) != -1)
                {
                    ret = 0;
                }
            }
            break;
        case IOCTL_SI4709_READ_BYTE:
            if(!copy_from_user((void*)&sData, (const void*)arg, _IOC_SIZE(cmd)))
            {
                if(si4709_read_reg(sData.addr, &sData.value) != -1)
                {
                    if(!copy_to_user((void*)arg, &sData, sizeof(sSi4709_t)))
                    {
                        ret = 0;
                    }
                }
            }
            break;
        case IOCTL_SI4709_I2C_READ:
            if(!copy_from_user((void*)&i2cData, (const void*)arg, _IOC_SIZE(cmd)))
            {
                if(si4709_i2c_read(i2cData.size, i2cData.buf) != -1)
                {
                    if(!copy_to_user((void*)arg, &i2cData, sizeof(i2cData)))
                    {
                        ret = 0;
                    }
                }
            }
            break;
        case IOCTL_SI4709_I2C_WRITE:
            if(!copy_from_user((void*)&i2cData, (const void*)arg, _IOC_SIZE(cmd)))
            {
                if(si4709_i2c_write(i2cData.size, i2cData.buf) != -1)
                {
                    ret = 0;
                }
            }
            break;
        default:
            break;
    }

    if (ret == -1)
    {
        LOG("ioctl failed!\n");
    }

    return ret;
}

/* Hardware power management
 * Works setting up the RST pin (->GPIO: MX37_PIN_AUD5_RXC)
 */
static void
si4709_power_on(void) {

    if (!is_si4709_power) {
        mxc_request_iomux(MX37_PIN_AUD5_RXC, 4);
        mxc_iomux_set_pad(MX37_PIN_AUD5_RXC, 5);
        mxc_set_gpio_direction(MX37_PIN_AUD5_RXC, 0);
        /* HIGH: turns on radio chip */
        mxc_set_gpio_dataout(MX37_PIN_AUD5_RXC, 1);
        mdelay(100);
        is_si4709_power = 1;
        LOG("device powered on!\n");
    }

}

static void
si4709_power_off(void) {

    if (is_si4709_power) {
        /* LOW: turns off radio chip, registers are reseted */
        mxc_set_gpio_dataout(MX37_PIN_AUD5_RXC, 0);
        mxc_free_iomux(MX37_PIN_AUD5_RXC, 4);
        mdelay(100);
        is_si4709_power = 0;
        LOG("device powered off!\n");
    }

}

/* Macro + 2 methods slightly modified from: mxc_si4702 char device module available in kernel */

#define REG_to_BUF(reg) (((reg >= 0) && (reg < SI4702_STATUSRSSI))?\
        (reg - SI4702_STATUSRSSI + SI4709_REG_NUM):\
        ((reg >= SI4702_STATUSRSSI) && (reg < SI4709_REG_NUM))?\
        (reg - SI4702_STATUSRSSI) : -1)

static int si4709_read_reg(int reg, uint16_t *value)
{
    int ret, index;

    if (NULL == si4709_i2c)
        return -1;

    index = REG_to_BUF(reg);

    if (-1 == index)
        return -1;
    
    mutex_lock(&si4709_lock);

    ret = i2c_master_recv(si4709_i2c, reg_rw_buf, SI4702_REG_BYTE);
    
    mutex_unlock(&si4709_lock);

    *value = (reg_rw_buf[index * 2] << 8) & 0xFF00;
    *value |= reg_rw_buf[index * 2 + 1] & 0x00FF;

    return ret;
}

/* Direct i2c channel reading */
static int
si4709_i2c_read(int size, unsigned char *buf) {
    
    int ret;

    if (NULL == si4709_i2c)
        return -1;

    mutex_lock(&si4709_lock);

    ret = i2c_master_recv(si4709_i2c, buf, size);

    mutex_unlock(&si4709_lock);

    return ret;
    
}

static int si4709_write_reg(int reg, uint16_t value)
{
    int index;
    int ret = -1;

    if (NULL == si4709_i2c)
        return -1;

    index = REG_to_BUF(reg);

    if (-1 == index)
        return -1;

    reg_rw_buf[index * 2] = (value & 0xFF00) >> 8;
    reg_rw_buf[index * 2 + 1] = value & 0x00FF;
    
    mutex_lock(&si4709_lock);
    
    ret = i2c_master_send(si4709_i2c,
                   &reg_rw_buf[SI4702_RW_OFFSET * 2],
                   (SI4702_STATUSRSSI - SI4702_POWERCFG) * 2);
    
    mutex_unlock(&si4709_lock);
    
    return ret;
}

/* Direct i2c channel writing */
static int
si4709_i2c_write(int size, unsigned char *buf) {
    
    int ret;

    if (NULL == si4709_i2c)
        return -1;

    mutex_lock(&si4709_lock);

    ret = i2c_master_send(si4709_i2c, buf, size);

    mutex_unlock(&si4709_lock);

    return ret;
    
}

/* I2C initialization stuff */
static int
SI4709_detach(struct i2c_client* client)
{
    int ret;

    ret = i2c_detach_client(client);
    if(ret < 0)
        LOG("detach failed %d\n", ret);

    return ret;
}

static int
SI4709_probe(struct i2c_adapter* adapter)
{
    int ret;

    ret = i2c_probe(adapter, &addr_data, &SI4709_attach);
    if(ret < 0)
        LOG("probe failed -> adapter id[%d] : %d\n", i2c_adapter_id(adapter), ret);

    return ret;
}

static int
SI4709_attach(struct i2c_adapter* adapter, int address, int kind)
{
    int ret;

    if(i2c_adapter_id(adapter) != 0)
    {
        LOG("Different i2c adapter\n");
        return -ENODEV;
    }

    if(address != SI4709_I2C_SLAVE_ADDR)
    {
        LOG("Different i2c address\n");
        return -ENODEV;
    }
    
    si4709_i2c = kzalloc(sizeof(struct i2c_client), GFP_KERNEL);
    if (si4709_i2c == NULL)
        return -ENOMEM;

    si4709_i2c->addr = address;
    si4709_i2c->adapter = adapter;
    si4709_i2c->driver = &SI4709_driver;
    si4709_i2c->flags = 0;
    strlcpy(si4709_i2c->name, SI4709_DEV_NAME, I2C_NAME_SIZE);

    ret = i2c_attach_client(si4709_i2c);
    if (ret < 0)
        LOG("i2c attach failed [%d]\n", i2c_adapter_id(adapter));
    else
        LOG("i2c attached : adapter[%d] addr[0x%X]\n", i2c_adapter_id(adapter), si4709_i2c->addr);

    return ret;
}

/* This is to catch the interrupt of the GPIO pin of the radio chip */
irqreturn_t
si4709_irq_handler (int irq, void* dev_id) {
    /* TODO */
    LOG("IRQ callback!");
    return IRQ_HANDLED;
}

/* Things to be done here: initialize pins and set RST pin to HIGH */
void si4709_initialize(void)
{
    /* Read a "random" register in order to initialize register cache */
    uint16_t dummy;
    si4709_read_reg(SI4702_POWERCFG, &dummy);

    /* Radio Chip GPIO pin setup (input, right?) */
    mxc_request_iomux(MX37_PIN_GPIO1_4, 4);
    mxc_iomux_set_pad(MX37_PIN_GPIO1_4, 5);
    mxc_set_gpio_direction(MX37_PIN_GPIO1_4, 1);
    
    //0x84, 0x2 -> reversed parameters, to be checked
    set_irq_type(FM_GPIO_INT_IRQ, IRQT_FALLING);
    if(request_irq(FM_GPIO_INT_IRQ, si4709_irq_handler, IRQF_DISABLED, SI4709_DEV_NAME, NULL))
    {
        LOG("si4709: IRQ Register Failed!\n");
        return;
    }
    set_irq_wake(FM_GPIO_INT_IRQ, 1);
    
    //SOME TESTS

   // si4709_write_reg(SI4702_POWERCFG, 0x6001);
  //  mdelay(500);
  //  si4709_read_reg(SI4702_TEST1, &dummy);
  //  si4709_write_reg(SI4702_POWERCFG, 0);
  //  LOG("Magic: %i\n", dummy);

    is_si4709_init = 1;
}

static ssize_t si4709_show_registers(struct device *dev, struct device_attribute *attr, char *buf)
{
    int i;
    ssize_t len = 0;

    len += sprintf(buf+len, "\n");
    len += sprintf(buf+len, "       0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f\n");
    len += sprintf(buf+len, "      -----------------------------------------------\n");
    len += sprintf(buf+len, "[%02Xh]:", 0);
    for(i=0; i<SI4709_REG_NUM; i++)
    {
        if(i && !(i%0x10))
            len += sprintf(buf+len, "\n[%02Xh]:", i);
        len += sprintf(buf+len, "%04X ", reg_rw_buf[i]);
    }
    len += sprintf(buf+len, "\n");

    return len;
}

static DEVICE_ATTR(registers, S_IRUSR | S_IRGRP, si4709_show_registers, NULL);

static struct attribute *si4709_attributes[] = {
    &dev_attr_registers.attr,
    NULL
};

static struct attribute_group si4709_attr_group = {
    .attrs = si4709_attributes
};

static int si4709attr_probe(struct platform_device *pdev)
{
    int ret = 0;

    LOG("si4709 platform driver attached\n");
    ret = sysfs_create_group(&pdev->dev.kobj, &si4709_attr_group);
    if( ret < 0 )
    {
        LOG("si4709 sysfs create group error\n");
        return -EINVAL;
    }

    return 0;
}

static int si4709attr_remove(struct platform_device *pdev)
{
    sysfs_remove_group(&pdev->dev.kobj, &si4709_attr_group);
    return 0;
}

static struct platform_driver si4709attr_driver = {
    .probe  = si4709attr_probe,
    .remove = si4709attr_remove,
    .driver      = {
        .name    = SI4709_DEV_NAME,
        .owner   = THIS_MODULE,
    },
};

static struct platform_device si4709attr_device = {
    .name    = SI4709_DEV_NAME,
    .id        = 0,
};

/* Turn on chip, attach i2c driver and device driver, initialize and then turn off */
static int
__init SI4709_init(void)
{
    int ret;
    
    si4709_power_on();

    ret = i2c_add_driver(&SI4709_driver);
    if (ret)    /* >0 means error for i2c_add_driver */
    {
        LOG("Can't add i2c driver\n");
        return ret;
    }

    ret = register_chrdev(SI4709_DEV_NUM, SI4709_DEV_NAME, &SI4709_fops);

    if (ret < 0)
    {
        LOG("Can't get major number for si4709 driver\n");
        return ret;
    }

    si4709_initialize();

    /* register platform device */
    ret = platform_device_register(&si4709attr_device);
    if(ret)
    {
        LOG("failed to add platform device %s (%d) \n", si4709attr_device.name, ret);
        return ret;
    }

    ret = platform_driver_register(&si4709attr_driver);
    if(ret)
    {
        LOG("failed to add platrom driver %s (%d) \n", si4709attr_driver.driver.name, ret);
        return ret;
    }

    si4709_power_off();

    return 0;
}

static void
__exit SI4709_exit(void)
{

    platform_driver_unregister(&si4709attr_driver);
    platform_device_unregister(&si4709attr_device);

    free_irq(FM_GPIO_INT_IRQ, si4709_irq_handler);
    mxc_free_iomux(MX37_PIN_GPIO1_4 ,4);

    unregister_chrdev(SI4709_DEV_NUM, SI4709_DEV_NAME);
    i2c_del_driver(&SI4709_driver);
}

module_init(SI4709_init);
module_exit(SI4709_exit);

MODULE_AUTHOR("Lorenzo Miori (C) 2012");
MODULE_DESCRIPTION("SI4709 FM Radio Chip Driver for Samsung YP-R0");
MODULE_SUPPORTED_DEVICE("si470x");
MODULE_LICENSE("GPL");
