/*
  data source for libisoburn.

  Copyright 2007 - 2010 Vreixo Formoso Lopes <metalpain2002@yahoo.es>
                    and Thomas Schmitt <scdbackup@gmx.net>
  Provided under GPL version 2 or later.
*/

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

#include <stdio.h>


#ifndef Xorriso_standalonE

#include <libburn/libburn.h>

#include <libisofs/libisofs.h>

#else /* ! Xorriso_standalonE */

#include "../libisofs/libisofs.h"
#include "../libburn/libburn.h"

#endif /* Xorriso_standalonE */


#include "isoburn.h"


/* Cached reading of image tree data */
/* Multi tile:  32 * 64 kB */

/* The size of a single tile.
   Powers of 2 only ! Less than 16 makes not much sense.
*/
#define Libisoburn_tile_blockS 32

/* The number of tiles in the cache
*/
#define Libisoburn_cache_tileS 32


/* Debugging only: This reports cache loads on stderr.
#define Libisoburn_read_cache_reporT 1
*/


struct isoburn_cache_tile {
 char cache_data[Libisoburn_tile_blockS * 2048];
 uint32_t cache_lba;
 uint32_t last_error_lba;
 uint32_t last_aligned_error_lba;
 int cache_hits;
 int age;
};

struct isoburn_cached_drive {
 struct burn_drive *drive;
 struct isoburn_cache_tile tiles[Libisoburn_cache_tileS];
 int current_age;
};

#define Libisoburn_max_agE 2000000000

static int ds_inc_age(struct isoburn_cached_drive *icd, int idx, int flag);


int ds_read_block(IsoDataSource *src, uint32_t lba, uint8_t *buffer)
{
 int ret, i, oldest, oldest_age;
 struct burn_drive *d;
 off_t count;
 uint32_t aligned_lba;
 char msg[80];
 struct isoburn_cache_tile *tiles;
 struct isoburn_cached_drive *icd;

 if(src == NULL || buffer == NULL)
   /* It is not required by the specs of libisofs but implicitely assumed
      by its current implementation that a data source read result <0 is
      a valid libisofs error code.
   */
   return ISO_NULL_POINTER;

 icd = (struct isoburn_cached_drive *) src->data;
 d = (struct burn_drive*) icd->drive;

 if(d == NULL) {
   /* This would happen if libisoburn saw output data in the fifo and
      performed early drive release and afterwards libisofs still tries
      to read data.
      That would constitute a bad conceptual problem in libisoburn.
   */
   isoburn_msgs_submit(NULL, 0x00060000,
     "Programming error: Drive released while libisofs still attempts to read",
     0, "FATAL", 0);
   return ISO_ASSERT_FAILURE;
 }

 tiles = (struct isoburn_cache_tile *) icd->tiles;

 aligned_lba= lba & ~(Libisoburn_tile_blockS - 1);

 for(i=0; i<Libisoburn_cache_tileS; i++) {
   if(aligned_lba == tiles[i].cache_lba && tiles[i].cache_lba != 0xffffffff) {
     (tiles[i].cache_hits)++;
     memcpy(buffer, tiles[i].cache_data + (lba - aligned_lba) * 2048, 2048);
     count= 2048;
     ds_inc_age(icd, i, 0);
     return 1;
   }
 }

 /* find oldest tile */
 oldest_age= Libisoburn_max_agE;
 oldest= 0;
 for(i= 0; i<Libisoburn_cache_tileS; i++) {
   if(tiles[i].cache_lba == 0xffffffff) {
     oldest= i;
 break;
   }
   if(tiles[i].age<oldest_age) {
     oldest_age= tiles[i].age;
     oldest= i;
   }
 }

 tiles[oldest].cache_lba= 0xffffffff; /* invalidate cache */
 if(tiles[oldest].last_aligned_error_lba == aligned_lba) {
   ret = 0;
 } else {
   ret = burn_read_data(d, (off_t) aligned_lba * (off_t) 2048,
                        (char *) tiles[oldest].cache_data,
                        Libisoburn_tile_blockS * 2048, &count, 2);
 }
 if (ret <= 0 ) {
   tiles[oldest].last_aligned_error_lba = aligned_lba;

   /* Read-ahead failure ? Try to read 2048 directly. */
   if(tiles[oldest].last_error_lba == lba)
     ret = 0;
   else
     ret = burn_read_data(d, (off_t) lba * (off_t) 2048, (char *) buffer,
                        2048, &count, 0);
   if (ret > 0)
     return 1;
   tiles[oldest].last_error_lba = lba;

#ifdef ISO_DATA_SOURCE_MISHAP
   ret= ISO_DATA_SOURCE_MISHAP;
#else
   /* <<< pre libisofs-0.6.7 */
   /* It is not required by the specs of libisofs but implicitely assumed
      ...
      But it is not possible to ignore FAILURE.
      libisofs insists in original error codes, i.e. libisoburn cannot
      change severity FAILURE associated with ISO_FILE_READ_ERROR.
      So ISO_FILE_READ_ERROR is not an option and libisoburn has to
      misuse ISO_FILE_CANT_WRITE, which is actually for image generation
      and not for image reading.
      This is quite wrong, although the error message text is unclear
      enough to make it appear plausible.
   */
   ret= ISO_FILE_CANT_WRITE;
#endif

   if(ret >= 0)
     ret = -1;
   sprintf(msg, "ds_read_block(%lu) returns %d", (unsigned long) lba, ret);
   isoburn_msgs_submit(NULL, 0x00060000, msg, 0, "DEBUG", 0);
   return ret; 
 }

#ifdef Libisoburn_read_cache_reporT
 fprintf(stderr, "Tile %2.2d : After %3d hits, new load from %8x , count= %d\n",
         oldest, tiles[oldest].cache_hits, aligned_lba, (int) count);
#endif

 tiles[oldest].cache_lba= aligned_lba;
 tiles[oldest].cache_hits= 1;
 ds_inc_age(icd, oldest, 0);

 memcpy(buffer, tiles[oldest].cache_data + (lba - aligned_lba) * 2048, 2048);
 count= 2048;

 return 1;
}


