/*
   File:        cachedinterp.c
   Author:      Weng-Keen Wong
   Created:     Fri Nov 24 20:04:00 EDT 2000
   Description: Lazy N-dimensional function interpolation using cached values

   Copyright (C) 2000, Weng-Keen Wong
*/

#define MAX_KEY_BUF_SIZE 1024

#include <math.h>
#include "cachedinterp.h"

/*
static void* data_copy_string(void* data)
{
  return (void*)mk_copy_string((char*)data);
}
*/

/****************************************************************************
    Utility functions for hash table
 ****************************************************************************/
static void data_free_string(void* data)
{
  free_string((char*)data);
}

static void* data_copy_double(void* data)
{
  double* d = NULL;
  d = AM_MALLOC(double);
  *d = *(double*)data;
  return (void*)d;
}

static void data_free_double(void* data)
{
  AM_FREE((double*)data, double);
}

static void data_fprintf_string(FILE* fp, char* m1, void* data, char* m2)
{
  fprintf(fp,"%s\n",(char*)data);
}

/***************************************************************************/

double cachedinterp_knot_index_to_coord(cachedinterp* ci,
					int dim,
					int knot_index)
{
  double coord;
  double lo;
  double hi;
  double width;

  lo = hrect_lo_ref(ci->bounds,dim);
  hi = hrect_hi_ref(ci->bounds,dim);
  width = (hi - lo)/(ivec_ref(ci->knots_per_dimension,dim) - 1);
  coord = lo + knot_index * width;
  return coord;
}

/*
     Given an ivec of knot indices, this function returns a dyv of the
     actual coordinate values corresponding to the knot indices
 */
static dyv* mk_coords_from_knot_indices(cachedinterp* ci,
					ivec* knot_indices)
{
  int dim;
  dyv* coords = NULL;
  
  coords = mk_dyv(0);
  for( dim = 0; dim < ivec_size(knot_indices); dim++ )
    {
      add_to_dyv(coords,cachedinterp_knot_index_to_coord(ci,
						   dim,
						   ivec_ref(knot_indices,dim)));
      
    }
  return coords;
}

/*
    Given a corner (see header file for what a corner is), mk_key_for_cache
    returns a string key that is used to look up the value of the corner
    in the hash table.
 */
static char* mk_key_for_cache(ivec* corner)
{
  int i;
  int key_size = 0;
  char key[MAX_KEY_BUF_SIZE];
  char buf[255];

  memset(key,0,sizeof(key));
  memset(buf,0,sizeof(buf));
  for( i = 0; i < ivec_size(corner); i++ )
    {
      if( i != (ivec_size(corner) - 1))
	{
	  sprintf(buf,"%d ",ivec_ref(corner,i));
	}
      else
	{
	  sprintf(buf,"%d",ivec_ref(corner,i));
	}
      key_size += strlen(buf);
      if(( key_size + 1 ) > MAX_KEY_BUF_SIZE )
	{
	  my_error("Out of buffer space for creation of key for cache.  Increase MAX_KEY_BUF_SIZE value\n");
	}
      strcat(key,buf);
    }
  return mk_copy_string(key);
}

cachedinterp* mk_cachedinterp(int cache_size,
			      hrect* bounds,
			      ivec* knots_per_dimension,
			      bool is_data_copied,
			      dym* input,
			      dym* output)
{
  cachedinterp* ci = NULL;
  int i;

  ci = AM_MALLOC(cachedinterp);
  ci->num_dims = ivec_size(knots_per_dimension);
  ci->cache_size = cache_size;
  ci->bounds = mk_copy_hrect(bounds);
  ci->knots_per_dimension = mk_copy_ivec(knots_per_dimension);
  for( i = 0; i < ivec_size(knots_per_dimension); i++ )
    {
      if( ivec_ref(knots_per_dimension,i) <= 1 )
	{
	  my_error("Cannot have less than 2 knots points in each dimension\n");
	}
    }
  ci->cache = mk_empty_hash_table(data_copy_double,
				  data_free_double,
				  double_num_bytes);
  set_hash_table_key_type(ci->cache, CHAR_KEY_TYPE);
  ci->order = mk_empty_stack(data_free_string,data_fprintf_string);
  ci->hits = 0;
  ci->accesses = 0;
  ci->is_data_copied = is_data_copied;
  if( is_data_copied )
    {
      ci->input = mk_copy_dym(input);
      ci->output = mk_copy_dym(output);
    }
  else
    {
      ci->input = input;
      ci->output = output;
    }
  return ci;
}

