/*
* Driver interface to the ASIC Complasion chip on the iPAQ H3800
*
* Copyright 2001 Compaq Computer Corporation.
*
* Use consistent with the GNU GPL is permitted,
* provided that this copyright notice is
* preserved in its entirety in all copies and derived works.
*
* COMPAQ COMPUTER CORPORATION MAKES NO WARRANTIES, EXPRESSED OR IMPLIED,
* AS TO THE USEFULNESS OR CORRECTNESS OF THIS CODE OR ITS
* FITNESS FOR ANY PARTICULAR PURPOSE.
*
* Author:  Andrew Christian
*          <Andrew.Christian@compaq.com>
*          October 2001
*/

#include <linux/module.h>
#include <linux/version.h>
#include <linux/config.h>

#include <linux/init.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/sched.h>
#include <linux/pm.h>
#include <linux/sysctl.h>
#include <linux/proc_fs.h>
#include <linux/mtd/mtd.h>
#include <linux/ctype.h>
#include <linux/delay.h>

#include <asm/irq.h>
#include <asm/uaccess.h>   /* for copy to/from user space */
#include <asm/arch/hardware.h>
#include <asm/arch-sa1100/h3600_hal.h>
#include <asm/arch/h3900_asic.h>

#include "../mach-sa1100/h3600_asic_io.h"
#include "../mach-sa1100/h3600_asic_mmc.h"
#include "../mach-sa1100/h3600_asic_core.h"
#include "../mach-ipaq/h3600_asic_battery.h"
#include "asm/arch-sa1100/ipaq-mtd-asset.h"

#define H3600_ASIC_PROC_DIR     "asic"
#define H3600_ASIC_PROC_STATS   "stats"
#define REG_DIRNAME "registers"

MODULE_AUTHOR("Andrew Christian");
MODULE_DESCRIPTION("Hardware abstraction layer for the iPAQ H3800");
MODULE_LICENSE("Dual BSD/GPL");

/* Statistics */
struct asic_statistics g_h3600_asic_statistics;

#define PDEBUG(format,arg...) printk(KERN_DEBUG __FILE__ ":%s - " format "\n", __FUNCTION__, ## arg)
#define PALERT(format,arg...) printk(KERN_ALERT __FILE__ ":%s - " format "\n", __FUNCTION__, ## arg)
#define PERROR(format,arg...) printk(KERN_ERR __FILE__ ":%s - " format "\n", __FUNCTION__, ## arg)

extern struct asic2_battery_ops h3900_battery_ops;

/***********************************************************************************
 *      Power button handling
 ***********************************************************************************/

static void h3600_asic_power_isr(int irq, void *dev_id, struct pt_regs *regs)
{
        int down = (GPLR0 & GPIO_H3900_POWER_BUTTON_N) ? 0 : 1;
	h3600_hal_keypress( H3600_MAKEKEY( H3600_KEYCODE_SUSPEND, down ) );
}

/***********************************************************************************
 *      Generic IRQ handling
 ***********************************************************************************/

