/* Tile tile cache from tiff2vips ... broken out so it can be shared with
 * openexr read.
 *
 * This isn't the same as im_cache(): we don't sub-divide, and we 
 * single-thread our callee.
 *
 * 23/8/06
 * 	- take ownership of reused tiles in case the cache is being shared
 * 13/2/07
 * 	- release ownership after fillng with pixels in case we read across
 * 	  threads
 * 4/2/10
 * 	- gtkdoc
 */

/*

    This file is part of VIPS.
    
    VIPS is free software; you can redistribute it and/or modify
    it under the terms of the GNU Lesser 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 Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser 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

 */

/*

    These files are distributed with VIPS - http://www.vips.ecs.soton.ac.uk

 */

/* Turn on debugging output.
#define DEBUG
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /*HAVE_CONFIG_H*/
#include <vips/intl.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

#include <vips/vips.h>
#include <vips/thread.h>

#ifdef WITH_DMALLOC
#include <dmalloc.h>
#endif /*WITH_DMALLOC*/

/* Lower and upper bounds for tile cache size. Choose an exact number based on
 * tile size.
 */
#define IM_MAX_TILE_CACHE (250)
#define IM_MIN_TILE_CACHE (5)

/* A tile in our cache.
 */
typedef struct {
	struct _Read *read;

	REGION *region;			/* REGION with private mem for data */
	int time;			/* Time of last use for flush */
	int x;				/* xy pos in VIPS image cods */
	int y;
} Tile;

/* Stuff we track during a read.
 */
typedef struct _Read {
	/* Parameters.
	 */
	IMAGE *in;
	IMAGE *out;
	int tile_width;	
	int tile_height;
	int max_tiles;

	/* Cache.
	 */
	int time;			/* Update ticks for LRU here */
	int ntiles;			/* Current cache size */
	GMutex *lock;			/* Lock everything here */
	GSList *cache;			/* List of tiles */
} Read;

static void
tile_destroy( Tile *tile )
{
	Read *read = tile->read;

	read->cache = g_slist_remove( read->cache, tile );
	read->ntiles -= 1;
	assert( read->ntiles >= 0 );
	tile->read = NULL;

	IM_FREEF( im_region_free, tile->region );

	im_free( tile );
}

static void
read_destroy( Read *read )
{
	IM_FREEF( g_mutex_free, read->lock );

	while( read->cache ) {
		Tile *tile = (Tile *) read->cache->data;

		tile_destroy( tile );
	}

	im_free( read );
}

static Read *
read_new( IMAGE *in, IMAGE *out, 
	int tile_width, int tile_height, int max_tiles )
{
	Read *read;

	if( !(read = IM_NEW( NULL, Read )) )
		return( NULL );
	read->in = in;
	read->out = out;
	read->tile_width = tile_width;
	read->tile_height = tile_height;
	read->max_tiles = max_tiles;
	read->time = 0;
	read->ntiles = 0;
	read->lock = g_mutex_new();
	read->cache = NULL;

	if( im_add_close_callback( out, 
		(im_callback_fn) read_destroy, read, NULL ) ) {
		read_destroy( read );
		return( NULL );
	}

	return( read );
}

static Tile *
tile_new( Read *read )
{
	Tile *tile;

	if( !(tile = IM_NEW( NULL, Tile )) )
		return( NULL );

	tile->read = read;
	tile->region = NULL;
	tile->time = read->time;
	tile->x = -1;
	tile->y = -1;
	read->cache = g_slist_prepend( read->cache, tile );
	assert( read->ntiles >= 0 );
	read->ntiles += 1;

	if( !(tile->region = im_region_create( read->in )) ) {
		tile_destroy( tile );
		return( NULL );
	}

	return( tile );
}

/* Do we have a tile in the cache?
 */
static Tile *
tile_search( Read *read, int x, int y )
{
	GSList *p;

	for( p = read->cache; p; p = p->next ) {
		Tile *tile = (Tile *) p->data;

		if( tile->x == x && tile->y == y )
			return( tile );
	}

	return( NULL );
}

static void
tile_touch( Tile *tile )
{
	assert( tile->read->ntiles >= 0 );

	tile->time = tile->read->time++;
}

/* Fill a tile with pixels.
 */
static int
tile_fill( Tile *tile, int x, int y )
{
	Rect area;

	tile->x = x;
	tile->y = y;

#ifdef DEBUG
	printf( "im_tile_cache: filling tile %d x %d\n", tile->x, tile->y );
#endif /*DEBUG*/

	area.left = x;
	area.top = y;
	area.width = tile->read->tile_width;
	area.height = tile->read->tile_height;
	if( im_prepare( tile->region, &area ) )
		return( -1 );

	/* Make sure these pixels aren't part of this thread's buffer cache
	 * ... they may be read out by another thread.
	 */
	im__region_no_ownership( tile->region );

	tile_touch( tile );

	return( 0 );
}

