#ifndef __KERNEL__
#  define __KERNEL__
#endif

#ifndef MODULE
#  define MODULE
#endif

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

#if defined(MODVERSIONS)
#include <linux/modversions.h>
#endif

#include <linux/sched.h>
#include <linux/kernel.h>
#include <linux/stddef.h>
#include <linux/proc_fs.h>
#include <linux/malloc.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/ioport.h>
#include <linux/file.h>
#include <linux/smp_lock.h>
#include <linux/mc146818rtc.h>
#include <linux/ctype.h>

#include <asm/semaphore.h>

int errno;

#include "cwkeyer.h"
#include "morse_code.c"

#define CWKEYER_MAJOR 60
int cwkeyer_major =   CWKEYER_MAJOR;

#include <asm/uaccess.h>

#define IRQ_FREQ 		512

typedef struct device_param
{
	int			delay;
	int			rtc_thread_shutdown;
	struct semaphore 	startstop_sem;
	int			par_base;
	int			par_value;
} device_param;
device_param device_params;


typedef struct setting
{
	int 	orig_speed;	/* morse speed in wpm			*/
	int 	speed;		/* morse speed in wpm			*/
	int	frequency;	/* sidetone frequency 			*/
	int 	freq_div;	/* freqency divider for sidetone	*/
	int	monitor;	/* sidetone on/off 			*/
	int 	weigth;		/* weigth value for dash -5..+5		*/
	int	tx_delay;	/* time between PTT and start keying 	*/
	int	number_opt;	/* change number to letters		*/
	int	pos_neg_key;	/* positive or negative keying		*/
	char	*qsonumber;	/* qso number 			(#)	*/
	char	*call;		/* call of other station 	(@)	*/
	char	*rst;		/* report 			(~)	*/
} setting;
setting settings;

/* 
 * Ringbuffer 
 */

#define BUF_SIZE 128

typedef struct {
	char	buf[BUF_SIZE];
	int	begin;
	int	end;
} ring_buf;

ring_buf	message_buf;	/* incoming messages (ASCII)		*/
ring_buf	morse_buf;	/* incoming translated to morse		*/
ring_buf	call_buf;	/* call translated to morse		*/
ring_buf	rst_buf;	/* rst translated to morse		*/
ring_buf	qsonumber_buf;	/* qsonumber translated to morse	*/

/**
 * Ring buffer functions 
 */

/* initialise */
void buf_init(ring_buf *buf)
{
	buf->begin = 0;
	buf->end = 0;
}

/* empty the buffer */
void flush_buf(ring_buf *buf)
{
	buf->begin = buf->end;
}

/* check whether buffer is empty */
int buf_empty(ring_buf *buf)
{
	return (buf->begin == buf->end ? 1 : 0);
}

/* check whether buffer is full */
int buf_full(ring_buf *buf)
{
	return (buf->begin == (buf->end+1)%BUF_SIZE ? 1 : 0);
}

/* add character to buffer */
int buf_add(ring_buf *buf, char x)
{
	int 	retval = 0;

	if	(!buf_full(buf))
	{
		buf->buf[buf->end] = x;

		buf->end++;
		buf->end = (buf->end == BUF_SIZE) ? 0 : buf->end;

		retval = 1;	
	}

	return retval;
}

/* add string to start of buffer */
void buf_add_szstr_raw(ring_buf *buf, const char* str)
{
	int	i;

	for	(i=0; i<strlen(str); i++)
	{
		buf->buf[i] = toupper(str[i]);
	}

	buf->end = strlen(str);
}

/* add string to buffer */
int buf_add_str(ring_buf *buf, const char *str, int count)
{
	int 	i;

	for	(i=0; i<count; i++)
	{
		if	(!buf_add(buf, toupper(str[i])))
		{
			return i;
		}
	}

	return i;
}

/* add zero terminated string to buffer */
void buf_add_szstr(ring_buf *buf, const char *str)
{
	buf_add_str(buf, str, strlen(str));
}

/* get one character from buffer */
char buf_get(ring_buf *buf)
{
	char x = buf->buf[buf->begin];

	buf->begin++;
	buf->begin = (buf->begin == BUF_SIZE) ? 0 : buf->begin;

	return x;
}