static int __init h3600_asic_init_isr( void )
{
	int result;
	DEBUG_INIT();

	/* Request IRQs */
        set_GPIO_IRQ_edge( GPIO_NR_H3900_POWER_BUTTON_N, GPIO_BOTH_EDGES );
	result = request_irq(IRQ_GPIO_H3900_POWER_BUTTON_N, 
			     h3600_asic_power_isr, 
			     SA_INTERRUPT | SA_SAMPLE_RANDOM,
			     "h3600_suspend", NULL);

	if ( result ) {
		PERROR("unable to grab power button IRQ %d, error %d", IRQ_GPIO_H3900_POWER_BUTTON_N, result);
		return result;
	}

	set_GPIO_IRQ_edge( GPIO_NR_H3900_AC_IN_N, GPIO_BOTH_EDGES );
	result = request_irq(IRQ_GPIO_H3900_AC_IN_N,
			     h3600_asic_ac_in_isr,
			     SA_INTERRUPT | SA_SAMPLE_RANDOM,
			     "h3800_ac_in", NULL );
	if ( result ) {
		PERROR("unable to grab AC in IRQ");
		free_irq(IRQ_GPIO_H3900_POWER_BUTTON_N, NULL );
		return result;
	}

	set_GPIO_IRQ_edge( GPIO_NR_H3900_OPT_IND_N, GPIO_BOTH_EDGES );
	result = request_irq(IRQ_GPIO_H3900_OPT_IND_N,
			     h3600_asic_sleeve_isr,
			     SA_INTERRUPT | SA_SAMPLE_RANDOM,
			     "h3800_sleeve", NULL );
	if ( result ) {
		PERROR("unable to grab sleeve insertion IRQ");
		free_irq(IRQ_GPIO_H3900_POWER_BUTTON_N, NULL );
		free_irq(IRQ_GPIO_H3900_AC_IN_N, NULL );
		return result;
	}

	return 0;
}

static void  h3600_asic_release_isr( void )
{
	DEBUG_INIT();

//	H3800_ASIC2_KPIINTSTAT  = 0;    
//	H3800_ASIC2_GPIINTSTAT  = 0;

	free_irq(IRQ_GPIO_H3900_POWER_BUTTON_N, NULL);
	free_irq(IRQ_GPIO_H3900_AC_IN_N, NULL);
	free_irq(IRQ_GPIO_H3900_OPT_IND_N, NULL);
}


/***********************************************************************************/
/*      Standard entry points for HAL requests                                     */
/***********************************************************************************/

static int h3900_asic_version( struct h3600_ts_version *result )
{
	return -EINVAL;
}

static int h3900_asic_eeprom_read( unsigned short address, unsigned char *data, unsigned short len )
{
	if (0) PDEBUG(": address=%d len=%d", address, len);
        return -EINVAL;
}

static int h3900_asic_eeprom_write( unsigned short address, unsigned char *data, unsigned short len )
{
        return -EINVAL;
}

static int h3900_asic_thermal_sensor( unsigned short *result )
{
	return -EINVAL;
}

static int h3900_asic_notify_led( unsigned char mode, unsigned char duration, 
			    unsigned char ontime, unsigned char offtime )
{
	return -EINVAL;
}

static int h3900_asic_spi_write(unsigned short address, unsigned char *data, unsigned short len)
{
	return -EINVAL;
}

static int h3900_asic_read_light_sensor( unsigned char *result )
{
	int value = h3600_asic_adc_read_channel( ASIC2_ADMUX_0_LIGHTSENSOR );
	if ( value >= 0 ) {
		*result = value >> 2;
		return 0;
	}
		
	return -EIO;
}

static int h3900_asic_option_pack( int *result )
{
        int opt_ndet = (GPLR0 & GPIO_H3900_OPT_IND_N);

	if (0) PDEBUG(": opt_ndet=%d", opt_ndet);
	*result = opt_ndet ? 0 : 1;
	return 0;
}

/***********************************************************************************
 *   Proc filesystem interface
 ***********************************************************************************/

static int h3600_asic_reset_handler(ctl_table *ctl, int write, struct file * filp,
			     void *buffer, size_t *lenp);

static struct ctl_table h3600_asic_table[] =
{
	{ 1, "reset", NULL, 0, 0600, NULL, (proc_handler *) &h3600_asic_reset_handler },
	{ 2, "battery", NULL, 0, 055, h3600_asic_battery_table },
	{0}
};

static struct ctl_table h3600_asic_dir_table[] =
{
	{1, "asic", NULL, 0, 0555, h3600_asic_table},
	{0}
};

static struct ctl_table_header *h3600_asic_sysctl_header = NULL;

static struct proc_dir_entry   *asic_proc_dir;
static struct proc_dir_entry   *adc_proc_dir;

#define PRINT_DATA(x,s) \
	p += sprintf (p, "%-28s : %d\n", s, g_h3600_asic_statistics.x)

