/* Copyright 1996 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 and memory management for Filer_Action

Revision History:

0.07  13-May-90  ARW  Totally new implementation to old interface
*/

#define Timing 0

/*

Theory:

(1) read blocks (or all) of source file(s) into a contiguous chunk of store
    - extend as necessary up to next slot size
    - be prepared to loose information if slot size has to be shrunk

(2) write blocks to destination file(s)

(3) repeat until done

Notes:

(a) dynamically resize the block quanta for source and destination according
    to the real time it took: its programmed to update the user information
    (or allow other tasks in) around time_quanta (may get to 2*time_quanta).
    Hysteresis built in to the change in block size:
    - growth: <4096: +1024 AND NOT 1023
              <16K:  +4K AND NOT (4K-1)
              else:  double size
    - shrink: <4096: -256
              <16K:  -1024
              else:  -(quarter size)
    Initial block size is initial_block_size for both (=4096?)

(b) if a file is smaller than src_block, it is *loaded
    if a file is smaller than dest_block, it is *saved
    - these operations are not checked against time_quanta

(c) care needed to ensure that a contiguous block exists and can be moved
    by memcpy when it needs to be (user changes slot allocation - "action_slot").

 JRS 17/1/92 Measure the average transfer rate at the initial block size. If the blocksize
 is reduced below a certain critical value, then the transfer rate is compared with the initial
 value and if it is substantially worse, the value of time_quanta is increased, to allow
 blocksize to increase and get a better transfer rate at the expense of 'lumpy' response.
 Added hysteresis to time_quanta to prevent it always growing and shrinking

*/

#if 0
#define debugmem
#endif

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

#include "kernel.h"

#include "os.h"
#include "wimp.h"

#include "Options.h"
#include "chains.h"
#include "allerrs.h"
#include "malloc+.h"
#include "memmanage.h"
#include "debug.h"

#if Timing
static int copytime;
#endif

#define Yes 1
#define No 0

#define ObjectType_NotFound   0
#define ObjectType_File       1
#define ObjectType_Directory  2


#define InitialBlockSize 4096
#define SmallBlockSize   3072 /* check the transfer rate is not too low if the blocksize reduces below this */
#define MinimumWorkingSize InitialBlockSize*3

#define NominalTimeQuanta 25 /* cs */
#define MaxTimeQuanta    100
#define TimeQuantaHysteresis 3

/*
     The file headers are held in a doubly linked list, on the malloc+ heap
*/
typedef enum {
    write_not_started,
    write_f_tried_unlock,   /* open/save failed - have tried to unlock it */
    write_f_tried_cdirs,    /* open/save failed - have tried to cdir it */
    write_f_tried_both,     /* open/save failed - have tried both cdirs and unlock */
    write_openit,           /* have created it, now open it */
    write_truncateopen,     /* have opened it now truncate it */
    write_blocks,           /* next thing is writing blocks out */
    write_closeit,          /* next thing is to close it */
    write_adjust_access,    /* next thing is adjust the access */
    write_adjust_info,      /* next thing is adjust the info */
    write_all_done          /* and junk the fh */
}       write_activity;

typedef struct
{
    chain_link      link;
    char            *source_filename;
    int             source_file_handle;
    char            *destination_filename;
    int             destination_file_handle;
    int             size;
    int             reload;
    int             execute;
    int             attributes;
    int             objecttype;
    int             forceit;
    int             read_to;
    int             written_to;
    write_activity  write_state;
    int             start_of_buffer;        /* offset into memory */

#ifdef PROGRESS_BAR
    unsigned int total_progress, read_progress, write_progress;
#endif
} files_header;

/*
     Structures for copying files
*/
static chain_header files_chain;        /* All the files registered with memmanage */
static int application_max_size;
static char *buffer_base_address;       /* Base of buffer area */
static int buffer_offset;               /* offset to add to botom, top, and any other offsets into the area */
                                    /* used when the area contents are shuffled */
static int buffer_bottom;               /* offset to 1st used byte in buffer area */
static int buffer_top;                  /* offset to top free section in buffer area */
static int buffer_end;                  /* offset to end of buffer area */
static int last_requested_size;
static int src_block_size;
static int dest_block_size;
static int time_quanta = NominalTimeQuanta;
static int minimum_block_size = 512;
int size_of_finished_file;
char *source_of_finished_file;

/* JRS addition 16/1/92 */
/* accumulate transfer times at initial blocksize to compare timings at smaller
 * blocksizes to detect when the time per byte increases unacceptably. This triggers
 * an increase in time_quanta which causes the blocksize to grow and the overall
 * transfer rate to improve */

#define timing_Reading 0
#define timing_Writing 1

#define timing_NAccumulated 4  /* accumulate this many timings to average out variation */

static struct
  {
  int rate; /* transfer rate calculated from accumulator and blocksize */
  int accumulator;
  int naccumulated;
  } timing[2]; /* read and write initial timing */

static void timing_init(void)
{
    int rw;
    for (rw = 0; rw < 2; rw++) /* reading and writing */
    {
        timing[rw].rate = 0;
        timing[rw].accumulator = 0;
        timing[rw].naccumulated = 0;
    }
}

static int t__accumulatortorate(int accumulated_time, int blocksize)
{ /* convert an accumulated timing to a transfer rate value */
    if (accumulated_time < 1) accumulated_time = 1; /* ensure no div by zero */
#ifdef debugmem
dprintf("calcrate %d,%d->%d\n", accumulated_time, blocksize,(blocksize<<8) / accumulated_time);
#endif
    return (blocksize<<8) / accumulated_time; /* shift is to gain integer value range */
}