/**
 * Sound functions
 */

/* sound off */
static inline void kd_nosound(void)
{
    	cli();

        outb(inb_p(0x61)&0xFC, 0x61);

    	sti();
}

/* sound on */
static inline void kd_mksound(void)
{
   	cli();

	/* enable counter 2 */
	outb_p(inb_p(0x61)|3, 0x61);

	/* set command for counter 2, 2 byte write */
	outb_p(0xB6, 0x43);

	/* select desired HZ */
	outb_p(settings.freq_div & 0xff, 0x42);
	outb((settings.freq_div >> 8) & 0xff, 0x42);

	sti();
}

/** 
 *  Hardware functions
 */

#define	PTT (1<<2)	/* bit 2 */
#define KEY (1<<3)	/* bit 3 */

void hw_key_up(void)
{
	if	(settings.pos_neg_key == 0)
	{
		device_params.par_value |= KEY;
	}
	else
	{
		device_params.par_value &= (~KEY);
	}

	outb_p(device_params.par_value, device_params.par_base+2);
}

void hw_key_down(void)
{
	if	(settings.pos_neg_key == 0)
	{
		device_params.par_value &= (~KEY);
	}
	else
	{
		device_params.par_value |= KEY;
	}

	outb_p(device_params.par_value, device_params.par_base+2);
}

void hw_ptt_on(void)
{
	device_params.par_value |= PTT;
	outb_p(device_params.par_value, device_params.par_base+2);
}

void hw_ptt_off(void)
{
	device_params.par_value &= (~PTT);
	outb_p(device_params.par_value, device_params.par_base+2);
}

void hw_enable(void)
{
	device_params.par_value = 1;
	hw_key_up();
	hw_ptt_off();
}

void hw_disable(void)
{
	device_params.par_value = 0;
	hw_key_up();
	hw_ptt_off();
}

/**
 * Wrapper functions to control the hardware
 */

void key_down(void)
{
	hw_key_down();
	if(settings.monitor) kd_mksound(); 
}

void key_up(void)
{
	hw_key_up();
	if(settings.monitor) kd_nosound(); 
}

void ptt_on(void)
{
	hw_ptt_on();
}

void ptt_off(void)
{
	hw_ptt_off();
}

/* various functions */
char*	optimize(char *dest, char* src)
{
	int	i;

	for	(i=0; i<=strlen(src); i++)
	{
		switch(src[i])
		{
			case '9':
				dest[i] = 'N';
				break;
			case '1':
				dest[i] = 'A';
				break;
			case '0':
				dest[i] = 'T';
				break;
			default:
				dest[i] = src[i];
				break;
		}
	}

	return dest;
}

/**
 * Kernel module fuctions
 */

static int cwkeyer_proc_read(char *buf, char **start, off_t offset, int len, int *eof, void *data)
{
    return sprintf(buf,"Speed      [%02d] WPM\nFreq     [%04d] HZ\nMonitor   [%3s]\nTX delay [%04d] ms\nCall     [%s]\nQSOnr    [%s]\nRST      [%s]\n"
		, settings.speed
		, settings.frequency
		, settings.monitor == 1 ? "On" : "Off"
		, settings.tx_delay
		, settings.call
		, settings.qsonumber
		, settings.rst);
}
struct proc_dir_entry *cwkeyer_proc;

/*
 * ioctl() implementation
 */