static int ds_open(IsoDataSource *src)
{
 /* nothing to do, device is always grabbed */
 return 1;
}

static int ds_close(IsoDataSource *src)
{
 /* nothing to do, device is always grabbed */
 return 1;
}

static void ds_free_data(IsoDataSource *src)
{
 /* nothing to do */;
 if(src->data != NULL)
   free(src->data);
 src->data= NULL;
}


int isoburn_data_source_shutdown(IsoDataSource *src, int flag)
{
 struct isoburn_cached_drive *icd;

 if(src==NULL)
   return(0);
 icd= (struct isoburn_cached_drive *) src->data;
 icd->drive= NULL;
 return(1);
}


IsoDataSource *isoburn_data_source_new(struct burn_drive *d)
{
 IsoDataSource *ret;
 struct isoburn_cached_drive *icd= NULL;
 int i;

 if (d==NULL)
   return NULL;
 ret = malloc(sizeof(IsoDataSource));
 icd = calloc(1,sizeof(struct isoburn_cached_drive));
 if (ret == NULL || icd == NULL)
   return NULL;
 ret->refcount = 1;
 ret->read_block = ds_read_block;
 ret->open = ds_open;
 ret->close = ds_close;
 ret->free_data = ds_free_data;
 ret->data = icd;
 icd->drive = d;
 icd->current_age= 0;
 for(i= 0; i<Libisoburn_cache_tileS; i++) {
   icd->tiles[i].cache_lba = 0xffffffff;
   icd->tiles[i].cache_hits = 0;
   icd->tiles[i].last_error_lba = 0xffffffff;
   icd->tiles[i].last_aligned_error_lba = 0xffffffff;
   icd->tiles[i].age= 0;
 }
 return ret;
}


static int ds_inc_age(struct isoburn_cached_drive *icd, int idx, int flag)
{
 int i;

 (icd->current_age)++;
 if(icd->current_age>=Libisoburn_max_agE) { /* reset all ages (allow waste) */
   for(i= 0; i<Libisoburn_cache_tileS; i++)
     (icd->tiles)[i].age= 0;
   icd->current_age= 1;
 }
 (icd->tiles)[idx].age= icd->current_age;
 return(1);
}