static void timing_accumulate(int rw, int t, int blocksize)
/* accumulate a timing for read or write, return Yes if it is full (enough have been accumulated) */
{
    assert((rw==timing_Reading)||(rw==timing_Writing));
    assert(timing[rw].naccumulated < timing_NAccumulated);
    timing[rw].naccumulated++;
    timing[rw].accumulator += t;
    if ( timing[rw].naccumulated >= timing_NAccumulated )
    { /* accumulator is full and rate is not yet calculated; calculate rate */
        assert(timing[rw].rate == 0);
        timing[rw].rate = t__accumulatortorate(timing[rw].accumulator, blocksize);
    }
}

static int timing_accumulating(int rw)
{
    assert((rw==timing_Reading)||(rw==timing_Writing));
    return (timing[rw].naccumulated < timing_NAccumulated);
}

static int timing_poorrate(int rw, int t, int blocksize)
{ /* compare the given time, converted to a rate against the rate for the initial blocksize.
   * Return Yes if it is worse than 2/3 of the initial rate */
    assert((rw==timing_Reading)||(rw==timing_Writing));
    return ( t__accumulatortorate(t * timing_NAccumulated, blocksize) <  (2*timing[rw].rate)/3 );
}


static int application_current_size( void )
{
    int currentslot = -1;
    int nextslot = -1;
    int freepool;

    wimp_slotsize( &currentslot, &nextslot, &freepool );

    return currentslot;
}

static int current_next_slot( void )
{
    int currentslot = -1;
    int nextslot = -1;
    int freepool;

    wimp_slotsize( &currentslot, &nextslot, &freepool );

    return nextslot;
}

/*
    returns amount actually changed by
*/
static int extend_application_slot( int amount )
{
    int oldslot;
    int currentslot;
    int nextslot = -1;
    int freepool;

    if ( amount != 0 )
    {
        oldslot = application_current_size();

        currentslot = oldslot + amount;

        wimp_slotsize( &currentslot, &nextslot, &freepool );

#ifdef debugmem
dprintf("extend_application_slot: amount = %d, new size = %d\n",amount,currentslot);
#endif

        return currentslot - oldslot;
    }
    else
    {
        return 0;
    }
}

/*
    Ensure that the top to end amount free is at least size
*/
static int buffer_ensure( int size )
{
    if ( buffer_end - buffer_top < size )
    {
        buffer_end += extend_application_slot( size - (buffer_end - buffer_top) );
    }

    return buffer_end - buffer_top >= size;
}

/*
    Grow the buffer so that "size" is available, limiting by application_max_siz.
    Reduces *size to fit space.
    Returns Yes if not enough space available for original requested *size.
*/
static int grow_buffer( int *size )
{
    int answer = No;

    /*
        Upper bound by application_max_size
    */
    if ( (int)(buffer_base_address + *size + buffer_top + buffer_offset) > application_max_size + 0x8000 )
    {
        *size = application_max_size + 0x8000 - (int)(buffer_base_address + buffer_top + buffer_offset);
        answer = Yes;
    }

    /*
        Try to ensure that much space
    */
    answer = answer || !buffer_ensure( *size );

    if ( buffer_end - buffer_top < *size )
    {
        *size = buffer_end - buffer_top;
        answer = Yes;
    }

    return answer;
}

static os_error *close_file( int *handle )
{
    os_regset r;
    os_error *err;

    r.r[0] = 0; /* close file */
    r.r[1] = *handle;

    err = os_find( &r );

    /*
         Even a failed close leaves the file closed afterwards
    */
    *handle = 0;

    return err;
}

/*
     Creates the file to be the given size, dead, with read/write
     access only.
*/
static os_error *create_file_dead( char *filename, int size )
{
    os_filestr fileplace;
    os_error *err;

    fileplace.action   = 7; /* create a file */
    fileplace.name     = filename;
    fileplace.loadaddr = (int) 0xdeaddead;
    fileplace.execaddr = (int) 0xdeaddead;
    fileplace.start    = 0;
    fileplace.end      = size;

    err = os_file( &fileplace );

    if ( err )
         return err;

    fileplace.action = 4; /* set access to a file */
    fileplace.name = filename;
    fileplace.end = 0x3; /* read/write only */

    return os_file( &fileplace );
}

static os_error *create_directory( char *dirname )
{
    os_filestr fileplace;

    fileplace.action = 8; /* create a directory */
    fileplace.name = dirname;
    fileplace.start = 0;

    return os_file( &fileplace );
}

static os_error *write_catalogue_information( char *filename, int reload, int execute, int attributes )
{
    os_filestr fileplace;

    fileplace.action    = 1; /* write catalogue information */
    fileplace.name      = filename;
    fileplace.loadaddr  = reload;
    fileplace.execaddr  = execute;
    fileplace.end       = attributes;

    return os_file( &fileplace );
}

static os_error *write_attributes( char *filename, int attributes )
{
    os_filestr fileplace;

    fileplace.action    = 4; /* write attributes */
    fileplace.name      = filename;
    fileplace.end       = attributes;

    return os_file( &fileplace );
}