#define PRINT_OWM_DATA(x,s) \
	p += sprintf (p, "%-28s : %d\n", s, g_owm_statistics.x)

static int h3600_asic_proc_stats_read(char *page, char **start, off_t off,
				      int count, int *eof, void *data)
{
	char *p = page;
	int len;
	int i;
	int ex1, ex2;

	PRINT_DATA(spi_bytes,   "SPI bytes sent/received");
	PRINT_DATA(spi_wip,     "SPI write-in-process delays");
	PRINT_DATA(spi_timeout, "SPI timeouts");
	PRINT_OWM_DATA(owm_timeout, "OWM timeouts");
	PRINT_OWM_DATA(owm_reset,   "OWM reset");
	PRINT_OWM_DATA(owm_written, "OWM written");
	PRINT_OWM_DATA(owm_read,    "OWM read");
	p += sprintf(p,         "OWM ISR received   Valid Invalid Post\n");
	for ( i = 0 ; i < 5 ; i++ )
		p += sprintf(p, "     %10s : %4d   %4d   %4d\n", 
			     owm_state_names[i],
			     g_owm_statistics.owm_valid_isr[i],
			     g_owm_statistics.owm_invalid_isr[i],
			     g_owm_statistics.owm_post_isr[i]);

	h3600_asic_shared_debug (&ex1, &ex2);
	p += sprintf(p, "%-28s : %d\n", "EX1 usage", ex1 );
	p += sprintf(p, "%-28s : %d\n", "EX2 usage", ex2 );

	PRINT_DATA(mmc_insert,  "MMC insert events");
	PRINT_DATA(mmc_eject,   "MMC eject event");
	PRINT_DATA(mmc_reset,   "MMC reset commands");
	PRINT_DATA(mmc_command, "MMC commands issued");
	PRINT_DATA(mmc_read,    "MMC blocks read");
	PRINT_DATA(mmc_written, "MMC blocks written");
	PRINT_DATA(mmc_timeout, "MMC timeout interrupts");
	PRINT_DATA(mmc_error ,  "MMC response errors");

	len = (p - page) - off;
	if (len < 0)
		len = 0;

	*eof = (len <= count) ? 1 : 0;
	*start = page + off;

	return len;
}

static int h3600_asic_proc_adc_read(char *page, char **start, off_t off,
				    int count, int *eof, void *data )
{
	int result;
	char *p = page;

	MOD_INC_USE_COUNT;
	result = h3600_asic_adc_read_channel((int) data);
	if ( result < 0 )
		p += sprintf(p, "Error code %d\n", result );
	else
		p += sprintf(p, "0x%04x\n",(unsigned int) result );

	*eof = 1;
	MOD_DEC_USE_COUNT;
	return (p - page);
}

struct simple_proc_entry {
	char *        name;
	read_proc_t * read_proc;
};

static const struct simple_proc_entry sproc_list[] = {
	{ "stats",      h3600_asic_proc_stats_read },
	{ "battery",    h3600_asic_proc_battery },
	{ "ds2760",     h3600_asic_proc_battery_ds2760 },
	{ "ds2760_raw", h3600_asic_proc_battery_raw },
	{ "wakeup",     h3600_asic_proc_wakeup_read },
};

static int __init h3600_asic_register_procfs( void )
{
	int i;

	asic_proc_dir = proc_mkdir(H3600_ASIC_PROC_DIR, NULL);
	if ( !asic_proc_dir ) {
		PALERT("unable to create proc entry %s", H3600_ASIC_PROC_DIR);
		return -ENOMEM;
	}

	for ( i = 0 ; i < ARRAY_SIZE(sproc_list) ; i++ )
		create_proc_read_entry(sproc_list[i].name, 0, asic_proc_dir, 
				       sproc_list[i].read_proc, NULL);
	adc_proc_dir = proc_mkdir("adc", asic_proc_dir);
	if (adc_proc_dir == NULL) {
		PERROR("can't create /proc/adc");
		return(-ENOMEM);
	}
	for (i=0;i<5;i++) {
		unsigned char name[10];
		sprintf(name,"%d",i);
		create_proc_read_entry(name, S_IWUSR |S_IRUSR | S_IRGRP | S_IROTH, 
					       adc_proc_dir, h3600_asic_proc_adc_read, (void *) i );
	}

	h3600_asic_sysctl_header = register_sysctl_table(h3600_asic_dir_table, 0 );
	return 0;
}

