/*
  Copyright 2006 Jean-Baptiste Note

  This file is part of islsm.

  islsm 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.

  islsm 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 islsm; if not, write to the Free Software Foundation, Inc.,
  59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

*/

/* for sprintf */
#include <linux/kernel.h>
#include <linux/spinlock.h>
#include <linux/slab.h>
/* for EXPORT_SYMBOL */
#include <linux/module.h>

#include "islsm_alloc.h"
#include "islsm_log.h"

/* simple first-fit allocation */
static inline int
find_first_hole(const unsigned start_addr,
		const unsigned end_addr,
		struct islsm_alloc_cb *cb,
		struct list_head *head) {
	struct islsm_alloc_cb *skb_cb;
	struct list_head *new = &cb->chain;
	unsigned size = cb->len;
 	unsigned last_free_addr = start_addr;

	/* FIXME is destination address 32 bits aligned ? */
	list_for_each_entry(skb_cb, head, chain) {
		unsigned lmac_addr = skb_cb->lmac_addr - ISLSM_80211_FW_HEADROOM;
		unsigned hole_size = lmac_addr - last_free_addr;
		BUG_ON(lmac_addr < last_free_addr);
		islog(L_ALLOC, "hole size %u\n", hole_size);
		if (hole_size > size) {
			cb->lmac_addr = last_free_addr + ISLSM_80211_FW_HEADROOM;
 			list_add_tail(new, &skb_cb->chain);
			return 0;
		}
		last_free_addr = lmac_addr + skb_cb->len;
	}

	if (end_addr - last_free_addr > size) {
		islog(L_ALLOC, "end hole size %u\n", end_addr - last_free_addr);
		cb->lmac_addr = last_free_addr + ISLSM_80211_FW_HEADROOM;
		list_add_tail(new, head);
		return 0;
	}

	return -ENOSPC;
}

/*
 * Warning: this function must not touch the control block in case of a
 * failed allocation.
 */
int
islsm_alloc(struct mem_descr *mem, struct sk_buff *skb)
{
	struct islsm_alloc_cb *alloc = (void *)skb->cb;
	/* round to 32-bits -- need 3 for alignement purposes */
	unsigned len = ((skb->len + ISLSM_80211_FW_HEADROOM + ISLSM_80211_FW_TAILROOM + 3) >> 2) << 2;
	unsigned long irqflags;
	int err;

	islog(L_ALLOC, "alloc chunck of len %x\n", len);

	alloc->len = len;

	/* Walk through the chain of skbs and
	   see where we can put ourselves */
	spin_lock_irqsave(&mem->slock, irqflags);
	err = find_first_hole(mem->start_addr, mem->end_addr,
			      alloc, &mem->indevice_skbs);
	spin_unlock_irqrestore(&mem->slock, irqflags);

	if (!err) {
		islog(L_ALLOC, "actually allocating skb %p\n", skb);
		skb_get(skb);
	}

	return err;
}

void
islsm_free(mem_descr_t *mem, uint32_t addr)
{
	struct list_head *head = &mem->indevice_skbs;
	int found = 0;
	struct islsm_alloc_cb *skb_cb;
	unsigned long irqflags;

	islog(L_ALLOC, "free address %x\n", addr);

	/* get the real start-of-packet address */
	addr -= ISLSM_80211_FW_HEADROOM;

	/* walk through the chain of skbs and find the good one */
	spin_lock_irqsave(&mem->slock, irqflags);
	list_for_each_entry(skb_cb, head, chain) {
		unsigned lmac_addr = skb_cb->lmac_addr - ISLSM_80211_FW_HEADROOM;
		if ((addr >= lmac_addr) && (addr < lmac_addr + skb_cb->len)) {
			struct sk_buff *skb = container_of((void *)skb_cb,
							   struct sk_buff, cb);
			list_del(&skb_cb->chain);
			/* drop the allocator reference to this skb */
			islog(L_ALLOC, "actually freeing skb %p\n", skb);
			kfree_skb(skb);
			found = 1;
			break;
		}
	}
	spin_unlock_irqrestore(&mem->slock, irqflags);

	if (!found)
		printk(KERN_ERR "%s: unallocate bogus address %08x\n",
		       "islsm", addr);
	return;
}
EXPORT_SYMBOL(islsm_free);

int
islsm_alloc_create(mem_descr_t *mem, uint32_t start, uint32_t end)
{
	if (end <= start)
		return -EINVAL;

	mem->start_addr = start;
	mem->end_addr = end;

	return 0;
}

void
islsm_alloc_destroy(mem_descr_t *mem)
{
	unsigned long flags;

	spin_lock_irqsave(&mem->slock, flags);
	mem->start_addr = 0;
	mem->end_addr = 0;

	/* kfree the remaining skbs in the list */
/* 	list_for_each_safe(pos, n, head) { */
/* 		struct sk_buff *skb; */
/* 		list_del(pos); */
/* 		kfree_skb(skb); */
/* 	} */

	spin_unlock_irqrestore(&mem->slock, flags);
}

/* export a sysfs function for this */
static ssize_t
sprint_alloc_skb(char *buf, struct sk_buff *skb) {
	unsigned offset = 0;
	struct islsm_alloc_cb *alloc = (void *)skb->cb;
	unsigned char *data = (unsigned char *)skb->data;
	int i;

	offset += sprintf(buf, "%p\t%08x\t%04x\t%04x\t",
			  skb, alloc->flags,
			  alloc->lmac_addr-ISLSM_80211_FW_HEADROOM,
			  alloc->len+ISLSM_80211_FW_HEADROOM);

	/* print the first few bytes of data -- makes debugging easier */
	for(i=0; i < 16 && i < skb->len; i++)
		offset += sprintf(buf + offset, "%02x ", data[i]);

	offset += sprintf(buf + offset, "...\n");

	return offset;
}

ssize_t
islsm_alloc_dump(mem_descr_t *mem, char *buf)
{
	struct list_head *head = &mem->indevice_skbs;
	unsigned offset = 0;
	struct islsm_alloc_cb *skb_cb;
	unsigned long irqflags;

	/* print header */
	offset += sprintf(buf + offset, "skb\t\tflags\t\tlmac\tlen\tdata\n");

	/* loop throught skbs */
	spin_lock_irqsave(&mem->slock, irqflags);
	list_for_each_entry(skb_cb, head, chain) {
		struct sk_buff *skb;
		skb = container_of((void *)skb_cb, struct sk_buff, cb);
		offset += sprint_alloc_skb(buf + offset, skb);
	}
	spin_unlock_irqrestore(&mem->slock, irqflags);

	return offset;
}

EXPORT_SYMBOL(islsm_alloc_dump);