static char *first_dirbreak( char *filename )
{
    char *trypos;
    char *endpos;

    /*
        Point trypos past the -<fsname>- / <fsname>: bit
    */
    if ( filename[0] == '-' )
    {
        /*
            -<fsname>-
        */
        trypos = strchr( filename + 1, '-' ) + 1;
    }
    else if ( filename[0] != ':' )
    {
        /*
            <fsname>: perhaps
        */
        trypos = strchr( filename, ':' );

        if ( trypos == NULL )
        {
            /*
                 It wasn't <fsname>:, never mind
            */
            trypos = filename;
        }
        else
        {
            trypos++;
        }
    }
    else
    {
        trypos = filename;
    }

    /*
        Move trypos past the :<devname> or :<devname>. bit if it's there
    */
    if ( *trypos == ':' )
    {
        endpos = strchr( trypos, '.' );

        if ( endpos == NULL )
        {
            trypos = trypos + strlen( trypos );
        }
        else
        {
            trypos = endpos + 1;
        }
    }

    /*
         Move past the first component (and any $. &. %. @. or \.)
    */
    if ( *trypos != '\0' )
    {
        switch( *trypos )
        {
        case '$':
        case '&':
        case '%':
        case '@':
        case '\\':

            trypos ++;

            if ( *trypos == '.' )
                trypos++;

             /*
                Fall through to skipping over the next component
             */

        default:
            endpos = strchr( trypos, '.' );

            if ( endpos == NULL )
            {
                trypos = trypos + strlen( trypos );
            }
            else
            {
                trypos = endpos;
            }

            break;
        }
    }

    /*
         trypos now points past the first non-special component
         of the filename or at the terminating nul
    */

    return trypos;
}

static os_error *ensure_file_directories( char *filename )
{
    char *currpos;
    os_error *err;

    for ( currpos = first_dirbreak( filename );
          currpos && *currpos;
          currpos = strchr( currpos + 1, '.' ) )
    {
        *currpos = '\0';

        err = create_directory( filename );

        *currpos = '.';

        if ( err )
            return err;
    }

    return NULL;
}

/*
     Ensures files used by fh are closed, and ignores any errors it gets
     back. The destination file, if closed, will be truncated to the
     written_to size, and set to dead.
*/
static void ensure_files_closed( files_header *fh )
{
    if ( fh->destination_file_handle )
        close_file( &fh->destination_file_handle );

    if ( fh->source_file_handle )
        close_file( &fh->source_file_handle );
}

/*
     Totally junk an fh: remove it from the files chain
*/
static void remove_fh( files_header *fh )
{
    chain_remove_link( &fh->link );

    ensure_files_closed( fh );

    if ( fh->start_of_buffer + fh->read_to - fh->written_to >= buffer_top )
    {
        /*
            removing buffer at end
        */

        if ( fh->start_of_buffer < buffer_top )
            buffer_top = fh->start_of_buffer;
    }
    else
    {
        /*
            removing buffer at start
        */
        buffer_bottom += fh->read_to - fh->written_to;
    }

    if ( fh->source_filename )
        overflowing_free( fh->source_filename );
    overflowing_free( fh->destination_filename );
    overflowing_free( fh );
}

/*
     Assumuing no buffer in buffer chain, and no open files, remove
     forwards file from files_chain.
*/
static void remove_file_from_chain( void )
{
    chain_link *link = files_chain.forwards;
    files_header *fh;

    if ( !link->forwards )
        return;

    fh = chain_link_Wrapper( link );

    /*
        Special indication that it has been a directory that's just
        been finished
    */
    if ( fh->objecttype == ObjectType_Directory )
    {
        size_of_finished_file = -1;
    }
    else
    {
        size_of_finished_file = fh->size;
    }

    /*
        Record the source filename of the finished file
    */
    if ( source_of_finished_file )
        overflowing_free( source_of_finished_file );

    source_of_finished_file = fh->source_filename;
    fh->source_filename = NULL;

    remove_fh( fh );
}

/*
     returns the files_handle of the next file to read
*/
static files_header *next_fh_to_read( void )
{
    chain_link *link;
    files_header *fh;

    for ( link = files_chain.forwards; link->forwards; link = link->forwards )
    {
        fh = chain_link_Wrapper( link );

        if ( fh->objecttype != ObjectType_Directory &&
             ( fh->read_to < fh->size ||
               fh->source_file_handle ) )
        {
            return fh;
        }
    }

    return NULL;
}

/*
     skip a general file
*/
static void skip_file( files_header *fh )
{
    if ( fh == NULL )
        return;

    remove_fh( fh );
}

/*
     algorithms to grow and shrink block size on time out
*/

static int grow( int block )
{
    if ( block < 4096 )
    {
        block = (block + 1024) & ~1023;
    }
    else if ( block < 16384 )
    {
        block = (block + 4096) & ~4095;
    }
    else
    {
        block = (block + (block>>1)) & ~4095;
    }

#ifdef debugmem
dprintf( "growing to %d\n", block );
#endif

    return block;
}

static int shrink( int block )
{
    if ( block < 4096 )
    {
        block = (block - 256) & ~255;
    }
    else if ( block < 16384 )
    {
        block = (block - 1024) & ~1023;
    }
    else
    {
        block = (block - (block>>2)) & ~1023;
    }

    if ( block < minimum_block_size )
        block = minimum_block_size;

#ifdef debugmem
dprintf( "shrinking to %d\n", block );
#endif

    return block;
}

/*
    Go through all readable files and discard their buffers if they overhang
    the top of the buffer
*/
static void truncate_overhanging_files( void )
{
    chain_link *link;
    files_header *fh;

    for ( link = files_chain.forwards; link->forwards; link = link->forwards )
    {
        fh = chain_link_Wrapper( link );

        if ( fh->objecttype != ObjectType_Directory )
        {
            if ( fh->start_of_buffer >= buffer_top )
            {
                /*
                    file buffer starts beyond end of buffer
                */
                fh->start_of_buffer = 0;
                fh->read_to = fh->written_to;
            }
            else if ( fh->start_of_buffer + fh->read_to - fh->written_to > buffer_top )
            {
                /*
                    file buffer hangs over end of buffer
                */
                fh->read_to = buffer_top - (fh->start_of_buffer - fh->written_to);
            }
        }
    }
}

