/*
  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.
*/

#ifdef HAVE_CONFIG_H
#include "../config.h"
#endif

#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;

 /**    
  Offset to be applied to all block addresses to compensate for an
  eventual displacement of the block addresses relative to the image
  start block address that was assumed when the image was created.
  E.g. if track number 2 gets copied into a disk file and shall then
  be loaded as ISO filesystem.
  If displacement_sign is 1 then the displacement number will be
  added to .read_block() addresses, if -1 it will be subtracted.
  Else it will be ignored.
 */
 uint32_t displacement;
 int      displacement_sign;

};

#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;

 if(icd->displacement_sign == 1) {
   if(lba + icd->displacement < lba) {
address_rollover:;
      return ISO_DISPLACE_ROLLOVER;
   } else
     lba += icd->displacement;
 } else if(icd->displacement_sign == -1) {
   if(lba < icd->displacement )
     goto address_rollover;
   else
     lba -= icd->displacement;
 }

 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;
   sprintf(msg, "ds_read_block(%lu) returns %lX",
           (unsigned long) lba, (unsigned long) ret);
   isoburn_msgs_submit(NULL, 0x00060000, msg, 0, "DEBUG", 0);
   return ISO_DATA_SOURCE_MISHAP;
 }

#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,
                                  uint32_t displacement, int displacement_sign)
{
 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->version = 0;
 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;
 }
 icd->displacement = displacement;
 icd->displacement_sign = displacement_sign;
 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);
}


