/*
 * Copyright (C) 2007 Michael Brown <mbrown@fensystems.co.uk>.
 *
 * 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 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., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 *
 * You can also choose to distribute this program under the terms of
 * the Unmodified Binary Distribution Licence (as given in the file
 * COPYING.UBDL), provided that you have satisfied its requirements.
 */

FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );

/**
 * @file
 *
 * External memory allocation
 *
 */

#include <limits.h>
#include <errno.h>
#include <ipxe/uaccess.h>
#include <ipxe/hidemem.h>
#include <ipxe/io.h>
#include <ipxe/memblock.h>
#include <ipxe/umalloc.h>

/** Maximum usable address for external allocated memory */
#define EM_MAX_ADDRESS 0xffffffffUL

/** Alignment of external allocated memory */
#define EM_ALIGN ( 4 * 1024 )

/** Equivalent of NOWHERE for user pointers */
#define UNOWHERE ( ~UNULL )

/** An external memory block */
struct external_memory {
	/** Size of this memory block (excluding this header) */
	size_t size;
	/** Block is currently in use */
	int used;
};

/** Top of heap */
static userptr_t top = UNULL;

/** Bottom of heap (current lowest allocated block) */
static userptr_t bottom = UNULL;

/** Remaining space on heap */
static size_t heap_size;

/**
 * Find largest usable memory region
 *
 * @ret start		Start of region
 * @ret len		Length of region
 */
size_t largest_memblock ( userptr_t *start ) {
	struct memory_map memmap;
	struct memory_region *region;
	physaddr_t max = EM_MAX_ADDRESS;
	physaddr_t region_start;
	physaddr_t region_end;
	size_t region_len;
	unsigned int i;
	size_t len = 0;

	/* Avoid returning uninitialised data on error */
	*start = UNULL;

	/* Scan through all memory regions */
	get_memmap ( &memmap );
	for ( i = 0 ; i < memmap.count ; i++ ) {
		region = &memmap.regions[i];
		DBG ( "Considering [%llx,%llx)\n", region->start, region->end );

		/* Truncate block to maximum physical address */
		if ( region->start > max ) {
			DBG ( "...starts after maximum address %lx\n", max );
			continue;
		}
		region_start = region->start;
		if ( region->end > max ) {
			DBG ( "...end truncated to maximum address %lx\n", max);
			region_end = 0; /* =max, given the wraparound */
		} else {
			region_end = region->end;
		}
		region_len = ( region_end - region_start );

		/* Use largest block */
		if ( region_len > len ) {
			DBG ( "...new best block found\n" );
			*start = phys_to_user ( region_start );
			len = region_len;
		}
	}

	return len;
}

/**
 * Initialise external heap
 *
 */
static void init_eheap ( void ) {
	userptr_t base;

	heap_size = largest_memblock ( &base );
	bottom = top = userptr_add ( base, heap_size );
	DBG ( "External heap grows downwards from %lx (size %zx)\n",
	      user_to_phys ( top, 0 ), heap_size );
}

/**
 * Collect free blocks
 *
 */
static void ecollect_free ( void ) {
	struct external_memory extmem;
	size_t len;

	/* Walk the free list and collect empty blocks */
	while ( bottom != top ) {
		copy_from_user ( &extmem, bottom, -sizeof ( extmem ),
				 sizeof ( extmem ) );
		if ( extmem.used )
			break;
		DBG ( "EXTMEM freeing [%lx,%lx)\n", user_to_phys ( bottom, 0 ),
		      user_to_phys ( bottom, extmem.size ) );
		len = ( extmem.size + sizeof ( extmem ) );
		bottom = userptr_add ( bottom, len );
		heap_size += len;
	}
}

/**
 * Reallocate external memory
 *
 * @v old_ptr		Memory previously allocated by umalloc(), or UNULL
 * @v new_size		Requested size
 * @ret new_ptr		Allocated memory, or UNULL
 *
 * Calling realloc() with a new size of zero is a valid way to free a
 * memory block.
 */
static userptr_t memtop_urealloc ( userptr_t ptr, size_t new_size ) {
	struct external_memory extmem;
	userptr_t new = ptr;
	size_t align;

	/* (Re)initialise external memory allocator if necessary */
	if ( bottom == top )
		init_eheap();

	/* Get block properties into extmem */
	if ( ptr && ( ptr != UNOWHERE ) ) {
		/* Determine old size */
		copy_from_user ( &extmem, ptr, -sizeof ( extmem ),
				 sizeof ( extmem ) );
	} else {
		/* Create a zero-length block */
		if ( heap_size < sizeof ( extmem ) ) {
			DBG ( "EXTMEM out of space\n" );
			return UNULL;
		}
		ptr = bottom = userptr_add ( bottom, -sizeof ( extmem ) );
		heap_size -= sizeof ( extmem );
		DBG ( "EXTMEM allocating [%lx,%lx)\n",
		      user_to_phys ( ptr, 0 ), user_to_phys ( ptr, 0 ) );
		extmem.size = 0;
	}
	extmem.used = ( new_size > 0 );

	/* Expand/shrink block if possible */
	if ( ptr == bottom ) {
		/* Update block */
		new = userptr_add ( ptr, - ( new_size - extmem.size ) );
		align = ( user_to_phys ( new, 0 ) & ( EM_ALIGN - 1 ) );
		new_size += align;
		new = userptr_add ( new, -align );
		if ( new_size > ( heap_size + extmem.size ) ) {
			DBG ( "EXTMEM out of space\n" );
			return UNULL;
		}
		DBG ( "EXTMEM expanding [%lx,%lx) to [%lx,%lx)\n",
		      user_to_phys ( ptr, 0 ),
		      user_to_phys ( ptr, extmem.size ),
		      user_to_phys ( new, 0 ),
		      user_to_phys ( new, new_size ));
		memmove_user ( new, 0, ptr, 0, ( ( extmem.size < new_size ) ?
						 extmem.size : new_size ) );
		bottom = new;
		heap_size -= ( new_size - extmem.size );
		extmem.size = new_size;
	} else {
		/* Cannot expand; can only pretend to shrink */
		if ( new_size > extmem.size ) {
			/* Refuse to expand */
			DBG ( "EXTMEM cannot expand [%lx,%lx)\n",
			      user_to_phys ( ptr, 0 ),
			      user_to_phys ( ptr, extmem.size ) );
			return UNULL;
		}
	}

	/* Write back block properties */
	copy_to_user ( new, -sizeof ( extmem ), &extmem,
		       sizeof ( extmem ) );

	/* Collect any free blocks and update hidden memory region */
	ecollect_free();
	hide_umalloc ( user_to_phys ( bottom, ( ( bottom == top ) ?
						0 : -sizeof ( extmem ) ) ),
		       user_to_phys ( top, 0 ) );

	return ( new_size ? new : UNOWHERE );
}

PROVIDE_UMALLOC ( memtop, urealloc, memtop_urealloc );
