/*
 * File: dicache.c
 *
 * Copyright 2000 Jorge Arellano Cid <jcid@dillo.org>
 *
 * 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
 * (at your option) any later version.
 */

#include <glib.h>           /* GHashTable functions */
#include <gtk/gtk.h>
#include <sys/time.h>       /* for libc5 compatibility */
#include <string.h>         /* for memset */
#include <stdio.h>

#include "image.h"
#include "web.h"
#include "dicache.h"
#include "cache.h"
#include "interface.h"
#include "prefs.h"

typedef struct _DICacheHashEntry DICacheHashEntry;

struct _DICacheHashEntry {
   gint valid;
   DilloUrl *url;       /* "Key" of this hash entry */
   DICacheEntry* next;  /* pointer to the first dicache entry in this list */
};

static GHashTable *dicache_hash;

static gint dicache_size_total; /* invariant: dicache_size_total is
                                 * the sum of the image sizes of all
                                 * cache lines in dicache. */

/*
 * Determine if two hash entries are equal (used by GHashTable)
 */
static gint Dicache_hash_entry_equal(gconstpointer v1, gconstpointer v2)
{
   return (!a_Url_cmp((DilloUrl *)v1, (DilloUrl *)v2));
}

/*
 * Determine the hash value given the key (used by GHashTable)
 */
static guint Dicache_url_hash(gconstpointer key)
{
   return g_str_hash(URL_STR(((DilloUrl *)key)));
}

/*
 * Initialize dicache data
 */
void a_Dicache_init(void)
{
   dicache_hash = g_hash_table_new(Dicache_url_hash, Dicache_hash_entry_equal);
   dicache_size_total = 0;
}

/*
 * Create, and initialize a new, empty, dicache entry
 */
static DICacheEntry *Dicache_entry_new(void)
{
   DICacheEntry *entry = g_new(DICacheEntry, 1);

   entry->width = 0;
   entry->height = 0;
   entry->type = DILLO_IMG_TYPE_NOTSET;
   entry->cmap = NULL;
   entry->ImageBuffer = NULL;
   entry->RefCount = 1;
   entry->TotalSize = 0;
   entry->Y = 0;
   entry->BitVec = NULL;
   entry->State = DIC_Empty;
   entry->version = 0;
   entry->next = NULL;

   return entry;
}

/*
 * Add a new entry to the hash
 */
DICacheEntry *a_Dicache_add_entry(const DilloUrl *Url)
{
   DICacheEntry *entry;
   DICacheHashEntry *hash_entry;

   entry = Dicache_entry_new();

   if ( (hash_entry = g_hash_table_lookup(dicache_hash, Url))) {
      /* this URL is already in the hash, add entry at the END of the list */
      DICacheEntry *ptr = hash_entry->next;

      hash_entry->valid = 1;
      for (  ; ptr->next; ptr = ptr->next);
      ptr->next = entry;
      entry->version = ptr->version+1;
      entry->url = hash_entry->url;

   } else { /* no hash entry yet, so create one */
      DICacheHashEntry *hash_entry = g_new(DICacheHashEntry, 1);

      hash_entry->url = a_Url_dup(Url);
      entry->url = hash_entry->url;
      hash_entry->next = entry;
      hash_entry->valid = 1;
      g_hash_table_insert(dicache_hash, hash_entry->url, hash_entry);
   }

   return entry;
}

/*
 * Search an entry in the dicache (given the Url).
 * Return value: a pointer to the entry of the _newest_ (i.e. highest)
 *               version if found; NULL otherwise.
 */
DICacheEntry *a_Dicache_get_entry(const DilloUrl *Url)
{
   DICacheHashEntry *hash_entry = g_hash_table_lookup(dicache_hash, Url);
   DICacheEntry *entry;

   if (!hash_entry || !hash_entry->valid)
      return NULL;

   for (entry = hash_entry->next; (entry && entry->next); entry = entry->next);

   return entry;
}

/*
 * Search a particular version of an URL in the Dicache.
 * Return value: a pointer to the entry if found; NULL otherwise.
 */
static DICacheEntry *Dicache_get_entry_version(const DilloUrl *Url,
                                               gint version)
{
   DICacheHashEntry *hash_entry = g_hash_table_lookup(dicache_hash, Url);
   DICacheEntry *entry = hash_entry ? hash_entry->next : NULL;

   while (entry && entry->version != version)
      entry = entry->next;

   return entry;
}

