/* 2009-08-18: File added and changed by Sony Corporation */
/*
 *  Copyright 2009 Sony Corporation.
 *
 *  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 of the  License.
 *
 *  THIS  SOFTWARE  IS PROVIDED   ``AS  IS'' AND   ANY  EXPRESS OR IMPLIED
 *  WARRANTIES,   INCLUDING, BUT NOT  LIMITED  TO, THE IMPLIED WARRANTIES OF
 *  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN
 *  NO  EVENT  SHALL   THE AUTHOR  BE    LIABLE FOR ANY   DIRECT, INDIRECT,
 *  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 *  NOT LIMITED   TO, PROCUREMENT OF  SUBSTITUTE GOODS  OR SERVICES; LOSS OF
 *  USE, DATA,  OR PROFITS; OR  BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 *  ANY THEORY OF LIABILITY, WHETHER IN  CONTRACT, STRICT LIABILITY, OR TORT
 *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 *  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 *  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/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/wait.h>
#include <linux/freezer.h>
#include <linux/snsc_nblargs.h>
#include <asm/irq.h>
#include <asm/uaccess.h>
#include <asm/semaphore.h>

#include "internal.h"
#include "navu2pm_irq.h"
#include "controller.h"
#include "event.h"
#include "chrdv.h"
#include "nup_custom_nblargs.h"

static int              navu2pm_ioctl(struct inode *inode, struct file *filep, 
									  unsigned int cmd, unsigned long arg);
static int              navu2pm_open(struct inode *inode, struct file *filep);
static int              navu2pm_release(struct inode *inode, struct file *filep);
static int              navu2pm_fasync(int fd, struct file *filep, int mode);
static navu2pm_status_t navu2pm_wait_irq(navu2pm_irq_t wait_irq);

static void             navu2pm_wait_irq_cancel(navu2pm_irq_t wait_irq);
static void             init_wait_irq(void);
static unsigned int     get_cradle_id(void);
static navu2pm_status_t get_boot_voltage(void);

typedef struct {
	navu2pm_eventset_t eventset;
} chrdv_data_t;

typedef struct {
	int  ready;
	wait_queue_head_t comp;
	int  (*judge)(void);
	int  canceled;
} wait_irq_t;

static struct fasync_struct *_chrdv_queue;

static struct file_operations navu2pm_fops = {
	.owner   = THIS_MODULE,
	.ioctl   = navu2pm_ioctl,
	.open    = navu2pm_open,
	.release = navu2pm_release,
	.fasync  = navu2pm_fasync,
};

static struct miscdevice navu2pm_miscdev = {
	.minor   = NAVU2PM_MISCDEV_MINOR,
	.name    = NAVU2PM_DEV_NAME,
	.fops    = &navu2pm_fops,
};

static DECLARE_MUTEX(_chrdv_mutex);

static wait_irq_t wait_irq[NAVU2PM_IRQ_NUM_MAX];
static int        boot_voltage_low;             

static int navu2pm_open(struct inode *inode, struct file *filp)
{
	navu2pm_status_t   status;
	chrdv_data_t       *chrdv_data;
	navu2pm_eventset_t *eventset;

	down(&_chrdv_mutex);
	chrdv_data = kmalloc(sizeof(chrdv_data_t), GFP_KERNEL);
	if(!chrdv_data){
		DPRINT("## Error : kmalloc\n");
		return NAVU2PM_ERR_NOMEM;
	}

	status = event_add_eventset_list(&chrdv_data->eventset);
	if(NAVU2PM_SUCCESS != status){
		DPRINT("## Error event_add_eventset_list\n");
		return -1;
	}

	eventset = &chrdv_data->eventset;
	eventset->num_event = 0;
	eventset->index = 0;

	filp->private_data = chrdv_data;

	up(&_chrdv_mutex);

	return 0;
}

static int navu2pm_ioctl(struct inode *inode, struct file *filp, 
						 unsigned int cmd, unsigned long arg)
{
	unsigned short           data;
	unsigned int             val;
	chrdv_data_t             *chrdv_data;
	navu2pm_charge_status_t  state;
	navu2pm_mode_t           mode;
	navu2pm_battery_status_t batt;
	navu2pm_irq_t            irq;
	int                      judge;     
	navu2_factor_t           factor; 
	int                      ret;
	int                      acc;

	switch(cmd){
	case NAVU2PM_GET_BATTERY_VOLTAGE:
		data = controller_get_battery_voltage();
		if(copy_to_user((unsigned short *)arg, &data, sizeof(unsigned short))){
			DPRINT("## Error : copy_to_user\n");

			return NAVU2PM_ERR_NOMEM;
		}
		break;

	case NAVU2PM_GET_BATTERY_CURRENT:
		data = controller_get_battery_current();
		if(copy_to_user((unsigned short*)arg, &data, sizeof(unsigned short))){
			DPRINT("## Error : copy_to_user\n");

			return NAVU2PM_ERR_NOMEM;
		}
		break;

	case NAVU2PM_GET_BATTERY_TEMPERATURE:
		data = controller_get_battery_temperature();
		if(copy_to_user((unsigned short*)arg, &data, sizeof(unsigned short))){
			DPRINT("## Error : copy_to_user\n");

			return NAVU2PM_ERR_NOMEM;
		}
		break;

	case NAVU2PM_GET_SUPPLY_VOLTAGE:
		data = controller_get_supply_voltage();
		if(copy_to_user((unsigned short*)arg, &data, sizeof(unsigned short))){
			DPRINT("## Error : copy_to_user\n");

			return NAVU2PM_ERR_NOMEM;
		}
		break;

	case NAVU2PM_GET_CHARGE_STATE:
		state= controller_get_charge_status();
		if(copy_to_user((navu2pm_charge_status_t *)arg, &state, sizeof(navu2pm_charge_status_t))){
			DPRINT("## Error : copy_to_user\n");

			return NAVU2PM_ERR_NOMEM;
		}
		break;

	case NAVU2PM_GET_MODE:
		mode= controller_get_mode();
		if(copy_to_user((navu2pm_mode_t*)arg, &mode, sizeof(navu2pm_mode_t))){
			DPRINT("## Error : copy_to_user\n");

			return NAVU2PM_ERR_NOMEM;
		}
		break;

	case NAVU2PM_GET_BATTERY_STATE:
		batt= controller_get_battery_status();
		if(copy_to_user((navu2pm_battery_status_t*)arg, &batt, sizeof(navu2pm_battery_status_t))){
			DPRINT("## Error : copy_to_user\n");

			return NAVU2PM_ERR_NOMEM;
		}
		break;

	case NAVU2PM_GET_DC_CONNECT:
		judge = controller_get_dc_connect();
		if(copy_to_user((int*)arg, &judge, sizeof(int))){
			DPRINT("## Error : copy_to_user\n");

			return NAVU2PM_ERR_NOMEM;
		}
		
		break;

	case NAVU2PM_GET_USB_CONNECT:
		judge = controller_get_usb_connect();
		if(copy_to_user((int*)arg, &judge, sizeof(int))){
			DPRINT("## Error : copy_to_user\n");

			return NAVU2PM_ERR_NOMEM;
		}
		break;		

	case NAVU2PM_GET_BATTERY_LOW: 
		judge = controller_get_supply_low();
		if(copy_to_user((int*)arg, &judge, sizeof(int))){
			DPRINT("## Error : copy_to_user\n");

			return NAVU2PM_ERR_NOMEM;
		}
		break;		

	case NAVU2PM_GET_PM_EVENT:
		chrdv_data = (chrdv_data_t *)filp->private_data;
		if(copy_to_user((navu2pm_eventset_t *)arg, &chrdv_data->eventset, sizeof(navu2pm_eventset_t))){
			DPRINT("## Error : copy_to_user\n");

			return NAVU2PM_ERR_NOMEM;
		}
		chrdv_data->eventset.num_event = 0;
		chrdv_data->eventset.index     = 0;
		
		break;

	case NAVU2PM_IRQ_WAIT:

		if(copy_from_user(&irq, (navu2pm_irq_t*)arg, sizeof(navu2pm_irq_t))){
			DPRINT("## Error : copy_from_user()\n");

			return NAVU2PM_ERR_NOMEM;
		}

		ret = navu2pm_wait_irq(irq);
		if(NAVU2PM_SUCCESS != ret){
			return ret;
		}

		break;

	case NAVU2PM_IRQ_WAIT_CANCEL:

		if(copy_from_user(&irq, (navu2pm_irq_t*)arg, sizeof(navu2pm_irq_t))){
			DPRINT("## Error : copy_from_user()\n");

			return NAVU2PM_ERR_NOMEM;
		}

		navu2pm_wait_irq_cancel(irq);

		break;

	case NAVU2PM_SET_MODE_SUSPEND:

		controller_request_suspend();

		break;

	case NAVU2PM_GET_ACC_WAKE:

		acc = controller_get_acc_wake();
		if(copy_to_user((int*)arg, &acc, sizeof(int ))){
			DPRINT("## Error : copy_to_user\n");

			return NAVU2PM_ERR_NOMEM;
		}
		
		break;

	case NAVU2PM_SET_ACC_WAKE:

		if(copy_from_user(&acc, (int*)arg, sizeof(int))){
			DPRINT("## Error : copy_from_user()\n");

			return NAVU2PM_ERR_NOMEM;
		}

		controller_set_acc_wake(acc);		

		break;

	case NAVU2PM_GET_CRADLE_ID:

		val = get_cradle_id();
		if(copy_to_user((unsigned int*)arg, &val, sizeof(unsigned int))){
			DPRINT("## Error : copy_to_user()\n");

			return NAVU2PM_ERR_NOMEM;
		}
		
		break;

	case NAVU2PM_GET_RESUME_FACTOR:

		factor = controller_get_resume_factor();
		if(copy_to_user((navu2_factor_t*)arg, &factor, sizeof(factor))){
			DPRINT("## Error : copy_to_user()\n");

			return NAVU2PM_ERR_NOMEM;
		}

		break;

	case NAVU2PM_GET_BOOT_VOLTAGE_LOW:

		if(copy_to_user((int*)arg, &boot_voltage_low, sizeof(int))){
			DPRINT("## Error : copy_to_user\n");

			return NAVU2PM_ERR_NOMEM;
		}

		break;

	default:
		DPRINT("## Invalid command!!\n");

		return NAVU2PM_ERR_NOMEM;

		break;		
	}


	return NAVU2PM_SUCCESS;
}

static int navu2pm_release(struct inode *inode, struct file *filp)
{
	chrdv_data_t *chrdv_data;

	chrdv_data = (chrdv_data_t *)filp->private_data;
	event_del_eventset_list(&chrdv_data->eventset);
	kfree(chrdv_data);
	
	return 0;
}

static int navu2pm_fasync(int fd, struct file *filp, int mode)
{
	return fasync_helper(fd, filp, mode, &_chrdv_queue);
}

static navu2pm_status_t navu2pm_wait_irq(navu2pm_irq_t irq)
{
	int judge;
	int ret;

	if(irq < 0 || irq >= NAVU2PM_IRQ_NUM_MAX){
		DPRINT("## invalid irq num\n");

		return NAVU2PM_ERR_OTHER;
	}

	/* CIGAR IN/OUT invalidated */
	if(irq == NAVU2PM_IRQ_CIGAR_IN || 
	   irq == NAVU2PM_IRQ_CIGAR_OUT){
		DPRINT("## Cigar In/Out Invalidated!! \n");

		return NAVU2PM_ERR_OTHER;
	}

	if(FALSE == wait_irq[irq].ready){
		init_waitqueue_head(&(wait_irq[irq].comp));
		wait_irq[irq].canceled = FALSE;
		wait_irq[irq].ready = TRUE;
	}
	
	if(NULL != wait_irq[irq].judge){
		judge = wait_irq[irq].judge();
		if(TRUE == judge){
			wait_irq[irq].ready = FALSE;	
			return NAVU2PM_SUCCESS;
		}
	}

	ret = wait_event_freezable(wait_irq[irq].comp, wait_irq[irq].ready == FALSE);
	if(-ERESTARTSYS == ret){
		return ret;		
	} 

	if(TRUE == wait_irq[irq].canceled){
		DPRINT("## Wait Canceled\n");
		return NAVU2PM_ERR_CANCEL;
	}

	return NAVU2PM_SUCCESS;
}