int cwkeyer_ioctl (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)
{

	/**
	 * extract the type and number bitfields, and don't decode
	 * wrong cmds: return EINVAL before verify_area()
	 */

	if	(_IOC_TYPE(cmd) != CWKEYER_IOC_MAGIC) 
	{
		return -EINVAL;
	}

	if	(_IOC_NR(cmd) > CWKEYER_IOC_MAXNR)
	{
		return -EINVAL;
	}

	/**
	 * the direction is a bitmask, and VERIFY_WRITE catches R/W
	 * transfers. `Type' is user-oriented, while
	 * verify_area is kernel-oriented, so the concept of "read" and
	 * "write" is reversed
	 */

	switch(cmd) 
	{

	case CWKEYER_IOCSSPEED:
		settings.orig_speed = (int)arg;
		settings.speed = (int)arg;
		break;

	case CWKEYER_IOCSFREQ:
		if	((int)arg >= 400 && (int)arg <= 1500)
		{
			settings.frequency = (int)arg;
			settings.freq_div  = 1193180 / (int)arg;
		}
		else
		{
			return -EINVAL;
		}
		break;

	case CWKEYER_IOCSMONI:
		if	((int)arg == 1 || (int)arg == 0)
		{
			settings.monitor = (int)arg;
		}
		else
		{
			return -EINVAL;
		}
		break;

	case CWKEYER_IOCSWEIGTH:
		if	((int)arg >= -5 && (int)arg <= 5)
		{
			settings.weigth = (int)arg;
		}
		else
		{
			return -EINVAL;
		}
		break;

	case CWKEYER_IOCSTXDELAY:
		settings.tx_delay = (int)arg;
		break;

	case CWKEYER_IOCSFLUSH:
		flush_buf(&message_buf);
		flush_buf(&morse_buf);
		flush_buf(&call_buf);
		flush_buf(&rst_buf);
		flush_buf(&qsonumber_buf);
		key_up();
		ptt_off();
		settings.speed = settings.orig_speed;

		break;

	case CWKEYER_IOCSCALL:
		if	(settings.call)
		{
			kfree(settings.call);
		}
		
		if      ((settings.call = kmalloc(_IOC_SIZE(cmd),GFP_KERNEL)) == NULL)
		{
			return -ENOMEM;
		}

		copy_from_user(settings.call, (char*)arg, _IOC_SIZE(cmd));
		buf_add_szstr_raw(&call_buf, settings.call);

		break;

	case CWKEYER_IOCSRST:
		if	(settings.rst)
		{
			kfree(settings.rst);
		}
		
		if      ((settings.rst = kmalloc(_IOC_SIZE(cmd), GFP_KERNEL)) == NULL)
		{
			return -ENOMEM;
		}

		copy_from_user(settings.rst, (char*)arg, _IOC_SIZE(cmd));

		if	(settings.number_opt == 1)
		{
			char	msg[16];

			optimize(msg, settings.rst);
			buf_add_szstr_raw(&rst_buf, msg);
		}
		else
		{
			buf_add_szstr_raw(&rst_buf, settings.rst);
		}

		break;

	case CWKEYER_IOCSQSONUMBER:
		if	(settings.qsonumber)
		{
			kfree(settings.qsonumber);
		}
		
		if      ((settings.qsonumber = kmalloc(_IOC_SIZE(cmd), GFP_KERNEL)) == NULL)
		{
			return -ENOMEM;
		}

		copy_from_user(settings.qsonumber, (char*)arg, _IOC_SIZE(cmd));
		if	(settings.number_opt == 1)
		{
			char	msg[16];

			optimize(msg, settings.qsonumber);
			buf_add_szstr_raw(&qsonumber_buf, msg);
		}
		else
		{
			buf_add_szstr_raw(&qsonumber_buf, settings.qsonumber);
		}
		
		break;

	case CWKEYER_IOCSPOSNEGKEY:
		if	((int)arg == 1 || (int)arg == 0)
		{
			settings.pos_neg_key = (int)arg;
		}
		else
		{
			return -EINVAL;
		}
		break;

	default:  
		return -EINVAL;
	}

    return 0;
}

/* Data management: read and write */

static ssize_t cwkeyer_write (struct file* file, const char* buf, size_t count, loff_t* loff)
{
	return buf_add_str(&message_buf, buf, count);
}

int cwkeyer_open (struct inode *inode, struct file *filp)
{
	MOD_INC_USE_COUNT; 
	return 0;          /* success */
}

int cwkeyer_release (struct inode *inode, struct file *filp)
{
	MOD_DEC_USE_COUNT; 
	return 0;          /* success */
}

/* The file operations */

struct file_operations cwkeyer_fops = {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)
	THIS_MODULE,
