/* Copyright 1997 Acorn Computers Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/***************************************************/
/* File   : ImgHistory.c                           */
/*                                                 */
/* Purpose: Remembering image sizes, in case the   */
/*          HTML doesn't specify it.               */
/*                                                 */
/* Author : A.D.Hodgkinson                         */
/*                                                 */
/* History: 22-Nov-97: Created.                    */
/***************************************************/

#include <stdlib.h>
#include <string.h>
#include <time.h>

#include "kernel.h"
#include "swis.h"
#include "flex.h"

#include "wimp.h"
#include "wimplib.h"
#include "event.h"

#include "toolbox.h"

#include "svcprint.h"
#include "Global.h"
#include "Utils.h"

#include "ChoiceDefs.h"
#include "Images.h"
#include "Save.h"     /* For Save_ScrapFile definition */
#include "URLutils.h" /* For FileMethod definition     */

#include "ImgHistory.h"

/* Image history structure. It's so small that these */
/* are kept in an array (one malloc block).          */

typedef struct imghistory_entry
{
  char                    * url;
  unsigned int              hash;

  int                       os_x;
  int                       os_y;

  unsigned int              last_accessed;
}
imghistory_entry;

/* Static variables */

static imghistory_entry * imghistory_base    = NULL;
int                       imghistory_entries = 0;

/* Local statics */

static int  imghistory_find_entry   (const char ** url_base, int url_offset);

static void imghistory_remove_entry (int entry);
static int  imghistory_count        (void);

/*************************************************/
/* imghistory_find_entry()                       */
/*                                               */
/* Searches for a given image in the image       */
/* history, and returns an array index for that  */
/* image if it is present, else -1.              */
/*                                               */
/* If the parameters seem odd, it may help to    */
/* realise that this allows a flex block to hold */
/* the URL without any worries about flex        */
/* shifting.                                     */
/*                                               */
/* Parameters: Pointer to a pointer to a block   */
/*             containing the image's full URL;  */
/*                                               */
/*             Offset into the block at which    */
/*             the image lies.                   */
/*                                               */
/* Returns:    Index in the image history array  */
/*             of the entry representing the     */
/*             image if found, else -1.          */
/*************************************************/

static int imghistory_find_entry(const char ** url_base, int url_offset)
{
  int          entry;
  unsigned int hash;

  if (!imghistory_entries || !url_base) return -1;

  hash = utils_return_hash(*url_base + url_offset);

  /* Go through the array */

  for (entry = 0; entry < imghistory_entries; entry++)
  {
    if (imghistory_base[entry].hash == hash)
    {
      /* Use strcmp. We're going to be very strict in this; strcmp is not */
      /* a technically valid way to compare URLs, but it's fast and you   */
      /* don't need much storage space for the URLs themselves. The image */
      /* history is not sufficiently important to warrant anything more   */
      /* robust than this.                                                */

      if (!strcmp(imghistory_base[entry].url, *url_base + url_offset)) return entry;
    }
  }

  /* Nothing found */

  return -1;
}

/*************************************************/
/* imghistory_record()                           */
/*                                               */
/* Records a given image in the image history.   */
/* If an entry is already present for the image, */
/* it just updates the entry's last_accessed     */
/* field and ensures the size is set to the      */
/* given values.                                 */
/*                                               */
/* Parameters: Pointer to a pointer to a block   */
/*             containing the image's full URL;  */
/*                                               */
/*             Offset into the block at which    */
/*             the image lies;                   */
/*                                               */
/*             x size of the image, in OS units; */
/*                                               */
/*             y size of the image, in OS units. */
/*************************************************/