static void navu2pm_wait_irq_cancel(navu2pm_irq_t irq)
{
	if(irq < 0 || irq >= NAVU2PM_IRQ_NUM_MAX){
		DPRINT("## invalid irq num\n");

		return ;
	}

	if(wait_irq[irq].ready){
		wait_irq[irq].canceled = TRUE;
		wait_irq[irq].ready = FALSE;
		wake_up_all(&(wait_irq[irq].comp));
	}

	return ;
}

static void init_wait_irq(void)
{
	navu2pm_irq_t count;

	for(count = 0; count < NAVU2PM_IRQ_NUM_MAX; count++){
		wait_irq[count].ready = FALSE;

		init_waitqueue_head(&(wait_irq[count].comp));

		switch(count){
		case NAVU2PM_IRQ_DC_IN:
			wait_irq[count].judge = controller_judge_dc_in;
			break;

		case NAVU2PM_IRQ_DC_OUT:
			wait_irq[count].judge = controller_judge_dc_out;
			break;

		case NAVU2PM_IRQ_USB_IN:
			wait_irq[count].judge = controller_judge_usb_in;
			break;

		case NAVU2PM_IRQ_USB_OUT:
			wait_irq[count].judge = controller_judge_usb_out;
			break;

		case NAVU2PM_IRQ_LOW_BATTERY:
			wait_irq[count].judge = controller_judge_supply_low;
			break;

		case NAVU2PM_IRQ_POWER_KEY:
			wait_irq[count].judge = controller_judge_pk_press;
			break;

		default:
			wait_irq[count].judge = NULL;
			break;
		}
	}

	return ;
}