/* Find existing tile, make a new tile, or if we have a full set of tiles, 
 * reuse LRU.
 */
static Tile *
tile_find( Read *read, int x, int y )
{
	Tile *tile;
	int oldest;
	GSList *p;

	/* In cache already?
	 */
	if( (tile = tile_search( read, x, y )) ) {
		tile_touch( tile );

		return( tile );
	}

	/* Cache not full?
	 */
	if( read->max_tiles == -1 ||
		read->ntiles < read->max_tiles ) {
		if( !(tile = tile_new( read )) ||
			tile_fill( tile, x, y ) )
			return( NULL );

		return( tile );
	}

	/* Reuse an old one.
	 */
	oldest = read->time;
	tile = NULL;
	for( p = read->cache; p; p = p->next ) {
		Tile *t = (Tile *) p->data;

		if( t->time < oldest ) {
			oldest = t->time;
			tile = t;
		}
	}

	assert( tile );

	/* The tile may have been created by another thread if we are sharing
	 * the tile cache between several readers. Take ownership of the tile
	 * to stop assert() failures in im_prepare(). This is safe, since we
	 * are in a mutex.
	 */
	im__region_take_ownership( tile->region );

#ifdef DEBUG
	printf( "im_tile_cache: reusing tile %d x %d\n", tile->x, tile->y );
#endif /*DEBUG*/

	if( tile_fill( tile, x, y ) )
		return( NULL );

	return( tile );
}

/* Copy rect from from to to.
 */
static void
copy_region( REGION *from, REGION *to, Rect *area )
{
	int y;

	/* Area should be inside both from and to.
	 */
	assert( im_rect_includesrect( &from->valid, area ) );
	assert( im_rect_includesrect( &to->valid, area ) );

	/* Loop down common area, copying.
	 */
	for( y = area->top; y < IM_RECT_BOTTOM( area ); y++ ) {
		PEL *p = (PEL *) IM_REGION_ADDR( from, area->left, y );
		PEL *q = (PEL *) IM_REGION_ADDR( to, area->left, y );

		memcpy( q, p, IM_IMAGE_SIZEOF_PEL( from->im ) * area->width );
	}
}

/* Loop over the output region, filling with data from cache.
 */
static int
fill_region( REGION *out, void *seq, void *a, void *b )
{
	Read *read = (Read *) a;
	const int tw = read->tile_width;
	const int th = read->tile_height;
	Rect *r = &out->valid;

	/* Find top left of tiles we need.
	 */
	int xs = (r->left / tw) * tw;
	int ys = (r->top / th) * th;

	int x, y;

	g_mutex_lock( read->lock );

	for( y = ys; y < IM_RECT_BOTTOM( r ); y += th )
		for( x = xs; x < IM_RECT_RIGHT( r ); x += tw ) {
			Tile *tile;
			Rect tarea;
			Rect hit;

			if( !(tile = tile_find( read, x, y )) ) {
				g_mutex_unlock( read->lock );
				return( -1 );
			}

			/* The area of the tile.
			 */
			tarea.left = x;
			tarea.top = y;
			tarea.width = tw;
			tarea.height = th;

			/* The part of the tile that we need.
			 */
			im_rect_intersectrect( &tarea, r, &hit );

			copy_region( tile->region, out, &hit );
		}

	g_mutex_unlock( read->lock );

	return( 0 );
}

/** 
 * im_tile_cache:
 * @in: input image
 * @out: output image
 * @tile_width: tile width
 * @tile_height: tile height
 * @max_tiles: maximum number of tiles to cache
 *
 * This operation behaves rather like im_copy() between images
 * @in and @out, except that it keeps a cache of computed pixels. 
 * This cache is made of up to @max_tiles tiles (a value of -1 for
 * means any number of tiles), and each tile is of size @tile_width
 * by @tile_height pixels. Each cache tile is made with a single call to 
 * im_prepare().
 *
 * This is a lower-level operation than im_cache() since it does no 
 * subdivision. It is suitable for caching the output of operations like
 * im_exr2vips() on tiled images.
 *
 * See also: im_cache().
 *
 * Returns: 0 on success, -1 on error.
 */
int
im_tile_cache( IMAGE *in, IMAGE *out,
	int tile_width, int tile_height, int max_tiles )
{
	Read *read;

	if( tile_width <= 0 || tile_height <= 0 || max_tiles < -1 ) {
		im_error( "im_tile_cache", "%s", _( "bad parameters" ) );
		return( -1 );
	}

	if( im_piocheck( in, out ) ||
		im_cp_desc( out, in ) ||
		im_demand_hint( out, IM_SMALLTILE, in, NULL ) ||
		!(read = read_new( in, out, 
			tile_width, tile_height, max_tiles )) ||
		im_generate( out, 
			NULL, fill_region, NULL, read, NULL ) )
		return( -1 );

	return( 0 );
}