static int memmanage_slotextend( int n, void **p )
{
    int size_grown;
    int movement;

    size_grown = MinimumWorkingSize - (buffer_end + buffer_offset - n);
    if ( size_grown > 0 )
    {
        /*
            Not enough room in current slot, so try to extend
            If fail to extend then can't satisfy request
        */
        buffer_end += extend_application_slot( size_grown );

        if ( buffer_end + buffer_offset - n < MinimumWorkingSize )
            return 0;

        if ( application_max_size < (int)(buffer_base_address + buffer_offset + buffer_end) - 0x8000 )
            application_max_size = (int)(buffer_base_address + buffer_offset + buffer_end) - 0x8000;
    }

    if ( buffer_bottom + buffer_offset >= n )
    {
        /*
            Already enough room below used section - no action necessary
        */
    }
    else if ( buffer_bottom + buffer_offset + buffer_end - buffer_top >= n )
    {
        /*
            Enough room in unused space, but not enough room below
            used section so move used section of buffer up
            to make room.
        */
        memmove( buffer_base_address + n,
            buffer_base_address + buffer_bottom + buffer_offset,
            buffer_top - buffer_bottom );

        movement = n - (buffer_offset + buffer_bottom);
        buffer_end -= movement;
        buffer_offset += movement;
    }
    else
    {
        /*
            Not enough room in the available slot - try to extend
        */
        size_grown = n - (buffer_bottom + buffer_offset + buffer_end - buffer_top);

        (void)grow_buffer( &size_grown );

        memmove( buffer_base_address + n,
            buffer_base_address + buffer_bottom + buffer_offset,
            buffer_end + buffer_offset - n );

        movement = n - (buffer_offset + buffer_bottom);
        buffer_end -= movement;
        buffer_offset += movement;

        if ( buffer_top > buffer_end )
        {
            buffer_top = buffer_end;
            truncate_overhanging_files();
        }
    }

    *p = buffer_base_address;
    buffer_base_address += n;
    buffer_offset -= n;

    return n;
}

/*****************************************************************

Below here is the external interface stuff

*****************************************************************/

/*
     Set the slot size to that passed.
     Return an error if the wimp_slotsize failed or the slot size
      obtained is less than that requested.
*/
void action_slot( int size )
{
    int size_change = size - application_current_size();

#ifdef debugmem
dprintf( "Change slot by %d to %d\n", size_change, size );
#endif

    if ( size_change > 0 )
    {
        /*
            Grow slot
        */
        buffer_end += extend_application_slot( size_change );
    }
    else if ( size_change < 0 )
    {
        /*
            Shrink slot
        */

        /*
            Lower-bound size change to ensure we still have MinimumWorkingSize
        */
        if ( buffer_end + buffer_offset + size_change < MinimumWorkingSize )
            size_change = MinimumWorkingSize - buffer_end - buffer_offset;

        if ( buffer_top > buffer_end + size_change )
        {
            /*
                Ensure the data is at the bottom of the buffer area
            */
#ifdef debugmem
dprintf( "Base=%#010x, offset=%d, bottom=%d, top=%d, end=%d\n", (int)buffer_base_address, buffer_offset, buffer_bottom, buffer_top, buffer_end );
#endif
            memmove(
                buffer_base_address,
                buffer_base_address + buffer_bottom + buffer_offset,
                buffer_top - buffer_bottom );

            buffer_end += buffer_bottom + buffer_offset;
            buffer_offset -= buffer_bottom + buffer_offset;
        }

        size_change = extend_application_slot( size_change );

        /*
            Adjust buffer_end by actual amount changed by
        */
        buffer_end += size_change;

        /*
            Upper bound buffer_top by buffer_end
        */
        if ( buffer_top > buffer_end )
            buffer_top = buffer_end;
    }

    truncate_overhanging_files();

    application_max_size = (int)(buffer_base_address + buffer_offset + buffer_end) - 0x8000;
#ifdef debugmem
dprintf( "Application max size is now %d\n", application_max_size );
dprintf( "Base=%#010x, offset=%d, bottom=%d, top=%d, end=%d\n", (int)buffer_base_address, buffer_offset, buffer_bottom, buffer_top, buffer_end );
#endif
}

/*
     Initialise to end in suitable place for closedown
*/
os_error *init_memmanagement( void )
{
    /*
        Things that use real memory ( base = null )
    */
    chain_initialise_header( &files_chain );

    /*
        This determins the maximum size we will allow buffer
        allocation to grow to. This starts at the
        current next slot size, then gets adjusted when the
        user drags the memory slider
    */
    application_max_size = current_next_slot();

    /*
        The buffer area
    */
    buffer_base_address = (char *)(application_current_size() + 0x8000);
    buffer_offset = 0;
    buffer_bottom = 0;
    buffer_top = 0;
    buffer_end = 0;

    /*
        A cludge to stop memory size dragging from oscillating
        about a non-memory block size boundary.
    */
    last_requested_size = -1;

    source_of_finished_file = NULL;

    (void)_kernel_register_slotextend( memmanage_slotextend );

    return NULL;
}