_kernel_oserror * imghistory_record(const char ** url_base, int url_offset, int os_x, int os_y)
{
  imghistory_entry * new_base;
  char             * new_url;
  int                entry;

  if (!url_base) return NULL;

  /* Don't store scrap file transfer images */

  if (!strncmp(*url_base + url_offset, FileMethod, sizeof(FileMethod) - 1))
  {
    char test_url[Limits_URL];

    /* Build the URL we'd be on for a scrap file fetch */

    StrNCpy0(test_url, Save_ScrapFile);

    urlutils_pathname_to_url(test_url, sizeof(test_url));

    /* If it's the same as the URL we've been given, don't store */
    /* this image in the image history.                          */

    if (!strcmp(*url_base + url_offset, test_url)) return NULL;
  }

  /* Try to find the item */

  entry = imghistory_find_entry(url_base, url_offset);

  /* Found it */

  if (entry >= 0)
  {
    imghistory_base[entry].last_accessed = time(NULL);

    /* Ensure the recorded size matches that given! */

    imghistory_base[entry].os_x = os_x;
    imghistory_base[entry].os_y = os_y;

    /* That's all we need to do in this case */

    return NULL;
  }

  /* Oh well, it wasn't there. So have to make a new entry */

  new_base = realloc(imghistory_base, sizeof(imghistory_entry) * (imghistory_entries + 1));

  /* Claim failed, so exit with error */

  if (!new_base) return make_no_memory_error(28);

  /* OK, managed that; set imghistory_base to whatever was returned */
  /* by realloc but don't increment the items counter yet           */

  imghistory_base = new_base;

  /* Try to allocate space for the URL too */

  new_url = malloc(strlen(*url_base + url_offset) + 1);

  /* If this claim fails, don't bother shrinking the array back; */
  /* we haven't increased the item count, so things will take    */
  /* care of themselves on the next addition (assuming that the  */
  /* lack of memory doesn't prove catastrophic elsewhere!).      */

  if (!new_url) return make_no_memory_error(28);

  /* If we reach here, both claims were successful */

  imghistory_base[imghistory_entries].url           = new_url;
  imghistory_base[imghistory_entries].os_x          = os_x;
  imghistory_base[imghistory_entries].os_y          = os_y;
  imghistory_base[imghistory_entries].last_accessed = time(NULL);

  strcpy(new_url, *url_base + url_offset);
  imghistory_base[imghistory_entries].hash = utils_return_hash(new_url);

  imghistory_entries ++;

  /* Call expiry functions */

  if (choices.image_expiry_age) imghistory_expire(time(NULL) - choices.image_expiry_age);
  if (choices.image_max_size)   imghistory_limit(choices.image_max_size);

  #ifdef SINGLE_USER

    /* Finished - exit by saving, if required */

    if (choices.save_image_history == Choices_SaveImageHistory_Always)
    {
      return imghistory_save(lookup_choice("ImageHistorySave:Browse:User.Images",0,0));
    }

  #endif

  return NULL;
}

/*************************************************/
/* imghistory_return_size()                      */
/*                                               */
/* Returns the size of an image based on the     */
/* given URL. If the image has no entry in the   */
/* image history, -1 is returned for the x and   */
/* y sizes.                                      */
/*                                               */
/* Parameters: Pointer to a pointer to a block   */
/*             containing the image's full URL;  */
/*                                               */
/*             Offset into the block at which    */
/*             the image lies;                   */
/*                                               */
/*             Pointer to an int, in which the x */
/*             size of the image is returned in  */
/*             OS units, or -1 if the image has  */
/*             no entry in the image history;    */
/*                                               */
/*             Similarly, pointer to an int for  */
/*             the y size.                       */
/*                                               */
/* Assumes:    The int pointers may be NULL (but */
/*             this would of course be pretty    */
/*             pointless!).                      */
/*************************************************/

void imghistory_return_size(const char ** url_base, int url_offset, int * os_x, int * os_y)
{
  int entry;

  if (os_x) *os_x = -1;
  if (os_y) *os_y = -1;

  if (!url_base) return;

  /* Try and find the entry for this image */

  entry = imghistory_find_entry(url_base, url_offset);

  /* Not found */

  if (entry < 0) return;

  /* Found, so return the sizes and exit */

  if (os_x) *os_x = imghistory_base[entry].os_x;
  if (os_y) *os_y = imghistory_base[entry].os_y;

  return;
}

/*************************************************/
/* imghistory_remove_entry()                     */
/*                                               */
/* Removes an entry from the image history       */
/* array by copying the last item over it and    */
/* shrinking the array size.                     */
/*                                               */
/* Parameters: Entry number.                     */
/*************************************************/