#endif
	NULL,			/* cwkeyer_lseek */
	NULL,			/* cwkeyer_read */
	cwkeyer_write,   
	NULL,			/* cwkeyer_readdir */
	NULL,			/* cwkeyer_select */
	cwkeyer_ioctl,
	NULL,			/* cwkeyer_mmap */
	cwkeyer_open, 
	NULL,			/* cwkeyer_flush */
	cwkeyer_release,
                   /* nothing more, fill with NULLs */
};

void ascii2morse(char x)
{
	u16	morse_char = morse[x-' '];

	if	((x < ' ') || (x > 'Z'))
	{
		return;
	}

	while	(morse_char)
	{
		buf_add(&morse_buf, (morse_char & 0xC000) >> 14);
		buf_add(&morse_buf, NIL);

		morse_char <<= 2;
	}

	/**
	 * add inter character space (3 times the dih_value)
	 * one dih-value was part of the data above
	 * two are added here 
	 */
	
	buf_add(&morse_buf, NIL);
	buf_add(&morse_buf, NIL);
}

static void do_cw(void)
{
	static int	delay	= 0;
	static ring_buf *buffer	= &message_buf;
	static int	idle	= 0;

	/* when the still active return immediate */
	if	(delay)
	{
		delay--;
		return;
	}

	/* if the macro buffer is empty run from the message_buf */
	if	(buf_empty(buffer))
	{
		buffer = &message_buf;
	}

	if	(buf_empty(&message_buf))
	{
		idle = 1;
		ptt_off();
	}

	if	(!buf_empty(&message_buf) && idle && settings.tx_delay)
	{
		delay = (IRQ_FREQ*settings.tx_delay)/1000;	
		idle = 0;

		ptt_on();
		return;
	}
	
	while	(!buf_empty(buffer) && buf_empty(&morse_buf))
	{
		char morse = buf_get(buffer);

		switch	(morse)
		{
			case MACRO_SPEED_UP:
				settings.speed += 3;
				break;

			case MACRO_SPEED_DOWN:
				settings.speed -= 3;
				break;

			case MACRO_CALL:
				call_buf.begin = 0;
				buffer = &call_buf;
				break;

			case MACRO_RST:
				rst_buf.begin = 0;
				buffer = &rst_buf;
				break;

			case MACRO_QSONUMBER:
				qsonumber_buf.begin = 0;
				buffer = &qsonumber_buf;
				break;

			default:
				ascii2morse(morse);
				break;
		}
	}

	if	(!buf_empty(&morse_buf))
	{
		switch	(buf_get(&morse_buf))
		{
			case DAH:
                                key_down();
                                delay = (((3+settings.weigth/10)*IRQ_FREQ*1.25)/(settings.speed))-1;
                                break;

                        case DIH:
                                key_down();
                                delay = (IRQ_FREQ*1.25/settings.speed)-1;
                                break;

                        case NIL:
                                key_up();
                                delay = (IRQ_FREQ*1.25/settings.speed)-1;
                                break;
		}
	}
}

#if LINUX_VERSION_CODE < KERNEL_VERSION(2,2,16)
void daemonize(void)
{
	struct fs_struct	*fs;

	exit_mm(current);

	current->session = 1;
	current->pgrp = 1;

	exit_fs(current);

	fs = init_task.fs;
	current->fs = fs;

	atomic_inc(&fs->count);

}
#endif

static void setup_thread(void)
{
	lock_kernel();

	daemonize();

#if LINUX_VERSION_CODE <  KERNEL_VERSION(2,4,0)
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,16)
	/* the 2.2.16 daemonize does not call exit_files() */

	exit_files(current);
	current->files = init_task.files;

	atomic_inc(&current->files->count);
#endif
#endif

        strcpy(current->comm, "cwkeyer");

	siginitsetinv(&current->blocked, sigmask(SIGKILL)|sigmask(SIGINT)|sigmask(SIGTERM));

	unlock_kernel();

	up(&device_params.startstop_sem);
}

static void leave_thread(void)
{
	lock_kernel();
	up(&device_params.startstop_sem);
}

