/*
 * This is a debug register allocation mechanism, to be used by
 * all debuggers, which use debug registers.
 *
 * 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 program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  
 */

#include <linux/kernel.h>
#include <linux/spinlock.h>
#include <linux/module.h>

#include <asm/system.h>
#include <asm/debugreg.h>
#include <asm/dr_alloc.h>

struct dr_struct dr_list[DR_MAX];
unsigned long dr7_global_mask = 0;
static spinlock_t dr_lock = SPIN_LOCK_UNLOCKED;

#ifdef CONFIG_DPROBES_CORE
void dr_alloc_asm_start(void) { }
#endif

static inline void set_dr7_global_mask(int regnum) 
{
	switch(regnum) {
		case 0: dr7_global_mask |= DR7_DR0_BITS; break;
		case 1: dr7_global_mask |= DR7_DR1_BITS; break;
		case 2: dr7_global_mask |= DR7_DR2_BITS; break;
		case 3: dr7_global_mask |= DR7_DR3_BITS; break;
	}
	return;
}

static inline void clear_dr7_global_mask(int regnum) 
{
	switch(regnum) {
		case 0: dr7_global_mask &= ~DR7_DR0_BITS; break;
		case 1: dr7_global_mask &= ~DR7_DR1_BITS; break;
		case 2: dr7_global_mask &= ~DR7_DR2_BITS; break;
		case 3: dr7_global_mask &= ~DR7_DR3_BITS; break;
	}
	return;
}

static int get_dr(int regnum, int flag)
{
	if ((flag == DR_ALLOC_GLOBAL) && (dr_list[regnum].flag == DR_UNUSED)) {
		dr_list[regnum].flag = DR_GLOBAL;
		set_dr7_global_mask(regnum);
		return regnum;
	}
	else if ((dr_list[regnum].flag == DR_UNUSED) || (dr_list[regnum].flag == DR_LOCAL)) {
		dr_list[regnum].use_count++;
		dr_list[regnum].flag = DR_LOCAL;
		return regnum;
	}
	return -1;
}
	
static int get_any_dr(int flag)
{
	int i;
	if (flag == DR_ALLOC_LOCAL) {
		for (i = 0; i < DR_MAX; i++) {
			if (dr_list[i].flag == DR_LOCAL) {
				dr_list[i].use_count++;
				return i;
			} else if (dr_list[i].flag == DR_UNUSED) {
				dr_list[i].flag = DR_LOCAL;
				dr_list[i].use_count = 1;
				return i;
			}
		}
	} else { /* flag == DR_ALLOC_GLOBAL */
		for (i = DR_MAX-1; i >= 0; i--) {
			if (dr_list[i].flag == DR_UNUSED) {
				dr_list[i].flag = DR_GLOBAL;
				set_dr7_global_mask(i);
				return i;
			}
		}
	}
	return -1;
}

static inline void dr_free_local(int regnum)
{
	if (! (--dr_list[regnum].use_count)) 
		dr_list[regnum].flag = DR_UNUSED;
	return;
}

static inline void dr_free_global(int regnum) 
{
	dr_list[regnum].flag = DR_UNUSED;
	dr_list[regnum].use_count = 0;
	clear_dr7_global_mask(regnum);
	return;
}
		
int dr_alloc(int regnum, int flag)
{
	int ret;
	unsigned long eflags;
	spin_lock_irqsave(&dr_lock, eflags);
	if (regnum == DR_ANY) 
		ret = get_any_dr(flag);
	else if (regnum >= DR_MAX)
		ret = -1;
	else
		ret = get_dr(regnum, flag);
	spin_unlock_irqrestore(&dr_lock, eflags);
	return ret;
}

int dr_free(int regnum)
{
	unsigned long eflags;
	spin_lock_irqsave(&dr_lock, eflags);
	if (regnum >= DR_MAX || dr_list[regnum].flag == DR_UNUSED) {
		spin_unlock_irqrestore(&dr_lock, eflags);
		return -1;
	}
	if (dr_list[regnum].flag == DR_LOCAL) 
		dr_free_local(regnum);
	else 
		dr_free_global(regnum);
	spin_unlock_irqrestore(&dr_lock, eflags);
	return 0;
}

int dr_inc_use_count(unsigned long mask)
{
	int i;
	unsigned long eflags;
	
	spin_lock_irqsave(&dr_lock, eflags);
	for (i =0; i < DR_MAX; i++) {
		if (DR_IS_LOCAL(mask, i))
			dr_list[i].use_count++;
	}
	spin_unlock_irqrestore(&dr_lock, eflags);
	return 0;
}

int dr_dec_use_count(unsigned long mask)
{
	int i;
	unsigned long eflags;
	
	spin_lock_irqsave(&dr_lock, eflags);
	for (i =0; i < DR_MAX; i++) {
		if (DR_IS_LOCAL(mask, i))
			dr_free_local(i);
	}
	spin_unlock_irqrestore(&dr_lock, eflags);
	return 0;
}

EXPORT_SYMBOL(dr_alloc);
EXPORT_SYMBOL(dr_free);

#ifdef CONFIG_DPROBES_CORE
void dr_alloc_asm_end(void) { }
#endif
