/* cache.c - */

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

#ifndef WIN32
#include <unistd.h>
#endif

#include <errno.h>
#include <string.h>
#include <pthread.h>
#include <ctype.h>

#include "gnut_lib.h"
#include "Gnut_List.h"
#include "share.h"
#include "conf.h"
#include "gnut_threads.h"


/* Global variables
 * anything > than this is in the cache */
int share_cache_divider;
int share_size_nocache;

#ifndef PTHREADS_DRAFT4
pthread_mutex_t cache_mutex=PTHREAD_MUTEX_INITIALIZER;
extern pthread_mutex_t share_mutex;
#else
pthread_mutex_t cache_mutex;
#endif

/* Extern variables from share.c */
extern int share_num, share_size;
extern Gnut_List *share_root;

/* Local variables */
static int cache_last_update = 0;
static int cache_locked = 0;


void cache_snapshot()
{
  share_cache_divider = share_num;
  share_size_nocache = share_size;
}

/* Gnut_List *cache_clear_list(Gnut_List *l)
 *
 * clears the share list specified by l of any cache entries, and sets
 * share_num and share_size to their non-cache values */
Gnut_List *cache_clear_list(Gnut_List *list)
{
  share_item *si;

  gd_s(3, "cache_clear_list entering\n");

  pthread_mutex_lock(&share_mutex);

  /* NOTE
   * This code relies on the cache being at the head of the share list.
   * If this behavior changes, this code will break. */
  while (list) {
    si = (share_item *) list->data;
    if (si->ref > share_cache_divider) {
      share_delete(si, NULL);
      list = gnut_list_remove(list, list->data);
    } else {
      break;
    }
  } 

  share_num = share_cache_divider;
  share_size = share_size_nocache;

  pthread_mutex_unlock(&share_mutex);

  gd_s(3,"cache_clear_list returning\n");
  return list;
}

/* int rescan_cache()
 *
 * clear the share list of cached items, and rescan the cache into the
 * share */
int rescan_cache()
{
  char *b;
  char *c;

  gd_s(3, "rescan_cache entering\n");

  share_root = cache_clear_list(share_root);

  b=conf_get_str("cache_path");
  if ((b) && (strlen(b)>1)) {
    c=expand_path(b);
    gd_s(5, "scanning cache: ");
    gd_s(5, c);
    gd_s(5, "\n");
    share_scan_dir(c, 0);
    xfree(c);
  }

  gd_s(3,"rescan_cache returning\n");
  return 0;
}

/* callback, prints a share entry */
int cache_print(void * data, void * user_data)
{
  share_item *si;

  gd_s(3, "cache_print entering\n");

  si=data;
  if (si->ref > share_cache_divider) {
    printf("CACHE: ");
  }
  printf("%s\n", si->path);

  gd_s(3,"cache_print returning\n");
  return 0;
}

void print_share()
{
  int dt;

  gd_s(3, "print_share entering\n");
  
  pthread_mutex_lock(&share_mutex);

  printf("CACHE is %s\n", cache_locked ? "LOCKED" : "UNLOCKED");

  if ((dt = conf_get_int("cache_refresh")) > 0) {
    printf("Next update in: %d\n", (int) (time(NULL) - (cache_last_update + dt)));
  }

  gnut_list_foreach(share_root,cache_print,NULL);
  
  pthread_mutex_unlock(&share_mutex);

  gd_s(3,"print_share returning\n");
}

int cache_total_size(void * data, void * user_data)
{
  share_item *si;
  int *total_size;

  gd_s(3, "cache_total_size entering\n");

  si=data;
  total_size = user_data;

  if (si->ref > share_cache_divider) {
    (*total_size) += si->size;
  }

  gd_s(3,"cache_total_size returning\n");
  return 0;
}

int cache_find_oldest(void * data, void * user_data)
{
  share_item *si, **ret_si;

  gd_s(3, "cache_find_oldest entering\n");

  si=data;
  ret_si = user_data;

  if ((si->ref > share_cache_divider) &&
      ((*ret_si == NULL) ||
       ((*ret_si)->mtime > si->mtime))) {
    *ret_si = si;
  }

  gd_s(3,"cache_find_oldest returning\n");
  return 0;
}

int cache_shrink(int desired_free)
{
  int max_cache_size, cache_size;
  share_item *si;

  gd_s(3, "cache_shrink entering\n");

  max_cache_size = conf_get_int("cache_size") * 1024;
  if ((max_cache_size - desired_free) < 0) {
    fprintf(stderr, "Trying to free more space in cache than cache size!\n");
    gd_s(3, "cache_shrink a returning -1\n");
    return -1;
  }

  pthread_mutex_lock(&share_mutex);
  cache_size = 0;
  gnut_list_foreach(share_root,cache_total_size,&cache_size);
  pthread_mutex_unlock(&share_mutex);

  while ((max_cache_size - cache_size) < desired_free) {

    si = NULL;
    pthread_mutex_lock(&share_mutex);
    gnut_list_foreach(share_root,cache_find_oldest,&si);
    pthread_mutex_unlock(&share_mutex);

    if (si == NULL) {
      fprintf(stderr, "Failed to find an oldest member of the cache!\n");
      gd_s(3, "cache_shrink b returning -1\n");
      return -1;
    }
    
    /* remove the oldest file in the cache, and adjust the cache size */
    if (unlink(si->fpath) != 0) {
      fprintf(stderr, "Failed to remove %s from cache!\n", si->fpath);
      gd_s(3, "cache_shrink c returning -1\n");
      return -1;
    }

    pthread_mutex_lock(&share_mutex);
    cache_size -= si->size;
    share_size -= (si->size/1024);
    share_num--;

    share_delete(si, NULL);
    share_root = gnut_list_remove(share_root, si);
    pthread_mutex_unlock(&share_mutex);
  }

  gd_s(3, "cache_shrink returning 0\n");

  return 0;
}

int cache_time_for_update()
{
  int dt;
  char *c;
  
  c=conf_get_str("cache_path");
  if (c && (strlen(c)>1)) {

    /* is there already a cache download going on? */
    if (cache_locked) return 0;
  
    if ((dt = conf_get_int("cache_refresh")) > 0) {
      int pass = (time(NULL) > (cache_last_update + dt));
      return (pass);
    }
  }
  /* no update time set */
  return 0;
}

int cache_lock()
{
  int r = (pthread_mutex_trylock(&cache_mutex) == 0);
  if (r) {
    cache_locked = 1;
  }
  return r;
}

void cache_unlock()
{
  cache_locked = 0;
  pthread_mutex_unlock(&cache_mutex);
}

void cache_timestamp()
{
  cache_last_update = time(NULL);
}