static int rtc_thread(void* params)
{
	struct file	*rtc;
	int		retval = 0;

	setup_thread();

	rtc = filp_open("/dev/rtc", O_RDONLY, 0);
	
	if	(rtc && 
		 rtc->f_op && 
		 rtc->f_op->ioctl &&
		 rtc->f_op->read &&
		 rtc->f_dentry &&
		 rtc->f_dentry->d_inode)
	{	
		unsigned long 	arg;

		/* set RTC to generate interrupts at IRQ_FREQ Hz rate */
		arg = IRQ_FREQ;
		rtc->f_op->ioctl(rtc->f_dentry->d_inode,rtc,RTC_IRQP_SET,arg);

		/* Enable periodic interrupts */
		arg = 0;
		rtc->f_op->ioctl(rtc->f_dentry->d_inode,rtc,RTC_PIE_ON,arg);

		/**
		 * This is the main loop which runs as long as 
 		 * the kernel thread lives
 		 */

		while	(!device_params.rtc_thread_shutdown)
		{
			unsigned long	value;

			/** 
			 * Every hardware interrupt results in 
			 * one unsigned long read from the device
			 * (4 bytes) containing the interrupt number
			 */

			rtc->f_op->read(rtc,(void*)&value,sizeof(value),&rtc->f_pos);

			do_cw();
			
			if	(signal_pending(current))
			{
				retval = -ERESTARTSYS;
				break;
			}
		}

		/* stop periodic interrupts */
		arg = 0;
		rtc->f_op->ioctl(rtc->f_dentry->d_inode, rtc, RTC_PIE_OFF, arg);

		filp_close(rtc, NULL);
	}

	leave_thread();

	return retval;
}


int init_module(void)
{
	int result; 

	device_params.rtc_thread_shutdown = 0;

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)
	init_MUTEX_LOCKED(&device_params.startstop_sem);
#else
	device_params.startstop_sem = MUTEX_LOCKED;
#endif

	kernel_thread(rtc_thread, NULL, 0);

	down(&device_params.startstop_sem);
	
	/* Register major, and accept a dynamic number (is so asked) */

	result = register_chrdev(cwkeyer_major, "cwkeyer", &cwkeyer_fops);

	if	(result < 0)
	{
		return result;
	}

	if	(cwkeyer_major == 0) 
	{
		cwkeyer_major = result; /* dynamic */
	}
#ifdef CONFIG_PROC_FS
	cwkeyer_proc = create_proc_entry("cwkeyer", S_IFREG | S_IRUGO, 0);

        if (cwkeyer_proc)
	{
                cwkeyer_proc->read_proc = cwkeyer_proc_read;
	}
#endif
	printk("<1>CW Keyer v0.06 (PA3FKN)\n");

	settings.speed 		= 30;
	settings.frequency	= 800;
	settings.freq_div 	= (1193180 / settings.frequency);
	settings.monitor	= 1;
	settings.weigth		= 0;
	settings.tx_delay	= 20;
	settings.number_opt	= 1;
	settings.pos_neg_key	= 0;
	settings.qsonumber	= NULL;
	settings.call		= NULL;
	settings.rst		= NULL;

	device_params.delay 	= 0;
	device_params.par_base	= 0x3bc;
	device_params.par_value	= 0;

	buf_init(&message_buf);
	buf_init(&morse_buf);
	buf_init(&call_buf);
	buf_init(&rst_buf);
	buf_init(&qsonumber_buf);

	hw_enable();

	buf_add_szstr(&message_buf, "PA3FKN");

	return 0;
}

void cleanup_module(void)
{
	if	(settings.qsonumber)
	{
		kfree(settings.qsonumber);
	}

	if	(settings.call)
	{
		kfree(settings.call);
	}

	if	(settings.rst)
	{
		kfree(settings.rst);
	}

	hw_disable();

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)
	init_MUTEX_LOCKED(&device_params.startstop_sem);
#else
	device_params.startstop_sem = MUTEX_LOCKED;
#endif

	device_params.rtc_thread_shutdown = 1;
	down(&device_params.startstop_sem);

	/* switch off sound in case it would be still swicthed on */
	if	(settings.monitor)
	{
		kd_nosound();
	}

	unregister_chrdev(cwkeyer_major, "cwkeyer");

#ifdef CONFIG_PROC_FS
        if (cwkeyer_proc)
	{
                remove_proc_entry("cwkeyer", 0);
	}
#endif

	printk("<1>Goodbye cruel world\n");
}