static unsigned int get_cradle_id(void)
{
	unsigned int val;
	
	pwc_write(PWC_ADMODE,  0x16, 0x37);

	msleep(100);

	pwc_read(PWC_ADDATA7, &val);

	pwc_write(PWC_ADMODE,  0x00, 0x37);

	return val;
}

static navu2pm_status_t get_boot_voltage(void)
{
	struct nblargs_entry na;
	struct nup_custom_nblargs_t *pargs;

	if(nblargs_get_key(NUP_CUSTOM_NBLARGS_KEY, &na) < 0) {
		DPRINT("NBLArgs key %s not found\n", NUP_CUSTOM_NBLARGS_KEY);
		return -1;
	}

	pargs = (struct nup_custom_nblargs_t *)nbl_to_va(na.addr);
	if(NUP_CUSTOM_MAGIC != pargs->magic){
		DPRINT("NUP_CUSTOM_MAGIC(0x%04x) is invalid(0x%04x)\n",
			   NUP_CUSTOM_MAGIC, pargs->magic);
		goto exit;
	}

	boot_voltage_low = pargs->suspend;

  exit:
	nblargs_free_key(NUP_CUSTOM_NBLARGS_KEY);

	return 0;
}

navu2pm_status_t complete_wait_irq(navu2pm_irq_t irq)
{
	if(irq < 0 || irq >= NAVU2PM_IRQ_NUM_MAX){
		return NAVU2PM_ERR_OTHER;
	}

	if(!wait_irq[irq].ready){
		return NAVU2PM_ERR_NOT_READY;
	}

	wait_irq[irq].ready = FALSE;
	wake_up_all(&(wait_irq[irq].comp));

	return NAVU2PM_SUCCESS;
}

navu2pm_status_t chrdv_init(void)
{
	navu2pm_status_t ret;

	init_wait_irq();

	ret = misc_register(&navu2pm_miscdev);
	if(ret < 0){
		DPRINT("## Error misc_register = %d\n", ret);
		return NAVU2PM_ERR_OTHER;
	}

	ret = navu2pm_register_irq();
	if(ret != NAVU2PM_SUCCESS){
		DPRINT("## Error navu2pm_register_irq()\n");
	}

	ret = get_boot_voltage();
	if(ret){
		DPRINT("## Error get_boot_voltage()\n");
	}

	return NAVU2PM_SUCCESS;
}

void chrdv_cleanup(void)
{
	misc_deregister(&navu2pm_miscdev);

	return ;
}

void chrdv_notify(void)
{
	kill_fasync(&_chrdv_queue, SIGIO, POLL_IN);

	return ;
}