static void imghistory_remove_entry(int entry)
{
  if (entry < 0 || entry >= imghistory_entries) return;

  /* If this is the last item, only need to shrink   */
  /* the array and decrement the item counter. Else, */
  /* copy the top item over the one being removed    */
  /* and then shrink the array.                      */

  if (entry < imghistory_entries - 1)
  {
    memcpy(&imghistory_base[entry],
           &imghistory_base[imghistory_entries - 1],
           sizeof(imghistory_entry));
  }

  imghistory_entries --;

  /* Shouldn't fail, but if it does... Well, we ditch the history. */

  imghistory_base = realloc(imghistory_base,
                            imghistory_entries * sizeof(imghistory_entry));

  /* Finished */

  return;
}

/*************************************************/
/* imghistory_expire()                           */
/*                                               */
/* Remove all entiries for images which have     */
/* last been seen before a given time (i.e. are  */
/* greater than a certain age).                  */
/*                                               */
/* Parameters: Time (in time() function format)  */
/*             which an image must not have been */
/*             visited on or since for expiry to */
/*             take place - so to expire 1 day   */
/*             old images, say, you'd pass       */
/*             time() - 60*60*24.                */
/*************************************************/

void imghistory_expire(unsigned int time)
{
  int entry = 0;

  /* Go through removing items. The removal routine copies the last */
  /* array item over the one being removed, so we only imcrement    */
  /* the entry number if nothing has been removed.                  */

  while (entry < imghistory_entries)
  {
    if (imghistory_base[entry].last_accessed <= time) imghistory_remove_entry(entry);
    else entry++;
  }

  return;
}

/*************************************************/
/* imghistory_count()                            */
/*                                               */
/* Count the total space used by the image       */
/* history.                                      */
/*                                               */
/* Returns:    Space occupied by the image       */
/*             history array and strings         */
/*             attached to this, including their */
/*             terminators, in bytes.            */
/*************************************************/

static int imghistory_count(void)
{
  int count = 0;
  int entry;

  for (entry = 0; entry < imghistory_entries; entry ++)
  {
    count += sizeof(imghistory_entry);
    count += imghistory_base[entry].url ? strlen(imghistory_base[entry].url) + 1 : 0;
  }

  return count;
}

/*************************************************/
/* imghistory_limit()                            */
/*                                               */
/* Limit the space occupied by the image history */
/* to a given amount.                            */
/*                                               */
/* Parameters: Maximum size the history may be,  */
/*             in bytes.                         */
/*************************************************/

void imghistory_limit(unsigned int size)
{
  int entry;
  int oldest;
  int found;

  /* Keep removing items as long as the actual size is */
  /* greater than the maximum size specified.          */

  while (imghistory_count() > size)
  {
    /* Find the oldest entry */

    oldest = 0;
    found  = 0;

    for (entry = 0; entry < imghistory_entries; entry++)
    {
      if (
           imghistory_base[entry].last_accessed < oldest ||
           !oldest
         )
         oldest = imghistory_base[entry].last_accessed, found = entry;
    }

    /* Remove the oldest entry, if found */

    if (oldest) imghistory_remove_entry(found);
  }

  /* Finished */

  return;
}

/*************************************************/
/* imghistory_load()                             */
/*                                               */
/* Loads the global history from the given path. */
/* Will raise errors (usually, due to RISC OS    */
/* C's file I/O, the wrong ones...) if there is  */
/* a failure during loading, but not if the file */
/* refuses to open or the number of items cannot */
/* be read from it (i.e. a missing or zero       */
/* length History file).                         */
/*                                               */
/* Parameters: Pointer to the full pathname for  */
/*             the history file.                 */
/*************************************************/