static void  h3600_asic_unregister_procfs( void )
{
	int i;
	if ( asic_proc_dir ) {
		unregister_sysctl_table(h3600_asic_sysctl_header);
		for(i=0;i<5;i++) {
			char name[10];
			sprintf(name,"%d",i);
			remove_proc_entry(name,adc_proc_dir);
		}
		remove_proc_entry("adc",asic_proc_dir);
		for (i=0 ; i<ARRAY_SIZE(sproc_list) ; i++ )
			remove_proc_entry(sproc_list[i].name, asic_proc_dir);
		remove_proc_entry(H3600_ASIC_PROC_DIR, NULL );
		asic_proc_dir = NULL;
	}
}

/***********************************************************************************
 *   Utility routines
 ***********************************************************************************/

struct asic_system_handler { 
	char *name;
	int  (*init)( void );
	void (*cleanup)( void );
	int  (*suspend)( void );
	void (*resume)( void );
};

/* 
   We initialize and resume from top to bottom,
   cleanup and suspend from bottom to top 
*/
   
const struct asic_system_handler h3900_asic_system_handlers[] = {
	{ 
		name:    "shared",
		init:    h3600_asic_shared_init,      
		cleanup: h3600_asic_shared_cleanup 
	},{ 
		name:   "adc",
		init:    h3600_asic_adc_init,
		cleanup: h3600_asic_adc_cleanup,
		suspend: h3600_asic_adc_suspend,
		resume:  h3600_asic_adc_resume,
	},{ 
		name:    "key",
		init:    h3600_asic_key_init,
		cleanup: h3600_asic_key_cleanup,
		suspend: h3600_asic_key_suspend,
		resume:  h3600_asic_key_resume 
	},{ 
		name:    "spi",
		init:    h3600_asic_spi_init,
		cleanup: h3600_asic_spi_cleanup,
		suspend: h3600_asic_spi_suspend,
		resume:  h3600_asic_spi_resume 
	},{ 
		name:    "backlight",
		init:    h3600_asic_backlight_init,   
		cleanup: h3600_asic_backlight_cleanup 
	},{ 
		name:    "touchscreen",
		init:    h3600_asic_touchscreen_init, 
		cleanup: h3600_asic_touchscreen_cleanup,
		suspend: h3600_asic_touchscreen_suspend, 
		resume:  h3600_asic_touchscreen_resume 
	},{ 
		name:    "owm",
		init:    h3600_asic_owm_init, 
		cleanup: h3600_asic_owm_cleanup,
		suspend: h3600_asic_owm_suspend, 
		resume:  h3600_asic_owm_resume 
	},{ 
		name:    "battery",
		init:    h3600_asic_battery_init, 
		cleanup: h3600_asic_battery_cleanup,
		suspend: h3600_asic_battery_suspend, 
		resume:  h3600_asic_battery_resume 
	},{ 
		name:    "audio",
		init:    h3600_asic_audio_init, 
		cleanup: h3600_asic_audio_cleanup,
		suspend: h3600_asic_audio_suspend, 
		resume:  h3600_asic_audio_resume  
	},{ 
		name:    "asset",
		init:    ipaq_mtd_asset_init,   
		cleanup: ipaq_mtd_asset_cleanup 
	},
#if defined(CONFIG_MMC) || defined(CONFIG_MMC_MODULE)
	{
		name:    "mmc",
		init:    h3600_asic_mmc_init,
		cleanup: h3600_asic_mmc_cleanup,
		suspend: h3600_asic_mmc_suspend,
		resume:  h3600_asic_mmc_resume
	}
#endif
};