/*
     Get into a fit state for copying. The chains and area
     pointers should already been zeroed out by calling
     init_memmangement. This routine starts off flex and heap,
     which grab more wimp space, which is why this part of the
     initialisation is left out of the general initialisation
     sequence. The routine also tries to grab a buffer, just to
     make sure this is feasible. No buffer => No copying!
*/
os_error *init_for_copying( void )
{
#if Timing
    copytime = clock();
#endif
    src_block_size = InitialBlockSize;
    dest_block_size = InitialBlockSize;
    timing_init();

    if ( buffer_ensure( MinimumWorkingSize ) )
    {
        if ( application_max_size < application_current_size() )
            application_max_size = application_current_size();

        return NULL;
    }
    else
    {
        return error( mb_slotsize_too_small ); /* was mb_malloc_failed */
    }
}

/*
     Misc. ESSENTIAL tidying up:
      Close down any open files
*/
void closedown_memmanagement( void )
{
    chain_link *link;
    files_header *fh;

    for ( link = files_chain.forwards; link->forwards; link = link->forwards )
    {
        fh = chain_link_Wrapper( link );

        ensure_files_closed( fh );
    }
#if Timing
    copytime = clock() - copytime;
    werr(0, "time taken for file copy = %d.%02ds", copytime/100, copytime%100);
#endif
#ifdef debug
#ifdef debugfile
    if ( debugf != NULL ) fclose( debugf );
#endif
#endif
}

/*
     Add a file to the end of the files chain
*/
os_error *add_file_to_chain( char *destination, char *source, int size, int reload, int execute, int attributes, int objecttype, int forceit, int *i_am_full
#ifdef PROGRESS_BAR
, unsigned int progress, void **ref
#endif
) {
    files_header *fh;

    fh = overflowing_malloc( sizeof( files_header ));
    if ( fh )
    {
        fh->source_filename = overflowing_malloc( strlen( source ) + 1 );

        if ( fh->source_filename )
        {
            fh->destination_filename = overflowing_malloc( strlen( destination ) + 1 );

            if ( fh->destination_filename )
            {
                /*
                     Initialise the structure
                */
                strcpy( fh->source_filename, source );
                fh->source_file_handle = 0;
                strcpy( fh->destination_filename, destination );
                fh->destination_file_handle = 0;
                fh->size = size;
                fh->reload = reload;
                fh->execute = execute;
                fh->attributes = attributes;
                fh->objecttype = objecttype;
                fh->forceit = forceit;
                fh->read_to = 0;
                fh->written_to = 0;
                fh->write_state = write_not_started;
                fh->start_of_buffer = 0;

                #ifdef PROGRESS_BAR
                fh->total_progress = progress / 2;
                fh->write_progress = fh->read_progress = fh->total_progress;
                if (ref != NULL) *ref = fh;
                DEBUG(("add_file_to_chain: %s type %d progress %08x\n", destination, objecttype, fh->total_progress));
                #endif

                /*
                     link to backwards end of files chain
                */
                chain_insert_before_header( &fh->link, fh, &files_chain );

                *i_am_full = No;

                return NULL;
            }

            overflowing_free( fh->source_filename );
        }

        overflowing_free( fh );
    }

    *i_am_full = Yes;

    /*
         one of the heap_allocs failed
    */
    return error( mb_malloc_failed );
}


#ifdef PROGRESS_BAR
void modify_chain_file_progress(void *ref, unsigned int progress) {
  files_header *fh = (files_header *)ref;

  if (fh != NULL) {
    DEBUG(("\nmodify chain: %s %x\n", fh->destination_filename, progress));
    fh->write_progress = fh->total_progress = progress;
  }
}
#endif