void free_cachedinterp(cachedinterp* ci)
{
  free_hrect(ci->bounds);
  free_ivec(ci->knots_per_dimension);
  free_hash_table(ci->cache);
  free_stack(ci->order);

  if( ci->is_data_copied )
    {
      /* If the data is copied, free it */
      if( ci->input != NULL )
	{
	  free_dym(ci->input);
	}
      if( ci->output != NULL )
	{
	  free_dym(ci->output);
	}
    }
  else
    {
      /* Something else still points to the data, so assume it will free
	 the data.  Just NULL out the fields here */
      ci->input = NULL;
      ci->output = NULL;
    }
  AM_FREE(ci,cachedinterp);
}


/* 
   cachedinterp_get_function_value obtains the function value.  First it
   calculates the key for the given corner.  If the value of that key is
   already in the cache, it retrieves the cached value.  Otherwise, it
   calls the cachedinterp_fn to obtain the value and stores that value in
   the cache.  If the cache goes beyond its allowable size, then the
   bottom most element is removed from the stack (ie. in FIFO order).
 */
static double cachedinterp_get_function_value(cachedinterp* ci,
					      cachedinterp_fn fn,
					      ivec* corner,
					      void* data)
{
  double result = 0.0;
  double* malloced_result = NULL;
  char* key = NULL;
  char* key_to_delete = NULL;
  void* cache_data = NULL;
  dyv* query = NULL;

  key = mk_key_for_cache(corner);
  cache_data = lookup_hash_entry(ci->cache, key);
  ci->accesses++;
  if( cache_data == NULL )
    {
      /* Data has not already been computed and stored in the cache */
      query = mk_coords_from_knot_indices(ci,corner);
      result = (*fn)(query, ci->input, ci->output, data);
      malloced_result = AM_MALLOC(double);
      *malloced_result = result;
      add_hash_entry(ci->cache,key,(void*)malloced_result);
      AM_FREE(malloced_result,double);
      stack_push(ci->order,(void*)mk_copy_string(key));

      /* Now we need to delete something from the hash since we only
	 store ci->size number of items in the cache */
      if( stack_size(ci->order) > ci->cache_size )
	{
	  key_to_delete = (char*)stack_pop(ci->order);
	  delete_hash_entry(ci->cache,key_to_delete);
	  free_string(key_to_delete);
	}
      free_dyv(query);
    }
  else
    {
      /* Just return the result stored in the cache */
      ci->hits++;
      result = *((double*)cache_data);
    }
  free_string(key);
  return result;
}

/* 
   Obtains the knot indices of the corners belonging to a particular 
   coordinate for the given dimension.  For instance, suppose we are 
   given the coordinate x, which lies between corners c1 and c2:

   c1-----x------c2
   This function returns the knot indices of c1 and c2.
 */
static void cachedinterp_get_corners_for_coord_by_dim(cachedinterp* ci,
						      int dim,
						      double coord,
						      int* r_lo_knot_index,
						      int* r_hi_knot_index)
{
  double width;
  double lo;
  double hi;
#ifndef AM_FAST
  if(( dim < 0 ) || ( dim > ( ci->num_dims - 1 )))
    {
      my_error("Dimension index passed to cachedinterp_get_corners_for_query_by_dim is invalid\n");
    }
#endif
  lo = hrect_lo_ref(ci->bounds,dim);
  hi = hrect_hi_ref(ci->bounds,dim);
  width = (hi - lo)/(ivec_ref(ci->knots_per_dimension,dim) - 1);
  *r_lo_knot_index = (int)((coord - lo)/width);
  *r_hi_knot_index = (int)(((coord - lo)/width) + 0.5);
  if( *r_lo_knot_index == *r_hi_knot_index )
    {
      /* This exception condition occurs when the query point is on a
	 knot point in a dimension.  We fix it by bumping the hi knot
	 index up by one.
	  */
      (*r_hi_knot_index)++;
    }

}
						      
/*
    Given a query point, this function creates an ivec_array with each
    ivec in the array corresponding to the corners that are needed to do the
    interpolation for that query.
 */						      
