#include "SCK.H"

typedef struct {
	LIST_ENTRY hash;
	LIST_ENTRY lru;
	blk_t blk;
	int h;
	char *data;
	char dirty;
} BUFFER;

#define HASH(hn, x)		(((x) / SECTORS_PER_BUFFER + (unsigned)(hn) * (HASH_SIZE / 2)) & (HASH_SIZE - 1))

static XLIST_HEAD buffer_hash[HASH_SIZE];
static LIST_HEAD buffer_lru;
static __u64 xdata[BUFFER_SIZE / 8];
static BUFFER *pinned_buffer;
static unsigned long buffer_memory;

int buffer_nocache = 0;
char buffer_error[__MAX_STR_LEN];

static void free_buffer(BUFFER *b);

static void claim_write_error(int h, blk_t blk, unsigned len, int err)
{
	if (!err) _snprintf(buffer_error, sizeof buffer_error, "WRITING BEYOND END OF DEVICE (%"blk_format"X,%X)", blk, SECTORS_PER_BUFFER), err = EEOF;
	else _snprintf(buffer_error, sizeof buffer_error, "ERROR WRITING TO SECTORS (%"blk_format"X,%X): %s", blk, SECTORS_PER_BUFFER, strerror(err));
	log_printf(1, "%s", buffer_error);
}

static __finline__ void check_buffer_memory(void)
{
/*{
	static unsigned long am = 0;
	if (map_allocated > am) __debug_printf("%ld   \n", map_allocated), am = map_allocated;
}*/
#ifdef TESTCODE
	if (always_swap) enable_swapping();
#endif
	while (__unlikely(buffer_memory + map_allocated >= memory_limit)) {
		BUFFER *b;
		if (__unlikely(map_allocated > memory_limit / 2)) enable_swapping();
		if (__unlikely(buffer_memory < memory_limit / 4)) return;
		b = LIST_STRUCT(buffer_lru.next, BUFFER, lru);
		if (__unlikely(b == pinned_buffer)) b = LIST_STRUCT(b->lru.next, BUFFER, lru);
		if (__unlikely(b == LIST_STRUCT(&buffer_lru, BUFFER, lru))) return;
		free_buffer(b);
	}
}

static BUFFER *find_buffer(int hn, blk_t blk)
{
	BUFFER *b;
	XLIST_FOR_EACH(b, &buffer_hash[HASH(hn, blk)], BUFFER, hash) if (__likely(b->blk == (blk & ~(blk_t)(SECTORS_PER_BUFFER - 1))) && __likely(b->h == hn)) {
		DEL_FROM_LIST(&b->lru);
		ADD_TO_LIST_END(&buffer_lru, &b->lru);
		return b;
	}
	return NULL;
}

static BUFFER *read_buffer_internal(int hn, blk_t blk)
{
	BUFFER *b;
	if (__likely((b = find_buffer(hn, blk)) != NULL)) {
		ret:
		return b;
	}
	check_buffer_memory();
	if (__unlikely(buffer_nocache)) {
		errno = EOPNOTSUPP;
		return NULL;
	}
	b = mem_alloc(sizeof(BUFFER));
	if (__unlikely(!b)) goto xdatar;
	b->blk = blk & ~(blk_t)(SECTORS_PER_BUFFER - 1);
	b->h = hn;
	b->dirty = 0;
	b->data = mem_alloc(BUFFER_SIZE);
	if (__unlikely(!b->data)) goto xdataf;
	errno = 0;
	if (__likely(pread(hn, b->data, BUFFER_SIZE, (io_t)b->blk << SSIZE_BITS) == BUFFER_SIZE))
	{
		buffer_memory += BUFFER_SIZE;
		ADD_TO_LIST_END(&buffer_lru, &b->lru);
		ADD_TO_XLIST(&buffer_hash[HASH(hn, blk)], &b->hash);
		goto ret;
	}
	mem_free(b->data);
	xdataf:
	mem_free(b);
	xdatar:
	return NULL;
}

void *read_buffer(blk_t blk, unsigned n)
{
	BUFFER *b;
	if (__unlikely(((unsigned)blk & (SECTORS_PER_BUFFER - 1)) + n > SECTORS_PER_BUFFER)) {
		_snprintf(buffer_error, sizeof buffer_error, "UNALIGNED READ (%"blk_format"X,%X)", blk, n);
		return NULL;
	}
	pinned_buffer = NULL;
#ifdef TESTCODE
	{
		int bnc = buffer_nocache;
		if (nocache) buffer_nocache = 1;
#endif
		b = read_buffer_internal(h, blk);
#ifdef TESTCODE
		buffer_nocache = bnc;
	}
#endif
	if (__likely(b != NULL)) {
		pinned_buffer = b;
		return b->data + (((unsigned)blk & (SECTORS_PER_BUFFER - 1)) << SSIZE_BITS);
	}
	errno = 0;
	if (__unlikely(pread(h, xdata, n << SSIZE_BITS, (io_t)blk << SSIZE_BITS) != n << SSIZE_BITS))
	{
		if (!errno) _snprintf(buffer_error, sizeof buffer_error, "ACCESS BEYOND END OF DEVICE (%"blk_format"X,%X)", blk, n);
		else _snprintf(buffer_error, sizeof buffer_error, "ERROR %s (%"blk_format"X,%X)", strerror(errno), blk, n);
		return NULL;
	}
	return xdata;
}