_kernel_oserror * imghistory_load(const char * pathname)
{
  FILE        * file;
  char        * url        = NULL;
  static char * local_path = NULL;
  int           result;
  int           last_accessed, os_x, os_y, url_len;
  int           items, item;
  int           cc, ch;

  if (!pathname || !*pathname) return NULL;

  local_path = malloc(strlen(pathname) + 1);
  if (!local_path) return NULL;

  strcpy(local_path, pathname);

  file = fopen(local_path, "rb");

  if (!file)
  {
    free(local_path);
    return NULL; /* Fail silently - there may be no History file; this is OK */
  }

  /* Read how many items there are - again file silently, the */
  /* file may just be zero bytes long.                        */

  result = fscanf(file, "%d\n", &items);

  if (result == EOF)
  {
    fclose(file);
    free(local_path);

    return NULL;
  }

  /* Allocate space for the array */

  imghistory_base = malloc(sizeof(imghistory_entry) * items);

  if (!imghistory_base)
  {
    fclose(file);
    free(local_path);

    return make_no_memory_error(29);
  }

  imghistory_entries = 0; /* We'll increment this as each gets loaded successfully */

  /* Now load each entry's details */

  _swix(Hourglass_On, 0);

  for (item = 0; item < items; item++)
  {
    _swix(Hourglass_Percentage,
          _IN(0),

          (100 * item) / items);

    /* Read the last accessed time and required string lengths */

    result = fscanf(file, "%d,%d,%d,%d\n", &last_accessed, &os_x, &os_y, &url_len);
    if (result == EOF) goto imghistory_load_exit;

    /* Allocate buffers for the strings */

    url = malloc(url_len + 1);

    if (!url)
    {
      imghistory_entries = item; /* E.g. if item 0 fails to load, we say there are zero entries */

      fclose(file);

      _swix(Hourglass_Off,0);

      free(local_path);

      return make_no_memory_error(29);
    }

    /* Read the URL */

    for (cc = 0; cc < url_len; cc++)
    {
      int ch = fgetc(file);

      if (result == ch) goto imghistory_load_exit;

      url[cc] = (char) ch;
    }

    url[url_len] = 0;

    /* Skip the '\n' */

    ch = fgetc(file);
    if (result == ch) goto imghistory_load_exit;

    /* Now make the entry */

    imghistory_base[item].url  = url;
    imghistory_base[item].hash = utils_return_hash(url);
    url                        = NULL; /* Make sure we don't go and free it or something! */

    imghistory_base[item].os_x          = os_x;
    imghistory_base[item].os_y          = os_y;
    imghistory_base[item].last_accessed = last_accessed;

    /* Successful addition */

    imghistory_entries ++;

  /* (Closure of 'for' loop) */
  }

  fclose(file);

  _swix(Hourglass_Off,0);

  free(local_path);

  if (choices.image_expiry_age) imghistory_expire(time(NULL) - choices.image_expiry_age);
  if (choices.image_max_size)   imghistory_limit(choices.image_max_size);

  return NULL;

  /* Error condition exit */

imghistory_load_exit:

  fclose(file);

  _swix(Hourglass_Off,0);

  free(url);
  free(local_path);

  RetLastE;
}

/*************************************************/
/* imghistory_save()                             */
/*                                               */
/* Saves the global history to the given path.   */
/*                                               */
/* Parameters: Pointer to the full pathname for  */
/*             the history file.                 */
/*************************************************/

_kernel_oserror * imghistory_save(const char * pathname)
{
  static char * local_path = NULL;
  FILE        * file;
  int           entry;
  int           wrote;

  if (!pathname || !*pathname) return NULL;

  /* Canonicalise the path */

  RetError(utils_canonicalise_path(pathname, &local_path));

  /* Ensure it is present */

  {
    _kernel_oserror * e = utils_build_tree(local_path);

    if (e)
    {
      free(local_path);
      return e;
    }
  }

  /* Create the file */

  file = fopen(local_path, "wb");

  if (!file)
  {
    free(local_path);

    RetLastE;
  }

  /* Write the number of items */

  wrote = fprintf(file, "%d\n", imghistory_entries);

  if (wrote <= 0)
  {
    fclose(file);
    free(local_path);

    RetLastE;
  }

  /* Write the item contents */

  if (imghistory_entries)
  {
    for (entry = 0; entry < imghistory_entries; entry++)
    {
      wrote = fprintf(file,

                      "%d,%d,%d,%d\n%s\n",

                      imghistory_base[entry].last_accessed,

                      imghistory_base[entry].os_x,
                      imghistory_base[entry].os_y,

                      imghistory_base[entry].url ? strlen(imghistory_base[entry].url) : 0,
                      imghistory_base[entry].url ? imghistory_base[entry].url         : "");

      if (wrote <= 0)
      {
        fclose(file);
        free(local_path);

        RetLastE;
      }
    }
  }

  /* Close the file and exit */

  fclose(file);
  free(local_path);

  return NULL;
}

/*************************************************/
/* imghistory_empty()                            */
/*                                               */
/* Find out whether or not the image history has */
/* any entries in it.                            */
/*                                               */
/* Returns:    1 if there are no entries (the    */
/*             image history is empty), else 0.  */
/*************************************************/

int imghistory_empty(void)
{
  return !imghistory_entries;
}