static void cachedinterp_add_corners_for_query(cachedinterp* ci,
					       dyv* query,
					       ivec_array* corners)
{
  int dim;
  int lo_knot_index;
  int hi_knot_index;
  ivec_array* knot_bounds = NULL;
  int num_corners;
  int c;
  ivec* knot_bounds_per_dim = NULL;
  ivec* corner = NULL;

  /* This first part calculates the lo and hi knot indexes that bound
     the query point in each dimension.  At the end of the for loop,
     the knot_bounds ivec_array contains num_dims ivecs.  Each ivec
     contains two elements, the lo knot index and the hi knot index.
     The knot_bounds ivec_array is then used to generate the 2^num_dims
     corners used for the interpolation
     */

  knot_bounds = mk_empty_ivec_array();
  knot_bounds_per_dim = mk_ivec(2);
  for( dim  = 0; dim < ci->num_dims; dim++ )
    {
      cachedinterp_get_corners_for_coord_by_dim(ci, dim, 
						dyv_ref(query,dim),
						&lo_knot_index,
						&hi_knot_index);
      ivec_set(knot_bounds_per_dim,0,lo_knot_index);
      ivec_set(knot_bounds_per_dim,1,hi_knot_index);
      add_to_ivec_array(knot_bounds,knot_bounds_per_dim);
    }
  free_ivec(knot_bounds_per_dim);
  knot_bounds_per_dim = NULL;

  /* This part needs a bit of explanation.  There are 2^(num_dims)
     corners that need to be calculated.  Note that these corners are
     located according to knot indices, not by their real coordinates.
     The job of this part is to create all 2^(num_dims) corner locations.

     We create a counter that runs from 0 to 2^(num_dims)-1.  If we
     think of the counter as a binary number, then there are num_dims
     digits, each of which takes on the value of 0 or 1.  We iterate through
     the digits in the counter, where the dth digit corresponds to the dth
     dimension.  If the value of a digit is 0, we take the lo_knot_index as 
     the value for that dimension.  If the value of a digit is 1, we take 
     the hi_knot_index as the value for that dimension.  The way we
     determine whether a digit is 0 or 1 is by ANDing the counter with
     2^d, where d is the current digit we are on.
     */

  corner = mk_ivec(ci->num_dims);
  num_corners = 1 << ci->num_dims; /* = (int)pow(2,ci->num_dims) */
  for( c = 0; c < num_corners; c++ )
    {
      for( dim = 0; dim < ci->num_dims; dim++ )
	{
	  knot_bounds_per_dim = ivec_array_ref(knot_bounds,dim);
	  if( c & (int)pow(2,dim))
	    {
	      ivec_set(corner,dim,ivec_ref(knot_bounds_per_dim,1));
	    }
	  else
	    {
	      ivec_set(corner,dim,ivec_ref(knot_bounds_per_dim,0));
	    }
	}
      add_to_ivec_array(corners,corner);
    }
  free_ivec(corner);
  free_ivec_array(knot_bounds);
}

  /* Note that corners is an ivec_array, meaning that the knot points
     are stored as knot indexes rather than the actual coords of the
     knots */
double cachedinterp_eval(cachedinterp* ci,
			 cachedinterp_fn fn,
			 dyv* query,
			 void* data)
{
  int c;
  int dim;
  double result = 0;
  double fn_val = 0;
  double hi;
  double lo;
  double width;
  double ivalue = 1;
  double corner_coord = 0;
  ivec_array* corners = NULL;
  ivec* corner = NULL;

  corners = mk_empty_ivec_array();
  cachedinterp_add_corners_for_query(ci, query, corners);
  for( c = 0; c < ivec_array_size(corners); c++ )
    {
      ivalue = 1;
      corner = ivec_array_ref(corners,c);
      fn_val = cachedinterp_get_function_value(ci, 
					       fn, 
					       corner,
					       data);
      for( dim = 0; dim <ci->num_dims; dim++ )
	{
	  lo = hrect_lo_ref(ci->bounds,dim);
	  hi = hrect_hi_ref(ci->bounds,dim);
	  width = (hi - lo)/(ivec_ref(ci->knots_per_dimension,dim) - 1);
	  corner_coord = cachedinterp_knot_index_to_coord(ci,
				       dim,
				       ivec_ref(corner,dim));
	  ivalue *= 1 - (fabs(dyv_ref(query,dim) - corner_coord)/width);
	}
      ivalue *= fn_val; 
      result += ivalue;
    }
  free_ivec_array(corners);
  return result;
}

int cachedinterp_num_knots(cachedinterp* ci, int dim)
{
#ifndef AM_FAST
  if(( dim < 0 ) || ( dim > (ci->num_dims - 1)))
    {
      my_error("Illegal dim index passed to cachedinterp_num_knots\n");
    }
#endif
  return ivec_ref(ci->knots_per_dimension,dim);
}

void cachedinterp_fprintf_hit_ratio(FILE* fp, cachedinterp* ci)
{
  if( ci->accesses != 0 )
    {
      fprintf(fp,"hits = %d, accesses = %d, hit ratio = %f\n",
	      ci->hits, ci->accesses, (double)ci->hits/(double)ci->accesses * 100.0);
    }
}

hrect* cachedinterp_get_bounds(cachedinterp* ci)
{
  return ci->bounds;
}