#define SYS_HANDLER_SIZE   (sizeof(h3900_asic_system_handlers)/sizeof(struct asic_system_handler))

static int h3600_asic_suspend_handlers( void )
{
	int i;
	int result;

	for ( i = SYS_HANDLER_SIZE - 1 ; i >= 0 ; i-- ) {
		if ( h3900_asic_system_handlers[i].suspend ) {
			if ((result = h3900_asic_system_handlers[i].suspend()) != 0 ) {
				while ( ++i < SYS_HANDLER_SIZE )
					if ( h3900_asic_system_handlers[i].resume )
						h3900_asic_system_handlers[i].resume();
				return result;
			}
		}
	}
	return 0;
}

static void h3600_asic_resume_handlers( void )
{
	int i;

	for ( i = 0 ; i < SYS_HANDLER_SIZE ; i++ )
		if ( h3900_asic_system_handlers[i].resume )
			h3900_asic_system_handlers[i].resume();
}

static int __init h3600_asic_init_handlers( void )
{
	int i;
	int result;

	for ( i = 0 ; i < SYS_HANDLER_SIZE ; i++ ) {
		if ( h3900_asic_system_handlers[i].init ) {
			if ( (result = h3900_asic_system_handlers[i].init()) != 0 ) {
				PDEBUG(": error %d", result);
				while ( --i >= 0 )
					if ( h3900_asic_system_handlers[i].cleanup )
						h3900_asic_system_handlers[i].cleanup();
				return result;
			}
		}
	}
	return 0;
}

static void  h3600_asic_cleanup_handlers( void )
{
	int i;

	for ( i = SYS_HANDLER_SIZE - 1 ; i >= 0 ; i-- )
		if ( h3900_asic_system_handlers[i].cleanup )
			h3900_asic_system_handlers[i].cleanup();
}

/***********************************************************************************
 *   Power management
 *
 *   On sleep, if we return anything other than "0", we will cancel sleeping.
 *
 *   On resume, if we return anything other than "0", we will put the iPAQ
 *     back to sleep immediately.
 ***********************************************************************************/

static int h3600_asic_pm_callback(pm_request_t req)
{
	int result = 0;
        if (0) PDEBUG(": req=%d", req );

	switch (req) {
	case PM_SUSPEND:
		H3800_ASIC2_INTR_MaskAndFlag &= ~ASIC2_INTMASK_GLOBAL;
		result = h3600_asic_suspend_handlers();
		H3800_ASIC2_KPIINTSTAT  = 0;    
		H3800_ASIC2_GPIINTSTAT  = 0;
		h3600_asic_initiate_sleep ();
		break;

	case PM_RESUME:
		result = h3600_asic_check_wakeup ();
		if ( !result ) {
			/* These probably aren't necessary */
			H3800_ASIC2_KPIINTSTAT     =  0;     /* Disable all interrupts */
			H3800_ASIC2_GPIINTSTAT     =  0;

			H3800_ASIC2_KPIINTCLR      =  0xffff;     /* Clear all KPIO interrupts */
			H3800_ASIC2_GPIINTCLR      =  0xffff;     /* Clear all GPIO interrupts */

			H3800_ASIC2_CLOCK_Enable       |= ASIC2_CLOCK_EX0;   /* 32 kHZ crystal on */
			H3800_ASIC2_INTR_ClockPrescale |= ASIC2_INTCPS_SET;
			H3800_ASIC2_INTR_ClockPrescale  = ASIC2_INTCPS_CPS(0x0e) | ASIC2_INTCPS_SET;
			H3800_ASIC2_INTR_TimerSet       = 1;

			h3600_asic_resume_handlers();
			H3800_ASIC2_INTR_MaskAndFlag  |= ASIC2_INTMASK_GLOBAL; /* Turn on ASIC interrupts */
		}
		break;
	}
	return result;
}