/*
 * Actually free a dicache entry, given the URL and the version number.
 */
static void Dicache_remove(const DilloUrl *Url, gint version)
{
   DICacheHashEntry *hash_entry = g_hash_table_lookup(dicache_hash, Url);
   DICacheEntry *entry = hash_entry ? hash_entry->next : NULL, *prev = entry;

   while (entry && (entry->version != version) ) {
      prev = entry;
      entry = entry->next;
   }

   if ( entry ) {
      /* Eliminate this dicache entry */
      g_free(entry->cmap);
      if (entry->ImageBuffer) {
         g_free(entry->ImageBuffer);
         dicache_size_total -= entry->TotalSize;
      }
      a_Bitvec_free(entry->BitVec);

      if (hash_entry->next == entry) {
         if (!entry->next) {
            /* last entry with this URL. Remove the hash entry as well */
            g_hash_table_remove(dicache_hash, hash_entry->url);
            a_Url_free(hash_entry->url);
            g_free(hash_entry);
         } else
            hash_entry->next = entry->next;
      } else
         prev->next = entry->next;
      g_free(entry);
   }
}

/*
 * Unrefs the counter of a dicache entry, and _if_ no DwImage is acessing
 * this buffer _and_ there is a higher version of this image, then we call
 * Dicache_free to do the dirty job.
 */
void a_Dicache_unref(const DilloUrl *Url, gint version)
{
   DICacheEntry *entry = NULL;

   if ( (entry = Dicache_get_entry_version(Url, version)) )
      if (--entry->RefCount == 0 && (entry->next || !prefs.use_dicache) )
         Dicache_remove(Url, version);
}

/*
 * Refs the counter of a dicache entry.
 */

DICacheEntry* a_Dicache_ref(const DilloUrl *Url, gint version)
{
   DICacheEntry *entry;

   if ( (entry = Dicache_get_entry_version(Url, version)) )
      ++entry->RefCount;

   return entry;
}

/*
 * Invalidate this entry. This is used for the reloading mechanism.
 * Can't erase current versions, but a_Dicache_get_entry must return NULL.
 */
void a_Dicache_invalidate_entry(const DilloUrl *Url)
{
   DICacheHashEntry *hash_entry = g_hash_table_lookup(dicache_hash, Url);

   if (hash_entry) hash_entry->valid = 0;
}


/* ------------------------------------------------------------------------- */

/*
 * This function is a cache client; (but feeds its clients from dicache)
 */
void a_Dicache_callback(int Op, CacheClient_t *Client)
{
   /* todo: Handle Op = CA_Abort (to show what was got)  --Jcid */
   gint i;
   DilloWeb *Web = Client->Web;
   DilloImage *Image = Web->Image;
   DICacheEntry *DicEntry = a_Dicache_get_entry(Web->url);

   g_return_if_fail ( DicEntry != NULL );

   if ( Op == CA_Send ) {
      if ( Image->height == 0 && DicEntry->State >= DIC_SetParms ) {
         /* Set parms */
         a_Image_set_parms(
            Image, DicEntry->ImageBuffer, DicEntry->url,
            DicEntry->version, DicEntry->width, DicEntry->height,
            DicEntry->type);
      }
      if ( DicEntry->State == DIC_Write ) {

         for ( i = 0; i < DicEntry->height; ++i )
            if ( a_Bitvec_get_bit(DicEntry->BitVec, i) &&
                 !a_Bitvec_get_bit(Image->BitVec, i) )
               a_Image_write(
                  Image, DicEntry->ImageBuffer + i*DicEntry->width*3, i,FALSE);

      }
   } else if ( Op == CA_Close || Op == CA_Abort ) {
      a_Image_close(Web->Image);
      a_Interface_close_client(Web->bw, Client->Key);
   }
}

/* ------------------------------------------------------------------------- */

/*
 * Set image's width, height & type
 * (By now, we'll use the image information despite the html tags --Jcid)
 */