/* Modified to return the progress value of the finished file	*/
os_error *read_a_block( int *i_am_full, int *need_another_file, int *that_finished_a_file, unsigned int *progress )
{
    files_header *fh;
    os_regset r;
    os_error *err;
    os_gbpbstr gbpbplace;
    int size_needed;
    int t, start_read_to;

    *i_am_full = No;
    *that_finished_a_file = No;

    fh = next_fh_to_read();

    if ( !fh )
    {
        *need_another_file = Yes;

        return NULL;
    }

    *need_another_file = No;

#ifdef debugmem
dprintf( "read_a_block from %s\n", fh->source_filename );
#endif

    start_read_to = fh->read_to;

    /*
         If we have to do some reading
    */
    if ( fh->read_to < fh->size )
    {
#ifdef debugmem
dprintf( "R" );
#endif
        /*
            Work out how much buffer is needed...
        */

        /*
            Upper bound to src_block_size and amount to read in file
        */
        if ( fh->size - fh->read_to > src_block_size )
        {
            size_needed = src_block_size;
        }
        else
        {
            size_needed = fh->size - fh->read_to;
        }

        /*
             Ensure word-alignment of file ptr and buffer address
             equal, to ensure DMA operation on DMA-capable disc
             systems (eg Iyonix ADFS).
        */
        if ( fh->read_to == fh->written_to &&
             (buffer_top & 3) != (fh->read_to & 3) )
        {
            int pad = ((fh->read_to & 3) - (buffer_top & 3)) & 3;
            *i_am_full = grow_buffer( &pad );
            if ( pad <= 0)
                return NULL;
            buffer_top += pad;
        }
        /*
             Enlarge the buffer (if possible)
        */
        *i_am_full = grow_buffer( &size_needed );

        if ( size_needed <= 0 )
            return NULL;

        /*
            Set up new buffer
        */
        if ( fh->read_to == fh->written_to )
        {
            fh->start_of_buffer = buffer_top;
        }

#if 0
/* Using OS_GBPB is faster if the file is already open */
        /*
            If haven't already started on the open-gbpb-close sequence AND
                file size fits into buffer allowed
        */
        if ( fh->read_to == 0 && fh->size <= size_needed )
        {
            /*
                If were reading by blocks, but now decided its OK to read by whole file
            */
            if ( fh->source_file_handle )
                close_file( &fh->source_file_handle );

            /*
                optimal read case: enough ram to read whole file
            */
            fileplace.action = 16; /* load file */
            fileplace.name = fh->source_filename;
            fileplace.loadaddr = (int)(buffer_base_address + buffer_offset + fh->start_of_buffer);
            fileplace.execaddr = 0;

            err = os_file( &fileplace );

            if ( err )
                return err;

            fh->read_to = fh->size;
            buffer_top += fh->size;
        }
        else
#endif
        {
            /*
                 Sub-optimal case: read file in chunks

                 If the file isn't open yet, then open it
            */
            if ( fh->source_file_handle == 0 )
            {
#ifdef debugmem
dprintf( "O" );
#endif
                r.r[0] = 0x4f;
                r.r[1] = (int)fh->source_filename;

                err = os_find( &r );

                if ( err )
                    return err;

                fh->source_file_handle = r.r[0];
            }

            t = clock();

            /*
                Read a block into the buffer.
            */
            gbpbplace.action = 3; /* read bytes from file at position */
            gbpbplace.file_handle = fh->source_file_handle;
            gbpbplace.data_addr = buffer_base_address + buffer_offset + buffer_top;
            gbpbplace.number = size_needed;
            gbpbplace.seq_point = fh->read_to;

#ifdef debugmem
dprintf( "r(%d,%d<-%d)", gbpbplace.data_addr, gbpbplace.number, gbpbplace.seq_point );
#endif

            err = os_gbpb( &gbpbplace );
            if ( err )
                return err;

            fh->read_to += size_needed;
            buffer_top += size_needed;

            if (src_block_size == size_needed)
            {  /* only time full block transfers */
                t = clock() - t;
                if ( timing_accumulating(timing_Reading) )
                { /* accumulating initial timings */
                    assert(src_block_size == InitialBlockSize);
                    timing_accumulate(timing_Reading, t, src_block_size);
                }
                else if (time_quanta < MaxTimeQuanta)
                {
                    if ( (src_block_size <= SmallBlockSize)
                      && timing_poorrate(timing_Reading, t, src_block_size) )
                    { /* if the transfer rate is suffering at this small blocksize,
                       * increase time_quanta to cause the blocksize to grow */
                        if (time_quanta < MaxTimeQuanta) time_quanta *= 2;
#ifdef debugmem
dprintf("change time_quanta to %d\n", time_quanta);
#endif
                    }
                    if ( t < time_quanta - TimeQuantaHysteresis) src_block_size = grow( src_block_size );
                    if ( t > time_quanta + TimeQuantaHysteresis) src_block_size = shrink( src_block_size );
                }
            }

            if ( fh->read_to >= fh->size )
            {
#ifdef debugmem
dprintf( "C" );
#endif
                err = close_file( &fh->source_file_handle );
                if ( err )
                    return err;
            }
        }
    }

    /*
         If we've finished reading the file
    */
    if ( fh->read_to >= fh->size )
    {
        *that_finished_a_file = Yes;

        #ifdef PROGRESS_BAR
        *progress = fh->read_progress;
        #endif

        /*
            If there isn't another file to read, tell the client about
            it now.
        */
        if ( next_fh_to_read() == NULL )
            *need_another_file = Yes;

    #ifdef PROGRESS_BAR
    } else {
        long long unsigned int p;
        int b;

        b = fh->read_to - start_read_to;	// bytes read this time
        p = (fh->total_progress * (long long) b) / (long long) fh->size;

        fh->read_progress -= (unsigned int) p;
        *progress = (unsigned int) p;

        DEBUG(("partial read: %d bytes / %d = %08x progress\n", (int)b, fh->size, p));
    #endif
    }


#ifdef debugmem
dprintf( "X\n" );
#endif

    return NULL;
}

void skip_file_read( void )
{
    skip_file( next_fh_to_read() );
}