void *dummy_buffer(void)
{
	return xdata;
}

static void free_buffer(BUFFER *b)
{
	if (__unlikely(b == pinned_buffer)) pinned_buffer = NULL;
	if (__unlikely(b->dirty)) {
		errno = 0;
		if (__unlikely(pwrite(b->h, b->data, BUFFER_SIZE, b->blk << SSIZE_BITS) != BUFFER_SIZE)) {
			claim_write_error(b->h, b->blk, SECTORS_PER_BUFFER, errno);
		}
	}
	DEL_FROM_LIST(&b->lru);
	DEL_FROM_LIST(&b->hash);
	mem_free(b->data);
	mem_free(b);
	buffer_memory -= BUFFER_SIZE;
}

int write_buffer(blk_t blk, unsigned n, void *ptr)
{
	int r;
	blk_t bblk;
	unsigned nn, nx;
	BUFFER *b;
	if (__unlikely(ro)) KERNEL$SUICIDE("write_buffer: writing when read-only");

	bblk = blk, nn = n;
	next_prune:
	if (__likely((b = find_buffer(h, bblk)) != NULL)) {
		if ((char *)ptr >= b->data && (char *)ptr < b->data + BUFFER_SIZE) {
			goto skip_prune;
		}
		free_buffer(b);
	}
	nx = SECTORS_PER_BUFFER - ((unsigned)bblk & (SECTORS_PER_BUFFER - 1));
	if (nx < nn) {
		nn -= nx;
		bblk += nx;
		goto next_prune;
	}

	skip_prune:
	modified = 1;
	errno = 0;
	if (__unlikely(pwrite(h, ptr, n << SSIZE_BITS, (io_t)blk << SSIZE_BITS) != n << SSIZE_BITS)) {
		r = -errno;
		claim_write_error(h, blk, n, -r);
		goto skip_sync;
	}
	if (__likely(!async_writes)) {
		if (__unlikely(fsync(h))) {
			r = -errno;
			_snprintf(buffer_error, sizeof buffer_error, "ERROR FLUSHING CACHE AFTER WRITE TO SECTORS (%"blk_format"X,%X): %s", blk, n, strerror(-r));
			log_printf(1, "%s", buffer_error);
		}
	}
	r = 0;
	skip_sync:;
	return r;
}

void *rw_swap_buffer(int hn, blk_t blk, int wr)
{
	BUFFER *b;
	int bnc = buffer_nocache;
	buffer_nocache = 0;
	b = read_buffer_internal(hn, blk);
	buffer_nocache = bnc;
	if (__unlikely(!b)) {
		if (!errno) _snprintf(buffer_error, sizeof buffer_error, "ACCESS BEYOND END OF DEVICE (%"blk_format"X)", blk);
		else _snprintf(buffer_error, sizeof buffer_error, "ERROR %s (%"blk_format"X)", strerror(errno), blk);
		return NULL;
	}
	if (__likely(wr) && __unlikely(!b->dirty)) b->dirty = 1;
	return b->data;
}

void init_buffers(void)
{
	unsigned i;
	for (i = 0; i < HASH_SIZE; i++) INIT_XLIST(&buffer_hash[i]);
	INIT_LIST(&buffer_lru);
	pinned_buffer = NULL;
	buffer_memory = 0;
}

void done_buffers(void)
{
	unsigned i;
	for (i = 0; i < HASH_SIZE; i++) while (!XLIST_EMPTY(&buffer_hash[i])) {
		BUFFER *b = LIST_STRUCT(buffer_hash[i].next, BUFFER, hash);
		if (__unlikely(b->dirty == 1)) b->dirty = 0;
		free_buffer(b);
	}
	pinned_buffer = NULL;
	if (__unlikely(buffer_memory != 0)) KERNEL$SUICIDE("done_buffers: %lx buffer memory leaked", buffer_memory);
	if (__unlikely(!LIST_EMPTY(&buffer_lru))) KERNEL$SUICIDE("done_buffers: buffer_lru not empty");
}