static int h3600_asic_reset_handler(ctl_table *ctl, int write, struct file * filp,
				    void *buffer, size_t *lenp)
{
	MOD_INC_USE_COUNT;
	h3600_asic_pm_callback( PM_SUSPEND );
	h3600_asic_pm_callback( PM_RESUME );
	MOD_DEC_USE_COUNT;

	return 0;
}


/***********************************************************************************
 *   Initialization code
 ***********************************************************************************/

static struct h3600_hal_ops h3800_asic_ops = {
	get_version         : h3900_asic_version,
	eeprom_read         : h3900_asic_eeprom_read,
	eeprom_write        : h3900_asic_eeprom_write,
	get_thermal_sensor  : h3900_asic_thermal_sensor,
	set_notify_led      : h3900_asic_notify_led,
	read_light_sensor   : h3900_asic_read_light_sensor,
	get_battery         : h3600_asic_battery_read,
	spi_read            : h3600_asic_spi_read,
	spi_write           : h3900_asic_spi_write,
	sleeve_battery_read : h3600_asic_spi_read_pcmcia_battery,
	get_option_detect   : h3900_asic_option_pack,
	audio_clock         : h3600_asic_audio_clock,
	audio_power         : h3600_asic_audio_power,
	audio_mute          : h3600_asic_audio_mute,
	backlight_control   : h3600_asic_backlight_control,
	asset_read          : ipaq_mtd_asset_read,
	set_ebat            : h3600_asic_spi_set_ebat,
        owner               : THIS_MODULE,
};

static void  h3600_asic_cleanup( void )
{
	h3600_unregister_pm_callback( h3600_asic_pm_callback );
	h3600_asic_unregister_procfs();
	h3600_hal_unregister_interface( &h3800_asic_ops );
	h3600_asic_cleanup_handlers();
	h3600_asic_release_isr();
}

int __init h3900_asic3_check( void )
{
	int i;
	u16 valid[] = HWPROTECT_ARRAY_VALUES;
	int result = 0;

	if (0) printk("Checking ASIC3 hardware register:");
	for ( i = 0 ; i < HWPROTECT_ARRAY_LEN ; i++ ) {
		u16 value = H3900_ASIC3_HWPROTECT_ARRAY[i] & 0xffff;
		if (0) printk(" 0x%04x", value);
		if ( value != valid[i] )
			result = 1;
	}
	if (0) printk("\n");
	if (0) printk("...%s", result ? "invalid" : "valid" );
	return result;
}

int __init h3600_asic_init( void )
{
	int result;

	PDEBUG("Loading....");

	if ( !machine_is_h3900() ) {
		PDEBUG(": unknown iPAQ model %s", h3600_generic_name() );
		return -ENODEV;
	}

	if ( h3900_asic3_check() ) {
		PDEBUG(": unknown ASIC3 configuration");
		return -ENODEV;
	}

	result = h3600_asic_init_isr();  
	if ( result ) return result;

	h3600_asic_register_battery_ops (&h3900_battery_ops);

	result = h3600_asic_init_handlers();
	if ( result ) goto init_fail;

	result = h3600_hal_register_interface( &h3800_asic_ops );
	if ( result ) goto init_fail;

	result = h3600_asic_register_procfs();
	if ( result ) goto init_fail;

	h3600_register_pm_callback( h3600_asic_pm_callback );

	H3800_ASIC2_INTR_MaskAndFlag  |= ASIC2_INTMASK_GLOBAL; /* Turn on ASIC interrupts */
	return 0;

init_fail:
	PDEBUG(": FAILURE!  Exiting...");
	h3600_asic_cleanup();
	return result;
}

void  h3600_asic_exit( void )
{
	h3600_asic_cleanup();
	H3800_ASIC2_INTR_MaskAndFlag &= ~ASIC2_INTMASK_GLOBAL;
}

module_init(h3600_asic_init)
module_exit(h3600_asic_exit)