static os_error *int_write_a_block( int *i_am_empty, int *that_finished_a_file, unsigned int *progress )
{
    os_gbpbstr gbpbplace;
    os_filestr fileplace;
    files_header *fh;
    os_regset r;
    os_error *err = NULL;
    int amount_to_write;
    int t, start_written_to;
    static int dont_set_dir_info;

    *i_am_empty = No;
    *that_finished_a_file = No;

    /*
        no more files to write?
    */
    if ( !files_chain.forwards->forwards )
    {
        *i_am_empty = Yes;

        return NULL;
    }

    fh = chain_link_Wrapper( files_chain.forwards );

#ifdef debugmem
dprintf( "int_write_a_block to %s\n", fh->destination_filename );
#endif

    /*
        no more buffer to write when there's more file to write?
    */
    if ( fh->objecttype != ObjectType_Directory &&
        fh->read_to - fh->written_to == 0 &&
        fh->written_to < fh->size )
    {
        *i_am_empty = Yes;

        return NULL;
    }

    start_written_to = fh->written_to;

#ifdef debugmem
dprintf( "W" );
#endif

    /*
        amount_to_write is lesser of a dest_block_size and the amount buffered of the file
    */
    if ( fh->read_to - fh->written_to > dest_block_size )
    {
        amount_to_write = dest_block_size;
    }
    else
    {
        amount_to_write = fh->read_to - fh->written_to;
    }

    while ( !err &&
           ( fh->write_state == write_not_started ||
         fh->write_state == write_f_tried_unlock ||
         fh->write_state == write_f_tried_cdirs ||
         fh->write_state == write_f_tried_both ))
    {
        if ( fh->objecttype == ObjectType_Directory )
        {
#ifdef debugmem
dprintf( "D" );
#endif
            /*
                Start up directory
            */
            err = create_directory( fh->destination_filename );

            if ( !err )
                fh->write_state = dont_set_dir_info ?
                            write_adjust_access :
                            write_adjust_info;
        }
        else
        {
            /*
                Start up file (or partition)
            */
            if ( fh->size <= amount_to_write )
            {
#ifdef debugmem
dprintf( "S" );
#endif
                /*
                    Optimised case - try save
                */
                fileplace.action = 0; /* save file */
                fileplace.name = fh->destination_filename;
                fileplace.loadaddr = fh->reload;
                fileplace.execaddr = fh->execute;
                fileplace.start = (int)(buffer_base_address + buffer_offset + fh->start_of_buffer);
                fileplace.end = fileplace.start + fh->size;

                err = os_file( &fileplace );

                if ( !err )
                {
                    fh->written_to = fh->size;
                    fh->start_of_buffer += fh->size;
                    buffer_bottom += fh->size;

                    fh->write_state = write_adjust_access;
                }
            }
            else
            {
#ifdef debugmem
dprintf( "c" );
#endif
                /*
                    Sub-optimal case - open, GBPB, close
                */
                err = create_file_dead( fh->destination_filename, fh->size );

                if ( !err )
                {
                    fh->write_state = write_openit;
                }
            }
        }

        if ( err )
        {
            if ( (err->errnum & FileError_Mask) == ErrorNumber_Locked &&
                fh->forceit &&
                fh->write_state != write_f_tried_unlock &&
                fh->write_state != write_f_tried_both )
            {
                /*
                    We haven't tried unlocking yet - give it a go
                */
                fileplace.action = 4;   /* set file attributes */
                fileplace.name = fh->destination_filename;
                fileplace.end = 3;      /* owner read/write */

                os_file( &fileplace );  /* ignore any error back */

                err = NULL;

                if ( fh->write_state == write_not_started )
                    fh->write_state = write_f_tried_unlock;
                else if ( fh->write_state == write_f_tried_cdirs )
                    fh->write_state = write_f_tried_both;
            }
            else if ( ((err->errnum & FileError_Mask) == ErrorNumber_NotFound ||
                   err->errnum == ErrorNumber_CantOpenFile) &&
                   fh->write_state != write_f_tried_cdirs &&
                   fh->write_state != write_f_tried_both )
            {
                /*
                    We haven't tried cdirs yet - give it a go
                */
                err = ensure_file_directories( fh->destination_filename );

                if ( !err )
                {
                    /*
                        no error - try openning/saveing again, but don't retry cdirs on fail
                    */
                    if ( fh->write_state == write_not_started )
                        fh->write_state = write_f_tried_cdirs;
                    else if ( fh->write_state == write_f_tried_unlock )
                        fh->write_state = write_f_tried_both;
                }
            }
        }
    }

    if ( !err && fh->write_state == write_openit )
    {
#ifdef debugmem
dprintf( "O" );
#endif
        /*
            Open it up. The file has been successfully created if we're here, so
            any failures at this point are bad news.
        */
        r.r[0] = 0xcf; /* open up and give an error if directory or not exists */
        r.r[1] = (int)fh->destination_filename;

        err = os_find( &r );

        if ( !err )
        {
            fh->write_state = write_truncateopen;
            fh->destination_file_handle = r.r[0];
        }
    }

    if ( !err && fh->write_state == write_truncateopen )
    {
#ifdef debugmem
dprintf( "T" );
#endif
        /*
            Once the file's open set its extent to 0. This
            prevents FileSwitch loading before update on
            blocks of the file.
        */
        r.r[0] = 3; /* set extent */
        r.r[1] = (int)fh->destination_file_handle;
        r.r[2] = 0; /* to 0 */

        err = os_args( &r );

        if ( !err )
        {
            fh->write_state = write_blocks;
        }
    }

    if ( !err && fh->write_state == write_blocks )
    {
        /*
            Write a block out
        */
        gbpbplace.action = 1;    /* write bytes at location */
        gbpbplace.file_handle = fh->destination_file_handle;
        gbpbplace.data_addr = buffer_base_address + buffer_offset + fh->start_of_buffer;
        gbpbplace.number = amount_to_write;
        gbpbplace.seq_point = fh->written_to;

#ifdef debugmem
dprintf( "w(%d,%d->%d)", (int)gbpbplace.data_addr, gbpbplace.number, gbpbplace.seq_point );
#endif

        t = clock();

        err = os_gbpb( &gbpbplace );

        if ( !err )
        {
            if (dest_block_size == amount_to_write)
            {  /* only time full block transfers */
                t = clock() - t;
                if ( timing_accumulating(timing_Writing) )
                { /* accumulating initial timings */
                    assert(dest_block_size == InitialBlockSize);
                    timing_accumulate(timing_Writing, t, dest_block_size);
                }
                else
                {
                    if ( (dest_block_size <= SmallBlockSize)
                      && timing_poorrate(timing_Writing, t, dest_block_size) )
                    { /* if the transfer rate is suffering at this small blocksize,
                       * increase time_quanta to cause the blocksize to grow */
                        if (time_quanta < MaxTimeQuanta) time_quanta *= 2;
#ifdef debugmem
dprintf( "change time_quanta to %d\n", time_quanta );
#endif
                    }
                    if ( t < time_quanta - TimeQuantaHysteresis) dest_block_size = grow( dest_block_size );
                    if ( t > time_quanta + TimeQuantaHysteresis) dest_block_size = shrink( dest_block_size );
                }
            }
            fh->written_to += amount_to_write;
            fh->start_of_buffer += amount_to_write;
            buffer_bottom += amount_to_write;

            if ( fh->written_to >= fh->size )
                fh->write_state = write_closeit;
        }
    }

    if ( !err && fh->write_state == write_closeit )
    {
#ifdef debugmem
dprintf( "C" );
#endif
        err = close_file( &fh->destination_file_handle );

        /*
            Regardless of whether the close worked, the file will be closed,
            so always move onto adjusting the file's info
        */
        fh->write_state = write_adjust_info;
    }

    if ( !err && fh->write_state == write_adjust_info )
    {
#ifdef debugmem
dprintf( "I" );
#endif
        /*
            Always adjust the info as its almost always wrong
        */
        err = write_catalogue_information(
              fh->destination_filename,
              fh->reload,
              fh->execute,
              fh->attributes );

        if ( err && fh->objecttype == ObjectType_Directory)
        {
            /* Some FS's can't do this for a directory - back
               off to just doing the access permission.
             */
            err = NULL;
            dont_set_dir_info = 1;
            fh->write_state = write_adjust_access;
        }
        else if ( !err )
            fh->write_state = write_all_done;
    }

    if ( !err && fh->write_state == write_adjust_access )
    {
#ifdef debugmem
dprintf( "A" );
#endif
        /*
            Only adjust attributes if they're non-default ones
        */
        err = write_attributes(
              fh->destination_filename,
              fh->attributes );

        if ( !err )
            fh->write_state = write_all_done;
    }

    if ( !err && fh->write_state == write_all_done )
    {
        *that_finished_a_file = Yes;
        #ifdef PROGRESS_BAR
        *progress = fh->write_progress;
        #endif

        remove_file_from_chain();

#ifdef PROGRESS_BAR
    } else {
      long long unsigned int p;
      int b;

      b = fh->written_to - start_written_to;
      p = (fh->total_progress * (long long) b) / (long long) fh->size;

      fh->write_progress -= (unsigned int) p;
      *progress = (unsigned int) p;

      DEBUG(("partial write: %d bytes / %d = %08x progress\n", (int)b, fh->size, p));
#endif
    }

#ifdef debugmem
dprintf( "X\n" );
#endif

    return err;
}