void a_Dicache_set_parms(DilloUrl *url, gint version, DilloImage *Image,
                         gint width, gint height, DilloImgType type)
{
   DICacheEntry *DicEntry;
   size_t Size = width * height * 3;

   g_return_if_fail ( Image != NULL && width && height );
   /* Find the DicEntry for this Image */
   DicEntry = Dicache_get_entry_version(url, version);
   g_return_if_fail ( DicEntry != NULL );

   /* Initialize the DicEntry */
   DicEntry->ImageBuffer = g_new(guchar, Size);
   DicEntry->TotalSize = Size;
   DicEntry->width = width;
   DicEntry->height = height;
   DicEntry->type = type;
   DicEntry->BitVec = a_Bitvec_new(height);
   DicEntry->State = DIC_SetParms;

   dicache_size_total += Size;

   /* For giggles, make the background of the undrawn parts interesting */
   memset(DicEntry->ImageBuffer, 0xdd, DicEntry->TotalSize);

   /* Allocate and initialize this image */
   a_Image_set_parms(Image, DicEntry->ImageBuffer, url, version,
                     width, height, type);
}

/*
 * Implement the set_cmap method for the Image
 */
void a_Dicache_set_cmap(DilloUrl *url, gint version, DilloImage *Image,
                        const guchar *cmap, gint num_colors,
                        gint num_colors_max, gint bg_index)
{
   DICacheEntry *DicEntry = Dicache_get_entry_version(url, version);

   g_return_if_fail ( DicEntry != NULL );

   g_free(DicEntry->cmap);
   DicEntry->cmap = g_new0(guchar, 3 * num_colors_max);
   memcpy(DicEntry->cmap, cmap, 3 * num_colors);
   if (bg_index >= 0 && bg_index < num_colors) {
      DicEntry->cmap[bg_index * 3]     = (Image->bg_color >> 16) & 0xff;
      DicEntry->cmap[bg_index * 3 + 1] = (Image->bg_color >> 8) & 0xff;
      DicEntry->cmap[bg_index * 3 + 2] = (Image->bg_color) & 0xff;
   }

   a_Image_set_cmap(Image, DicEntry->cmap);
   DicEntry->State = DIC_SetCmap;
}

/*
 * Implement the write method
 * (Write a scan line into the Dicache entry)
 * buf: row buffer
 * Y  : row number
 * x  : horizontal offset? (always zero)
 */
void a_Dicache_write(DilloImage *Image, DilloUrl *url, gint version,
                     const guchar *buf, gint x, gint Y)
{
   DICacheEntry *DicEntry;

   g_return_if_fail ( Image != NULL );
   DicEntry = Dicache_get_entry_version(url, version);
   g_return_if_fail ( DicEntry != NULL );
   g_return_if_fail ( DicEntry->width > 0 && DicEntry->height > 0 );

   a_Image_write(Image, buf, Y, TRUE);
   DicEntry->Y = Y;
   a_Bitvec_set_bit(DicEntry->BitVec, Y);
   DicEntry->State = DIC_Write;
}

/*
 * Implement the close method of the decoding process
 */
void a_Dicache_close(DilloUrl *url, gint version, CacheClient_t *Client)
{
   DilloWeb *Web = Client->Web;
   DICacheEntry *DicEntry = Dicache_get_entry_version(url, version);

   g_return_if_fail ( DicEntry != NULL );

   DicEntry->State = DIC_Close;
   if (DicEntry->cmap) {
      g_free(DicEntry->cmap);
      DicEntry->cmap = NULL;
   }
   a_Image_close(Web->Image);
   a_Interface_close_client(Web->bw, Client->Key);
}

/* ------------------------------------------------------------------------- */

static gboolean
 Dicache_remove_hash_entry(gpointer key, gpointer value, gpointer user_data)
{
   DICacheHashEntry *hash_entry = (DICacheHashEntry *)value;
   DICacheEntry *entry = hash_entry->next;

   /* Eliminate this (last) dicache entry */
   g_free(entry->cmap);
   if (entry->ImageBuffer) {
      g_free(entry->ImageBuffer);
      dicache_size_total -= entry->TotalSize;
   }
   a_Bitvec_free(entry->BitVec);
   a_Url_free(hash_entry->url);
   g_free(hash_entry);

   return TRUE;
}

/*
 * Deallocate memory used by dicache module
 * (Call this one at exit time)
 */
void a_Dicache_freeall(void)
{
   /* Remove every dicache entry */
   g_hash_table_foreach_remove(dicache_hash,
                               (GHRFunc)Dicache_remove_hash_entry, NULL);
   /* Remove the dicache hash */
   g_hash_table_destroy(dicache_hash);
}