os_error *write_a_block( int *i_am_empty, int *that_finished_a_file, unsigned int *progress )
{
    os_error *err = int_write_a_block( i_am_empty, that_finished_a_file, progress );

    if ( *i_am_empty )
    {
        buffer_end += buffer_offset;
        buffer_offset = buffer_bottom = buffer_top = 0;
    }

    return err;
}

void skip_file_write( void )
{
    if ( files_chain.forwards->forwards )
    {
        skip_file( chain_link_Wrapper( files_chain.forwards ));
    }
}

char *next_file_to_be_written( void )
{
    files_header *fh;

    if ( files_chain.forwards->forwards )
    {
        fh = chain_link_Wrapper( files_chain.forwards );

        return fh->destination_filename;
    }
    else
    {
        return NULL;
    }
}

char *next_file_to_be_read( void )
{
    files_header *fh = next_fh_to_read();

    if ( fh != NULL )
        return fh->source_filename;
    else
        return NULL;
}

void restart_file_read( void )
{
    files_header *fh = next_fh_to_read();

#ifdef debugmem
dprintf( "restart_file_read: fh=&08X\n", (int)fh );
#endif

    if ( fh != NULL )
    {
        ensure_files_closed( fh );
        buffer_top -= fh->read_to;
        if ( buffer_top < 0 ) buffer_bottom = buffer_top = buffer_offset = 0;
        fh->read_to = 0;
        fh->written_to = 0;
        fh->write_state = write_not_started;
    }
}

void restart_file_write( void )
{
    files_header *fh = chain_link_Wrapper( files_chain.forwards );

#ifdef debugmem
dprintf( "restart_file_write: fh=&08X\n", (int)fh );
#endif

    if ( fh != NULL )
    {
        /* Only restart file if we still have all of its contents so far.
         * If we don't then we can't just read from the start because we might
         * overwrite another file's data so just do nothing (== retry).
         */
        int file_start = fh->start_of_buffer - fh->written_to;
        if ( file_start >= 0 )
        {
            ensure_files_closed( fh );
            fh->start_of_buffer = file_start;
            buffer_bottom -= fh->written_to;
            fh->written_to = 0;
            fh->write_state = write_not_started;
#ifdef debugmem
dprintf( "restarting write: start_of_buffer=%08X, buffer_bottom=%08X\n", fh->start_of_buffer, buffer_bottom );
#endif
        }
    }
}

int bytes_left_to_read( void )
{
    files_header *fh = next_fh_to_read();

    if ( fh )
    {
        return fh->size - fh->read_to;
    }
    else
    {
        return 0;
    }
}

int bytes_left_to_write( void )
{
    files_header *fh;

    if ( files_chain.forwards->forwards )
    {
        fh = chain_link_Wrapper( files_chain.forwards );

        return fh->size - fh->written_to;
    }
    else
    {
        return 0;
    }
}

void copy_go_faster( int do_it )
{
    if ( do_it )
    {
        /*
            Go faster
        */

        time_quanta = MaxTimeQuanta;      /* centi-seconds */

        minimum_block_size = 15*1024; /* bytes: optimised for econet */

        if ( src_block_size < minimum_block_size )
            src_block_size = minimum_block_size;
        if ( dest_block_size < minimum_block_size )
            dest_block_size = minimum_block_size;
    }
    else
    {
        /*
            Go slower
        */

        time_quanta = NominalTimeQuanta;

        minimum_block_size = 512;
    }
}